Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
25 changes: 25 additions & 0 deletions recipes/types-is-native-error/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# `types.isNativeError` DEP0197

This recipe transforms the usage of `types.isNativeError` to use the `Error.isError`.

See [DEP0197](https://nodejs.org/api/deprecations.html#DEP0197).

## Example

**Before:**

```js
import { types } from "node:util";

if (types.isNativeError(err)) {
// handle the error
}
```

**After:**

```js
if (Error.isError(err)) {
// handle the error
}
```
22 changes: 22 additions & 0 deletions recipes/types-is-native-error/codemod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
schema_version: "1.0"
name: "@nodejs/types-is-native-error"
version: 1.0.0
description: Handle DEP0197 via transforming `types.isNativeError` to `Error.isError`
author: Bruno Rodrigues
license: MIT
workflow: workflow.yaml
category: migration

targets:
languages:
- javascript
- typescript

keywords:
- transformation
- migration

registry:
access: public
visibility: public

23 changes: 23 additions & 0 deletions recipes/types-is-native-error/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@nodejs/types-is-native-error",
"version": "1.0.0",
"description": "Handle DEP0197 via transforming `types.isNativeError` to `Error.isError`",
"type": "module",
"scripts": {
"test": "npx codemod@next jssg test -l typescript ./src/workflow.ts ./"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nodejs/userland-migrations.git",
"directory": "recipes/rmdirs",
"bugs": "https://github.com/nodejs/userland-migrations/issues"
},
"author": "Bruno Rodrigues",
"homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/types-is-native-error/README.md",
"devDependencies": {
"@codemod.com/jssg-types": "^1.0.3"
},
"dependencies": {
"@nodejs/codemod-utils": "*"
}
}
146 changes: 146 additions & 0 deletions recipes/types-is-native-error/src/workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import type { Edit, Range, SgNode, SgRoot } from "@codemod.com/jssg-types/main";
import type JS from "@codemod.com/jssg-types/langs/javascript";
import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call";
import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement";
import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path";
import { removeLines } from "@nodejs/codemod-utils/ast-grep/remove-lines";
import { removeBinding } from "@nodejs/codemod-utils/ast-grep/remove-binding";

type Binding = {
path: string;
lastPropertyAccess?: string;
propertyAccess?: string;
depth: number;
node: SgNode;
};

