mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
fix(runner): scan template interpolations in the real-process wait guard
The source-masking step treated a backtick template's `${...}`
expression as plain text and masked it, so a wait call written inside
an interpolation never matched. The step now masks only the literal
text of a template and scans the code inside `${...}` the same way it
scans code outside any literal.
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2 files changed
+93
-29
No files matched your search
@@ -158,6 +158,18 @@ test("ignores an expect.poll( written inside a template literal", () => {
|
||||
assert.deepEqual(findBareRealProcessWaits(source), []);
|
||||
});
|
||||
|
||||
test("rejects a vi.waitFor( written inside a template literal interpolation", () => {
|
||||
const source = [
|
||||
"async function example() {",
|
||||
" const message = `status: ${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() {",
|
||||
|
||||
@@ -48,19 +48,23 @@ async function collectSourceFiles(target) {
|
||||
return files;
|
||||
}
|
||||
|
||||
// Replace every comment and every string- or template-literal body with
|
||||
// spaces, one for one, so the result keeps the same length and the same
|
||||
// line breaks as `source`. A line count taken from the result still lines
|
||||
// up with `source`, and neither a wait-call pattern nor a `timeout` option
|
||||
// written inside a comment or a literal can survive into the result.
|
||||
function maskCommentsAndStrings(source) {
|
||||
const masked = source.split("");
|
||||
let index = 0;
|
||||
while (index < source.length) {
|
||||
// Scan `source` from `start` up to `end` as executable code, writing spaces
|
||||
// into `masked` for each comment and each string- or template-literal quote
|
||||
// character it finds, then handing the literal body to `maskLiteralBody`.
|
||||
// Every other character stays as it is in `source`, so a wait-call pattern
|
||||
// written as real code — including inside a template's `${...}`
|
||||
// expression — still matches once masking is done. When `stopAtOwnCloseBrace`
|
||||
// is true, `start` points just past a `${` this function did not consume,
|
||||
// and the function tracks nested `{`/`}` pairs so it returns right after the
|
||||
// `}` that closes this same interpolation, leaving both braces unmasked.
|
||||
function maskCode(source, masked, start, end, stopAtOwnCloseBrace) {
|
||||
let index = start;
|
||||
let braceDepth = stopAtOwnCloseBrace ? 1 : 0;
|
||||
while (index < end) {
|
||||
const twoCharacters = source.slice(index, index + 2);
|
||||
const character = source[index];
|
||||
if (twoCharacters === "//") {
|
||||
while (index < source.length && source[index] !== "\n") {
|
||||
while (index < end && source[index] !== "\n") {
|
||||
masked[index] = " ";
|
||||
index += 1;
|
||||
}
|
||||
@@ -70,40 +74,88 @@ function maskCommentsAndStrings(source) {
|
||||
masked[index] = " ";
|
||||
masked[index + 1] = " ";
|
||||
index += 2;
|
||||
while (index < source.length && source.slice(index, index + 2) !== "*/") {
|
||||
while (index < end && source.slice(index, index + 2) !== "*/") {
|
||||
if (source[index] !== "\n") masked[index] = " ";
|
||||
index += 1;
|
||||
}
|
||||
if (index < source.length) {
|
||||
if (index < end) {
|
||||
masked[index] = " ";
|
||||
masked[index + 1] = " ";
|
||||
index += 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (character === "'" || character === '"' || character === "`") {
|
||||
const quote = character;
|
||||
if (character === "'" || character === '"') {
|
||||
masked[index] = " ";
|
||||
index = maskLiteralBody(source, masked, index + 1, end, character, false);
|
||||
continue;
|
||||
}
|
||||
if (character === "`") {
|
||||
masked[index] = " ";
|
||||
index = maskLiteralBody(source, masked, index + 1, end, character, true);
|
||||
continue;
|
||||
}
|
||||
if (stopAtOwnCloseBrace && character === "{") {
|
||||
braceDepth += 1;
|
||||
index += 1;
|
||||
while (index < source.length && source[index] !== quote) {
|
||||
if (source[index] === "\\" && index + 1 < source.length) {
|
||||
masked[index] = " ";
|
||||
index += 1;
|
||||
if (source[index] !== "\n") masked[index] = " ";
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (source[index] !== "\n") masked[index] = " ";
|
||||
index += 1;
|
||||
}
|
||||
if (index < source.length) {
|
||||
masked[index] = " ";
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (stopAtOwnCloseBrace && character === "}") {
|
||||
braceDepth -= 1;
|
||||
index += 1;
|
||||
if (braceDepth === 0) return index;
|
||||
continue;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
// Mask a `'`/`"` string body, or the literal-text runs of a backtick
|
||||
// template, with spaces up to and including the matching `quote`. This
|
||||
// keeps `masked` the same length and the same line breaks as `source`, so a
|
||||
// pattern written as plain text inside the literal can never survive into
|
||||
// the result. For a template literal (`isTemplate` is true), a `${` inside
|
||||
// the body hands control to `maskCode` instead, so the interpolation is
|
||||
// scanned as executable code, not masked as text; masking of literal text
|
||||
// resumes on the matching `}` that `maskCode` returns.
|
||||
function maskLiteralBody(source, masked, start, end, quote, isTemplate) {
|
||||
let index = start;
|
||||
while (index < end) {
|
||||
const character = source[index];
|
||||
if (character === quote) {
|
||||
masked[index] = " ";
|
||||
return index + 1;
|
||||
}
|
||||
if (character === "\\" && index + 1 < end) {
|
||||
masked[index] = " ";
|
||||
index += 1;
|
||||
if (source[index] !== "\n") masked[index] = " ";
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (isTemplate && character === "$" && source[index + 1] === "{") {
|
||||
index += 2;
|
||||
index = maskCode(source, masked, index, end, true);
|
||||
continue;
|
||||
}
|
||||
if (character !== "\n") masked[index] = " ";
|
||||
index += 1;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
// Replace every comment and every string- or template-literal body with
|
||||
// spaces, one for one, so the result keeps the same length and the same
|
||||
// line breaks as `source`. A line count taken from the result still lines
|
||||
// up with `source`, and neither a wait-call pattern nor a `timeout` option
|
||||
// written inside a comment or plain literal text can survive into the
|
||||
// result. A wait-call pattern written inside a template's `${...}`
|
||||
// expression is executable code, so it survives masking and is still
|
||||
// scanned, the same as a call written outside any literal.
|
||||
function maskCommentsAndStrings(source) {
|
||||
const masked = source.split("");
|
||||
maskCode(source, masked, 0, source.length, false);
|
||||
return masked.join("");
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user