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/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/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..86b492b9 --- /dev/null +++ b/recipes/err-invalid-callback/src/workflow.ts @@ -0,0 +1,95 @@ +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'; + +/** + * 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 + * + * 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[] = []; + + // Match exact string fragments only in error-code-related AST contexts + const stringFragments = rootNode.findAll({ + rule: { + kind: 'string_fragment', + 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) { + edits.push(fragment.replace(NEW_CODE)); + } + + 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. + * + * 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 { + return code.replace( + /([\w.[\]"']+\s*===\s*["']ERR_INVALID_ARG_TYPE["'])\s*\|\|\s*\n?\s*\1/g, + '$1', + ); +} diff --git a/recipes/err-invalid-callback/tests/assert-throws-object/expected.ts b/recipes/err-invalid-callback/tests/assert-throws-object/expected.ts new file mode 100644 index 00000000..744d9c3d --- /dev/null +++ b/recipes/err-invalid-callback/tests/assert-throws-object/expected.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/assert-throws-object/input.ts b/recipes/err-invalid-callback/tests/assert-throws-object/input.ts new file mode 100644 index 00000000..aad5b5d8 --- /dev/null +++ b/recipes/err-invalid-callback/tests/assert-throws-object/input.ts @@ -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/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/deduplication/expected.ts b/recipes/err-invalid-callback/tests/deduplication/expected.ts new file mode 100644 index 00000000..7c864f97 --- /dev/null +++ b/recipes/err-invalid-callback/tests/deduplication/expected.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/deduplication/input.ts b/recipes/err-invalid-callback/tests/deduplication/input.ts new file mode 100644 index 00000000..8b5ffecf --- /dev/null +++ b/recipes/err-invalid-callback/tests/deduplication/input.ts @@ -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/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/if-statement-comparison/expected.ts b/recipes/err-invalid-callback/tests/if-statement-comparison/expected.ts new file mode 100644 index 00000000..755a1ed6 --- /dev/null +++ b/recipes/err-invalid-callback/tests/if-statement-comparison/expected.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/if-statement-comparison/input.ts b/recipes/err-invalid-callback/tests/if-statement-comparison/input.ts new file mode 100644 index 00000000..c7a2e495 --- /dev/null +++ b/recipes/err-invalid-callback/tests/if-statement-comparison/input.ts @@ -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/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/no-match-unchanged/expected.ts b/recipes/err-invalid-callback/tests/no-match-unchanged/expected.ts new file mode 100644 index 00000000..e8aeb7b3 --- /dev/null +++ b/recipes/err-invalid-callback/tests/no-match-unchanged/expected.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/no-match-unchanged/input.ts b/recipes/err-invalid-callback/tests/no-match-unchanged/input.ts new file mode 100644 index 00000000..e8aeb7b3 --- /dev/null +++ b/recipes/err-invalid-callback/tests/no-match-unchanged/input.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/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/single-quoted-string/expected.ts b/recipes/err-invalid-callback/tests/single-quoted-string/expected.ts new file mode 100644 index 00000000..f093c9f2 --- /dev/null +++ b/recipes/err-invalid-callback/tests/single-quoted-string/expected.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/single-quoted-string/input.ts b/recipes/err-invalid-callback/tests/single-quoted-string/input.ts new file mode 100644 index 00000000..19dbfbcd --- /dev/null +++ b/recipes/err-invalid-callback/tests/single-quoted-string/input.ts @@ -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/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/string-includes-check/expected.ts b/recipes/err-invalid-callback/tests/string-includes-check/expected.ts new file mode 100644 index 00000000..458b72bb --- /dev/null +++ b/recipes/err-invalid-callback/tests/string-includes-check/expected.ts @@ -0,0 +1,3 @@ +if (err.toString().includes("ERR_INVALID_ARG_TYPE")) { + // Handle callback error +} diff --git a/recipes/err-invalid-callback/tests/string-includes-check/input.ts b/recipes/err-invalid-callback/tests/string-includes-check/input.ts new file mode 100644 index 00000000..befa24e0 --- /dev/null +++ b/recipes/err-invalid-callback/tests/string-includes-check/input.ts @@ -0,0 +1,3 @@ +if (err.toString().includes("ERR_INVALID_CALLBACK")) { + // Handle callback error +} 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/switch-case/expected.ts b/recipes/err-invalid-callback/tests/switch-case/expected.ts new file mode 100644 index 00000000..c6e318e0 --- /dev/null +++ b/recipes/err-invalid-callback/tests/switch-case/expected.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; +} diff --git a/recipes/err-invalid-callback/tests/switch-case/input.ts b/recipes/err-invalid-callback/tests/switch-case/input.ts new file mode 100644 index 00000000..302e3a32 --- /dev/null +++ b/recipes/err-invalid-callback/tests/switch-case/input.ts @@ -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/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; +} diff --git a/recipes/err-invalid-callback/workflow.yaml b/recipes/err-invalid-callback/workflow.yaml new file mode 100644 index 00000000..17427600 --- /dev/null +++ b/recipes/err-invalid-callback/workflow.yaml @@ -0,0 +1,27 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json + +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: + - "**/*.cjs" + - "**/*.cts" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript