Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions recipes/err-invalid-callback/README.md
Original file line number Diff line number Diff line change
@@ -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";
```
23 changes: 23 additions & 0 deletions recipes/err-invalid-callback/codemod.yaml
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions recipes/err-invalid-callback/package.json
Original file line number Diff line number Diff line change
@@ -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": "*"
}
}
69 changes: 69 additions & 0 deletions recipes/err-invalid-callback/src/workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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
*
* 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<JS>): 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.
*
* 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 `<lhs> === <quote>ERR_INVALID_ARG_TYPE<quote>` expression,
* then matches `|| <same expression>`. 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',
);
}
7 changes: 7 additions & 0 deletions recipes/err-invalid-callback/tests/expected/file-1.js
Original file line number Diff line number Diff line change
@@ -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");
}
}
6 changes: 6 additions & 0 deletions recipes/err-invalid-callback/tests/expected/file-2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const assert = require("node:assert");

assert.throws(
() => fs.readFile("file.txt", 123),
{ code: "ERR_INVALID_ARG_TYPE" }
);
8 changes: 8 additions & 0 deletions recipes/err-invalid-callback/tests/expected/file-3.js
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions recipes/err-invalid-callback/tests/expected/file-4.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if (err.toString().includes("ERR_INVALID_ARG_TYPE")) {
// Handle callback error
}
9 changes: 9 additions & 0 deletions recipes/err-invalid-callback/tests/expected/file-5.js
Original file line number Diff line number Diff line change
@@ -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
}
}
8 changes: 8 additions & 0 deletions recipes/err-invalid-callback/tests/expected/file-6.js
Original file line number Diff line number Diff line change
@@ -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");
}
}
8 changes: 8 additions & 0 deletions recipes/err-invalid-callback/tests/expected/file-7.js
Original file line number Diff line number Diff line change
@@ -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");
}
}
7 changes: 7 additions & 0 deletions recipes/err-invalid-callback/tests/input/file-1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
try {
fs.readFile("file.txt", "invalid-callback");
} catch (err) {
if (err.code === "ERR_INVALID_CALLBACK") {
console.error("Invalid callback provided");
}
}
6 changes: 6 additions & 0 deletions recipes/err-invalid-callback/tests/input/file-2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const assert = require("node:assert");

assert.throws(
() => fs.readFile("file.txt", 123),
{ code: "ERR_INVALID_CALLBACK" }
);
8 changes: 8 additions & 0 deletions recipes/err-invalid-callback/tests/input/file-3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
switch (error.code) {
case "ERR_INVALID_CALLBACK":
console.log("Invalid callback");
break;
case "ENOENT":
console.log("File not found");
break;
}
3 changes: 3 additions & 0 deletions recipes/err-invalid-callback/tests/input/file-4.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if (err.toString().includes("ERR_INVALID_CALLBACK")) {
// Handle callback error
}
10 changes: 10 additions & 0 deletions recipes/err-invalid-callback/tests/input/file-5.js
Original file line number Diff line number Diff line change
@@ -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
}
}
8 changes: 8 additions & 0 deletions recipes/err-invalid-callback/tests/input/file-6.js
Original file line number Diff line number Diff line change
@@ -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");
}
}
8 changes: 8 additions & 0 deletions recipes/err-invalid-callback/tests/input/file-7.js
Original file line number Diff line number Diff line change
@@ -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");
}
}
27 changes: 27 additions & 0 deletions recipes/err-invalid-callback/workflow.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading