fix(runner): require a standalone await token in the wait guard

The regex-literal-start check treated an await-named property, such
as obj.await, as the await keyword. Code that divides after such a
property, like ${obj.await / divisor + (await vi.waitFor(...))},
could then mask the real vi.waitFor( call as part of a false regex
literal and hide it from the guard.

The check now looks at the character before the matched keyword. A
dot there means the word is a property name, not the keyword, so the
slash after it divides.

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Priya RamanandPaperclip committed 2026-09-04 02:00:34 +00:00
1 parent 11836700be
commit fdc16817d7
2 files changed
+30 -1

No files matched your search

@@ -216,6 +216,18 @@ test("ignores a vi.waitFor( written inside a regex literal's own text, inside a
assert.deepEqual(findBareRealProcessWaits(source), []);
});
test("rejects a vi.waitFor( that follows division after an await-named property, inside a template literal interpolation", () => {
const source = [
"async function example() {",
" const message = `status: ${obj.await / divisor + (await vi.waitFor(() => expect(x).toBe(1)))}`;",
"}",
].join("\n");
assert.deepEqual(findBareRealProcessWaits(source), [
{ line: 2, pattern: "vi.waitFor(" },
]);
});
test("still flags a real bare vi.waitFor( on the line after a comment mentioning the pattern", () => {
const source = [
"async function example() {",
@@ -62,6 +62,20 @@ const REGEX_PRECEDING_KEYWORDS = new Set([
"yield", "case", "throw", "do", "else", "await",
]);
// A word that reads as one of `REGEX_PRECEDING_KEYWORDS` is a property
// name, not the keyword, when a `.` (a plain member access or the last
// character of an optional-chain `?.`) sits directly before it, with only
// space allowed in between. `obj.await` is a value, so the `/` right after
// it divides; only a standalone `await` puts the `/` in an expression
// position.
function isKeywordShapedPropertyName(source, wordStartIndex) {
let beforeWordIndex = wordStartIndex - 1;
while (beforeWordIndex >= 0 && /\s/.test(source[beforeWordIndex])) {
beforeWordIndex -= 1;
}
return beforeWordIndex >= 0 && source[beforeWordIndex] === ".";
}
function isRegexLiteralStart(source, index) {
let previousIndex = index - 1;
while (previousIndex >= 0 && /\s/.test(source[previousIndex])) {
@@ -72,7 +86,10 @@ function isRegexLiteralStart(source, index) {
if (!/[\w$)\]]/.test(previousCharacter)) return true;
const wordMatch = source.slice(0, previousIndex + 1).match(/[A-Za-z_$][\w$]*$/);
const word = wordMatch?.[0] ?? "";
return REGEX_PRECEDING_KEYWORDS.has(word);
if (!REGEX_PRECEDING_KEYWORDS.has(word)) return false;
const wordStartIndex = previousIndex + 1 - word.length;
if (isKeywordShapedPropertyName(source, wordStartIndex)) return false;
return true;
}
// Mask a `/pattern/flags` regex literal, starting at the opening `/` in