From 11f5bec4376422e81a6973adf26705f1b2168e8e Mon Sep 17 00:00:00 2001 From: Yu-Hong Shen Date: Mon, 30 Mar 2026 03:46:21 +0800 Subject: [PATCH 1/3] feat(err-invalid-callback): add codemod for DEP0159 error code replacement Replace deprecated ERR_INVALID_CALLBACK with ERR_INVALID_ARG_TYPE in string literals. Handles error code comparisons, switch cases, test assertions, and string includes checks. Also deduplicates redundant || conditions that arise after replacement. Closes nodejs/userland-migrations#411 Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 15 +++++ recipes/err-invalid-callback/codemod.yaml | 23 +++++++ recipes/err-invalid-callback/package.json | 24 +++++++ recipes/err-invalid-callback/src/workflow.ts | 62 +++++++++++++++++++ .../tests/expected/file-1.js | 7 +++ .../tests/expected/file-2.js | 6 ++ .../tests/expected/file-3.js | 8 +++ .../tests/expected/file-4.js | 3 + .../tests/expected/file-5.js | 9 +++ .../tests/expected/file-6.js | 8 +++ .../tests/input/file-1.js | 7 +++ .../tests/input/file-2.js | 6 ++ .../tests/input/file-3.js | 8 +++ .../tests/input/file-4.js | 3 + .../tests/input/file-5.js | 10 +++ .../tests/input/file-6.js | 8 +++ recipes/err-invalid-callback/workflow.yaml | 25 ++++++++ 17 files changed, 232 insertions(+) create mode 100644 recipes/err-invalid-callback/codemod.yaml create mode 100644 recipes/err-invalid-callback/package.json create mode 100644 recipes/err-invalid-callback/src/workflow.ts create mode 100644 recipes/err-invalid-callback/tests/expected/file-1.js create mode 100644 recipes/err-invalid-callback/tests/expected/file-2.js create mode 100644 recipes/err-invalid-callback/tests/expected/file-3.js create mode 100644 recipes/err-invalid-callback/tests/expected/file-4.js create mode 100644 recipes/err-invalid-callback/tests/expected/file-5.js create mode 100644 recipes/err-invalid-callback/tests/expected/file-6.js create mode 100644 recipes/err-invalid-callback/tests/input/file-1.js create mode 100644 recipes/err-invalid-callback/tests/input/file-2.js create mode 100644 recipes/err-invalid-callback/tests/input/file-3.js create mode 100644 recipes/err-invalid-callback/tests/input/file-4.js create mode 100644 recipes/err-invalid-callback/tests/input/file-5.js create mode 100644 recipes/err-invalid-callback/tests/input/file-6.js create mode 100644 recipes/err-invalid-callback/workflow.yaml diff --git a/package-lock.json b/package-lock.json index 45c99671..3bc45a0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1507,6 +1507,10 @@ "resolved": "recipes/dirent-path-to-parent-path", "link": true }, + "node_modules/@nodejs/err-invalid-callback": { + "resolved": "recipes/err-invalid-callback", + "link": true + }, "node_modules/@nodejs/fs-access-mode-constants": { "resolved": "recipes/fs-access-mode-constants", "link": true @@ -4394,6 +4398,17 @@ "@codemod.com/jssg-types": "^1.5.0" } }, + "recipes/err-invalid-callback": { + "name": "@nodejs/err-invalid-callback", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.5.0" + } + }, "recipes/fs-access-mode-constants": { "name": "@nodejs/fs-access-mode-constants", "version": "1.0.1", diff --git a/recipes/err-invalid-callback/codemod.yaml b/recipes/err-invalid-callback/codemod.yaml new file mode 100644 index 00000000..536681b5 --- /dev/null +++ b/recipes/err-invalid-callback/codemod.yaml @@ -0,0 +1,23 @@ +schema_version: "1.0" +name: "@nodejs/err-invalid-callback" +version: "1.0.0" +description: Handle DEP0159 by replacing ERR_INVALID_CALLBACK with ERR_INVALID_ARG_TYPE. +author: Stanley Shen +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - errors + - DEP0159 + +registry: + access: public + visibility: public diff --git a/recipes/err-invalid-callback/package.json b/recipes/err-invalid-callback/package.json new file mode 100644 index 00000000..1d504c58 --- /dev/null +++ b/recipes/err-invalid-callback/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/err-invalid-callback", + "version": "1.0.0", + "description": "Handle DEP0159 by replacing ERR_INVALID_CALLBACK with ERR_INVALID_ARG_TYPE.", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/err-invalid-callback", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Stanley Shen", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/err-invalid-callback/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.5.0" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/err-invalid-callback/src/workflow.ts b/recipes/err-invalid-callback/src/workflow.ts new file mode 100644 index 00000000..1af028ae --- /dev/null +++ b/recipes/err-invalid-callback/src/workflow.ts @@ -0,0 +1,62 @@ +import type { SgRoot, Edit } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; + +const OLD_CODE = 'ERR_INVALID_CALLBACK'; +const NEW_CODE = 'ERR_INVALID_ARG_TYPE'; + +/** + * Transform function that replaces references to the deprecated + * ERR_INVALID_CALLBACK error code with ERR_INVALID_ARG_TYPE. + * + * See DEP0159: https://nodejs.org/api/deprecations.html#DEP0159 + * + * Handles: + * - String literals: "ERR_INVALID_CALLBACK" → "ERR_INVALID_ARG_TYPE" + * - Both single and double quoted strings + * - Deduplicates redundant checks after replacement (e.g., a === "X" || a === "X") + */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + + // Find all string_fragment nodes containing the old error code + const stringFragments = rootNode.findAll({ + rule: { + kind: 'string_fragment', + regex: OLD_CODE, + }, + }); + + for (const fragment of stringFragments) { + const text = fragment.text(); + const newText = text.replace( + new RegExp(OLD_CODE, 'g'), + NEW_CODE, + ); + if (newText !== text) { + edits.push(fragment.replace(newText)); + } + } + + if (!edits.length) return null; + + let result = rootNode.commitEdits(edits); + + // Post-process: remove duplicate conditions after replacement + // e.g., `err.code === "ERR_INVALID_ARG_TYPE" || \n err.code === "ERR_INVALID_ARG_TYPE"` + // becomes `err.code === "ERR_INVALID_ARG_TYPE"` + result = deduplicateBinaryExpressions(result); + + return result; +} + +/** + * Remove duplicate operands in || expressions that arise from the replacement. + */ +function deduplicateBinaryExpressions(code: string): string { + // Match patterns like: || where both sides reference ERR_INVALID_ARG_TYPE + return code.replace( + /(\S+\s*===\s*["']ERR_INVALID_ARG_TYPE["'])\s*\|\|\s*\n?\s*\1/g, + '$1', + ); +} diff --git a/recipes/err-invalid-callback/tests/expected/file-1.js b/recipes/err-invalid-callback/tests/expected/file-1.js new file mode 100644 index 00000000..755a1ed6 --- /dev/null +++ b/recipes/err-invalid-callback/tests/expected/file-1.js @@ -0,0 +1,7 @@ +try { + fs.readFile("file.txt", "invalid-callback"); +} catch (err) { + if (err.code === "ERR_INVALID_ARG_TYPE") { + console.error("Invalid callback provided"); + } +} diff --git a/recipes/err-invalid-callback/tests/expected/file-2.js b/recipes/err-invalid-callback/tests/expected/file-2.js new file mode 100644 index 00000000..744d9c3d --- /dev/null +++ b/recipes/err-invalid-callback/tests/expected/file-2.js @@ -0,0 +1,6 @@ +const assert = require("node:assert"); + +assert.throws( + () => fs.readFile("file.txt", 123), + { code: "ERR_INVALID_ARG_TYPE" } +); diff --git a/recipes/err-invalid-callback/tests/expected/file-3.js b/recipes/err-invalid-callback/tests/expected/file-3.js new file mode 100644 index 00000000..c6e318e0 --- /dev/null +++ b/recipes/err-invalid-callback/tests/expected/file-3.js @@ -0,0 +1,8 @@ +switch (error.code) { + case "ERR_INVALID_ARG_TYPE": + console.log("Invalid callback"); + break; + case "ENOENT": + console.log("File not found"); + break; +} diff --git a/recipes/err-invalid-callback/tests/expected/file-4.js b/recipes/err-invalid-callback/tests/expected/file-4.js new file mode 100644 index 00000000..458b72bb --- /dev/null +++ b/recipes/err-invalid-callback/tests/expected/file-4.js @@ -0,0 +1,3 @@ +if (err.toString().includes("ERR_INVALID_ARG_TYPE")) { + // Handle callback error +} diff --git a/recipes/err-invalid-callback/tests/expected/file-5.js b/recipes/err-invalid-callback/tests/expected/file-5.js new file mode 100644 index 00000000..7c864f97 --- /dev/null +++ b/recipes/err-invalid-callback/tests/expected/file-5.js @@ -0,0 +1,9 @@ +try { + fs.readFile("file.txt", "invalid-callback"); +} catch (err) { + const isCallbackError = + err.code === "ERR_INVALID_ARG_TYPE"; + if (isCallbackError) { + // Handle invalid callback error + } +} diff --git a/recipes/err-invalid-callback/tests/expected/file-6.js b/recipes/err-invalid-callback/tests/expected/file-6.js new file mode 100644 index 00000000..e8aeb7b3 --- /dev/null +++ b/recipes/err-invalid-callback/tests/expected/file-6.js @@ -0,0 +1,8 @@ +// No ERR_INVALID_CALLBACK references - should not be modified +try { + fs.readFile("file.txt", callback); +} catch (err) { + if (err.code === "ENOENT") { + console.error("File not found"); + } +} diff --git a/recipes/err-invalid-callback/tests/input/file-1.js b/recipes/err-invalid-callback/tests/input/file-1.js new file mode 100644 index 00000000..c7a2e495 --- /dev/null +++ b/recipes/err-invalid-callback/tests/input/file-1.js @@ -0,0 +1,7 @@ +try { + fs.readFile("file.txt", "invalid-callback"); +} catch (err) { + if (err.code === "ERR_INVALID_CALLBACK") { + console.error("Invalid callback provided"); + } +} diff --git a/recipes/err-invalid-callback/tests/input/file-2.js b/recipes/err-invalid-callback/tests/input/file-2.js new file mode 100644 index 00000000..aad5b5d8 --- /dev/null +++ b/recipes/err-invalid-callback/tests/input/file-2.js @@ -0,0 +1,6 @@ +const assert = require("node:assert"); + +assert.throws( + () => fs.readFile("file.txt", 123), + { code: "ERR_INVALID_CALLBACK" } +); diff --git a/recipes/err-invalid-callback/tests/input/file-3.js b/recipes/err-invalid-callback/tests/input/file-3.js new file mode 100644 index 00000000..302e3a32 --- /dev/null +++ b/recipes/err-invalid-callback/tests/input/file-3.js @@ -0,0 +1,8 @@ +switch (error.code) { + case "ERR_INVALID_CALLBACK": + console.log("Invalid callback"); + break; + case "ENOENT": + console.log("File not found"); + break; +} diff --git a/recipes/err-invalid-callback/tests/input/file-4.js b/recipes/err-invalid-callback/tests/input/file-4.js new file mode 100644 index 00000000..befa24e0 --- /dev/null +++ b/recipes/err-invalid-callback/tests/input/file-4.js @@ -0,0 +1,3 @@ +if (err.toString().includes("ERR_INVALID_CALLBACK")) { + // Handle callback error +} diff --git a/recipes/err-invalid-callback/tests/input/file-5.js b/recipes/err-invalid-callback/tests/input/file-5.js new file mode 100644 index 00000000..8b5ffecf --- /dev/null +++ b/recipes/err-invalid-callback/tests/input/file-5.js @@ -0,0 +1,10 @@ +try { + fs.readFile("file.txt", "invalid-callback"); +} catch (err) { + const isCallbackError = + err.code === "ERR_INVALID_CALLBACK" || + err.code === "ERR_INVALID_ARG_TYPE"; + if (isCallbackError) { + // Handle invalid callback error + } +} diff --git a/recipes/err-invalid-callback/tests/input/file-6.js b/recipes/err-invalid-callback/tests/input/file-6.js new file mode 100644 index 00000000..e8aeb7b3 --- /dev/null +++ b/recipes/err-invalid-callback/tests/input/file-6.js @@ -0,0 +1,8 @@ +// No ERR_INVALID_CALLBACK references - should not be modified +try { + fs.readFile("file.txt", callback); +} catch (err) { + if (err.code === "ENOENT") { + console.error("File not found"); + } +} diff --git a/recipes/err-invalid-callback/workflow.yaml b/recipes/err-invalid-callback/workflow.yaml new file mode 100644 index 00000000..0ddba406 --- /dev/null +++ b/recipes/err-invalid-callback/workflow.yaml @@ -0,0 +1,25 @@ +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + runtime: + type: direct + steps: + - name: Replace ERR_INVALID_CALLBACK with ERR_INVALID_ARG_TYPE (DEP0159) + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript From e119afdf22271813f3bb52e17192ebd7df229421 Mon Sep 17 00:00:00 2001 From: Yu-Hong Shen Date: Mon, 30 Mar 2026 04:24:36 +0800 Subject: [PATCH 2/3] fix(err-invalid-callback): improve codebase alignment and robustness - Add missing README.md (required per CONTRIBUTING.md) - Add yaml-language-server schema comment to workflow.yaml - Sort include file patterns alphabetically - Rename Js type alias to JS (codebase convention) - Reorder imports alphabetically (Edit, SgRoot) - Tighten deduplication regex: use [\w.[\]"']+ instead of \S+ - Add single-quote test case (file-7.js) Co-Authored-By: Claude Opus 4.6 (1M context) --- recipes/err-invalid-callback/README.md | 27 +++++++++++++++++++ recipes/err-invalid-callback/src/workflow.ts | 17 ++++++++---- .../tests/expected/file-7.js | 8 ++++++ .../tests/input/file-7.js | 8 ++++++ recipes/err-invalid-callback/workflow.yaml | 6 +++-- 5 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 recipes/err-invalid-callback/README.md create mode 100644 recipes/err-invalid-callback/tests/expected/file-7.js create mode 100644 recipes/err-invalid-callback/tests/input/file-7.js diff --git a/recipes/err-invalid-callback/README.md b/recipes/err-invalid-callback/README.md new file mode 100644 index 00000000..cd6adb98 --- /dev/null +++ b/recipes/err-invalid-callback/README.md @@ -0,0 +1,27 @@ +# DEP0159: `ERR_INVALID_CALLBACK` replaced by `ERR_INVALID_ARG_TYPE` + +This recipe replaces references to the deprecated `ERR_INVALID_CALLBACK` error code with `ERR_INVALID_ARG_TYPE`. + +See [DEP0159](https://nodejs.org/api/deprecations.html#DEP0159). + +## Example + +```diff + try { + fs.readFile("file.txt", "invalid-callback"); + } catch (err) { +- if (err.code === "ERR_INVALID_CALLBACK") { ++ if (err.code === "ERR_INVALID_ARG_TYPE") { + console.error("Invalid callback provided"); + } + } +``` + +Also handles deduplication when both codes were already checked: + +```diff + const isCallbackError = +- err.code === "ERR_INVALID_CALLBACK" || +- err.code === "ERR_INVALID_ARG_TYPE"; ++ err.code === "ERR_INVALID_ARG_TYPE"; +``` diff --git a/recipes/err-invalid-callback/src/workflow.ts b/recipes/err-invalid-callback/src/workflow.ts index 1af028ae..d96061e8 100644 --- a/recipes/err-invalid-callback/src/workflow.ts +++ b/recipes/err-invalid-callback/src/workflow.ts @@ -1,5 +1,5 @@ -import type { SgRoot, Edit } from '@codemod.com/jssg-types/main'; -import type Js from '@codemod.com/jssg-types/langs/javascript'; +import type { Edit, SgRoot } from '@codemod.com/jssg-types/main'; +import type JS from '@codemod.com/jssg-types/langs/javascript'; const OLD_CODE = 'ERR_INVALID_CALLBACK'; const NEW_CODE = 'ERR_INVALID_ARG_TYPE'; @@ -15,7 +15,7 @@ const NEW_CODE = 'ERR_INVALID_ARG_TYPE'; * - Both single and double quoted strings * - Deduplicates redundant checks after replacement (e.g., a === "X" || a === "X") */ -export default function transform(root: SgRoot): string | null { +export default function transform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; @@ -52,11 +52,18 @@ export default function transform(root: SgRoot): string | null { /** * Remove duplicate operands in || expressions that arise from the replacement. + * + * After replacing ERR_INVALID_CALLBACK → ERR_INVALID_ARG_TYPE, code that previously + * checked for both codes (e.g., `a === "ERR_INVALID_CALLBACK" || a === "ERR_INVALID_ARG_TYPE"`) + * will have two identical conditions that should be collapsed into one. + * + * The regex captures a ` === ERR_INVALID_ARG_TYPE` expression, + * then matches `|| `. The lhs is captured with [\w.[\]"']+ to + * support property access patterns like `err.code`, `err["code"]`, and simple identifiers. */ function deduplicateBinaryExpressions(code: string): string { - // Match patterns like: || where both sides reference ERR_INVALID_ARG_TYPE return code.replace( - /(\S+\s*===\s*["']ERR_INVALID_ARG_TYPE["'])\s*\|\|\s*\n?\s*\1/g, + /([\w.[\]"']+\s*===\s*["']ERR_INVALID_ARG_TYPE["'])\s*\|\|\s*\n?\s*\1/g, '$1', ); } diff --git a/recipes/err-invalid-callback/tests/expected/file-7.js b/recipes/err-invalid-callback/tests/expected/file-7.js new file mode 100644 index 00000000..f093c9f2 --- /dev/null +++ b/recipes/err-invalid-callback/tests/expected/file-7.js @@ -0,0 +1,8 @@ +// Single-quoted string usage +try { + fs.readFile("file.txt", "invalid-callback"); +} catch (err) { + if (err.code === 'ERR_INVALID_ARG_TYPE') { + console.error("Invalid callback provided"); + } +} diff --git a/recipes/err-invalid-callback/tests/input/file-7.js b/recipes/err-invalid-callback/tests/input/file-7.js new file mode 100644 index 00000000..19dbfbcd --- /dev/null +++ b/recipes/err-invalid-callback/tests/input/file-7.js @@ -0,0 +1,8 @@ +// Single-quoted string usage +try { + fs.readFile("file.txt", "invalid-callback"); +} catch (err) { + if (err.code === 'ERR_INVALID_CALLBACK') { + console.error("Invalid callback provided"); + } +} diff --git a/recipes/err-invalid-callback/workflow.yaml b/recipes/err-invalid-callback/workflow.yaml index 0ddba406..17427600 100644 --- a/recipes/err-invalid-callback/workflow.yaml +++ b/recipes/err-invalid-callback/workflow.yaml @@ -1,3 +1,5 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json + version: "1" nodes: @@ -12,11 +14,11 @@ nodes: js_file: src/workflow.ts base_path: . include: + - "**/*.cjs" + - "**/*.cts" - "**/*.js" - "**/*.jsx" - "**/*.mjs" - - "**/*.cjs" - - "**/*.cts" - "**/*.mts" - "**/*.ts" - "**/*.tsx" From e45b40a18a80a19eb4941566c32a366e0425ce0a Mon Sep 17 00:00:00 2001 From: Yu-Hong Shen Date: Thu, 2 Apr 2026 01:40:17 +0800 Subject: [PATCH 3/3] fix(err-invalid-callback): use single-file fixtures and reduce false positives Address review feedback: - Migrate tests to single-file fixture format (each case in its own directory with input.ts/output.ts) - Restrict AST matching to error-code contexts only (binary expressions, object pairs, switch cases, string matching calls) to avoid replacing unrelated string usages like console.warn("ERR_INVALID_CALLBACK") - Add false-positive-console-warn test case to verify the fix Co-Authored-By: Claude Opus 4.6 (1M context) --- recipes/err-invalid-callback/src/workflow.ts | 54 ++++++++++++++----- .../expected.ts} | 0 .../input.ts} | 0 .../tests/assert-throws-object/output.ts | 6 +++ .../file-5.js => deduplication/expected.ts} | 0 .../file-5.js => deduplication/input.ts} | 0 .../tests/deduplication/output.ts | 9 ++++ .../false-positive-console-warn/expected.ts | 9 ++++ .../false-positive-console-warn/input.ts | 9 ++++ .../false-positive-console-warn/output.ts | 9 ++++ .../expected.ts} | 0 .../input.ts} | 0 .../tests/if-statement-comparison/output.ts | 7 +++ .../expected.ts} | 0 .../file-6.js => no-match-unchanged/input.ts} | 0 .../tests/no-match-unchanged/output.ts | 8 +++ .../expected.ts} | 0 .../input.ts} | 0 .../tests/single-quoted-string/output.ts | 8 +++ .../expected.ts} | 0 .../input.ts} | 0 .../tests/string-includes-check/output.ts | 3 ++ .../file-3.js => switch-case/expected.ts} | 0 .../{input/file-3.js => switch-case/input.ts} | 0 .../tests/switch-case/output.ts | 8 +++ 25 files changed, 116 insertions(+), 14 deletions(-) rename recipes/err-invalid-callback/tests/{expected/file-2.js => assert-throws-object/expected.ts} (100%) rename recipes/err-invalid-callback/tests/{input/file-2.js => assert-throws-object/input.ts} (100%) create mode 100644 recipes/err-invalid-callback/tests/assert-throws-object/output.ts rename recipes/err-invalid-callback/tests/{expected/file-5.js => deduplication/expected.ts} (100%) rename recipes/err-invalid-callback/tests/{input/file-5.js => deduplication/input.ts} (100%) create mode 100644 recipes/err-invalid-callback/tests/deduplication/output.ts create mode 100644 recipes/err-invalid-callback/tests/false-positive-console-warn/expected.ts create mode 100644 recipes/err-invalid-callback/tests/false-positive-console-warn/input.ts create mode 100644 recipes/err-invalid-callback/tests/false-positive-console-warn/output.ts rename recipes/err-invalid-callback/tests/{expected/file-1.js => if-statement-comparison/expected.ts} (100%) rename recipes/err-invalid-callback/tests/{input/file-1.js => if-statement-comparison/input.ts} (100%) create mode 100644 recipes/err-invalid-callback/tests/if-statement-comparison/output.ts rename recipes/err-invalid-callback/tests/{expected/file-6.js => no-match-unchanged/expected.ts} (100%) rename recipes/err-invalid-callback/tests/{input/file-6.js => no-match-unchanged/input.ts} (100%) create mode 100644 recipes/err-invalid-callback/tests/no-match-unchanged/output.ts rename recipes/err-invalid-callback/tests/{expected/file-7.js => single-quoted-string/expected.ts} (100%) rename recipes/err-invalid-callback/tests/{input/file-7.js => single-quoted-string/input.ts} (100%) create mode 100644 recipes/err-invalid-callback/tests/single-quoted-string/output.ts rename recipes/err-invalid-callback/tests/{expected/file-4.js => string-includes-check/expected.ts} (100%) rename recipes/err-invalid-callback/tests/{input/file-4.js => string-includes-check/input.ts} (100%) create mode 100644 recipes/err-invalid-callback/tests/string-includes-check/output.ts rename recipes/err-invalid-callback/tests/{expected/file-3.js => switch-case/expected.ts} (100%) rename recipes/err-invalid-callback/tests/{input/file-3.js => switch-case/input.ts} (100%) create mode 100644 recipes/err-invalid-callback/tests/switch-case/output.ts diff --git a/recipes/err-invalid-callback/src/workflow.ts b/recipes/err-invalid-callback/src/workflow.ts index d96061e8..86b492b9 100644 --- a/recipes/err-invalid-callback/src/workflow.ts +++ b/recipes/err-invalid-callback/src/workflow.ts @@ -10,32 +10,58 @@ const NEW_CODE = 'ERR_INVALID_ARG_TYPE'; * * See DEP0159: https://nodejs.org/api/deprecations.html#DEP0159 * - * Handles: - * - String literals: "ERR_INVALID_CALLBACK" → "ERR_INVALID_ARG_TYPE" - * - Both single and double quoted strings - * - Deduplicates redundant checks after replacement (e.g., a === "X" || a === "X") + * Only matches string literals in error-code-related contexts: + * - Binary comparisons: err.code === "ERR_INVALID_CALLBACK" + * - Object properties: { code: "ERR_INVALID_CALLBACK" } + * - Switch cases: case "ERR_INVALID_CALLBACK": + * - String matching calls: .includes("ERR_INVALID_CALLBACK") + * + * Does NOT match strings used in non-error-code contexts such as + * console.warn("ERR_INVALID_CALLBACK") or throw new Error("ERR_INVALID_CALLBACK"). + * + * Deduplicates redundant checks after replacement (e.g., a === "X" || a === "X"). */ export default function transform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; - // Find all string_fragment nodes containing the old error code + // Match exact string fragments only in error-code-related AST contexts const stringFragments = rootNode.findAll({ rule: { kind: 'string_fragment', - regex: OLD_CODE, + regex: `^${OLD_CODE}$`, + inside: { + kind: 'string', + any: [ + // err.code === "ERR_INVALID_CALLBACK" + { inside: { kind: 'binary_expression' } }, + // { code: "ERR_INVALID_CALLBACK" } + { inside: { kind: 'pair' } }, + // case "ERR_INVALID_CALLBACK": + { inside: { kind: 'switch_case' } }, + // .includes("ERR_INVALID_CALLBACK"), .indexOf("ERR_INVALID_CALLBACK"), etc. + { + inside: { + kind: 'arguments', + inside: { + kind: 'call_expression', + has: { + kind: 'member_expression', + has: { + kind: 'property_identifier', + regex: '^(includes|indexOf|match|test|startsWith|endsWith)$', + }, + }, + }, + }, + }, + ], + }, }, }); for (const fragment of stringFragments) { - const text = fragment.text(); - const newText = text.replace( - new RegExp(OLD_CODE, 'g'), - NEW_CODE, - ); - if (newText !== text) { - edits.push(fragment.replace(newText)); - } + edits.push(fragment.replace(NEW_CODE)); } if (!edits.length) return null; diff --git a/recipes/err-invalid-callback/tests/expected/file-2.js b/recipes/err-invalid-callback/tests/assert-throws-object/expected.ts similarity index 100% rename from recipes/err-invalid-callback/tests/expected/file-2.js rename to recipes/err-invalid-callback/tests/assert-throws-object/expected.ts diff --git a/recipes/err-invalid-callback/tests/input/file-2.js b/recipes/err-invalid-callback/tests/assert-throws-object/input.ts similarity index 100% rename from recipes/err-invalid-callback/tests/input/file-2.js rename to recipes/err-invalid-callback/tests/assert-throws-object/input.ts diff --git a/recipes/err-invalid-callback/tests/assert-throws-object/output.ts b/recipes/err-invalid-callback/tests/assert-throws-object/output.ts new file mode 100644 index 00000000..744d9c3d --- /dev/null +++ b/recipes/err-invalid-callback/tests/assert-throws-object/output.ts @@ -0,0 +1,6 @@ +const assert = require("node:assert"); + +assert.throws( + () => fs.readFile("file.txt", 123), + { code: "ERR_INVALID_ARG_TYPE" } +); diff --git a/recipes/err-invalid-callback/tests/expected/file-5.js b/recipes/err-invalid-callback/tests/deduplication/expected.ts similarity index 100% rename from recipes/err-invalid-callback/tests/expected/file-5.js rename to recipes/err-invalid-callback/tests/deduplication/expected.ts diff --git a/recipes/err-invalid-callback/tests/input/file-5.js b/recipes/err-invalid-callback/tests/deduplication/input.ts similarity index 100% rename from recipes/err-invalid-callback/tests/input/file-5.js rename to recipes/err-invalid-callback/tests/deduplication/input.ts diff --git a/recipes/err-invalid-callback/tests/deduplication/output.ts b/recipes/err-invalid-callback/tests/deduplication/output.ts new file mode 100644 index 00000000..7c864f97 --- /dev/null +++ b/recipes/err-invalid-callback/tests/deduplication/output.ts @@ -0,0 +1,9 @@ +try { + fs.readFile("file.txt", "invalid-callback"); +} catch (err) { + const isCallbackError = + err.code === "ERR_INVALID_ARG_TYPE"; + if (isCallbackError) { + // Handle invalid callback error + } +} diff --git a/recipes/err-invalid-callback/tests/false-positive-console-warn/expected.ts b/recipes/err-invalid-callback/tests/false-positive-console-warn/expected.ts new file mode 100644 index 00000000..2b7d846f --- /dev/null +++ b/recipes/err-invalid-callback/tests/false-positive-console-warn/expected.ts @@ -0,0 +1,9 @@ +const myUtility = (file) => { + // do something + + file.method(); + + if (file.empty) { + console.warn("ERR_INVALID_CALLBACK"); + } +}; diff --git a/recipes/err-invalid-callback/tests/false-positive-console-warn/input.ts b/recipes/err-invalid-callback/tests/false-positive-console-warn/input.ts new file mode 100644 index 00000000..2b7d846f --- /dev/null +++ b/recipes/err-invalid-callback/tests/false-positive-console-warn/input.ts @@ -0,0 +1,9 @@ +const myUtility = (file) => { + // do something + + file.method(); + + if (file.empty) { + console.warn("ERR_INVALID_CALLBACK"); + } +}; diff --git a/recipes/err-invalid-callback/tests/false-positive-console-warn/output.ts b/recipes/err-invalid-callback/tests/false-positive-console-warn/output.ts new file mode 100644 index 00000000..2b7d846f --- /dev/null +++ b/recipes/err-invalid-callback/tests/false-positive-console-warn/output.ts @@ -0,0 +1,9 @@ +const myUtility = (file) => { + // do something + + file.method(); + + if (file.empty) { + console.warn("ERR_INVALID_CALLBACK"); + } +}; diff --git a/recipes/err-invalid-callback/tests/expected/file-1.js b/recipes/err-invalid-callback/tests/if-statement-comparison/expected.ts similarity index 100% rename from recipes/err-invalid-callback/tests/expected/file-1.js rename to recipes/err-invalid-callback/tests/if-statement-comparison/expected.ts diff --git a/recipes/err-invalid-callback/tests/input/file-1.js b/recipes/err-invalid-callback/tests/if-statement-comparison/input.ts similarity index 100% rename from recipes/err-invalid-callback/tests/input/file-1.js rename to recipes/err-invalid-callback/tests/if-statement-comparison/input.ts diff --git a/recipes/err-invalid-callback/tests/if-statement-comparison/output.ts b/recipes/err-invalid-callback/tests/if-statement-comparison/output.ts new file mode 100644 index 00000000..755a1ed6 --- /dev/null +++ b/recipes/err-invalid-callback/tests/if-statement-comparison/output.ts @@ -0,0 +1,7 @@ +try { + fs.readFile("file.txt", "invalid-callback"); +} catch (err) { + if (err.code === "ERR_INVALID_ARG_TYPE") { + console.error("Invalid callback provided"); + } +} diff --git a/recipes/err-invalid-callback/tests/expected/file-6.js b/recipes/err-invalid-callback/tests/no-match-unchanged/expected.ts similarity index 100% rename from recipes/err-invalid-callback/tests/expected/file-6.js rename to recipes/err-invalid-callback/tests/no-match-unchanged/expected.ts diff --git a/recipes/err-invalid-callback/tests/input/file-6.js b/recipes/err-invalid-callback/tests/no-match-unchanged/input.ts similarity index 100% rename from recipes/err-invalid-callback/tests/input/file-6.js rename to recipes/err-invalid-callback/tests/no-match-unchanged/input.ts diff --git a/recipes/err-invalid-callback/tests/no-match-unchanged/output.ts b/recipes/err-invalid-callback/tests/no-match-unchanged/output.ts new file mode 100644 index 00000000..e8aeb7b3 --- /dev/null +++ b/recipes/err-invalid-callback/tests/no-match-unchanged/output.ts @@ -0,0 +1,8 @@ +// No ERR_INVALID_CALLBACK references - should not be modified +try { + fs.readFile("file.txt", callback); +} catch (err) { + if (err.code === "ENOENT") { + console.error("File not found"); + } +} diff --git a/recipes/err-invalid-callback/tests/expected/file-7.js b/recipes/err-invalid-callback/tests/single-quoted-string/expected.ts similarity index 100% rename from recipes/err-invalid-callback/tests/expected/file-7.js rename to recipes/err-invalid-callback/tests/single-quoted-string/expected.ts diff --git a/recipes/err-invalid-callback/tests/input/file-7.js b/recipes/err-invalid-callback/tests/single-quoted-string/input.ts similarity index 100% rename from recipes/err-invalid-callback/tests/input/file-7.js rename to recipes/err-invalid-callback/tests/single-quoted-string/input.ts diff --git a/recipes/err-invalid-callback/tests/single-quoted-string/output.ts b/recipes/err-invalid-callback/tests/single-quoted-string/output.ts new file mode 100644 index 00000000..f093c9f2 --- /dev/null +++ b/recipes/err-invalid-callback/tests/single-quoted-string/output.ts @@ -0,0 +1,8 @@ +// Single-quoted string usage +try { + fs.readFile("file.txt", "invalid-callback"); +} catch (err) { + if (err.code === 'ERR_INVALID_ARG_TYPE') { + console.error("Invalid callback provided"); + } +} diff --git a/recipes/err-invalid-callback/tests/expected/file-4.js b/recipes/err-invalid-callback/tests/string-includes-check/expected.ts similarity index 100% rename from recipes/err-invalid-callback/tests/expected/file-4.js rename to recipes/err-invalid-callback/tests/string-includes-check/expected.ts diff --git a/recipes/err-invalid-callback/tests/input/file-4.js b/recipes/err-invalid-callback/tests/string-includes-check/input.ts similarity index 100% rename from recipes/err-invalid-callback/tests/input/file-4.js rename to recipes/err-invalid-callback/tests/string-includes-check/input.ts diff --git a/recipes/err-invalid-callback/tests/string-includes-check/output.ts b/recipes/err-invalid-callback/tests/string-includes-check/output.ts new file mode 100644 index 00000000..458b72bb --- /dev/null +++ b/recipes/err-invalid-callback/tests/string-includes-check/output.ts @@ -0,0 +1,3 @@ +if (err.toString().includes("ERR_INVALID_ARG_TYPE")) { + // Handle callback error +} diff --git a/recipes/err-invalid-callback/tests/expected/file-3.js b/recipes/err-invalid-callback/tests/switch-case/expected.ts similarity index 100% rename from recipes/err-invalid-callback/tests/expected/file-3.js rename to recipes/err-invalid-callback/tests/switch-case/expected.ts diff --git a/recipes/err-invalid-callback/tests/input/file-3.js b/recipes/err-invalid-callback/tests/switch-case/input.ts similarity index 100% rename from recipes/err-invalid-callback/tests/input/file-3.js rename to recipes/err-invalid-callback/tests/switch-case/input.ts diff --git a/recipes/err-invalid-callback/tests/switch-case/output.ts b/recipes/err-invalid-callback/tests/switch-case/output.ts new file mode 100644 index 00000000..c6e318e0 --- /dev/null +++ b/recipes/err-invalid-callback/tests/switch-case/output.ts @@ -0,0 +1,8 @@ +switch (error.code) { + case "ERR_INVALID_ARG_TYPE": + console.log("Invalid callback"); + break; + case "ENOENT": + console.log("File not found"); + break; +}