< Summary - Envilder CLI

Information
Class: src/envilder/apps/cli/errors/ParseErrorRedactor.ts
Assembly: Default
File(s): src/envilder/apps/cli/errors/ParseErrorRedactor.ts
Tag: 561_41292037308
Line coverage
93%
Covered lines: 61
Uncovered lines: 4
Coverable lines: 65
Total lines: 155
Line coverage: 93.8%
Branch coverage
81%
Covered branches: 31
Total branches: 38
Branch coverage: 81.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

src/envilder/apps/cli/errors/ParseErrorRedactor.ts

#LineLine coverage
 1/**
 2 * An option as registered with the CLI parser.
 3 */
 4export type KnownOption = {
 5  /** Flags string as declared, for example `--map <path>`. */
 6  flags: string;
 7  /** Long name, for example `--map`. */
 8  long?: string;
 9  /** Whether the option is listed in `--help`; only those are suggested. */
 10  visible: boolean;
 11};
 12
 513const GENERIC_ERROR = 'error: invalid command line. Run envilder --help.\n';
 514const GENERIC_UNKNOWN_OPTION = 'error: unknown option. Run envilder --help.\n';
 515const UNKNOWN_OPTION_PREFIX = "error: unknown option '";
 516const SAFE_LONG_NAME = /^--[A-Za-z0-9][A-Za-z0-9-]*/;
 517const SAFE_SHORT_NAME = /^-[A-Za-z0-9]/;
 18const TOO_MANY_ARGUMENTS =
 519  /^error: too many arguments(?: for '[A-Za-z0-9-]+')?\. Expected (\d+) (arguments?) but got (\d+)\b/;
 520const MISSING_ARGUMENT = /^error: option '([^\n]*)' argument missing\s*$/;
 21
 22/**
 23 * Rebuilds a parse error from known-safe parts only (#554).
 24 *
 25 * The parser's messages quote what the user typed, and an option value, an
 26 * extra argument or even a mistyped option name can be a secret. Nothing
 27 * typed by the user is copied through unless it is provably harmless:
 28 *
 29 * - an unknown long option is named only when it is a near-typo of a visible
 30 *   option of the command being parsed, with that option as the suggestion;
 31 * - an unknown short option is named by its single character;
 32 * - "too many arguments" keeps only the counts;
 33 * - "argument missing" keeps only a registered option's own flags.
 34 *
 35 * Anything else is replaced with a generic message.
 36 */
 37export function redactParseError(
 38  message: string,
 39  knownOptions: readonly KnownOption[],
 40): string {
 2141  if (message.startsWith(UNKNOWN_OPTION_PREFIX)) {
 1842    return rebuildUnknownOption(
 43      message.slice(UNKNOWN_OPTION_PREFIX.length),
 44      knownOptions,
 45    );
 46  }
 47
 348  const tooMany = TOO_MANY_ARGUMENTS.exec(message);
 349  if (tooMany) {
 150    const [, expected, noun, received] = tooMany;
 151    return `error: too many arguments. Expected ${expected} ${noun} but got ${received}.\n`;
 52  }
 53
 254  const missing = MISSING_ARGUMENT.exec(message);
 955  if (missing && knownOptions.some((option) => option.flags === missing[1])) {
 256    return missingArgumentMessage(missing[1]);
 57  }
 58
 059  return GENERIC_ERROR;
 60}
 61
 62/**
 63 * The message for an option whose value is missing. Only ever built from a
 64 * registered option's flags.
 65 */
 66export function missingArgumentMessage(flags: string): string {
 1467  return `error: option '${flags}' argument missing\n`;
 68}
 69
 70function rebuildUnknownOption(
 71  token: string,
 72  knownOptions: readonly KnownOption[],
 73): string {
 1874  if (!token.startsWith('--')) {
 275    const short = SAFE_SHORT_NAME.exec(token)?.[0];
 276    return short === undefined
 77      ? GENERIC_UNKNOWN_OPTION
 78      : `error: unknown option '${short}'\n`;
 79  }
 80
 1681  const name = SAFE_LONG_NAME.exec(token)?.[0];
 1882  if (name === undefined) {
 183    return GENERIC_UNKNOWN_OPTION;
 84  }
 1585  const candidates = knownOptions
 18386    .filter((option) => option.visible && option.long !== undefined)
 14787    .map((option) => option.long as string);
 1588  const similar = suggestSimilar(name, candidates);
 1589  if (similar.length === 0) {
 1090    return GENERIC_UNKNOWN_OPTION;
 91  }
 92  const suggestion =
 593    similar.length === 1 ? similar[0] : `one of ${similar.join(', ')}`;
 1894  return `error: unknown option '${name}' (Did you mean ${suggestion}?)\n`;
 95}
 96
 597const MAX_DISTANCE = 3;
 598const MIN_SIMILARITY = 0.4;
 99
 100/**
 101 * Same matching as Commander's own suggestions (optimal string alignment
 102 * distance, at most 3 edits, more than 40% similar, closest matches only),
 103 * applied to the safe option name because Commander offers none when the
 104 * typed option carries a value.
 105 */
 106function suggestSimilar(flag: string, candidates: readonly string[]): string[] {
 15107  const word = flag.slice(2);
 15108  let best = MAX_DISTANCE;
 15109  let similar: string[] = [];
 15110  for (const candidate of new Set(candidates)) {
 147111    const name = candidate.slice(2);
 147112    if (name.length <= 1) {
 0113      continue;
 114    }
 147115    const distance = editDistance(word, name);
 147116    const length = Math.max(word.length, name.length);
 147117    if ((length - distance) / length <= MIN_SIMILARITY) {
 142118      continue;
 119    }
 5120    if (distance < best) {
 5121      best = distance;
 5122      similar = [candidate];
 0123    } else if (distance === best) {
 0124      similar.push(candidate);
 125    }
 126  }
 15127  return similar.sort((a, b) => a.localeCompare(b));
 128}
 129
 130function editDistance(a: string, b: string): number {
 147131  if (Math.abs(a.length - b.length) > MAX_DISTANCE) {
 72132    return Math.max(a.length, b.length);
 133  }
 75134  const d: number[][] = [];
 75135  for (let i = 0; i <= a.length; i++) {
 405136    d[i] = [i];
 137  }
 75138  for (let j = 0; j <= b.length; j++) {
 463139    d[0][j] = j;
 140  }
 75141  for (let j = 1; j <= b.length; j++) {
 388142    for (let i = 1; i <= a.length; i++) {
 1961143      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
 1961144      d[i][j] = Math.min(
 145        d[i - 1][j] + 1,
 146        d[i][j - 1] + 1,
 147        d[i - 1][j - 1] + cost,
 148      );
 1961149      if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
 9150        d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
 151      }
 152    }
 153  }
 75154  return d[a.length][b.length];
 155}