/**
* Extracts property access information from a dot-notation path string.
*
* @param path - A dot-notation string representing a property path (e.g., "object.property.subProperty")
* @returns An object containing:
* - `path`: The original path string
* - `lastPropertyAccess`: The last segment of the path (e.g., "subProperty" from "object.property.subProperty")
* - `propertyAccess`: The path without the last segment (e.g., "object.property" from "object.property.subProperty")
* - `depth`: The number of segments in the path
*
* @example
* ```typescript
* removeLastPropertyAccess("foo.bar.baz");
* // Returns: { path: "foo.bar.baz", propertyAccess: "foo.bar", lastPropertyAccess: "baz", depth: 3 }
*
* removeLastPropertyAccess("foo");
* // Returns: { path: "foo", propertyAccess: "", lastPropertyAccess: "foo", depth: 1 }
* ```
*/
function removeLastPropertyAccess(
path: string,
): Pick<Binding, "path" | "lastPropertyAccess" | "propertyAccess" | "depth"> {
const pathArr = path.split(".");

if (!pathArr) {
return {
path,
depth: 1,
};
}

const lastPropertyAccess = pathArr.at(-1);
const propertyAccess = pathArr.slice(0, pathArr.length - 1).join(".");

if (!propertyAccess) {
return {
path,
propertyAccess,
lastPropertyAccess,
depth: pathArr.length,
};
}

return {
path,
propertyAccess,
lastPropertyAccess,
depth: pathArr.length,
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the same O.o

}

/**
* Transforms `util.types.isNativeError` usage to `Error.isError`.
*
* This transformation handles various import/require patterns and usage scenarios:
*
* 1. Identifies all require/import statements from 'node:util' or 'util' module that
* include access to `types.isNativeError`
*
* 2. Replaces all matching code references:
* - `util.types.isNativeError(...)` → `Error.isError(...)`
* - `types.isNativeError(...)` → `Error.isError(...)`
* - `isNativeError(...)` → `Error.isError(...)`
*
* 3. Removes unused bindings when all references to the imported/required
* isNativeError have been replaced
*
*/
export default function transform(root: SgRoot<JS>): string | null {
const rootNode = root.root();
const bindings: Binding[] = [];
const edits: Edit[] = [];
const linesToRemove: Range[] = [];

// @ts-ignore - ast-grep types are not fully compatible with JSSG types
const nodeRequires = getNodeRequireCalls(root, "util");
// @ts-ignore - ast-grep types are not fully compatible with JSSG types
const nodeImports = getNodeImportStatements(root, "util");
const path = "$.types.isNativeError";

for (const stmt of [...nodeRequires, ...nodeImports]) {
const bindToReplace = resolveBindingPath(stmt, path);

if (!bindToReplace) {
continue;
}

bindings.push({
...removeLastPropertyAccess(bindToReplace),
node: stmt,
});
}

for (const binding of bindings) {
const nodes = rootNode.findAll({
rule: {
pattern: `${binding.propertyAccess || binding.path}${binding.depth > 1 ? ".$$$FN" : ""}`,
},
});

const nodesToEdit = rootNode.findAll({
rule: {
pattern: binding.path,
},
});

for (const node of nodesToEdit) {
edits.push(node.replace("Error.isError"));
}

if (nodes.length === nodesToEdit.length) {
const result = removeBinding(
// @ts-ignore - ast-grep types are not fully compatible with JSSG types
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// @ts-ignore - ast-grep types are not fully compatible with JSSG types
// @ts-expect-error - ast-grep types are not fully compatible with JSSG types

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't necessary any more, so removed

binding.node,
binding.path.split(".").at(0),
);

if (result?.edit) {
edits.push(result.edit);
}

if (result?.lineToRemove) {
linesToRemove.push(result.lineToRemove);
}
}
}

const sourceCode = rootNode.commitEdits(edits);
return removeLines(sourceCode, linesToRemove);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const {
types: { isMap },
} = require("util");

if (Error.isError(err)) {
// handle the error
}

if (isMap([])) {
}
4 changes: 4 additions & 0 deletions recipes/types-is-native-error/tests/expected/named-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

if (Error.isError(err)) {
// handle the error
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

if (Error.isError(err)) {
// handle the error
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { types: test } = require("util");

if (Error.isError(err)) {
// handle the error
}

if (test.isMap([])) {
}
13 changes: 13 additions & 0 deletions recipes/types-is-native-error/tests/expected/require-usage.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const err = new Error();

if (Error.isError(err)) {
// handle the error
}

if (Error.isError(err)) {
// handle the error
}

if (Error.isError(err)) {
// handle the error
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { types } = require("util");

if (Error.isError(err)) {
// handle the error
}

if (types.isMap([])) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const {
types: { isNativeError, isMap },
} = require("util");

if (isNativeError(err)) {
// handle the error
}

if (isMap([])) {
}
5 changes: 5 additions & 0 deletions recipes/types-is-native-error/tests/input/named-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { types } from "node:util";

if (types.isNativeError(err)) {
// handle the error
}
5 changes: 5 additions & 0 deletions recipes/types-is-native-error/tests/input/renamed-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { types } from "node:util";

if (types.isNativeError(err)) {
// handle the error
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { types: test } = require("util");

if (test.isNativeError(err)) {
// handle the error
}

if (test.isMap([])) {
}
18 changes: 18 additions & 0 deletions recipes/types-is-native-error/tests/input/require-usage.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const util = require("node:util");
const { types } = require("util");
const {
types: { isNativeError },
} = require("util");
const err = new Error();

if (util.types.isNativeError(err)) {
// handle the error
}

if (types.isNativeError(err)) {
// handle the error
}

if (isNativeError(err)) {
// handle the error
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { types } = require("util");

if (types.isNativeError(err)) {
// handle the error
}

if (types.isMap([])) {
}
23 changes: 23 additions & 0 deletions recipes/types-is-native-error/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"allowImportingTsExtensions": true,
"allowJs": true,
"alwaysStrict": true,
"baseUrl": "./",
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true,
"lib": ["ESNext", "DOM"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noImplicitThis": true,
"removeComments": true,
"strict": true,
"stripInternal": true,
"target": "esnext"
},
"include": ["./"],
"exclude": [
"tests/**"
]
}
26 changes: 26 additions & 0 deletions recipes/types-is-native-error/workflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
version: "1"

nodes:
- id: apply-transforms
name: Apply AST Transformations
type: automatic
runtime:
type: direct
steps:
- name: Handle DEP0197 via transforming `types.isNativeError` to `Error.isError`.
js-ast-grep:
js_file: src/workflow.ts
base_path: .
include:
- "**/*.cjs"
- "**/*.js"
- "**/*.jsx"
- "**/*.mjs"
- "**/*.cts"
- "**/*.mts"
- "**/*.ts"
- "**/*.tsx"
exclude:
- "**/node_modules/**"
language: typescript

Loading
Loading