Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 24 additions & 37 deletions scripts/check-dispatcher-error-vocabulary.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,21 @@
*/

import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
import { maskComments, scanSource } from './js-comment-mask.mjs';
// [#15487] The SHARED half of the regex-literal recogniser: the 14-word
// `REGEX_AFTER_KEYWORD` set, `IDENT_CHAR`, and the literal WALK (character
// class, backslash sequences, line-terminator refusal, flags) live ONCE, in
// `js-comment-mask.mjs`, which already owns "which bytes are literal". What
// stays in this file is `regexMayBeginAt` — the POSITION RULE, the one half the
// two consumers answer differently and on purpose. See `walkRegexBody`'s
// docblock for why that is the line, and `makeRegexRecogniser`'s for why the
// rule is bound once here rather than passed at each of this file's call sites.
import {
IDENT_CHAR,
REGEX_AFTER_KEYWORD,
makeRegexRecogniser,
maskComments,
scanSource,
} from './js-comment-mask.mjs';
// [#12925] The OTHER gate's detector, imported so the kebab declaration below is
// pinned against its real recognizers rather than a paraphrase of them. That gate
// imports nothing from here, so this is a one-way edge.
Expand Down Expand Up @@ -1481,16 +1495,14 @@ function unwrapExpression(expr) {
* divergence itself, so neither the copy nor the deliberate difference can
* drift unnoticed.
*/
const REGEX_AFTER_KEYWORD = new Set([
'return', 'typeof', 'instanceof', 'in', 'of', 'case', 'delete', 'void',
'yield', 'await', 'new', 'do', 'else', 'throw',
]);

/** IdentifierPart, near enough for the ASCII this population is written in. */
const IDENT_CHAR = /[\w$]/;

/** LineTerminator — the four the grammar names, not just `\n`. */
const isLineTerminator = (c) => c === '\n' || c === '\r' || c === '\u2028' || c === '\u2029';
// `REGEX_AFTER_KEYWORD` and `IDENT_CHAR` are IMPORTED (#15487). They used to be
// byte-identical copies of the two constants in `scripts/js-comment-mask.mjs`,
// and that module's own header records what a drifting copy costs: dropping
// `return` from the set passes all 23 of its self-test cases and is caught only
// by the corpus sweep, on a file this tree writes today. A copy that can drift
// silently in exactly the direction its owner warns about is not one to keep.
// The LineTerminator test went with the walk, which is the only thing that
// consulted it.

/**
* [#14742] Is the `/` at `at` in a position where an expression may BEGIN?
Expand Down Expand Up @@ -1544,32 +1556,7 @@ function regexMayBeginAt(src, at) {
* so a `/` whose body does not close on its line was a division operator, and
* answering "not a regex" skips nothing.
*/
export function regexLiteralAt(src, at) {
if (src[at] !== '/') return -1;
const first = src[at + 1];
// RegularExpressionFirstChar excludes `*` and `/`, which is the grammar's own
// reason comments are unambiguous.
if (first === undefined || first === '*' || first === '/') return -1;
if (!regexMayBeginAt(src, at)) return -1;
let inClass = false;
for (let i = at + 1; i < src.length; i += 1) {
const c = src[i];
if (isLineTerminator(c)) return -1;
if (c === '\\') {
// RegularExpressionBackslashSequence :: `\` RegularExpressionNonTerminator
if (i + 1 >= src.length || isLineTerminator(src[i + 1])) return -1;
i += 1;
continue;
}
if (inClass) {
if (c === ']') inClass = false;
continue; // ⭐ a `/`, a quote or a backtick in here is an ORDINARY CHARACTER
}
if (c === '[') { inClass = true; continue; }
if (c === '/') return i; // flags are IdentifierPartChars — code bytes, walked as such
}
return -1;
}
export const regexLiteralAt = makeRegexRecogniser({ mayBeginAt: regexMayBeginAt });

/**
* [#14742] Does a literal OPEN at `at`? The dispatch the four primitives share,
Expand Down
42 changes: 42 additions & 0 deletions scripts/js-comment-mask.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,48 @@ export function stripComments(source: string): string;
*/
export function maskComments(source: string): string;

/**
* The outcome of walking the body of the regex literal opening at `at`.
*
* `closed: true` means `end` is the index of the CLOSING `/`. `closed: false`
* means the body ran into a LineTerminator or EOF and `end` is where it
* stopped — what THAT means is the caller's to decide, and the two consumers of
* this walk decide it differently on purpose (see the module's header).
*/
export interface RegexBodyWalk {
end: number;
closed: boolean;
}

/** IdentifierPart, near enough for the ASCII this tree is written in. */
export const IDENT_CHAR: RegExp;

/**
* Keywords after which a `/` opens a REGEX rather than a division. Shared, so
* that a second copy cannot drift: dropping `return` from it passes every
* pinned case and is caught only by the corpus sweep.
*/
export const REGEX_AFTER_KEYWORD: ReadonlySet<string>;

/** LineTerminator — the four the grammar names, which a regex body excludes. */
export function isRegexLineTerminator(c: string): boolean;

/**
* Walk the body of the regex literal opening at `at`, which the CALLER has
* already decided sits where an expression may begin.
*/
export function walkRegexBody(src: string, at: number): RegexBodyWalk;

/**
* Bind a POSITION RULE to the shared walk and get back a recogniser answering
* "index of the closing `/`, or -1". There is no default rule and omitting one
* throws — a default would be one of two opposite failure directions, handed
* silently to whichever caller forgot.
*/
export function makeRegexRecogniser(options: {
mayBeginAt: (src: string, at: number) => boolean;
}): (src: string, at: number) => number;

/**
* Drive the scanner over its own fixture corpus, printing a line per case.
*
Expand Down
Loading
Loading