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 e368533e0..e4ae99fd6 100644 --- a/packages/agent-bundle/tests/check-test-remove-tree.test.ts +++ b/packages/agent-bundle/tests/check-test-remove-tree.test.ts @@ -418,3 +418,24 @@ it('flags promises-namespace and asserted options without maxRetries', () => { `await rm(root, { ${recursiveTrue}, maxRetries: 5 });`, ]))).toEqual([]); }); + +it('unwraps non-null asserted options, alone and nested in other wrappers', () => { + expect(recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + "import * as fs from 'node:fs/promises';", + "import { promises as fsp } from 'node:fs';", + `await rm(root, { ${recursiveTrue} }!);`, + `await fs.rm(root, { ${recursiveTrue} }!);`, + `await fsp.rm(root, { ${recursiveTrue} }!);`, + `await rm(root, ({ ${recursiveTrue} } as const)!);`, + `await rm(root, { ${recursiveTrue} }! satisfies Options);`, + `await rm(root, { ${recursiveTrue} }!);`, + `await rm(root, ({ ${recursiveTrue} }));`, + ]))).toEqual([4, 5, 6, 7, 8, 9, 10].map((line) => expect.objectContaining({ hasRetries: false, line }))); + + expect(bareRecursiveRmFailures('packages/agent-bundle/tests/example.test.ts', sample([ + "import { rm } from 'node:fs/promises';", + `await rm(root, { ${recursiveTrue}, maxRetries: 5 }!);`, + `await rm(root, ({ ${recursiveTrue}, maxRetries: 5 } as const)!);`, + ]))).toEqual([]); +}); diff --git a/scripts/check-test-remove-tree.mjs b/scripts/check-test-remove-tree.mjs index 925cf2ca8..ecfc4f2f5 100644 --- a/scripts/check-test-remove-tree.mjs +++ b/scripts/check-test-remove-tree.mjs @@ -9,7 +9,7 @@ * only real node:fs(/promises) ImportDeclaration bindings count, only Node-bound * call expressions are considered, and `recursive` / `maxRetries` are read from * the second argument's object-literal properties (including quoted keys, - * shorthand `maxRetries`, and Parenthesized / As / Satisfies wrappers). Nested + * shorthand `maxRetries`, and Parenthesized / As / Satisfies / `` / `!` / instantiation wrappers). Nested * objects in the path argument, member calls, comments, strings, regexes, and * template substitutions are handled by the AST rather than text masking. * Named `promises` rebinds from `fs` / `node:fs` count as `.rm` carriers. @@ -125,6 +125,8 @@ const unwrapExpression = (node) => { || ts.isAsExpression(current) || ts.isSatisfiesExpression(current) || ts.isTypeAssertionExpression(current) + || ts.isNonNullExpression(current) + || ts.isExpressionWithTypeArguments(current) ) ) { current = current.expression;