diff --git a/package-lock.json b/package-lock.json index 6b046abb..99501226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1432,6 +1432,10 @@ "resolved": "recipes/import-assertions-to-attributes", "link": true }, + "node_modules/@nodejs/process-assert-to-assert": { + "resolved": "recipes/process-assert-to-assert", + "link": true + }, "node_modules/@nodejs/process-main-module": { "resolved": "recipes/process-main-module", "link": true @@ -4118,6 +4122,17 @@ "@codemod.com/jssg-types": "^1.0.3" } }, + "recipes/process-assert-to-assert": { + "name": "@nodejs/process-assert-to-assert", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.3" + } + }, "recipes/process-main-module": { "name": "@nodejs/process-main-module", "version": "1.0.1", diff --git a/recipes/process-assert-to-assert/README.md b/recipes/process-assert-to-assert/README.md new file mode 100644 index 00000000..7fb6e9b8 --- /dev/null +++ b/recipes/process-assert-to-assert/README.md @@ -0,0 +1,20 @@ +# `process.assert` DEP0100 + +This recipe transforms the usage of `process.assert` to use `assert` module. + +See [DEP0100](https://github.com/nodejs/userland-migrations/issues/197). + +## Example + +**Before:** + +```js +process.assert(condition, "Assertion failed"); +``` + +**After:** + +```js +import assert from "node:assert"; +assert(condition, "Assertion failed"); +``` diff --git a/recipes/process-assert-to-assert/codemod.yaml b/recipes/process-assert-to-assert/codemod.yaml new file mode 100644 index 00000000..abd4f604 --- /dev/null +++ b/recipes/process-assert-to-assert/codemod.yaml @@ -0,0 +1,21 @@ +schema_version: "1.0" +name: "@nodejs/process-assert-to-assert" +version: 1.0.0 +description: Handle DEP0100 via transforming `process.assert` to `assert`. +author: matheusmorett2 +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + +registry: + access: public + visibility: public diff --git a/recipes/process-assert-to-assert/package.json b/recipes/process-assert-to-assert/package.json new file mode 100644 index 00000000..6c773436 --- /dev/null +++ b/recipes/process-assert-to-assert/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/process-assert-to-assert", + "version": "1.0.0", + "description": "Handle DEP0100 via transforming `process.assert` to `assert`.", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript --ignore-whitespace ./src/workflow.ts ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/process-assert-to-assert", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "matheusmorett2", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/process-assert-to-assert/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.3" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} \ No newline at end of file diff --git a/recipes/process-assert-to-assert/src/workflow.ts b/recipes/process-assert-to-assert/src/workflow.ts new file mode 100644 index 00000000..16d39a1b --- /dev/null +++ b/recipes/process-assert-to-assert/src/workflow.ts @@ -0,0 +1,183 @@ +import type { Edit, Range, Rule, SgNode, SgRoot } from "@codemod.com/jssg-types/main"; +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 { removeBinding } from "@nodejs/codemod-utils/ast-grep/remove-binding"; +import { removeLines } from "@nodejs/codemod-utils/ast-grep/remove-lines"; +import { EOL } from "node:os"; + +/** + * Transforms deprecated `process.assert` usage to the standard `assert` module. + * + * Transformations: + * 1. Replaces all `process.assert` references with `assert` + * 2. Adds the necessary import/require statement if not already present: + * - For ESM or files without require calls: adds `import assert from "node:assert"` + * - For CommonJS (.cjs files or files using require): adds `const assert = require("node:assert")` + * 3. Removes process import/require if it was only used for assert + * + * Examples: + * + * **Before**: + * ```js + * import process from "node:process"; + * process.assert(value); + * process.assert.strictEqual(a, b); + * ``` + * + * **After**: + * ```js + * import assert from "node:assert"; + * assert(value); + * assert.strictEqual(a, b); + * ``` + */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + const linesToRemove: Range[] = []; + const replaceRules: Array<{ + importNode?: SgNode; + binding?: string; + rule: Rule; + replaceWith?: string; + }> = [{ + rule: { + kind: 'member_expression', + pattern: "process.assert", + }, + replaceWith: "assert" + }]; + + const processImportsToRemove = new Set(); + + function processImports(moduleName: "process" | "node:process") { + const requireCalls = getNodeRequireCalls(root, moduleName); + const importStatements = getNodeImportStatements(root, moduleName); + const allImports = [...requireCalls, ...importStatements]; + + for (const processImport of allImports) { + const binding = resolveBindingPath(processImport, "$.assert"); + replaceRules.push({ + importNode: processImport, + binding, + rule: { + kind: "identifier", + regex: binding, + inside: { + kind: 'call_expression', + } + }, + replaceWith: "assert" + }); + + if (binding) { + replaceRules.push({ + importNode: processImport, + binding, + rule: { + kind: "member_expression", + has: { + kind: "identifier", + regex: `^${binding}$`, + field: "object" + } + }, + replaceWith: "assert" + }); + } + + const processUsages = rootNode.findAll({ + rule: { + kind: 'member_expression', + has: { + kind: 'identifier', + regex: '^process$' + } + } + }); + + let hasNonAssertUsage = false; + for (const usage of processUsages) { + const propertyNode = usage.field("property"); + if (propertyNode && propertyNode.text() !== "assert") { + hasNonAssertUsage = true; + break; + } + } + + if (!hasNonAssertUsage && processUsages.length > 0) { + processImportsToRemove.add(processImport); + linesToRemove.push(processImport.range()); + } + } + } + + processImports("process"); + processImports("node:process"); + + for (const replaceRule of replaceRules) { + const nodes = rootNode.findAll({ + rule: replaceRule.rule + }); + + for (const node of nodes) { + if (replaceRule.importNode) { + if (!processImportsToRemove.has(replaceRule.importNode)) { + const removeBind = removeBinding(replaceRule.importNode, replaceRule.binding); + + if (removeBind.edit) { + edits.push(removeBind.edit); + } + + if (removeBind.lineToRemove) { + linesToRemove.push(removeBind.lineToRemove); + } + } + } + + if (replaceRule.rule.kind === "member_expression" && replaceRule.binding) { + const objectNode = node.field("object"); + if (objectNode) { + edits.push(objectNode.replace("assert")); + } + } else { + const replaceText = replaceRule.replaceWith || "assert"; + edits.push(node.replace(replaceText)); + } + } + } + + let sourceCode = rootNode.commitEdits(edits); + sourceCode = removeLines(sourceCode, linesToRemove); + + if (edits.length === 0 && linesToRemove) { + return sourceCode; + } + + const alreadyRequiringAssert = getNodeRequireCalls(root, "assert"); + const alreadyImportingAssert = getNodeImportStatements(root, "assert"); + + if (!alreadyRequiringAssert.length && !alreadyImportingAssert.length) { + const usingRequire = rootNode.find({ + rule: { + kind: 'call_expression', + has: { + kind: 'identifier', + field: 'function', + regex: 'require' + } + } + }); + + const isCommonJs = root.filename().includes('.cjs'); + + if (Boolean(usingRequire) || isCommonJs) { + return `const assert = require("node:assert");${EOL}${sourceCode}`; + } + + return `import assert from "node:assert";${EOL}${sourceCode}`; + } + + return sourceCode; +} \ No newline at end of file diff --git a/recipes/process-assert-to-assert/tests/expected/01-basic-global-process-assert.js b/recipes/process-assert-to-assert/tests/expected/01-basic-global-process-assert.js new file mode 100644 index 00000000..08f8977f --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/01-basic-global-process-assert.js @@ -0,0 +1,3 @@ +import assert from "node:assert"; +assert(condition, "Basic assertion"); +assert.strictEqual(a, b, "Values should be equal"); diff --git a/recipes/process-assert-to-assert/tests/expected/02-basic-commonjs.cjs b/recipes/process-assert-to-assert/tests/expected/02-basic-commonjs.cjs new file mode 100644 index 00000000..3ae76b87 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/02-basic-commonjs.cjs @@ -0,0 +1,3 @@ +const assert = require("node:assert"); +assert(condition, "Basic assertion"); +assert.strictEqual(a, b, "Values should be equal"); diff --git a/recipes/process-assert-to-assert/tests/expected/03-esm-import-process.mjs b/recipes/process-assert-to-assert/tests/expected/03-esm-import-process.mjs new file mode 100644 index 00000000..18f54fdb --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/03-esm-import-process.mjs @@ -0,0 +1,3 @@ +import assert from "node:assert"; +assert(value, "Process assertion"); +assert.strictEqual(obj1, obj2); diff --git a/recipes/process-assert-to-assert/tests/expected/04-mixed-process-usage.js b/recipes/process-assert-to-assert/tests/expected/04-mixed-process-usage.js new file mode 100644 index 00000000..4d46b15f --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/04-mixed-process-usage.js @@ -0,0 +1,5 @@ +import assert from "node:assert"; +import process from "node:process"; +assert(value, "Process assertion"); +process.env.NODE_ENV = "test"; +console.log(process.pid); diff --git a/recipes/process-assert-to-assert/tests/expected/05-require-process.js b/recipes/process-assert-to-assert/tests/expected/05-require-process.js new file mode 100644 index 00000000..e54c017d --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/05-require-process.js @@ -0,0 +1,3 @@ +const assert = require("node:assert"); +assert(value, "Process assertion"); +assert.throws(() => { throw new Error(); }); diff --git a/recipes/process-assert-to-assert/tests/expected/06-destructured-assert-only.js b/recipes/process-assert-to-assert/tests/expected/06-destructured-assert-only.js new file mode 100644 index 00000000..24569401 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/06-destructured-assert-only.js @@ -0,0 +1,3 @@ +import assert from "node:assert"; +assert(condition, "Assertion from destructured import"); +assert.throws(() => { throw new Error("test"); }); diff --git a/recipes/process-assert-to-assert/tests/expected/07-destructured-mixed-bindings.js b/recipes/process-assert-to-assert/tests/expected/07-destructured-mixed-bindings.js new file mode 100644 index 00000000..0aff0c40 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/07-destructured-mixed-bindings.js @@ -0,0 +1,4 @@ +import assert from "node:assert"; +import { env } from "node:process"; +assert(value, "Using destructured assert"); +console.log(env.NODE_ENV); diff --git a/recipes/process-assert-to-assert/tests/expected/08-aliased-destructured-assert.js b/recipes/process-assert-to-assert/tests/expected/08-aliased-destructured-assert.js new file mode 100644 index 00000000..cf8634e6 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/08-aliased-destructured-assert.js @@ -0,0 +1,3 @@ +import assert from "node:assert"; +assert(value, "Using aliased assert"); +assert.notStrictEqual(a, b); diff --git a/recipes/process-assert-to-assert/tests/expected/09-aliased-destructured-mixed.js b/recipes/process-assert-to-assert/tests/expected/09-aliased-destructured-mixed.js new file mode 100644 index 00000000..e1b0edd5 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/09-aliased-destructured-mixed.js @@ -0,0 +1,5 @@ +import assert from "node:assert"; +import { env } from "node:process"; +assert(value, "Using aliased assert"); +assert.strictEqual(a, b); +console.log(env.NODE_ENV); diff --git a/recipes/process-assert-to-assert/tests/expected/10-require-destructured.js b/recipes/process-assert-to-assert/tests/expected/10-require-destructured.js new file mode 100644 index 00000000..78b4324f --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/10-require-destructured.js @@ -0,0 +1,3 @@ +const assert = require("node:assert"); +assert(value, "Destructured assert from require"); +assert.strictEqual(a, b, "Should be equal"); diff --git a/recipes/process-assert-to-assert/tests/expected/11-already-has-assert-import.js b/recipes/process-assert-to-assert/tests/expected/11-already-has-assert-import.js new file mode 100644 index 00000000..db459c1a --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/11-already-has-assert-import.js @@ -0,0 +1,3 @@ +import assert from "node:assert"; +assert(value, "This should be transformed"); +assert.strictEqual(a, b, "This should remain unchanged"); diff --git a/recipes/process-assert-to-assert/tests/expected/12-already-has-assert-require.js b/recipes/process-assert-to-assert/tests/expected/12-already-has-assert-require.js new file mode 100644 index 00000000..a0f7c1b4 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/12-already-has-assert-require.js @@ -0,0 +1,3 @@ +const assert = require("node:assert"); +assert(value, "This should be transformed"); +assert.strictEqual(a, b, "This should remain unchanged"); diff --git a/recipes/process-assert-to-assert/tests/expected/13-no-process-assert.js b/recipes/process-assert-to-assert/tests/expected/13-no-process-assert.js new file mode 100644 index 00000000..d7717c73 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/13-no-process-assert.js @@ -0,0 +1,6 @@ +const config = { + port: 3000, + host: "localhost" +}; + +console.log("Server config:", config); diff --git a/recipes/process-assert-to-assert/tests/expected/14-nested-in-function.js b/recipes/process-assert-to-assert/tests/expected/14-nested-in-function.js new file mode 100644 index 00000000..92d74048 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/14-nested-in-function.js @@ -0,0 +1,10 @@ +import assert from "node:assert"; +function testFunction() { + assert(condition, "Assertion inside function"); + + if (someCondition) { + assert.deepStrictEqual(obj1, obj2, "Deep comparison"); + } + + return assert.ok(value) && true; +} diff --git a/recipes/process-assert-to-assert/tests/expected/15-multiple-assert-methods.js b/recipes/process-assert-to-assert/tests/expected/15-multiple-assert-methods.js new file mode 100644 index 00000000..5db73614 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/15-multiple-assert-methods.js @@ -0,0 +1,6 @@ +import assert from "node:assert"; +assert(condition); +assert.ok(value); +assert.strictEqual(a, b); +assert.notStrictEqual(a, c); +assert.throws(() => { throw new Error(); }); diff --git a/recipes/process-assert-to-assert/tests/expected/16-mixed-require-and-assert.js b/recipes/process-assert-to-assert/tests/expected/16-mixed-require-and-assert.js new file mode 100644 index 00000000..e0389c84 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/16-mixed-require-and-assert.js @@ -0,0 +1,9 @@ +const assert = require("node:assert"); +const fs = require("fs"); + +function readConfig(path) { + assert(fs.existsSync(path), "Config file must exist"); + const data = fs.readFileSync(path, "utf8"); + assert.ok(data.length > 0, "Config file cannot be empty"); + return JSON.parse(data); +} diff --git a/recipes/process-assert-to-assert/tests/expected/17-complex-nested-class.js b/recipes/process-assert-to-assert/tests/expected/17-complex-nested-class.js new file mode 100644 index 00000000..1cd40c48 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/expected/17-complex-nested-class.js @@ -0,0 +1,19 @@ +import assert from "node:assert"; +class Validator { + static validate(data) { + assert(data, "Data is required"); + + try { + assert.strictEqual(typeof data, "object", "Data must be object"); + } catch (error) { + assert.fail("Validation failed"); + } + + const results = [1, 2, 3].map(item => { + assert.ok(item > 0, "Item must be positive"); + return item * 2; + }); + + return results; + } +} diff --git a/recipes/process-assert-to-assert/tests/input/01-basic-global-process-assert.js b/recipes/process-assert-to-assert/tests/input/01-basic-global-process-assert.js new file mode 100644 index 00000000..f96e309c --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/01-basic-global-process-assert.js @@ -0,0 +1,2 @@ +process.assert(condition, "Basic assertion"); +process.assert.strictEqual(a, b, "Values should be equal"); diff --git a/recipes/process-assert-to-assert/tests/input/02-basic-commonjs.cjs b/recipes/process-assert-to-assert/tests/input/02-basic-commonjs.cjs new file mode 100644 index 00000000..f96e309c --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/02-basic-commonjs.cjs @@ -0,0 +1,2 @@ +process.assert(condition, "Basic assertion"); +process.assert.strictEqual(a, b, "Values should be equal"); diff --git a/recipes/process-assert-to-assert/tests/input/03-esm-import-process.mjs b/recipes/process-assert-to-assert/tests/input/03-esm-import-process.mjs new file mode 100644 index 00000000..8137a947 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/03-esm-import-process.mjs @@ -0,0 +1,3 @@ +import process from "node:process"; +process.assert(value, "Process assertion"); +process.assert.strictEqual(obj1, obj2); diff --git a/recipes/process-assert-to-assert/tests/input/04-mixed-process-usage.js b/recipes/process-assert-to-assert/tests/input/04-mixed-process-usage.js new file mode 100644 index 00000000..4c0286f3 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/04-mixed-process-usage.js @@ -0,0 +1,4 @@ +import process from "node:process"; +process.assert(value, "Process assertion"); +process.env.NODE_ENV = "test"; +console.log(process.pid); diff --git a/recipes/process-assert-to-assert/tests/input/05-require-process.js b/recipes/process-assert-to-assert/tests/input/05-require-process.js new file mode 100644 index 00000000..8467b5b4 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/05-require-process.js @@ -0,0 +1,3 @@ +const process = require("node:process"); +process.assert(value, "Process assertion"); +process.assert.throws(() => { throw new Error(); }); diff --git a/recipes/process-assert-to-assert/tests/input/06-destructured-assert-only.js b/recipes/process-assert-to-assert/tests/input/06-destructured-assert-only.js new file mode 100644 index 00000000..bfd159da --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/06-destructured-assert-only.js @@ -0,0 +1,3 @@ +import { assert } from "node:process"; +assert(condition, "Assertion from destructured import"); +assert.throws(() => { throw new Error("test"); }); diff --git a/recipes/process-assert-to-assert/tests/input/07-destructured-mixed-bindings.js b/recipes/process-assert-to-assert/tests/input/07-destructured-mixed-bindings.js new file mode 100644 index 00000000..dcb14937 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/07-destructured-mixed-bindings.js @@ -0,0 +1,3 @@ +import { assert, env } from "node:process"; +assert(value, "Using destructured assert"); +console.log(env.NODE_ENV); diff --git a/recipes/process-assert-to-assert/tests/input/08-aliased-destructured-assert.js b/recipes/process-assert-to-assert/tests/input/08-aliased-destructured-assert.js new file mode 100644 index 00000000..3fe72101 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/08-aliased-destructured-assert.js @@ -0,0 +1,3 @@ +import { assert as nodeAssert } from "node:process"; +nodeAssert(value, "Using aliased assert"); +nodeAssert.notStrictEqual(a, b); diff --git a/recipes/process-assert-to-assert/tests/input/09-aliased-destructured-mixed.js b/recipes/process-assert-to-assert/tests/input/09-aliased-destructured-mixed.js new file mode 100644 index 00000000..ee1ad631 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/09-aliased-destructured-mixed.js @@ -0,0 +1,4 @@ +import { assert as nodeAssert, env } from "node:process"; +nodeAssert(value, "Using aliased assert"); +nodeAssert.strictEqual(a, b); +console.log(env.NODE_ENV); diff --git a/recipes/process-assert-to-assert/tests/input/10-require-destructured.js b/recipes/process-assert-to-assert/tests/input/10-require-destructured.js new file mode 100644 index 00000000..aeda6e77 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/10-require-destructured.js @@ -0,0 +1,3 @@ +const { assert } = require("node:process"); +assert(value, "Destructured assert from require"); +assert.strictEqual(a, b, "Should be equal"); diff --git a/recipes/process-assert-to-assert/tests/input/11-already-has-assert-import.js b/recipes/process-assert-to-assert/tests/input/11-already-has-assert-import.js new file mode 100644 index 00000000..f1c2bd59 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/11-already-has-assert-import.js @@ -0,0 +1,3 @@ +import assert from "node:assert"; +process.assert(value, "This should be transformed"); +assert.strictEqual(a, b, "This should remain unchanged"); diff --git a/recipes/process-assert-to-assert/tests/input/12-already-has-assert-require.js b/recipes/process-assert-to-assert/tests/input/12-already-has-assert-require.js new file mode 100644 index 00000000..3d98a928 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/12-already-has-assert-require.js @@ -0,0 +1,3 @@ +const assert = require("node:assert"); +process.assert(value, "This should be transformed"); +assert.strictEqual(a, b, "This should remain unchanged"); diff --git a/recipes/process-assert-to-assert/tests/input/13-no-process-assert.js b/recipes/process-assert-to-assert/tests/input/13-no-process-assert.js new file mode 100644 index 00000000..d7717c73 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/13-no-process-assert.js @@ -0,0 +1,6 @@ +const config = { + port: 3000, + host: "localhost" +}; + +console.log("Server config:", config); diff --git a/recipes/process-assert-to-assert/tests/input/14-nested-in-function.js b/recipes/process-assert-to-assert/tests/input/14-nested-in-function.js new file mode 100644 index 00000000..336d1695 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/14-nested-in-function.js @@ -0,0 +1,9 @@ +function testFunction() { + process.assert(condition, "Assertion inside function"); + + if (someCondition) { + process.assert.deepStrictEqual(obj1, obj2, "Deep comparison"); + } + + return process.assert.ok(value) && true; +} diff --git a/recipes/process-assert-to-assert/tests/input/15-multiple-assert-methods.js b/recipes/process-assert-to-assert/tests/input/15-multiple-assert-methods.js new file mode 100644 index 00000000..086caef7 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/15-multiple-assert-methods.js @@ -0,0 +1,5 @@ +process.assert(condition); +process.assert.ok(value); +process.assert.strictEqual(a, b); +process.assert.notStrictEqual(a, c); +process.assert.throws(() => { throw new Error(); }); diff --git a/recipes/process-assert-to-assert/tests/input/16-mixed-require-and-assert.js b/recipes/process-assert-to-assert/tests/input/16-mixed-require-and-assert.js new file mode 100644 index 00000000..34229dbf --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/16-mixed-require-and-assert.js @@ -0,0 +1,8 @@ +const fs = require("fs"); + +function readConfig(path) { + process.assert(fs.existsSync(path), "Config file must exist"); + const data = fs.readFileSync(path, "utf8"); + process.assert.ok(data.length > 0, "Config file cannot be empty"); + return JSON.parse(data); +} diff --git a/recipes/process-assert-to-assert/tests/input/17-complex-nested-class.js b/recipes/process-assert-to-assert/tests/input/17-complex-nested-class.js new file mode 100644 index 00000000..7306d2e7 --- /dev/null +++ b/recipes/process-assert-to-assert/tests/input/17-complex-nested-class.js @@ -0,0 +1,18 @@ +class Validator { + static validate(data) { + process.assert(data, "Data is required"); + + try { + process.assert.strictEqual(typeof data, "object", "Data must be object"); + } catch (error) { + process.assert.fail("Validation failed"); + } + + const results = [1, 2, 3].map(item => { + process.assert.ok(item > 0, "Item must be positive"); + return item * 2; + }); + + return results; + } +} diff --git a/recipes/process-assert-to-assert/workflow.yaml b/recipes/process-assert-to-assert/workflow.yaml new file mode 100644 index 00000000..b2db413f --- /dev/null +++ b/recipes/process-assert-to-assert/workflow.yaml @@ -0,0 +1,25 @@ +# 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 + steps: + - name: Handle DEP0100 via transforming `process.assert` to `assert`. + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript diff --git a/utils/src/ast-grep/remove-binding.test.ts b/utils/src/ast-grep/remove-binding.test.ts index 1c1d6b16..99e097cf 100644 --- a/utils/src/ast-grep/remove-binding.test.ts +++ b/utils/src/ast-grep/remove-binding.test.ts @@ -29,6 +29,29 @@ describe("remove-binding", () => { }); }); + it("should not remove non-related named requires", () => { + const code = dedent` + const { assert: nodeAssert, env } = require("process"); + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const requireStatement = node.find({ + rule: { + kind: "lexical_declaration", + }, + }); + + const change = removeBinding(requireStatement!, "nodeAssert"); + + const sourceCode = node.commitEdits([change?.edit!]); + assert.strictEqual(sourceCode, `const { env } = require("process");`); + + assert.notEqual(change, null); + assert.strictEqual(change?.lineToRemove, undefined); + }) + it("should return undefined when the binding does not match the imported name", () => { const code = dedent` const util = require('node:util'); diff --git a/utils/src/ast-grep/remove-binding.ts b/utils/src/ast-grep/remove-binding.ts index 04c1f44e..a099276e 100644 --- a/utils/src/ast-grep/remove-binding.ts +++ b/utils/src/ast-grep/remove-binding.ts @@ -184,7 +184,14 @@ function handleNamedRequireBindings( const declarations = node.findAll({ rule: { - kind: "shorthand_property_identifier_pattern", + any: [ + { + kind: "pair_pattern", + }, + { + kind: "shorthand_property_identifier_pattern", + }, + ] }, }); @@ -195,7 +202,18 @@ function handleNamedRequireBindings( } if (declarations.length > 1) { - const restDeclarations = declarations.map((d) => d.text()).filter((d) => d !== binding); + const restDeclarations = declarations.map((d) => { + if (d.kind() === 'pair_pattern') { + const alias = d.find({ + rule: { + kind: 'identifier', + } + }) + + return alias.text() + } + return d.text() + }).filter((d) => d !== binding); return { edit: objectPattern.replace(`{ ${restDeclarations.join(", ")} }`),