Skip to content

Commit 74ad7bf

Browse files
brunocrohAugustinMauroyJakobJingleheimer
authored
feat(types-is-native-error): introduce (#157)
Co-authored-by: Augustin Mauroy <[email protected]> Co-authored-by: Jacob Smith <[email protected]>
1 parent b470a15 commit 74ad7bf

21 files changed

+409
-8
lines changed

package-lock.json

Lines changed: 17 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# `types.isNativeError` DEP0197
2+
3+
This recipe transforms the usage of `types.isNativeError` to use the `Error.isError`.
4+
5+
See [DEP0197](https://nodejs.org/api/deprecations.html#DEP0197).
6+
7+
## Example
8+
9+
**Before:**
10+
11+
```js
12+
import { types } from "node:util";
13+
14+
if (types.isNativeError(err)) {
15+
// handle the error
16+
}
17+
```
18+
19+
**After:**
20+
21+
```js
22+
if (Error.isError(err)) {
23+
// handle the error
24+
}
25+
```
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
schema_version: "1.0"
2+
name: "@nodejs/types-is-native-error"
3+
version: 1.0.0
4+
description: Handle DEP0197 via transforming `types.isNativeError` to `Error.isError`
5+
author: Bruno Rodrigues
6+
license: MIT
7+
workflow: workflow.yaml
8+
category: migration
9+
10+
targets:
11+
languages:
12+
- javascript
13+
- typescript
14+
15+
keywords:
16+
- transformation
17+
- migration
18+
19+
registry:
20+
access: public
21+
visibility: public
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "@nodejs/types-is-native-error",
3+
"version": "1.0.0",
4+
"description": "Handle DEP0197 via transforming `types.isNativeError` to `Error.isError`",
5+
"type": "module",
6+
"scripts": {
7+
"test": "npx codemod jssg test -l typescript ./src/workflow.ts ./"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/nodejs/userland-migrations.git",
12+
"directory": "recipes/rmdirs",
13+
"bugs": "https://github.com/nodejs/userland-migrations/issues"
14+
},
15+
"author": "Bruno Rodrigues",
16+
"homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/types-is-native-error/README.md",
17+
"devDependencies": {
18+
"@codemod.com/jssg-types": "^1.0.3"
19+
},
20+
"dependencies": {
21+
"@nodejs/codemod-utils": "*"
22+
}
23+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import type { Edit, Range, SgNode, SgRoot } from "@codemod.com/jssg-types/main";
2+
import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call";
3+
import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement";
4+
import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path";
5+
import { removeLines } from "@nodejs/codemod-utils/ast-grep/remove-lines";
6+
import { removeBinding } from "@nodejs/codemod-utils/ast-grep/remove-binding";
7+
8+
type Binding = {
9+
path: string;
10+
lastPropertyAccess?: string;
11+
propertyAccess?: string;
12+
depth: number;
13+
node: SgNode;
14+
};
15+
16+
/**
17+
* Extracts property access information from a dot-notation path string.
18+
*
19+
* @param path - A dot-notation string representing a property path (e.g., "object.property.subProperty")
20+
* @returns An object containing:
21+
* - `path`: The original path string
22+
* - `lastPropertyAccess`: The last segment of the path (e.g., "subProperty" from "object.property.subProperty")
23+
* - `propertyAccess`: The path without the last segment (e.g., "object.property" from "object.property.subProperty")
24+
* - `depth`: The number of segments in the path
25+
*
26+
* @example
27+
* ```typescript
28+
* createPropBinding("foo.bar.baz");
29+
* // Returns: { path: "foo.bar.baz", propertyAccess: "foo.bar", lastPropertyAccess: "baz", depth: 3 }
30+
*
31+
* createPropBinding("foo");
32+
* // Returns: { path: "foo", propertyAccess: "", lastPropertyAccess: "foo", depth: 1 }
33+
* ```
34+
*/
35+
function createPropBinding(
36+
path: string,
37+
): Pick<Binding, "path" | "lastPropertyAccess" | "propertyAccess" | "depth"> {
38+
const pathArr = path.split(".");
39+
40+
if (!pathArr) {
41+
return {
42+
path,
43+
depth: 1,
44+
};
45+
}
46+
47+
const lastPropertyAccess = pathArr.at(-1);
48+
const propertyAccess = pathArr.slice(0, -1).join(".");
49+
50+
return {
51+
path,
52+
propertyAccess,
53+
lastPropertyAccess,
54+
depth: pathArr.length,
55+
};
56+
}
57+
58+
/**
59+
* Transforms `util.types.isNativeError` usage to `Error.isError`.
60+
*
61+
* This transformation handles various import/require patterns and usage scenarios:
62+
*
63+
* 1. Identifies all require/import statements from 'node:util' or 'util' module that
64+
* include access to `types.isNativeError`
65+
*
66+
* 2. Replaces all matching code references:
67+
* - `util.types.isNativeError(...)` → `Error.isError(...)`
68+
* - `types.isNativeError(...)` → `Error.isError(...)`
69+
* - `isNativeError(...)` → `Error.isError(...)`
70+
*
71+
* 3. Removes unused bindings when all references to the imported/required
72+
* isNativeError have been replaced
73+
*
74+
*/
75+
export default function transform(root: SgRoot): string | null {
76+
const rootNode = root.root();
77+
const bindings: Binding[] = [];
78+
const edits: Edit[] = [];
79+
const linesToRemove: Range[] = [];
80+
81+
const nodeRequires = getNodeRequireCalls(root, "util");
82+
const nodeImports = getNodeImportStatements(root, "util");
83+
const path = "$.types.isNativeError";
84+
85+
for (const stmt of [...nodeRequires, ...nodeImports]) {
86+
const bindToReplace = resolveBindingPath(stmt, path);
87+
88+
if (!bindToReplace) {
89+
continue;
90+
}
91+
92+
bindings.push({
93+
...createPropBinding(bindToReplace),
94+
node: stmt,
95+
});
96+
}
97+
98+
for (const binding of bindings) {
99+
const nodes = rootNode.findAll({
100+
rule: {
101+
pattern: `${binding.propertyAccess || binding.path}${binding.depth > 1 ? ".$$$FN" : ""}`,
102+
},
103+
});
104+
105+
const nodesToEdit = rootNode.findAll({
106+
rule: {
107+
pattern: binding.path,
108+
},
109+
});
110+
111+
for (const node of nodesToEdit) {
112+
edits.push(node.replace("Error.isError"));
113+
}
114+
115+
if (nodes.length === nodesToEdit.length) {
116+
const bindToRemove = binding.path.includes(".")
117+
? binding.path.split(".").at(0)!
118+
: binding.path;
119+
120+
const result = removeBinding(binding.node, bindToRemove);
121+
122+
if (result?.edit) {
123+
edits.push(result.edit);
124+
}
125+
126+
if (result?.lineToRemove) {
127+
linesToRemove.push(result.lineToRemove);
128+
}
129+
}
130+
}
131+
132+
const sourceCode = rootNode.commitEdits(edits);
133+
return removeLines(sourceCode, linesToRemove);
134+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const {
2+
types: { isMap },
3+
} = require("util");
4+
5+
if (Error.isError(err)) {
6+
// handle the error
7+
}
8+
9+
if (isMap([])) {
10+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
if (Error.isError(err)) {
3+
// handle the error
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
if (Error.isError(err)) {
3+
// handle the error
4+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const { types: test } = require("util");
2+
3+
if (Error.isError(err)) {
4+
// handle the error
5+
}
6+
7+
if (test.isMap([])) {
8+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const err = new Error();
2+
3+
if (Error.isError(err)) {
4+
// handle the error
5+
}
6+
7+
if (Error.isError(err)) {
8+
// handle the error
9+
}
10+
11+
if (Error.isError(err)) {
12+
// handle the error
13+
}

0 commit comments

Comments
 (0)