Skip to content

Commit c8d1b7b

Browse files
authored
fix(scripts): the vitest-timeout audit reads a regex literal after an arrow (#3910)
Main's Node tests job fails on `check:vitest-timeout-audit` since #3856: two call sites in `packages/bcf/src/document-reference-guid.test.ts` could not be parsed. The gate asks for the parser to be fixed rather than the file, and that is what this does. Mechanism: `REGEX_PRECEDING_RE` in `scripts/lib/vitest-timeout-audit.mjs` lacked `>`, and `isRegexStart` inspects only the last significant character, so a `/` right after `=>` was read as division. Line 162 of that test, `const line = (xml: string) => /<DocumentReference Guid="[^"]+"/.exec(xml)![0];`, then opened a phantom string at its first `"` that closed on the next `"` fourteen lines later, blanking an `it` outright and unbalancing the parens of the `describe` and `it` the gate reported. `>` is added. `<` is deliberately not: in a `.test.tsx` a JSX closing tag puts `/` straight after `<`, and treating that as a regex start parsed a legitimate JSX test to zero call sites. Two regression tests, mutation-checked in both directions: dropping `>` fails only the arrow test, adding `<` fails only the JSX test. Repo-wide listing diff before and after: zero call sites lost, five recovered, only the errors disappear. Exit codes on the head: `node --test scripts/lib/vitest-timeout-audit.test.mjs` 0 (79 pass), `pnpm run check:vitest-timeout-audit` 0, check-module-size 0 (the module stays at exactly its allowlisted 1165 lines).
1 parent c5da727 commit c8d1b7b

2 files changed

Lines changed: 36 additions & 4 deletions

File tree

scripts/lib/vitest-timeout-audit.mjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,13 @@
7171
* heuristic: a `/` starts a regex when the last significant token
7272
* before it is an operator, punctuator, or one of the keywords in
7373
* `REGEX_PRECEDING_RE` (`(`, `,`, `=`, `:`, `[`, `!`, `&`, `|`, `?`,
74-
* `{`, `;`, `return`, `typeof`, … or start-of-file), and is division
75-
* otherwise — including right after a string or template literal,
74+
* `{`, `;`, `>`, `return`, `typeof`, … or start-of-file), and is
75+
* division otherwise — including right after a string or template literal,
7676
* which `stripNoise` tracks separately because a blanked literal is
7777
* indistinguishable from whitespace. This covers every shape actually
7878
* seen in this codebase. NOT handled, and each of these is a REAL
7979
* defect verified by running this module, not a theoretical one:
80-
* * a regex after an operator absent from that list;
80+
* * a regex after an operator absent from that list (`<` is out DELIBERATELY -- see the JSX test);
8181
* * a regex spanning a line break (bailed on, and the `/` is then read
8282
* as ordinary);
8383
* * a regex immediately after `}` (a block close) or after `)` — both
@@ -199,7 +199,7 @@ const CALL_KEYWORD_RE = /(?<![.\w$])(describe|it|test)(?![\w$])/g;
199199
* modifier argument list, and must NOT be skipped as one.
200200
*/
201201
const PARAMETERIZED_MODIFIERS = new Set(['each', 'skipIf', 'runIf', 'extend']);
202-
const REGEX_PRECEDING_RE = /[([{,;:=!&|?~^%*+-]$|^$|(?:^|[^\w$])(return|typeof|instanceof|in|of|new|delete|void|throw|case|do|else|yield|await)$/;
202+
const REGEX_PRECEDING_RE = /[([{,;:=!&|?~^%*+>-]$|^$|(?:^|[^\w$])(return|typeof|instanceof|in|of|new|delete|void|throw|case|do|else|yield|await)$/;
203203

204204
/**
205205
* Replace every comment, string, and template literal in `source` with

scripts/lib/vitest-timeout-audit.test.mjs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,38 @@ test('a regex containing an unbalanced paren still does not corrupt the call it
663663
assert.deepEqual(unprotectedNames(src), ['next']);
664664
});
665665

666+
test('a regex opening right after an arrow (=>) is not read as division', () => {
667+
// #3856's `document-reference-guid.test.ts` shape: a one-line arrow whose
668+
// body IS a regex literal. `>` was absent from the regex-start heuristic, so
669+
// the `/` was read as division, the `"` in the pattern opened a phantom
670+
// string that ran to the next `"` fifteen lines later, and the call sites in
671+
// between vanished while the enclosing `describe` and `it` became unparsable.
672+
const src = [
673+
"describe('suite', () => {",
674+
" it('arrow regex', () => {",
675+
' const line = (xml) => /<Ref Guid="[^"]+"/.exec(xml)[0];',
676+
' expect(line(a)).toBe(line(b));',
677+
' });',
678+
" it('after', () => { doWork(); });",
679+
'});',
680+
].join('\n');
681+
assert.deepEqual(findUnparsedCallSites(src), []);
682+
assert.deepEqual(auditSource(src).map((r) => r.name), ['arrow regex', 'after']);
683+
});
684+
685+
// `<` is NOT a regex-starter here even though `a < /re/` is legal JS: in a
686+
// `.test.tsx` a JSX closing tag puts `/` straight after `<`, and treating that
687+
// as a regex opening runs it to the next `/` on the line, swallowing whatever
688+
// quote sits between. That costs a real, common shape to buy a contrived one.
689+
test('a JSX closing tag is not read as a regex opening', () => {
690+
const src = [
691+
"it('a', () => { render(<div>x</div>); expect(u).toBe('/p'); });",
692+
"it('b', () => { doWork(); });",
693+
].join('\n');
694+
assert.deepEqual(findUnparsedCallSites(src), []);
695+
assert.deepEqual(auditSource(src).map((r) => r.name), ['a', 'b']);
696+
});
697+
666698
test('a slash that is division after a string literal is not read as a regex opening', () => {
667699
// A blanked string is indistinguishable from whitespace, so without
668700
// tracking where the literal ended the `=` before it would make this `/`

0 commit comments

Comments
 (0)