From df0bea2da7463c6ce88003b6a90954d4564aa11a Mon Sep 17 00:00:00 2001 From: Priya Raman Date: Fri, 4 Sep 2026 02:16:24 +0000 Subject: [PATCH] fix(runner): recognize private-field property access in the wait guard A private field named `#await` made the regex-literal check extract the bare word `await`, so the scanner classified the `/` after it as a regex start instead of division. That let the fake regex literal mask a real `vi.waitFor(` or `expect.poll(` call on the same line. Widen the word extraction to include a leading `#`, so a private field's token (`#await`) never equals the bare keyword `await`. Co-authored-by: Paperclip --- .../scripts/check-bare-real-process-waits.test.mjs | 12 ++++++++++++ .../scripts/lib/bare-real-process-wait.mjs | 12 +++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/paperclip-runner/scripts/check-bare-real-process-waits.test.mjs b/packages/paperclip-runner/scripts/check-bare-real-process-waits.test.mjs index 907a68a6381..aceb09a5d50 100644 --- a/packages/paperclip-runner/scripts/check-bare-real-process-waits.test.mjs +++ b/packages/paperclip-runner/scripts/check-bare-real-process-waits.test.mjs @@ -228,6 +228,18 @@ test("rejects a vi.waitFor( that follows division after an await-named property, ]); }); +test("rejects a vi.waitFor( that follows division after a private #await field, inside a template literal interpolation", () => { + const source = [ + "async function example() {", + " const message = `status: ${this.#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() {", diff --git a/packages/paperclip-runner/scripts/lib/bare-real-process-wait.mjs b/packages/paperclip-runner/scripts/lib/bare-real-process-wait.mjs index dcc290e888e..1c448d6450a 100644 --- a/packages/paperclip-runner/scripts/lib/bare-real-process-wait.mjs +++ b/packages/paperclip-runner/scripts/lib/bare-real-process-wait.mjs @@ -76,6 +76,14 @@ function isKeywordShapedPropertyName(source, wordStartIndex) { return beforeWordIndex >= 0 && source[beforeWordIndex] === "."; } +// A private-field access such as `this.#await` reads a `#` right before the +// word. The `#` is part of the field's own name, not the `await` keyword, so +// the token this check extracts is `#await` — a word that can never equal +// the bare keyword `await` no matter what sits before the `#`. This keeps +// the standalone-keyword check in `isRegexLiteralStart` correct without a +// separate lookup for the `.` that a private-field access also requires. +const WORD_WITH_OPTIONAL_PRIVATE_FIELD_HASH = /#?[A-Za-z_$][\w$]*$/; + function isRegexLiteralStart(source, index) { let previousIndex = index - 1; while (previousIndex >= 0 && /\s/.test(source[previousIndex])) { @@ -84,7 +92,9 @@ function isRegexLiteralStart(source, index) { if (previousIndex < 0) return true; const previousCharacter = source[previousIndex]; if (!/[\w$)\]]/.test(previousCharacter)) return true; - const wordMatch = source.slice(0, previousIndex + 1).match(/[A-Za-z_$][\w$]*$/); + const wordMatch = source + .slice(0, previousIndex + 1) + .match(WORD_WITH_OPTIONAL_PRIVATE_FIELD_HASH); const word = wordMatch?.[0] ?? ""; if (!REGEX_PRECEDING_KEYWORDS.has(word)) return false; const wordStartIndex = previousIndex + 1 - word.length;