From 8024eb77d273ab7576502850edb1f45248bb1f27 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 25 Sep 2026 02:37:04 +0000 Subject: [PATCH] fix(test): flag recursive rm in a parameter decorator of a same-named parameter --- .../tests/check-test-remove-tree.test.ts | 30 +++++++++++++++++++ scripts/check-test-remove-tree.mjs | 10 ++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/agent-bundle/tests/check-test-remove-tree.test.ts b/packages/agent-bundle/tests/check-test-remove-tree.test.ts index 4f0d02119..e368533e0 100644 --- a/packages/agent-bundle/tests/check-test-remove-tree.test.ts +++ b/packages/agent-bundle/tests/check-test-remove-tree.test.ts @@ -311,6 +311,36 @@ it('does not let a parameter shadow a computed method key or decorator', () => { ` @hook(rm(root, { ${recursiveTrue} })) run(rm) {}`, '}', ]))).toEqual([expect.objectContaining({ hasRetries: false, line: 3 })]); + + expect(recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + 'class Suite {', + ` run(@hook(rm(root, { ${recursiveTrue} })) rm) {}`, + ` constructor(@hook(rm(root, { ${recursiveTrue} })) rm) {}`, + '}', + ]))).toEqual([ + expect.objectContaining({ hasRetries: false, line: 3 }), + expect.objectContaining({ hasRetries: false, line: 4 }), + ]); + + expect(recursiveRmCalls(sample([ + "import * as fs from 'node:fs/promises';", + `class Suite { run(@hook(fs.rm(root, { ${recursiveTrue} })) fs) {} }`, + ]))).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); +}); + +it('does not let a nested-block var hide a call outside its function', () => { + expect(recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + 'function setup() { if (ready) { var rm = mockRm; } }', + `function cleanup() { rm(root, { ${recursiveTrue} }); }`, + `function reset(done = () => rm(root, { ${recursiveTrue} })) { { var rm = mockRm; } }`, + `class Suite { static { { var rm = mockRm; } } run() { rm(root, { ${recursiveTrue} }); } }`, + ]))).toEqual([ + expect.objectContaining({ hasRetries: false, line: 3 }), + expect.objectContaining({ hasRetries: false, line: 4 }), + expect.objectContaining({ hasRetries: false, line: 5 }), + ]); }); it('parses TSX and JS test files with their own script kind', () => { diff --git a/scripts/check-test-remove-tree.mjs b/scripts/check-test-remove-tree.mjs index 32bebb86c..925cf2ca8 100644 --- a/scripts/check-test-remove-tree.mjs +++ b/scripts/check-test-remove-tree.mjs @@ -154,7 +154,10 @@ const statementDeclares = (statement, name) => { return false; }; -/** `from` is the child the walk came up through; names, computed keys, and decorators sit outside the function scope. */ +/** + * `from` is the child the walk came up through; names, computed keys, and decorators + * (including parameter decorators) sit outside the function scope. + */ const functionLikeDeclares = (node, from, name) => { if ( !( @@ -189,6 +192,11 @@ const identifierIsLocallyShadowed = (identifier) => { let from = identifier; let current = identifier.parent; while (current !== undefined) { + if (ts.isDecorator(current) && ts.isParameter(current.parent)) { + from = current.parent.parent; + current = from.parent; + continue; + } if ( ts.isSourceFile(current) || ts.isBlock(current)