From 39f92b07da462431718e0653cb11c001928ae197 Mon Sep 17 00:00:00 2001 From: "Usman S." Date: Wed, 13 Aug 2025 12:00:06 +0000 Subject: [PATCH 01/13] feat(crypto-fips): add migration recipe for transitioning from crypto.fips to crypto.getFips() and crypto.setFips() --- .vscode/settings.json | 24 +++-- package-lock.json | 18 +++- recipes/crypto-fips/README.md | 33 +++++++ recipes/crypto-fips/codemod.yaml | 21 +++++ recipes/crypto-fips/package.json | 24 +++++ recipes/crypto-fips/src/workflow.ts | 95 ++++++++++++++++++++ recipes/crypto-fips/tests/expected/file-1.js | 5 ++ recipes/crypto-fips/tests/expected/file-2.js | 3 + recipes/crypto-fips/tests/expected/file-3.js | 6 ++ recipes/crypto-fips/tests/expected/file-4.js | 4 + recipes/crypto-fips/tests/expected/file-5.js | 4 + recipes/crypto-fips/tests/expected/file-6.js | 4 + recipes/crypto-fips/tests/input/file-1.js | 5 ++ recipes/crypto-fips/tests/input/file-2.js | 3 + recipes/crypto-fips/tests/input/file-3.js | 6 ++ recipes/crypto-fips/tests/input/file-4.js | 4 + recipes/crypto-fips/tests/input/file-5.js | 4 + recipes/crypto-fips/tests/input/file-6.js | 4 + recipes/crypto-fips/tsconfig.json | 23 +++++ recipes/crypto-fips/workflow.yaml | 25 ++++++ 20 files changed, 305 insertions(+), 10 deletions(-) create mode 100644 recipes/crypto-fips/README.md create mode 100644 recipes/crypto-fips/codemod.yaml create mode 100644 recipes/crypto-fips/package.json create mode 100644 recipes/crypto-fips/src/workflow.ts create mode 100644 recipes/crypto-fips/tests/expected/file-1.js create mode 100644 recipes/crypto-fips/tests/expected/file-2.js create mode 100644 recipes/crypto-fips/tests/expected/file-3.js create mode 100644 recipes/crypto-fips/tests/expected/file-4.js create mode 100644 recipes/crypto-fips/tests/expected/file-5.js create mode 100644 recipes/crypto-fips/tests/expected/file-6.js create mode 100644 recipes/crypto-fips/tests/input/file-1.js create mode 100644 recipes/crypto-fips/tests/input/file-2.js create mode 100644 recipes/crypto-fips/tests/input/file-3.js create mode 100644 recipes/crypto-fips/tests/input/file-4.js create mode 100644 recipes/crypto-fips/tests/input/file-5.js create mode 100644 recipes/crypto-fips/tests/input/file-6.js create mode 100644 recipes/crypto-fips/tsconfig.json create mode 100644 recipes/crypto-fips/workflow.yaml diff --git a/.vscode/settings.json b/.vscode/settings.json index d3d7b2b1..a679eb95 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,17 @@ { - "editor.formatOnSave": true, - "javascript.updateImportsOnFileMove.enabled": "always", - "typescript.updateImportsOnFileMove.enabled": "always", - "editor.formatOnPaste": true, - "editor.wordWrap": "wordWrapColumn", - "editor.wordWrapColumn": 100, - "[markdown]": { - "editor.wordWrap": "off" - } + "editor.formatOnSave": true, + "javascript.updateImportsOnFileMove.enabled": "always", + "typescript.updateImportsOnFileMove.enabled": "always", + "editor.formatOnPaste": true, + "editor.wordWrap": "wordWrapColumn", + "editor.wordWrapColumn": 100, + "[javascript][typescript][json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[markdown]": { + "editor.wordWrap": "off" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + } } diff --git a/package-lock.json b/package-lock.json index 221e6678..933eceba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1420,6 +1420,10 @@ "resolved": "recipes/create-require-from-path", "link": true }, + "node_modules/@nodejs/crypto-fips": { + "resolved": "recipes/crypto-fips", + "link": true + }, "node_modules/@nodejs/import-assertions-to-attributes": { "resolved": "recipes/import-assertions-to-attributes", "link": true @@ -4072,6 +4076,17 @@ "@codemod.com/jssg-types": "^1.0.3" } }, + "recipes/crypto-fips": { + "name": "@nodejs/crypto-fips", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.3" + } + }, "recipes/import-assertions-to-attributes": { "name": "@nodejs/import-assertions-to-attributes", "version": "1.0.0", @@ -4110,7 +4125,7 @@ "@nodejs/codemod-utils": "*" }, "devDependencies": { - "@types/node": "^24.2.1" + "@codemod.com/jssg-types": "^1.0.3" } }, "utils": { @@ -4119,6 +4134,7 @@ "license": "MIT", "devDependencies": { "@ast-grep/napi": "^0.39.3", + "@codemod.com/jssg-types": "^1.0.3", "dedent": "^1.6.0" } } diff --git a/recipes/crypto-fips/README.md b/recipes/crypto-fips/README.md new file mode 100644 index 00000000..f4bfc5b6 --- /dev/null +++ b/recipes/crypto-fips/README.md @@ -0,0 +1,33 @@ +# `crypto.fips` DEP0093 + +This recipe provides a guide for migrating from the deprecated `crypto.fips` to `crypto.getFips()` and `crypto.setFips()`. + +See [DEP0093](https://nodejs.org/api/deprecations.html#DEP0093). + +## Examples + +**Before:** + +```js +// Using crypto.fips +crypto.fips; + +// Using crypto.fips = true +crypto.fips = true; + +// Using crypto.fips = false +crypto.fips = false; +``` + +**After:** + +```js +// Using crypto.getFips() +crypto.getFips(); + +// Using crypto.setFips(true) +crypto.setFips(true); + +// Using crypto.setFips(false) +crypto.setFips(false); +``` diff --git a/recipes/crypto-fips/codemod.yaml b/recipes/crypto-fips/codemod.yaml new file mode 100644 index 00000000..350ba1a1 --- /dev/null +++ b/recipes/crypto-fips/codemod.yaml @@ -0,0 +1,21 @@ +schema_version: "1.0" +name: "@nodejs/crypto-fips" +version: 1.0.0 +description: Handle DEP0093 via transforming `crypto.fips` to `crypto.getFips()` and `crypto.setFips()` +author: Usman S. +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + +registry: + access: public + visibility: public diff --git a/recipes/crypto-fips/package.json b/recipes/crypto-fips/package.json new file mode 100644 index 00000000..7d885910 --- /dev/null +++ b/recipes/crypto-fips/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/crypto-fips", + "version": "1.0.0", + "description": "Handle DEP0093 via transforming `crypto.fips` to `crypto.getFips()` and `crypto.setFips()`", + "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/crypto-fips", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Usman S.", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/crypto-fips/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.3" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/crypto-fips/src/workflow.ts b/recipes/crypto-fips/src/workflow.ts new file mode 100644 index 00000000..0af04ea8 --- /dev/null +++ b/recipes/crypto-fips/src/workflow.ts @@ -0,0 +1,95 @@ +import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import type { SgRoot, Edit, SgNode } from '@codemod.com/jssg-types/main'; + +function escapeRegExp(input: string): string { + return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * Transform function that converts deprecated crypto.fips calls + * to the new crypto.getFips() and crypto.setFips() syntax. + * + * Handles: + * 1. crypto.fips -> crypto.getFips() + * 2. crypto.fips = true -> crypto.setFips(true) + * 3. crypto.fips = false -> crypto.setFips(false) + */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + let hasChanges = false; + const edits: Edit[] = []; + + const cryptoBases = new Set(); + setCryptoBases(getNodeRequireCalls(root, 'crypto'), cryptoBases); + setCryptoBases(getNodeImportStatements(root, 'crypto'), cryptoBases); + + const assignmentResult = replaceAssignments(rootNode, cryptoBases); + edits.push(...assignmentResult.edits); + hasChanges = assignmentResult.hasChanges; + + const readResult = replaceReads(rootNode, cryptoBases); + edits.push(...readResult.edits); + hasChanges = readResult.hasChanges; + + if (!hasChanges) return null; + return rootNode.commitEdits(edits); +} + +function setCryptoBases(statements: SgNode[], cryptoBases: Set) { + for (const stmt of statements) { + const resolvedPath = resolveBindingPath(stmt, '$.fips'); + if (!resolvedPath || !resolvedPath.includes('.')) continue; + cryptoBases.add(resolvedPath.slice(0, resolvedPath.lastIndexOf('.'))); + } +} + +function replaceAssignments(rootNode: SgNode, cryptoBases: Set) { + const edits: Edit[] = []; + let hasChanges = false; + + for (const base of cryptoBases) { + const assignments = rootNode.findAll({ + rule: { + pattern: `${base}.fips = $VALUE`, + }, + }); + + for (const assign of assignments) { + const valueText = assign.getMatch('VALUE')?.text() ?? ''; + const basePropRegex = new RegExp( + `\\b${escapeRegExp(base)}\\.fips\\b`, + 'g', + ); + const transformedValue = valueText.replace( + basePropRegex, + `${base}.getFips()`, + ); + edits.push(assign.replace(`${base}.setFips(${transformedValue})`)); + hasChanges = true; + } + } + + return { edits, hasChanges }; +} + +function replaceReads(rootNode: SgNode, cryptoBases: Set) { + const edits: Edit[] = []; + let hasChanges = false; + + for (const base of cryptoBases) { + const reads = rootNode.findAll({ + rule: { + pattern: `${base}.fips`, + }, + }); + + for (const read of reads) { + edits.push(read.replace(`${base}.getFips()`)); + hasChanges = true; + } + } + + return { edits, hasChanges }; +} diff --git a/recipes/crypto-fips/tests/expected/file-1.js b/recipes/crypto-fips/tests/expected/file-1.js new file mode 100644 index 00000000..05341284 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-1.js @@ -0,0 +1,5 @@ +const crypto = require("node:crypto"); + +if (crypto.getFips()) { + console.log("FIPS mode is enabled"); +} diff --git a/recipes/crypto-fips/tests/expected/file-2.js b/recipes/crypto-fips/tests/expected/file-2.js new file mode 100644 index 00000000..a773caaa --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-2.js @@ -0,0 +1,3 @@ +const crypto = require("node:crypto"); + +crypto.setFips(true); diff --git a/recipes/crypto-fips/tests/expected/file-3.js b/recipes/crypto-fips/tests/expected/file-3.js new file mode 100644 index 00000000..7c18c4e3 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-3.js @@ -0,0 +1,6 @@ +const crypto = require("node:crypto"); + +if (process.env.ENABLE_FIPS === "true") { + crypto.setFips(true); +} +console.log("FIPS enabled:", crypto.getFips()); diff --git a/recipes/crypto-fips/tests/expected/file-4.js b/recipes/crypto-fips/tests/expected/file-4.js new file mode 100644 index 00000000..035297c2 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-4.js @@ -0,0 +1,4 @@ +import crypto from "node:crypto"; + +const fipsStatus = crypto.getFips(); +crypto.setFips(!fipsStatus); diff --git a/recipes/crypto-fips/tests/expected/file-5.js b/recipes/crypto-fips/tests/expected/file-5.js new file mode 100644 index 00000000..9663bd66 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-5.js @@ -0,0 +1,4 @@ +const nodeCrypto = require("node:crypto"); + +const currentFips = nodeCrypto.getFips(); +nodeCrypto.setFips(!currentFips); diff --git a/recipes/crypto-fips/tests/expected/file-6.js b/recipes/crypto-fips/tests/expected/file-6.js new file mode 100644 index 00000000..1c0f1aa9 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-6.js @@ -0,0 +1,4 @@ +const crypto = require("node:crypto"); + +console.log("FIPS enabled:", crypto.getFips()); +crypto.setFips(crypto.getFips() || process.env.FORCE_FIPS); diff --git a/recipes/crypto-fips/tests/input/file-1.js b/recipes/crypto-fips/tests/input/file-1.js new file mode 100644 index 00000000..867856b1 --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-1.js @@ -0,0 +1,5 @@ +const crypto = require("node:crypto"); + +if (crypto.fips) { + console.log("FIPS mode is enabled"); +} diff --git a/recipes/crypto-fips/tests/input/file-2.js b/recipes/crypto-fips/tests/input/file-2.js new file mode 100644 index 00000000..792d2317 --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-2.js @@ -0,0 +1,3 @@ +const crypto = require("node:crypto"); + +crypto.fips = true; diff --git a/recipes/crypto-fips/tests/input/file-3.js b/recipes/crypto-fips/tests/input/file-3.js new file mode 100644 index 00000000..f5800590 --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-3.js @@ -0,0 +1,6 @@ +const crypto = require("node:crypto"); + +if (process.env.ENABLE_FIPS === "true") { + crypto.fips = true; +} +console.log("FIPS enabled:", crypto.fips); diff --git a/recipes/crypto-fips/tests/input/file-4.js b/recipes/crypto-fips/tests/input/file-4.js new file mode 100644 index 00000000..50caa598 --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-4.js @@ -0,0 +1,4 @@ +import crypto from "node:crypto"; + +const fipsStatus = crypto.fips; +crypto.fips = !fipsStatus; diff --git a/recipes/crypto-fips/tests/input/file-5.js b/recipes/crypto-fips/tests/input/file-5.js new file mode 100644 index 00000000..aeca0226 --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-5.js @@ -0,0 +1,4 @@ +const nodeCrypto = require("node:crypto"); + +const currentFips = nodeCrypto.fips; +nodeCrypto.fips = !currentFips; diff --git a/recipes/crypto-fips/tests/input/file-6.js b/recipes/crypto-fips/tests/input/file-6.js new file mode 100644 index 00000000..0131d320 --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-6.js @@ -0,0 +1,4 @@ +const crypto = require("node:crypto"); + +console.log("FIPS enabled:", crypto.fips); +crypto.fips = crypto.fips || process.env.FORCE_FIPS; diff --git a/recipes/crypto-fips/tsconfig.json b/recipes/crypto-fips/tsconfig.json new file mode 100644 index 00000000..92c12497 --- /dev/null +++ b/recipes/crypto-fips/tsconfig.json @@ -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/**" + ] +} diff --git a/recipes/crypto-fips/workflow.yaml b/recipes/crypto-fips/workflow.yaml new file mode 100644 index 00000000..705eed97 --- /dev/null +++ b/recipes/crypto-fips/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 DEP0093 via transforming `crypto.fips` to `crypto.getFips()`, `crypto.setFips()` to `crypto.setFips(true)` and `crypto.setFips(false)`. + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript From 0b7a6649a3d5e4c916b13d43b1c73d3159ae6238 Mon Sep 17 00:00:00 2001 From: "Usman S. (Max Programming)" <51731966+max-programming@users.noreply.github.com> Date: Wed, 13 Aug 2025 18:05:28 +0530 Subject: [PATCH 02/13] Use one-liner arrow function Co-authored-by: Aviv Keller --- recipes/crypto-fips/src/workflow.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/recipes/crypto-fips/src/workflow.ts b/recipes/crypto-fips/src/workflow.ts index 0af04ea8..a457b42f 100644 --- a/recipes/crypto-fips/src/workflow.ts +++ b/recipes/crypto-fips/src/workflow.ts @@ -3,9 +3,7 @@ import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; import type { SgRoot, Edit, SgNode } from '@codemod.com/jssg-types/main'; -function escapeRegExp(input: string): string { - return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} +const escapeRegExp = (input: string) => input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); /** * Transform function that converts deprecated crypto.fips calls From 17c3bc9fbbec7d93303bdf5d8a4cc479537254ad Mon Sep 17 00:00:00 2001 From: "Usman S." Date: Wed, 13 Aug 2025 12:36:54 +0000 Subject: [PATCH 03/13] chore(vscode): revert settings json file to default --- .vscode/settings.json | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index a679eb95..46a1936e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,17 +1,11 @@ { - "editor.formatOnSave": true, - "javascript.updateImportsOnFileMove.enabled": "always", - "typescript.updateImportsOnFileMove.enabled": "always", - "editor.formatOnPaste": true, - "editor.wordWrap": "wordWrapColumn", - "editor.wordWrapColumn": 100, - "[javascript][typescript][json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[markdown]": { - "editor.wordWrap": "off" - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - } -} + "editor.formatOnSave": true, + "javascript.updateImportsOnFileMove.enabled": "always", + "typescript.updateImportsOnFileMove.enabled": "always", + "editor.formatOnPaste": true, + "editor.wordWrap": "wordWrapColumn", + "editor.wordWrapColumn": 100, + "[markdown]": { + "editor.wordWrap": "off" + } +} \ No newline at end of file From 078fecd48cdd383a84a99d6029ffffe8f2e8404d Mon Sep 17 00:00:00 2001 From: "Usman S." Date: Wed, 13 Aug 2025 12:47:19 +0000 Subject: [PATCH 04/13] refactor(crypto-fips): streamline crypto base path retrieval and improve code readability --- recipes/crypto-fips/src/workflow.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/recipes/crypto-fips/src/workflow.ts b/recipes/crypto-fips/src/workflow.ts index a457b42f..9e0cf539 100644 --- a/recipes/crypto-fips/src/workflow.ts +++ b/recipes/crypto-fips/src/workflow.ts @@ -3,7 +3,8 @@ import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; import type { SgRoot, Edit, SgNode } from '@codemod.com/jssg-types/main'; -const escapeRegExp = (input: string) => input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +const escapeRegExp = (input: string) => + input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); /** * Transform function that converts deprecated crypto.fips calls @@ -19,9 +20,10 @@ export default function transform(root: SgRoot): string | null { let hasChanges = false; const edits: Edit[] = []; - const cryptoBases = new Set(); - setCryptoBases(getNodeRequireCalls(root, 'crypto'), cryptoBases); - setCryptoBases(getNodeImportStatements(root, 'crypto'), cryptoBases); + const cryptoBases = new Set([ + ...getCryptoBasePaths('require', root), + ...getCryptoBasePaths('import', root), + ]); const assignmentResult = replaceAssignments(rootNode, cryptoBases); edits.push(...assignmentResult.edits); @@ -35,12 +37,23 @@ export default function transform(root: SgRoot): string | null { return rootNode.commitEdits(edits); } -function setCryptoBases(statements: SgNode[], cryptoBases: Set) { +function getCryptoBasePaths( + callType: 'require' | 'import', + root: SgRoot, +): string[] { + const paths: string[] = []; + const statements = + callType === 'require' + ? getNodeRequireCalls(root, 'crypto') + : getNodeImportStatements(root, 'crypto'); + for (const stmt of statements) { const resolvedPath = resolveBindingPath(stmt, '$.fips'); if (!resolvedPath || !resolvedPath.includes('.')) continue; - cryptoBases.add(resolvedPath.slice(0, resolvedPath.lastIndexOf('.'))); + paths.push(resolvedPath.slice(0, resolvedPath.lastIndexOf('.'))); } + + return paths; } function replaceAssignments(rootNode: SgNode, cryptoBases: Set) { From 7cd8777d0eb7e46c45ae00d946a3e13fd1acadef Mon Sep 17 00:00:00 2001 From: "Usman S." Date: Wed, 13 Aug 2025 12:50:31 +0000 Subject: [PATCH 05/13] refactor(crypto-fips): use generators for crypto base path collection Replaces array concatenation with generator-based iteration to improve readability, memory usage, and performance. --- recipes/crypto-fips/src/workflow.ts | 50 ++++++++++------------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/recipes/crypto-fips/src/workflow.ts b/recipes/crypto-fips/src/workflow.ts index 9e0cf539..11b9e7e0 100644 --- a/recipes/crypto-fips/src/workflow.ts +++ b/recipes/crypto-fips/src/workflow.ts @@ -1,10 +1,9 @@ -import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement'; -import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; -import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; -import type { SgRoot, Edit, SgNode } from '@codemod.com/jssg-types/main'; +import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement"; +import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call"; +import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path"; +import type { SgRoot, Edit, SgNode } from "@codemod.com/jssg-types/main"; -const escapeRegExp = (input: string) => - input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +const escapeRegExp = (input: string) => input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); /** * Transform function that converts deprecated crypto.fips calls @@ -20,10 +19,7 @@ export default function transform(root: SgRoot): string | null { let hasChanges = false; const edits: Edit[] = []; - const cryptoBases = new Set([ - ...getCryptoBasePaths('require', root), - ...getCryptoBasePaths('import', root), - ]); + const cryptoBases = new Set(getAllCryptoBasePaths(root)); const assignmentResult = replaceAssignments(rootNode, cryptoBases); edits.push(...assignmentResult.edits); @@ -37,23 +33,17 @@ export default function transform(root: SgRoot): string | null { return rootNode.commitEdits(edits); } -function getCryptoBasePaths( - callType: 'require' | 'import', - root: SgRoot, -): string[] { - const paths: string[] = []; - const statements = - callType === 'require' - ? getNodeRequireCalls(root, 'crypto') - : getNodeImportStatements(root, 'crypto'); - +function* getCryptoBasePaths(statements: SgNode[]) { for (const stmt of statements) { - const resolvedPath = resolveBindingPath(stmt, '$.fips'); - if (!resolvedPath || !resolvedPath.includes('.')) continue; - paths.push(resolvedPath.slice(0, resolvedPath.lastIndexOf('.'))); + const resolvedPath = resolveBindingPath(stmt, "$.fips"); + if (!resolvedPath || !resolvedPath.includes(".")) continue; + yield resolvedPath.slice(0, resolvedPath.lastIndexOf(".")); } +} - return paths; +function* getAllCryptoBasePaths(root: SgRoot) { + yield* getCryptoBasePaths(getNodeRequireCalls(root, "crypto")); + yield* getCryptoBasePaths(getNodeImportStatements(root, "crypto")); } function replaceAssignments(rootNode: SgNode, cryptoBases: Set) { @@ -68,15 +58,9 @@ function replaceAssignments(rootNode: SgNode, cryptoBases: Set) { }); for (const assign of assignments) { - const valueText = assign.getMatch('VALUE')?.text() ?? ''; - const basePropRegex = new RegExp( - `\\b${escapeRegExp(base)}\\.fips\\b`, - 'g', - ); - const transformedValue = valueText.replace( - basePropRegex, - `${base}.getFips()`, - ); + const valueText = assign.getMatch("VALUE")?.text() ?? ""; + const basePropRegex = new RegExp(`\\b${escapeRegExp(base)}\\.fips\\b`, "g"); + const transformedValue = valueText.replace(basePropRegex, `${base}.getFips()`); edits.push(assign.replace(`${base}.setFips(${transformedValue})`)); hasChanges = true; } From 866e1fcd796d1a4b7310edd418b370264dc43089 Mon Sep 17 00:00:00 2001 From: "Usman S." Date: Wed, 13 Aug 2025 12:55:39 +0000 Subject: [PATCH 06/13] refactor(crypto-fips): enhance code formatting and improve readability --- recipes/crypto-fips/src/workflow.ts | 35 ++++++++++++++++++----------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/recipes/crypto-fips/src/workflow.ts b/recipes/crypto-fips/src/workflow.ts index 11b9e7e0..8512a1b2 100644 --- a/recipes/crypto-fips/src/workflow.ts +++ b/recipes/crypto-fips/src/workflow.ts @@ -1,9 +1,10 @@ -import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement"; -import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call"; -import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path"; -import type { SgRoot, Edit, SgNode } from "@codemod.com/jssg-types/main"; +import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import type { SgRoot, Edit, SgNode } from '@codemod.com/jssg-types/main'; -const escapeRegExp = (input: string) => input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +const escapeRegExp = (input: string) => + input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); /** * Transform function that converts deprecated crypto.fips calls @@ -21,10 +22,12 @@ export default function transform(root: SgRoot): string | null { const cryptoBases = new Set(getAllCryptoBasePaths(root)); + // Handle crypto.fips = ... (value-modification) const assignmentResult = replaceAssignments(rootNode, cryptoBases); edits.push(...assignmentResult.edits); hasChanges = assignmentResult.hasChanges; + // Handle crypto.fips (value-access) const readResult = replaceReads(rootNode, cryptoBases); edits.push(...readResult.edits); hasChanges = readResult.hasChanges; @@ -35,15 +38,15 @@ export default function transform(root: SgRoot): string | null { function* getCryptoBasePaths(statements: SgNode[]) { for (const stmt of statements) { - const resolvedPath = resolveBindingPath(stmt, "$.fips"); - if (!resolvedPath || !resolvedPath.includes(".")) continue; - yield resolvedPath.slice(0, resolvedPath.lastIndexOf(".")); + const resolvedPath = resolveBindingPath(stmt, '$.fips'); + if (!resolvedPath || !resolvedPath.includes('.')) continue; + yield resolvedPath.slice(0, resolvedPath.lastIndexOf('.')); } } function* getAllCryptoBasePaths(root: SgRoot) { - yield* getCryptoBasePaths(getNodeRequireCalls(root, "crypto")); - yield* getCryptoBasePaths(getNodeImportStatements(root, "crypto")); + yield* getCryptoBasePaths(getNodeRequireCalls(root, 'crypto')); + yield* getCryptoBasePaths(getNodeImportStatements(root, 'crypto')); } function replaceAssignments(rootNode: SgNode, cryptoBases: Set) { @@ -58,9 +61,15 @@ function replaceAssignments(rootNode: SgNode, cryptoBases: Set) { }); for (const assign of assignments) { - const valueText = assign.getMatch("VALUE")?.text() ?? ""; - const basePropRegex = new RegExp(`\\b${escapeRegExp(base)}\\.fips\\b`, "g"); - const transformedValue = valueText.replace(basePropRegex, `${base}.getFips()`); + const valueText = assign.getMatch('VALUE')?.text() ?? ''; + const basePropRegex = new RegExp( + `\\b${escapeRegExp(base)}\\.fips\\b`, + 'g', + ); + const transformedValue = valueText.replace( + basePropRegex, + `${base}.getFips()`, + ); edits.push(assign.replace(`${base}.setFips(${transformedValue})`)); hasChanges = true; } From 7200be738d16db9836b50aaa3424412febe94dc8 Mon Sep 17 00:00:00 2001 From: "Usman S." Date: Wed, 13 Aug 2025 13:28:37 +0000 Subject: [PATCH 07/13] refactor(crypto-fips): rename functions and variables for clarity in fips context --- recipes/crypto-fips/src/workflow.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/recipes/crypto-fips/src/workflow.ts b/recipes/crypto-fips/src/workflow.ts index 8512a1b2..e7d6de8f 100644 --- a/recipes/crypto-fips/src/workflow.ts +++ b/recipes/crypto-fips/src/workflow.ts @@ -20,7 +20,7 @@ export default function transform(root: SgRoot): string | null { let hasChanges = false; const edits: Edit[] = []; - const cryptoBases = new Set(getAllCryptoBasePaths(root)); + const cryptoBases = new Set(getAllCryptoFipsBasePaths(root)); // Handle crypto.fips = ... (value-modification) const assignmentResult = replaceAssignments(rootNode, cryptoBases); @@ -36,7 +36,7 @@ export default function transform(root: SgRoot): string | null { return rootNode.commitEdits(edits); } -function* getCryptoBasePaths(statements: SgNode[]) { +function* getCryptoFipsBasePaths(statements: SgNode[]) { for (const stmt of statements) { const resolvedPath = resolveBindingPath(stmt, '$.fips'); if (!resolvedPath || !resolvedPath.includes('.')) continue; @@ -44,9 +44,9 @@ function* getCryptoBasePaths(statements: SgNode[]) { } } -function* getAllCryptoBasePaths(root: SgRoot) { - yield* getCryptoBasePaths(getNodeRequireCalls(root, 'crypto')); - yield* getCryptoBasePaths(getNodeImportStatements(root, 'crypto')); +function* getAllCryptoFipsBasePaths(root: SgRoot) { + yield* getCryptoFipsBasePaths(getNodeRequireCalls(root, 'crypto')); + yield* getCryptoFipsBasePaths(getNodeImportStatements(root, 'crypto')); } function replaceAssignments(rootNode: SgNode, cryptoBases: Set) { From b4d9c0cf828bc86d271d78d7435f347af5eba452 Mon Sep 17 00:00:00 2001 From: "Usman S." Date: Wed, 13 Aug 2025 15:14:37 +0000 Subject: [PATCH 08/13] refactor(crypto-fips): implement transformation for crypto.fips to getFips/setFips This update introduces a transformation for replacing deprecated `crypto.fips` calls with `crypto.getFips()` and `crypto.setFips()`. It includes enhancements to variable collection, import specifier updates, and destructuring adjustments to ensure proper usage of the new methods. Additionally, new test cases have been added to validate the changes. --- recipes/crypto-fips/src/workflow.ts | 285 ++++++++++++++---- recipes/crypto-fips/tests/expected/file-10.js | 6 + recipes/crypto-fips/tests/expected/file-7.js | 6 + recipes/crypto-fips/tests/expected/file-8.js | 6 + recipes/crypto-fips/tests/expected/file-9.js | 6 + recipes/crypto-fips/tests/input/file-10.js | 6 + recipes/crypto-fips/tests/input/file-7.js | 6 + recipes/crypto-fips/tests/input/file-8.js | 6 + recipes/crypto-fips/tests/input/file-9.js | 6 + 9 files changed, 279 insertions(+), 54 deletions(-) create mode 100644 recipes/crypto-fips/tests/expected/file-10.js create mode 100644 recipes/crypto-fips/tests/expected/file-7.js create mode 100644 recipes/crypto-fips/tests/expected/file-8.js create mode 100644 recipes/crypto-fips/tests/expected/file-9.js create mode 100644 recipes/crypto-fips/tests/input/file-10.js create mode 100644 recipes/crypto-fips/tests/input/file-7.js create mode 100644 recipes/crypto-fips/tests/input/file-8.js create mode 100644 recipes/crypto-fips/tests/input/file-9.js diff --git a/recipes/crypto-fips/src/workflow.ts b/recipes/crypto-fips/src/workflow.ts index e7d6de8f..7d73d49a 100644 --- a/recipes/crypto-fips/src/workflow.ts +++ b/recipes/crypto-fips/src/workflow.ts @@ -1,10 +1,10 @@ -import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement'; -import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; -import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; -import type { SgRoot, Edit, SgNode } from '@codemod.com/jssg-types/main'; +import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement"; +import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call"; +import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path"; +import type { SgRoot, Edit, SgNode } from "@codemod.com/jssg-types/main"; -const escapeRegExp = (input: string) => - input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +// Escape regexp characters - "crypto.fips" -> "crypto\.fips" +const escapeRegExp = (input: string) => input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); /** * Transform function that converts deprecated crypto.fips calls @@ -17,83 +17,260 @@ const escapeRegExp = (input: string) => */ export default function transform(root: SgRoot): string | null { const rootNode = root.root(); - let hasChanges = false; const edits: Edit[] = []; - const cryptoBases = new Set(getAllCryptoFipsBasePaths(root)); + const cryptoVars = collectCryptoFipsVariables(root); - // Handle crypto.fips = ... (value-modification) - const assignmentResult = replaceAssignments(rootNode, cryptoBases); - edits.push(...assignmentResult.edits); - hasChanges = assignmentResult.hasChanges; + for (const [varName, info] of cryptoVars) { + if (info.type === "member" && info.base) { + edits.push(...replaceMemberAssignments(rootNode, info.base)); + edits.push(...replaceMemberReads(rootNode, info.base)); + } else if (info.type === "named") { + edits.push(...replaceNamedAssignments(rootNode, varName)); + edits.push(...replaceNamedReads(rootNode, varName)); + } + } - // Handle crypto.fips (value-access) - const readResult = replaceReads(rootNode, cryptoBases); - edits.push(...readResult.edits); - hasChanges = readResult.hasChanges; + edits.push(...updateCryptoImportSpecifiers(root)); + edits.push(...updateCryptoRequireDestructuring(root)); - if (!hasChanges) return null; + if (edits.length === 0) return null; return rootNode.commitEdits(edits); } -function* getCryptoFipsBasePaths(statements: SgNode[]) { - for (const stmt of statements) { - const resolvedPath = resolveBindingPath(stmt, '$.fips'); - if (!resolvedPath || !resolvedPath.includes('.')) continue; - yield resolvedPath.slice(0, resolvedPath.lastIndexOf('.')); +/** + * Collect all crypto fips variables + */ +function collectCryptoFipsVariables(root: SgRoot) { + const map = new Map(); + const importNodes = getNodeImportStatements(root, "crypto"); + const requireNodes = getNodeRequireCalls(root, "crypto"); + const allStatementNodes = [...importNodes, ...requireNodes]; + + for (const { base, type } of getAllCryptoFipsBases(allStatementNodes)) { + map.set(base, { type, base: type === "member" ? base : undefined }); } + + return map; } -function* getAllCryptoFipsBasePaths(root: SgRoot) { - yield* getCryptoFipsBasePaths(getNodeRequireCalls(root, 'crypto')); - yield* getCryptoFipsBasePaths(getNodeImportStatements(root, 'crypto')); +/** + * Replace member access reads (crypto.fips) + */ +function replaceMemberReads(rootNode: SgNode, base: string) { + const edits: Edit[] = []; + const reads = rootNode.findAll({ + rule: { pattern: `${base}.fips` }, + }); + for (const read of reads) { + edits.push(read.replace(`${base}.getFips()`)); + } + return edits; } -function replaceAssignments(rootNode: SgNode, cryptoBases: Set) { +/** + * Replace member assignments (crypto.fips = val) + */ +function replaceMemberAssignments(rootNode: SgNode, base: string) { const edits: Edit[] = []; - let hasChanges = false; + const assignments = rootNode.findAll({ + rule: { pattern: `${base}.fips = $VALUE` }, + }); + for (const assign of assignments) { + const valueText = assign.getMatch("VALUE")?.text() ?? ""; + const basePropRegex = new RegExp(`\\b${escapeRegExp(base)}\\.fips\\b`, "g"); + const transformedValue = valueText.replace(basePropRegex, `${base}.getFips()`); + edits.push(assign.replace(`${base}.setFips(${transformedValue})`)); + } + return edits; +} - for (const base of cryptoBases) { - const assignments = rootNode.findAll({ - rule: { - pattern: `${base}.fips = $VALUE`, +/** + * Update import specifiers to include getFips and setFips + */ +function updateCryptoImportSpecifiers(root: SgRoot): Edit[] { + const edits: Edit[] = []; + + const importStmts = root.root().findAll({ + rule: { + kind: "import_statement", + has: { + field: "source", + kind: "string", + regex: `^['"](node:)?crypto['"]$`, }, - }); + }, + }); + + for (const stmt of importStmts) { + // import_clause contains default/namespace/named parts + const importClause = stmt.find({ rule: { kind: "import_clause" } }); + if (!importClause) continue; + + // named_imports = `{ ... }` + const namedImports = importClause.find({ rule: { kind: "named_imports" } }); + if (!namedImports) continue; // nothing to edit if there is no `{ ... }` + + // All specifiers inside `{ ... }` + const specifiers = namedImports.findAll({ rule: { kind: "import_specifier" } }); + if (!specifiers || specifiers.length === 0) continue; - for (const assign of assignments) { - const valueText = assign.getMatch('VALUE')?.text() ?? ''; - const basePropRegex = new RegExp( - `\\b${escapeRegExp(base)}\\.fips\\b`, - 'g', - ); - const transformedValue = valueText.replace( - basePropRegex, - `${base}.getFips()`, - ); - edits.push(assign.replace(`${base}.setFips(${transformedValue})`)); - hasChanges = true; + let hasFips = false; + let hasGet = false; + let hasSet = false; + + const keepTexts: string[] = []; + + for (const spec of specifiers) { + // imported name is in field "name" + const importedNameNode = spec.find({ + rule: { has: { field: "name", kind: "identifier" } }, + }); + const importedName = importedNameNode + ?.find({ + rule: { kind: "identifier" }, + }) + ?.text(); + + if (importedName === "fips") { + hasFips = true; // drop this one; we will add getFips/setFips instead + continue; + } + if (importedName === "getFips") hasGet = true; + if (importedName === "setFips") hasSet = true; + + // Preserve other specifiers as-is (including aliases like `name as alias`) + keepTexts.push(spec.text()); } + + if (!hasFips) continue; // only rewrite when file was importing `fips` + + // Ensure both getFips and setFips are present + if (!hasGet) keepTexts.push("getFips"); + if (!hasSet) keepTexts.push("setFips"); + + // Replace the whole `{ ... }` + edits.push(namedImports.replace(`{ ${keepTexts.join(", ")} }`)); } - return { edits, hasChanges }; + return edits; } -function replaceReads(rootNode: SgNode, cryptoBases: Set) { +/** + * Update require destructuring to include getFips and setFips + */ +function updateCryptoRequireDestructuring(root: SgRoot): Edit[] { const edits: Edit[] = []; - let hasChanges = false; - for (const base of cryptoBases) { - const reads = rootNode.findAll({ + const decls = getNodeRequireCalls(root, "crypto"); + + for (const decl of decls) { + const objPattern = decl.find({ rule: { kind: "object_pattern" } }); + if (!objPattern) continue; + + const props = objPattern.findAll({ rule: { - pattern: `${base}.fips`, + any: [ + { kind: "shorthand_property_identifier_pattern" }, // `{ foo }` + { kind: "pair_pattern" }, // `{ foo: bar }` + ], }, }); + if (!props || props.length === 0) continue; + + let hasFips = false; + let hasGet = false; + let hasSet = false; - for (const read of reads) { - edits.push(read.replace(`${base}.getFips()`)); - hasChanges = true; + const keepTexts: string[] = []; + + for (const p of props) { + if (p.kind() === "shorthand_property_identifier_pattern") { + const name = p.text().trim(); + if (name === "fips") { + hasFips = true; + continue; + } + if (name === "getFips") hasGet = true; + if (name === "setFips") hasSet = true; + keepTexts.push(name); + } else { + // pair_pattern: has key + value (e.g. `fips: myFips`, `getFips: gf`) + const keyNode = p.find({ rule: { kind: "property_identifier" } }); + const key = keyNode?.text(); + + if (key === "fips") { + hasFips = true; // drop any alias of fips + continue; + } + if (key === "getFips") hasGet = true; + if (key === "setFips") hasSet = true; + + // Keep other pairs as-is (preserves aliasing/spacing nicely) + keepTexts.push(p.text().trim()); + } } + + if (!hasFips) continue; // only rewrite when it actually destructured `fips` + + if (!hasGet) keepTexts.push("getFips"); + if (!hasSet) keepTexts.push("setFips"); + + edits.push(objPattern.replace(`{ ${keepTexts.join(", ")} }`)); } - return { edits, hasChanges }; + return edits; +} + +/** + * Replace named reads (fips -> getFips()) + */ +function replaceNamedReads(rootNode: SgNode, varName: string) { + const edits: Edit[] = []; + const reads = rootNode.findAll({ + rule: { pattern: varName }, + }); + for (const read of reads) { + edits.push(read.replace("getFips()")); + } + return edits; +} + +/** + * Replace named assignments (fips = val -> setFips(val)) + */ +function replaceNamedAssignments(rootNode: SgNode, varName: string) { + const edits: Edit[] = []; + const assignments = rootNode.findAll({ + rule: { pattern: `${varName} = $VALUE` }, + }); + for (const assign of assignments) { + const valueText = assign.getMatch("VALUE")?.text() ?? ""; + edits.push(assign.replace(`setFips(${valueText})`)); + } + return edits; +} + +/** + * Get the base of the crypto fips variable + * import { fips } from "node:crypto" -> "fips" + * import crypto from "node:crypto" -> "crypto" + * const { fips } = require("node:crypto") -> "fips" + * const crypto = require("node:crypto") -> "crypto" + */ +function* getCryptoFipsBase(statements: SgNode[], type: "member" | "named") { + for (const stmt of statements) { + const resolvedPath = resolveBindingPath(stmt, "$.fips"); + if (resolvedPath?.includes(".") && type === "member") { + const base = resolvedPath.slice(0, resolvedPath.lastIndexOf(".")); + yield { base, type }; + } else if (resolvedPath && type === "named") { + yield { base: resolvedPath, type }; + } + } +} + +function* getAllCryptoFipsBases(statements: SgNode[]) { + yield* getCryptoFipsBase(statements, "member"); + yield* getCryptoFipsBase(statements, "named"); } diff --git a/recipes/crypto-fips/tests/expected/file-10.js b/recipes/crypto-fips/tests/expected/file-10.js new file mode 100644 index 00000000..9ef80aab --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-10.js @@ -0,0 +1,6 @@ +import { getFips, setFips } from "node:crypto"; + +if (getFips()) { + console.log("FIPS mode is enabled"); +} +setFips(true); diff --git a/recipes/crypto-fips/tests/expected/file-7.js b/recipes/crypto-fips/tests/expected/file-7.js new file mode 100644 index 00000000..0c668e48 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-7.js @@ -0,0 +1,6 @@ +const { getFips, setFips } = require("node:crypto"); + +if (getFips()) { + console.log("FIPS mode is enabled"); +} +setFips(true); diff --git a/recipes/crypto-fips/tests/expected/file-8.js b/recipes/crypto-fips/tests/expected/file-8.js new file mode 100644 index 00000000..0c668e48 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-8.js @@ -0,0 +1,6 @@ +const { getFips, setFips } = require("node:crypto"); + +if (getFips()) { + console.log("FIPS mode is enabled"); +} +setFips(true); diff --git a/recipes/crypto-fips/tests/expected/file-9.js b/recipes/crypto-fips/tests/expected/file-9.js new file mode 100644 index 00000000..9ef80aab --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-9.js @@ -0,0 +1,6 @@ +import { getFips, setFips } from "node:crypto"; + +if (getFips()) { + console.log("FIPS mode is enabled"); +} +setFips(true); diff --git a/recipes/crypto-fips/tests/input/file-10.js b/recipes/crypto-fips/tests/input/file-10.js new file mode 100644 index 00000000..f59cec0f --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-10.js @@ -0,0 +1,6 @@ +import { fips as fipsRenamed } from "node:crypto"; + +if (fipsRenamed) { + console.log("FIPS mode is enabled"); +} +fipsRenamed = true; diff --git a/recipes/crypto-fips/tests/input/file-7.js b/recipes/crypto-fips/tests/input/file-7.js new file mode 100644 index 00000000..d2a79f8f --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-7.js @@ -0,0 +1,6 @@ +const { fips } = require("node:crypto"); + +if (fips) { + console.log("FIPS mode is enabled"); +} +fips = true; diff --git a/recipes/crypto-fips/tests/input/file-8.js b/recipes/crypto-fips/tests/input/file-8.js new file mode 100644 index 00000000..c21c960c --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-8.js @@ -0,0 +1,6 @@ +const { fips: fipsRenamed } = require("node:crypto"); + +if (fipsRenamed) { + console.log("FIPS mode is enabled"); +} +fipsRenamed = true; diff --git a/recipes/crypto-fips/tests/input/file-9.js b/recipes/crypto-fips/tests/input/file-9.js new file mode 100644 index 00000000..1cac100c --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-9.js @@ -0,0 +1,6 @@ +import { fips } from "node:crypto"; + +if (fips) { + console.log("FIPS mode is enabled"); +} +fips = true; From fe9ec4861dc234b69cb6684092961f40c4942142 Mon Sep 17 00:00:00 2001 From: "Usman S." Date: Wed, 13 Aug 2025 15:16:57 +0000 Subject: [PATCH 09/13] refactor(crypto-fips): update README examplesto have import statement --- recipes/crypto-fips/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/recipes/crypto-fips/README.md b/recipes/crypto-fips/README.md index f4bfc5b6..02ecf4c2 100644 --- a/recipes/crypto-fips/README.md +++ b/recipes/crypto-fips/README.md @@ -9,25 +9,37 @@ See [DEP0093](https://nodejs.org/api/deprecations.html#DEP0093). **Before:** ```js +import crypto from "node:crypto"; +import { fips } from "node:crypto"; + // Using crypto.fips crypto.fips; +fips; // Using crypto.fips = true crypto.fips = true; +fips = true; // Using crypto.fips = false crypto.fips = false; +fips = false; ``` **After:** ```js +import crypto from "node:crypto"; +import { getFips, setFips } from "node:crypto"; + // Using crypto.getFips() crypto.getFips(); +getFips(); // Using crypto.setFips(true) crypto.setFips(true); +setFips(true); // Using crypto.setFips(false) crypto.setFips(false); +setFips(false); ``` From 8c3bd847f2e39100d03856636c8cc3c58dd9bf52 Mon Sep 17 00:00:00 2001 From: "Usman S." Date: Wed, 13 Aug 2025 15:27:23 +0000 Subject: [PATCH 10/13] refactor(crypto-fips): use existing utility function and reformat code with biome --- recipes/crypto-fips/src/workflow.ts | 115 ++++++++++++++-------------- 1 file changed, 56 insertions(+), 59 deletions(-) diff --git a/recipes/crypto-fips/src/workflow.ts b/recipes/crypto-fips/src/workflow.ts index 7d73d49a..02e489c2 100644 --- a/recipes/crypto-fips/src/workflow.ts +++ b/recipes/crypto-fips/src/workflow.ts @@ -1,10 +1,11 @@ -import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement"; -import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call"; -import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path"; -import type { SgRoot, Edit, SgNode } from "@codemod.com/jssg-types/main"; +import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import type { SgRoot, Edit, SgNode } from '@codemod.com/jssg-types/main'; // Escape regexp characters - "crypto.fips" -> "crypto\.fips" -const escapeRegExp = (input: string) => input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +const escapeRegExp = (input: string) => + input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); /** * Transform function that converts deprecated crypto.fips calls @@ -22,10 +23,10 @@ export default function transform(root: SgRoot): string | null { const cryptoVars = collectCryptoFipsVariables(root); for (const [varName, info] of cryptoVars) { - if (info.type === "member" && info.base) { + if (info.type === 'member' && info.base) { edits.push(...replaceMemberAssignments(rootNode, info.base)); edits.push(...replaceMemberReads(rootNode, info.base)); - } else if (info.type === "named") { + } else if (info.type === 'named') { edits.push(...replaceNamedAssignments(rootNode, varName)); edits.push(...replaceNamedReads(rootNode, varName)); } @@ -42,13 +43,13 @@ export default function transform(root: SgRoot): string | null { * Collect all crypto fips variables */ function collectCryptoFipsVariables(root: SgRoot) { - const map = new Map(); - const importNodes = getNodeImportStatements(root, "crypto"); - const requireNodes = getNodeRequireCalls(root, "crypto"); + const map = new Map(); + const importNodes = getNodeImportStatements(root, 'crypto'); + const requireNodes = getNodeRequireCalls(root, 'crypto'); const allStatementNodes = [...importNodes, ...requireNodes]; for (const { base, type } of getAllCryptoFipsBases(allStatementNodes)) { - map.set(base, { type, base: type === "member" ? base : undefined }); + map.set(base, { type, base: type === 'member' ? base : undefined }); } return map; @@ -77,9 +78,12 @@ function replaceMemberAssignments(rootNode: SgNode, base: string) { rule: { pattern: `${base}.fips = $VALUE` }, }); for (const assign of assignments) { - const valueText = assign.getMatch("VALUE")?.text() ?? ""; - const basePropRegex = new RegExp(`\\b${escapeRegExp(base)}\\.fips\\b`, "g"); - const transformedValue = valueText.replace(basePropRegex, `${base}.getFips()`); + const valueText = assign.getMatch('VALUE')?.text() ?? ''; + const basePropRegex = new RegExp(`\\b${escapeRegExp(base)}\\.fips\\b`, 'g'); + const transformedValue = valueText.replace( + basePropRegex, + `${base}.getFips()`, + ); edits.push(assign.replace(`${base}.setFips(${transformedValue})`)); } return edits; @@ -91,28 +95,21 @@ function replaceMemberAssignments(rootNode: SgNode, base: string) { function updateCryptoImportSpecifiers(root: SgRoot): Edit[] { const edits: Edit[] = []; - const importStmts = root.root().findAll({ - rule: { - kind: "import_statement", - has: { - field: "source", - kind: "string", - regex: `^['"](node:)?crypto['"]$`, - }, - }, - }); + const importStmts = getNodeImportStatements(root, 'crypto'); for (const stmt of importStmts) { // import_clause contains default/namespace/named parts - const importClause = stmt.find({ rule: { kind: "import_clause" } }); + const importClause = stmt.find({ rule: { kind: 'import_clause' } }); if (!importClause) continue; // named_imports = `{ ... }` - const namedImports = importClause.find({ rule: { kind: "named_imports" } }); + const namedImports = importClause.find({ rule: { kind: 'named_imports' } }); if (!namedImports) continue; // nothing to edit if there is no `{ ... }` // All specifiers inside `{ ... }` - const specifiers = namedImports.findAll({ rule: { kind: "import_specifier" } }); + const specifiers = namedImports.findAll({ + rule: { kind: 'import_specifier' }, + }); if (!specifiers || specifiers.length === 0) continue; let hasFips = false; @@ -124,20 +121,20 @@ function updateCryptoImportSpecifiers(root: SgRoot): Edit[] { for (const spec of specifiers) { // imported name is in field "name" const importedNameNode = spec.find({ - rule: { has: { field: "name", kind: "identifier" } }, + rule: { has: { field: 'name', kind: 'identifier' } }, }); const importedName = importedNameNode ?.find({ - rule: { kind: "identifier" }, + rule: { kind: 'identifier' }, }) ?.text(); - if (importedName === "fips") { + if (importedName === 'fips') { hasFips = true; // drop this one; we will add getFips/setFips instead continue; } - if (importedName === "getFips") hasGet = true; - if (importedName === "setFips") hasSet = true; + if (importedName === 'getFips') hasGet = true; + if (importedName === 'setFips') hasSet = true; // Preserve other specifiers as-is (including aliases like `name as alias`) keepTexts.push(spec.text()); @@ -146,11 +143,11 @@ function updateCryptoImportSpecifiers(root: SgRoot): Edit[] { if (!hasFips) continue; // only rewrite when file was importing `fips` // Ensure both getFips and setFips are present - if (!hasGet) keepTexts.push("getFips"); - if (!hasSet) keepTexts.push("setFips"); + if (!hasGet) keepTexts.push('getFips'); + if (!hasSet) keepTexts.push('setFips'); // Replace the whole `{ ... }` - edits.push(namedImports.replace(`{ ${keepTexts.join(", ")} }`)); + edits.push(namedImports.replace(`{ ${keepTexts.join(', ')} }`)); } return edits; @@ -162,17 +159,17 @@ function updateCryptoImportSpecifiers(root: SgRoot): Edit[] { function updateCryptoRequireDestructuring(root: SgRoot): Edit[] { const edits: Edit[] = []; - const decls = getNodeRequireCalls(root, "crypto"); + const decls = getNodeRequireCalls(root, 'crypto'); for (const decl of decls) { - const objPattern = decl.find({ rule: { kind: "object_pattern" } }); + const objPattern = decl.find({ rule: { kind: 'object_pattern' } }); if (!objPattern) continue; const props = objPattern.findAll({ rule: { any: [ - { kind: "shorthand_property_identifier_pattern" }, // `{ foo }` - { kind: "pair_pattern" }, // `{ foo: bar }` + { kind: 'shorthand_property_identifier_pattern' }, // `{ foo }` + { kind: 'pair_pattern' }, // `{ foo: bar }` ], }, }); @@ -185,26 +182,26 @@ function updateCryptoRequireDestructuring(root: SgRoot): Edit[] { const keepTexts: string[] = []; for (const p of props) { - if (p.kind() === "shorthand_property_identifier_pattern") { + if (p.kind() === 'shorthand_property_identifier_pattern') { const name = p.text().trim(); - if (name === "fips") { + if (name === 'fips') { hasFips = true; continue; } - if (name === "getFips") hasGet = true; - if (name === "setFips") hasSet = true; + if (name === 'getFips') hasGet = true; + if (name === 'setFips') hasSet = true; keepTexts.push(name); } else { // pair_pattern: has key + value (e.g. `fips: myFips`, `getFips: gf`) - const keyNode = p.find({ rule: { kind: "property_identifier" } }); + const keyNode = p.find({ rule: { kind: 'property_identifier' } }); const key = keyNode?.text(); - if (key === "fips") { + if (key === 'fips') { hasFips = true; // drop any alias of fips continue; } - if (key === "getFips") hasGet = true; - if (key === "setFips") hasSet = true; + if (key === 'getFips') hasGet = true; + if (key === 'setFips') hasSet = true; // Keep other pairs as-is (preserves aliasing/spacing nicely) keepTexts.push(p.text().trim()); @@ -213,10 +210,10 @@ function updateCryptoRequireDestructuring(root: SgRoot): Edit[] { if (!hasFips) continue; // only rewrite when it actually destructured `fips` - if (!hasGet) keepTexts.push("getFips"); - if (!hasSet) keepTexts.push("setFips"); + if (!hasGet) keepTexts.push('getFips'); + if (!hasSet) keepTexts.push('setFips'); - edits.push(objPattern.replace(`{ ${keepTexts.join(", ")} }`)); + edits.push(objPattern.replace(`{ ${keepTexts.join(', ')} }`)); } return edits; @@ -231,7 +228,7 @@ function replaceNamedReads(rootNode: SgNode, varName: string) { rule: { pattern: varName }, }); for (const read of reads) { - edits.push(read.replace("getFips()")); + edits.push(read.replace('getFips()')); } return edits; } @@ -245,7 +242,7 @@ function replaceNamedAssignments(rootNode: SgNode, varName: string) { rule: { pattern: `${varName} = $VALUE` }, }); for (const assign of assignments) { - const valueText = assign.getMatch("VALUE")?.text() ?? ""; + const valueText = assign.getMatch('VALUE')?.text() ?? ''; edits.push(assign.replace(`setFips(${valueText})`)); } return edits; @@ -258,19 +255,19 @@ function replaceNamedAssignments(rootNode: SgNode, varName: string) { * const { fips } = require("node:crypto") -> "fips" * const crypto = require("node:crypto") -> "crypto" */ -function* getCryptoFipsBase(statements: SgNode[], type: "member" | "named") { +function* getCryptoFipsBase(statements: SgNode[], type: 'member' | 'named') { for (const stmt of statements) { - const resolvedPath = resolveBindingPath(stmt, "$.fips"); - if (resolvedPath?.includes(".") && type === "member") { - const base = resolvedPath.slice(0, resolvedPath.lastIndexOf(".")); + const resolvedPath = resolveBindingPath(stmt, '$.fips'); + if (resolvedPath?.includes('.') && type === 'member') { + const base = resolvedPath.slice(0, resolvedPath.lastIndexOf('.')); yield { base, type }; - } else if (resolvedPath && type === "named") { + } else if (resolvedPath && type === 'named') { yield { base: resolvedPath, type }; } } } function* getAllCryptoFipsBases(statements: SgNode[]) { - yield* getCryptoFipsBase(statements, "member"); - yield* getCryptoFipsBase(statements, "named"); + yield* getCryptoFipsBase(statements, 'member'); + yield* getCryptoFipsBase(statements, 'named'); } From 91a80beeaa6dc07e940fc09f1135449d49200268 Mon Sep 17 00:00:00 2001 From: "Usman S." Date: Wed, 13 Aug 2025 15:32:56 +0000 Subject: [PATCH 11/13] refactor(crypto-fips): added missing jsdoc for a functions --- recipes/crypto-fips/src/workflow.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/recipes/crypto-fips/src/workflow.ts b/recipes/crypto-fips/src/workflow.ts index 02e489c2..28dec5d2 100644 --- a/recipes/crypto-fips/src/workflow.ts +++ b/recipes/crypto-fips/src/workflow.ts @@ -267,6 +267,9 @@ function* getCryptoFipsBase(statements: SgNode[], type: 'member' | 'named') { } } +/** + * Get crypto bases/names for both member and named imports + */ function* getAllCryptoFipsBases(statements: SgNode[]) { yield* getCryptoFipsBase(statements, 'member'); yield* getCryptoFipsBase(statements, 'named'); From 92a829e505d9a20d2a38dacc2e35cbdb2cafc005 Mon Sep 17 00:00:00 2001 From: "Usman S. (Max Programming)" <51731966+max-programming@users.noreply.github.com> Date: Wed, 13 Aug 2025 21:12:53 +0530 Subject: [PATCH 12/13] refactor(crypto-fips): add an extra line Co-authored-by: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> --- recipes/crypto-fips/src/workflow.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/recipes/crypto-fips/src/workflow.ts b/recipes/crypto-fips/src/workflow.ts index 28dec5d2..52346fff 100644 --- a/recipes/crypto-fips/src/workflow.ts +++ b/recipes/crypto-fips/src/workflow.ts @@ -36,6 +36,7 @@ export default function transform(root: SgRoot): string | null { edits.push(...updateCryptoRequireDestructuring(root)); if (edits.length === 0) return null; + return rootNode.commitEdits(edits); } From 515da7d657e376c404e7d37e768a4fb1cb8a6509 Mon Sep 17 00:00:00 2001 From: "Usman S. (Max Programming)" <51731966+max-programming@users.noreply.github.com> Date: Fri, 15 Aug 2025 15:39:54 +0530 Subject: [PATCH 13/13] docs(crypto-fips): fix description Co-authored-by: Bruno Rodrigues --- recipes/crypto-fips/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/crypto-fips/README.md b/recipes/crypto-fips/README.md index 02ecf4c2..5a8bd9ea 100644 --- a/recipes/crypto-fips/README.md +++ b/recipes/crypto-fips/README.md @@ -1,6 +1,6 @@ # `crypto.fips` DEP0093 -This recipe provides a guide for migrating from the deprecated `crypto.fips` to `crypto.getFips()` and `crypto.setFips()`. +This recipe transforms the usage from the deprecated `crypto.fips` to `crypto.getFips()` and `crypto.setFips()`. See [DEP0093](https://nodejs.org/api/deprecations.html#DEP0093).