From cf5fc3ba4df7d485aa02a774e051a6401f356540 Mon Sep 17 00:00:00 2001 From: techflare641 Date: Wed, 8 Oct 2025 21:48:37 -0500 Subject: [PATCH 1/4] Add basic recipe --- recipes/fs-existssync-valid-args/README.md | 96 +++++++++ recipes/fs-existssync-valid-args/codemod.yaml | 25 +++ recipes/fs-existssync-valid-args/package.json | 25 +++ .../fs-existssync-valid-args/src/workflow.ts | 193 ++++++++++++++++++ .../tests/expected/case1-literal-number.js | 4 + .../tests/expected/case2-variable.js | 9 + .../tests/expected/case3-null.js | 4 + .../tests/expected/case4-object.mjs | 4 + .../tests/expected/case5-valid-string.js | 7 + .../tests/expected/case6-esm-namespace.mjs | 5 + .../tests/expected/case7-multiple-calls.js | 7 + .../expected/case8-mixed-valid-invalid.js | 7 + .../tests/input/case1-literal-number.js | 4 + .../tests/input/case2-variable.js | 6 + .../tests/input/case3-null.js | 4 + .../tests/input/case4-object.mjs | 4 + .../tests/input/case5-valid-string.js | 7 + .../tests/input/case6-esm-namespace.mjs | 5 + .../tests/input/case7-multiple-calls.js | 7 + .../tests/input/case8-mixed-valid-invalid.js | 7 + .../fs-existssync-valid-args/workflow.yaml | 26 +++ 21 files changed, 456 insertions(+) create mode 100644 recipes/fs-existssync-valid-args/README.md create mode 100644 recipes/fs-existssync-valid-args/codemod.yaml create mode 100644 recipes/fs-existssync-valid-args/package.json create mode 100644 recipes/fs-existssync-valid-args/src/workflow.ts create mode 100644 recipes/fs-existssync-valid-args/tests/expected/case1-literal-number.js create mode 100644 recipes/fs-existssync-valid-args/tests/expected/case2-variable.js create mode 100644 recipes/fs-existssync-valid-args/tests/expected/case3-null.js create mode 100644 recipes/fs-existssync-valid-args/tests/expected/case4-object.mjs create mode 100644 recipes/fs-existssync-valid-args/tests/expected/case5-valid-string.js create mode 100644 recipes/fs-existssync-valid-args/tests/expected/case6-esm-namespace.mjs create mode 100644 recipes/fs-existssync-valid-args/tests/expected/case7-multiple-calls.js create mode 100644 recipes/fs-existssync-valid-args/tests/expected/case8-mixed-valid-invalid.js create mode 100644 recipes/fs-existssync-valid-args/tests/input/case1-literal-number.js create mode 100644 recipes/fs-existssync-valid-args/tests/input/case2-variable.js create mode 100644 recipes/fs-existssync-valid-args/tests/input/case3-null.js create mode 100644 recipes/fs-existssync-valid-args/tests/input/case4-object.mjs create mode 100644 recipes/fs-existssync-valid-args/tests/input/case5-valid-string.js create mode 100644 recipes/fs-existssync-valid-args/tests/input/case6-esm-namespace.mjs create mode 100644 recipes/fs-existssync-valid-args/tests/input/case7-multiple-calls.js create mode 100644 recipes/fs-existssync-valid-args/tests/input/case8-mixed-valid-invalid.js create mode 100644 recipes/fs-existssync-valid-args/workflow.yaml diff --git a/recipes/fs-existssync-valid-args/README.md b/recipes/fs-existssync-valid-args/README.md new file mode 100644 index 00000000..ec4772ae --- /dev/null +++ b/recipes/fs-existssync-valid-args/README.md @@ -0,0 +1,96 @@ +# fs-existssync-valid-args + +This codemod validates and converts invalid argument types to `fs.existsSync()`. It's useful to migrate code that passes invalid argument types which now causes deprecation warnings or errors. + +## Description + +Starting with Node.js, passing invalid argument types to `fs.existsSync()` triggers a deprecation warning (DEP0187). The function should only receive `string`, `Buffer`, or `URL` arguments. + +This codemod automatically: +- Validates that `fs.existsSync()` receives valid argument types +- Converts invalid argument types to valid ones where possible +- Handles both CommonJS (`require`) and ESM (`import`) syntax +- Adds type checks or conversions to ensure argument validity + +## Examples + +### Case 1: Direct Literal Values + +**Before:** +```javascript +const fs = require("node:fs"); + +const exists = fs.existsSync(123); +``` + +**After:** +```javascript +const fs = require("node:fs"); + +const exists = fs.existsSync(String(123)); +``` + +### Case 2: Variable Arguments + +**Before:** +```javascript +const fs = require("node:fs"); + +function checkFile(path) { + return fs.existsSync(path); +} +``` + +**After:** +```javascript +const fs = require("node:fs"); + +function checkFile(path) { + if (typeof path !== 'string' && !Buffer.isBuffer(path) && !(path instanceof URL)) { + path = String(path); + } + return fs.existsSync(path); +} +``` + +### Case 3: Null Values + +**Before:** +```javascript +const fs = require("node:fs"); + +const fileExists = fs.existsSync(null); +``` + +**After:** +```javascript +const fs = require("node:fs"); + +const fileExists = fs.existsSync(String(null || '')); +``` + +### Case 4: Object Arguments + +**Before:** +```javascript +import { existsSync } from "node:fs"; + +const exists = existsSync({ path: '/some/file' }); +``` + +**After:** +```javascript +import { existsSync } from "node:fs"; + +const exists = existsSync(String({ path: '/some/file' })); +``` + +## References + +- [DEP0187: Passing invalid argument types to fs.existsSync](https://nodejs.org/api/deprecations.html#dep0187-passing-invalid-argument-types-to-fsexistssync) +- [Node.js fs.existsSync() documentation](https://nodejs.org/api/fs.html#fsexistssyncpath) + +## Usage + +See the main [README](../../README.md) for usage instructions. + diff --git a/recipes/fs-existssync-valid-args/codemod.yaml b/recipes/fs-existssync-valid-args/codemod.yaml new file mode 100644 index 00000000..7bdfe15d --- /dev/null +++ b/recipes/fs-existssync-valid-args/codemod.yaml @@ -0,0 +1,25 @@ +schema_version: "1.0" +name: "@nodejs/fs-existssync-valid-args" +version: "1.0.0" +description: Handle DEP0187 via validating and converting invalid argument types to fs.existsSync(). +author: Node.js Contributors +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - fs + - existsSync + - DEP0187 + +registry: + access: public + visibility: public + diff --git a/recipes/fs-existssync-valid-args/package.json b/recipes/fs-existssync-valid-args/package.json new file mode 100644 index 00000000..50dc88ee --- /dev/null +++ b/recipes/fs-existssync-valid-args/package.json @@ -0,0 +1,25 @@ +{ + "name": "@nodejs/fs-existssync-valid-args", + "version": "1.0.0", + "description": "Handle DEP0187 via validating and converting invalid argument types to fs.existsSync().", + "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/fs-existssync-valid-args", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Node.js Contributors", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/fs-existssync-valid-args/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} + diff --git a/recipes/fs-existssync-valid-args/src/workflow.ts b/recipes/fs-existssync-valid-args/src/workflow.ts new file mode 100644 index 00000000..3a248375 --- /dev/null +++ b/recipes/fs-existssync-valid-args/src/workflow.ts @@ -0,0 +1,193 @@ +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 } from "@codemod.com/jssg-types/main"; +import type JS from "@codemod.com/jssg-types/langs/javascript"; + +/** + * Transform function that validates and converts invalid argument types to fs.existsSync(). + * This is useful to migrate code that passes invalid argument types which now causes + * deprecation warnings or errors (DEP0187). + * + * Handles: + * 1. Direct literal values (numbers, objects) → wrap with String() + * 2. null values → convert to String(null || '') + * 3. Variables/parameters → add type check before the call + * 4. String, Buffer, or URL arguments → leave as is (already valid) + * + * Works with both CommonJS (require) and ESM (import) syntax. + */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + + // Collect all fs import/require statements + const allStatementNodes = [ + ...getNodeImportStatements(root, "fs"), + ...getNodeRequireCalls(root, "fs"), + ]; + + for (const statementNode of allStatementNodes) { + // Try to resolve the binding path for fs.existsSync + const bindingPath = resolveBindingPath(statementNode, "fs.existsSync"); + if (!bindingPath) continue; + + // Find all calls to fs.existsSync + const callNodes = rootNode.findAll({ + rule: { + pattern: `${bindingPath}($ARG)`, + }, + }); + + for (const callNode of callNodes) { + const argNode = callNode.getMatch("ARG"); + if (!argNode) continue; + + const argText = argNode.text(); + const argKind = argNode.kind(); + + // Skip if already valid types or wrapped in String/Buffer/URL constructor + if (isAlreadyValid(argText, argKind)) { + continue; + } + + // Handle different argument types + if (argKind === "null") { + // Case: fs.existsSync(null) → fs.existsSync(String(null || '')) + edits.push(argNode.replace(`String(${argText} || '')`)); + } else if (argKind === "identifier") { + // Case: fs.existsSync(path) → add type check before the call + const edit = addTypeCheckForVariable(callNode, argText); + if (edit) { + edits.push(edit); + } + } else if (isLiteralOrExpression(argKind)) { + // Case: fs.existsSync(123) or fs.existsSync({ path: '/file' }) + // → fs.existsSync(String(123)) or fs.existsSync(String({ path: '/file' })) + edits.push(argNode.replace(`String(${argText})`)); + } + } + } + + // Also handle destructured import/require: const { existsSync } = require('fs') + for (const statementNode of allStatementNodes) { + const bindingPath = resolveBindingPath(statementNode, "existsSync"); + if (!bindingPath) continue; + + // Find all calls to existsSync (destructured) + const callNodes = rootNode.findAll({ + rule: { + pattern: `${bindingPath}($ARG)`, + }, + }); + + for (const callNode of callNodes) { + const argNode = callNode.getMatch("ARG"); + if (!argNode) continue; + + const argText = argNode.text(); + const argKind = argNode.kind(); + + // Skip if already valid types or wrapped + if (isAlreadyValid(argText, argKind)) { + continue; + } + + // Handle different argument types + if (argKind === "null") { + edits.push(argNode.replace(`String(${argText} || '')`)); + } else if (argKind === "identifier") { + const edit = addTypeCheckForVariable(callNode, argText); + if (edit) { + edits.push(edit); + } + } else if (isLiteralOrExpression(argKind)) { + edits.push(argNode.replace(`String(${argText})`)); + } + } + } + + if (edits.length === 0) return null; + + return rootNode.commitEdits(edits); +} + +/** + * Check if the argument is already a valid type (string, Buffer, URL) + * or already wrapped in String/Buffer/URL constructor + */ +function isAlreadyValid(argText: string, argKind: string): boolean { + // Check if it's a string literal + if (argKind === "string" || argKind === "template_string") { + return true; + } + + // Check if already wrapped with String(), Buffer.from(), or new URL() + if ( + argText.startsWith("String(") || + argText.startsWith("Buffer.") || + argText.startsWith("new Buffer(") || + argText.startsWith("new URL(") || + argText.includes("Buffer.isBuffer(") || + argText.includes("instanceof URL") + ) { + return true; + } + + return false; +} + +/** + * Check if the argument kind is a literal or expression that should be wrapped + */ +function isLiteralOrExpression(argKind: string): boolean { + return [ + "number", + "object", + "array", + "true", + "false", + "undefined", + "binary_expression", + "unary_expression", + "call_expression", + ].includes(argKind); +} + +/** + * Add type check for variable arguments + * Wraps the fs.existsSync() call with a type check + */ +function addTypeCheckForVariable(callNode: any, varName: string): Edit | null { + // Find the statement containing the call + let statementNode = callNode.parent(); + while (statementNode && !isStatement(statementNode.kind())) { + statementNode = statementNode.parent(); + } + + if (!statementNode) return null; + + const statementText = statementNode.text(); + + // Add type check before the statement + const typeCheck = `if (typeof ${varName} !== 'string' && !Buffer.isBuffer(${varName}) && !(${varName} instanceof URL)) {\n ${varName} = String(${varName});\n }\n `; + + const newStatement = typeCheck + statementText; + return statementNode.replace(newStatement); +} + +/** + * Check if a node kind represents a statement + */ +function isStatement(kind: string): boolean { + return [ + "expression_statement", + "return_statement", + "variable_declaration", + "if_statement", + "for_statement", + "while_statement", + "do_statement", + "switch_statement", + ].includes(kind); +} diff --git a/recipes/fs-existssync-valid-args/tests/expected/case1-literal-number.js b/recipes/fs-existssync-valid-args/tests/expected/case1-literal-number.js new file mode 100644 index 00000000..5d964fb4 --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/expected/case1-literal-number.js @@ -0,0 +1,4 @@ +const fs = require("node:fs"); + +const exists = fs.existsSync(String(123)); + diff --git a/recipes/fs-existssync-valid-args/tests/expected/case2-variable.js b/recipes/fs-existssync-valid-args/tests/expected/case2-variable.js new file mode 100644 index 00000000..7dd96aa5 --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/expected/case2-variable.js @@ -0,0 +1,9 @@ +const fs = require("node:fs"); + +function checkFile(path) { + if (typeof path !== 'string' && !Buffer.isBuffer(path) && !(path instanceof URL)) { + path = String(path); + } + return fs.existsSync(path); +} + diff --git a/recipes/fs-existssync-valid-args/tests/expected/case3-null.js b/recipes/fs-existssync-valid-args/tests/expected/case3-null.js new file mode 100644 index 00000000..5ba331db --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/expected/case3-null.js @@ -0,0 +1,4 @@ +const fs = require("node:fs"); + +const fileExists = fs.existsSync(String(null || '')); + diff --git a/recipes/fs-existssync-valid-args/tests/expected/case4-object.mjs b/recipes/fs-existssync-valid-args/tests/expected/case4-object.mjs new file mode 100644 index 00000000..db77c14e --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/expected/case4-object.mjs @@ -0,0 +1,4 @@ +import { existsSync } from "node:fs"; + +const exists = existsSync(String({ path: '/some/file' })); + diff --git a/recipes/fs-existssync-valid-args/tests/expected/case5-valid-string.js b/recipes/fs-existssync-valid-args/tests/expected/case5-valid-string.js new file mode 100644 index 00000000..4807109d --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/expected/case5-valid-string.js @@ -0,0 +1,7 @@ +const fs = require("node:fs"); + +// These should not be modified as they are already valid +const exists1 = fs.existsSync('/path/to/file'); +const exists2 = fs.existsSync("another/path"); +const exists3 = fs.existsSync(`template/path`); + diff --git a/recipes/fs-existssync-valid-args/tests/expected/case6-esm-namespace.mjs b/recipes/fs-existssync-valid-args/tests/expected/case6-esm-namespace.mjs new file mode 100644 index 00000000..f3382d90 --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/expected/case6-esm-namespace.mjs @@ -0,0 +1,5 @@ +import fs from "node:fs"; + +const exists = fs.existsSync(String(123)); +const exists2 = fs.existsSync(String({ path: '/file' })); + diff --git a/recipes/fs-existssync-valid-args/tests/expected/case7-multiple-calls.js b/recipes/fs-existssync-valid-args/tests/expected/case7-multiple-calls.js new file mode 100644 index 00000000..23d9b6ff --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/expected/case7-multiple-calls.js @@ -0,0 +1,7 @@ +const fs = require("fs"); + +const a = fs.existsSync(String(123)); +const b = fs.existsSync(String(null || '')); +const c = fs.existsSync(String(false)); +const d = fs.existsSync(String([1, 2, 3])); + diff --git a/recipes/fs-existssync-valid-args/tests/expected/case8-mixed-valid-invalid.js b/recipes/fs-existssync-valid-args/tests/expected/case8-mixed-valid-invalid.js new file mode 100644 index 00000000..5f1e98ce --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/expected/case8-mixed-valid-invalid.js @@ -0,0 +1,7 @@ +const { existsSync } = require("node:fs"); + +// Mix of valid and invalid arguments +const valid = existsSync('/valid/path'); +const invalid1 = existsSync(String(456)); +const invalid2 = existsSync(String(undefined)); + diff --git a/recipes/fs-existssync-valid-args/tests/input/case1-literal-number.js b/recipes/fs-existssync-valid-args/tests/input/case1-literal-number.js new file mode 100644 index 00000000..9242061f --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/input/case1-literal-number.js @@ -0,0 +1,4 @@ +const fs = require("node:fs"); + +const exists = fs.existsSync(123); + diff --git a/recipes/fs-existssync-valid-args/tests/input/case2-variable.js b/recipes/fs-existssync-valid-args/tests/input/case2-variable.js new file mode 100644 index 00000000..f82d1511 --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/input/case2-variable.js @@ -0,0 +1,6 @@ +const fs = require("node:fs"); + +function checkFile(path) { + return fs.existsSync(path); +} + diff --git a/recipes/fs-existssync-valid-args/tests/input/case3-null.js b/recipes/fs-existssync-valid-args/tests/input/case3-null.js new file mode 100644 index 00000000..87cfb8a7 --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/input/case3-null.js @@ -0,0 +1,4 @@ +const fs = require("node:fs"); + +const fileExists = fs.existsSync(null); + diff --git a/recipes/fs-existssync-valid-args/tests/input/case4-object.mjs b/recipes/fs-existssync-valid-args/tests/input/case4-object.mjs new file mode 100644 index 00000000..6fed6c2d --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/input/case4-object.mjs @@ -0,0 +1,4 @@ +import { existsSync } from "node:fs"; + +const exists = existsSync({ path: '/some/file' }); + diff --git a/recipes/fs-existssync-valid-args/tests/input/case5-valid-string.js b/recipes/fs-existssync-valid-args/tests/input/case5-valid-string.js new file mode 100644 index 00000000..4807109d --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/input/case5-valid-string.js @@ -0,0 +1,7 @@ +const fs = require("node:fs"); + +// These should not be modified as they are already valid +const exists1 = fs.existsSync('/path/to/file'); +const exists2 = fs.existsSync("another/path"); +const exists3 = fs.existsSync(`template/path`); + diff --git a/recipes/fs-existssync-valid-args/tests/input/case6-esm-namespace.mjs b/recipes/fs-existssync-valid-args/tests/input/case6-esm-namespace.mjs new file mode 100644 index 00000000..e2550df1 --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/input/case6-esm-namespace.mjs @@ -0,0 +1,5 @@ +import fs from "node:fs"; + +const exists = fs.existsSync(123); +const exists2 = fs.existsSync({ path: '/file' }); + diff --git a/recipes/fs-existssync-valid-args/tests/input/case7-multiple-calls.js b/recipes/fs-existssync-valid-args/tests/input/case7-multiple-calls.js new file mode 100644 index 00000000..c00cfc9e --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/input/case7-multiple-calls.js @@ -0,0 +1,7 @@ +const fs = require("fs"); + +const a = fs.existsSync(123); +const b = fs.existsSync(null); +const c = fs.existsSync(false); +const d = fs.existsSync([1, 2, 3]); + diff --git a/recipes/fs-existssync-valid-args/tests/input/case8-mixed-valid-invalid.js b/recipes/fs-existssync-valid-args/tests/input/case8-mixed-valid-invalid.js new file mode 100644 index 00000000..9181062d --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/input/case8-mixed-valid-invalid.js @@ -0,0 +1,7 @@ +const { existsSync } = require("node:fs"); + +// Mix of valid and invalid arguments +const valid = existsSync('/valid/path'); +const invalid1 = existsSync(456); +const invalid2 = existsSync(undefined); + diff --git a/recipes/fs-existssync-valid-args/workflow.yaml b/recipes/fs-existssync-valid-args/workflow.yaml new file mode 100644 index 00000000..a8c6f9e6 --- /dev/null +++ b/recipes/fs-existssync-valid-args/workflow.yaml @@ -0,0 +1,26 @@ +# 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 DEP0187 via validating and converting invalid argument types to fs.existsSync(). + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript + From eb5953e1195b9ef84ec7d2487ec0bf4c854ec505 Mon Sep 17 00:00:00 2001 From: techflare641 Date: Fri, 10 Oct 2025 09:41:07 -0500 Subject: [PATCH 2/4] Update codemod.yaml & readme --- recipes/fs-existssync-valid-args/README.md | 11 +---------- recipes/fs-existssync-valid-args/codemod.yaml | 7 ------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/recipes/fs-existssync-valid-args/README.md b/recipes/fs-existssync-valid-args/README.md index ec4772ae..4951d2d6 100644 --- a/recipes/fs-existssync-valid-args/README.md +++ b/recipes/fs-existssync-valid-args/README.md @@ -4,7 +4,7 @@ This codemod validates and converts invalid argument types to `fs.existsSync()`. ## Description -Starting with Node.js, passing invalid argument types to `fs.existsSync()` triggers a deprecation warning (DEP0187). The function should only receive `string`, `Buffer`, or `URL` arguments. +Starting with Node.js, passing invalid argument types to `fs.existsSync()` triggers a deprecation warning ([DEP0187](https://nodejs.org/api/deprecations.html#dep0187-passing-invalid-argument-types-to-fsexistssync)). The function should only receive `string`, `Buffer`, or `URL` arguments as documented in the [Node.js fs.existsSync() documentation](https://nodejs.org/api/fs.html#fsexistssyncpath). This codemod automatically: - Validates that `fs.existsSync()` receives valid argument types @@ -85,12 +85,3 @@ import { existsSync } from "node:fs"; const exists = existsSync(String({ path: '/some/file' })); ``` -## References - -- [DEP0187: Passing invalid argument types to fs.existsSync](https://nodejs.org/api/deprecations.html#dep0187-passing-invalid-argument-types-to-fsexistssync) -- [Node.js fs.existsSync() documentation](https://nodejs.org/api/fs.html#fsexistssyncpath) - -## Usage - -See the main [README](../../README.md) for usage instructions. - diff --git a/recipes/fs-existssync-valid-args/codemod.yaml b/recipes/fs-existssync-valid-args/codemod.yaml index 7bdfe15d..531574e8 100644 --- a/recipes/fs-existssync-valid-args/codemod.yaml +++ b/recipes/fs-existssync-valid-args/codemod.yaml @@ -12,13 +12,6 @@ targets: - javascript - typescript -keywords: - - transformation - - migration - - fs - - existsSync - - DEP0187 - registry: access: public visibility: public From f9f11b19d44e064dd2fc5f4ab7be288d764950d2 Mon Sep 17 00:00:00 2001 From: techflare641 Date: Fri, 10 Oct 2025 09:45:45 -0500 Subject: [PATCH 3/4] Fix PR feedbacks --- recipes/fs-existssync-valid-args/src/workflow.ts | 13 ++++++++++--- utils/src/codemod-jssg-context.ts | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/recipes/fs-existssync-valid-args/src/workflow.ts b/recipes/fs-existssync-valid-args/src/workflow.ts index 3a248375..41c2cecd 100644 --- a/recipes/fs-existssync-valid-args/src/workflow.ts +++ b/recipes/fs-existssync-valid-args/src/workflow.ts @@ -1,7 +1,7 @@ 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 } from "@codemod.com/jssg-types/main"; +import type { SgRoot, Edit, SgNode } from "@codemod.com/jssg-types/main"; import type JS from "@codemod.com/jssg-types/langs/javascript"; /** @@ -27,9 +27,13 @@ export default function transform(root: SgRoot): string | null { ...getNodeRequireCalls(root, "fs"), ]; + // If any import found don't process the file + if (!allStatementNodes.length) return null; + for (const statementNode of allStatementNodes) { // Try to resolve the binding path for fs.existsSync const bindingPath = resolveBindingPath(statementNode, "fs.existsSync"); + if (!bindingPath) continue; // Find all calls to fs.existsSync @@ -72,6 +76,7 @@ export default function transform(root: SgRoot): string | null { // Also handle destructured import/require: const { existsSync } = require('fs') for (const statementNode of allStatementNodes) { const bindingPath = resolveBindingPath(statementNode, "existsSync"); + if (!bindingPath) continue; // Find all calls to existsSync (destructured) @@ -107,7 +112,7 @@ export default function transform(root: SgRoot): string | null { } } - if (edits.length === 0) return null; + if (!edits.length) return null; return rootNode.commitEdits(edits); } @@ -158,9 +163,10 @@ function isLiteralOrExpression(argKind: string): boolean { * Add type check for variable arguments * Wraps the fs.existsSync() call with a type check */ -function addTypeCheckForVariable(callNode: any, varName: string): Edit | null { +function addTypeCheckForVariable(callNode: SgNode, varName: string): Edit | null { // Find the statement containing the call let statementNode = callNode.parent(); + while (statementNode && !isStatement(statementNode.kind())) { statementNode = statementNode.parent(); } @@ -173,6 +179,7 @@ function addTypeCheckForVariable(callNode: any, varName: string): Edit | null { const typeCheck = `if (typeof ${varName} !== 'string' && !Buffer.isBuffer(${varName}) && !(${varName} instanceof URL)) {\n ${varName} = String(${varName});\n }\n `; const newStatement = typeCheck + statementText; + return statementNode.replace(newStatement); } diff --git a/utils/src/codemod-jssg-context.ts b/utils/src/codemod-jssg-context.ts index 659d81ee..1f575af8 100644 --- a/utils/src/codemod-jssg-context.ts +++ b/utils/src/codemod-jssg-context.ts @@ -4,9 +4,9 @@ import json from "@ast-grep/lang-json"; import { registerDynamicLanguage } from "@ast-grep/napi"; registerDynamicLanguage({ - // @ts-ignore - https://github.com/ast-grep/langs/tree/main/packages/json#usage + // @ts-expect-error - https://github.com/ast-grep/langs/tree/main/packages/json#usage json, - // @ts-ignore - https://github.com/ast-grep/langs/tree/main/packages/bash#usage + // @ts-expect-error - https://github.com/ast-grep/langs/tree/main/packages/bash#usage bash }); From 1e239a298b000d180978f40d0d6c69e343f4a96a Mon Sep 17 00:00:00 2001 From: techflare641 Date: Fri, 10 Oct 2025 13:30:06 -0500 Subject: [PATCH 4/4] Update based on PR reviews --- .../fs-existssync-valid-args/src/workflow.ts | 50 ++++++++++--------- .../tests/expected/case9-already-wrapped.js | 12 +++++ .../tests/input/case9-already-wrapped.js | 12 +++++ 3 files changed, 50 insertions(+), 24 deletions(-) create mode 100644 recipes/fs-existssync-valid-args/tests/expected/case9-already-wrapped.js create mode 100644 recipes/fs-existssync-valid-args/tests/input/case9-already-wrapped.js diff --git a/recipes/fs-existssync-valid-args/src/workflow.ts b/recipes/fs-existssync-valid-args/src/workflow.ts index 41c2cecd..9a479d65 100644 --- a/recipes/fs-existssync-valid-args/src/workflow.ts +++ b/recipes/fs-existssync-valid-args/src/workflow.ts @@ -65,7 +65,7 @@ export default function transform(root: SgRoot): string | null { if (edit) { edits.push(edit); } - } else if (isLiteralOrExpression(argKind)) { + } else if (LITERAL_OR_EXPRESSION_KINDS.includes(argKind)) { // Case: fs.existsSync(123) or fs.existsSync({ path: '/file' }) // → fs.existsSync(String(123)) or fs.existsSync(String({ path: '/file' })) edits.push(argNode.replace(`String(${argText})`)); @@ -106,7 +106,7 @@ export default function transform(root: SgRoot): string | null { if (edit) { edits.push(edit); } - } else if (isLiteralOrExpression(argKind)) { + } else if (LITERAL_OR_EXPRESSION_KINDS.includes(argKind)) { edits.push(argNode.replace(`String(${argText})`)); } } @@ -122,19 +122,23 @@ export default function transform(root: SgRoot): string | null { * or already wrapped in String/Buffer/URL constructor */ function isAlreadyValid(argText: string, argKind: string): boolean { - // Check if it's a string literal + // Check if it's a string literal (already valid) if (argKind === "string" || argKind === "template_string") { return true; } - // Check if already wrapped with String(), Buffer.from(), or new URL() + // Check if it's a new expression (e.g., new URL(), new Buffer()) + if (argKind === "new_expression") { + return true; + } + + // Check if already wrapped with String() or Buffer methods + // Using regex for more robust matching that handles whitespace if ( - argText.startsWith("String(") || - argText.startsWith("Buffer.") || - argText.startsWith("new Buffer(") || - argText.startsWith("new URL(") || - argText.includes("Buffer.isBuffer(") || - argText.includes("instanceof URL") + /^\s*String\s*\(/.test(argText) || + /^\s*Buffer\s*\./.test(argText) || + /Buffer\.isBuffer\s*\(/.test(argText) || + /instanceof\s+URL/.test(argText) ) { return true; } @@ -143,21 +147,19 @@ function isAlreadyValid(argText: string, argKind: string): boolean { } /** - * Check if the argument kind is a literal or expression that should be wrapped + * Node kinds that represent literals or expressions that should be wrapped with String() */ -function isLiteralOrExpression(argKind: string): boolean { - return [ - "number", - "object", - "array", - "true", - "false", - "undefined", - "binary_expression", - "unary_expression", - "call_expression", - ].includes(argKind); -} +const LITERAL_OR_EXPRESSION_KINDS = [ + "number", + "object", + "array", + "true", + "false", + "undefined", + "binary_expression", + "unary_expression", + "call_expression", +]; /** * Add type check for variable arguments diff --git a/recipes/fs-existssync-valid-args/tests/expected/case9-already-wrapped.js b/recipes/fs-existssync-valid-args/tests/expected/case9-already-wrapped.js new file mode 100644 index 00000000..f0e8bf02 --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/expected/case9-already-wrapped.js @@ -0,0 +1,12 @@ +const fs = require("node:fs"); + +// These should NOT be modified - already wrapped with String() +const exists1 = fs.existsSync(String(123)); +const exists2 = fs.existsSync(String(null)); + +// These should NOT be modified - already using Buffer +const exists3 = fs.existsSync(Buffer.from('/path')); + +// These should NOT be modified - already using new URL +const exists4 = fs.existsSync(new URL('file:///path')); + diff --git a/recipes/fs-existssync-valid-args/tests/input/case9-already-wrapped.js b/recipes/fs-existssync-valid-args/tests/input/case9-already-wrapped.js new file mode 100644 index 00000000..f0e8bf02 --- /dev/null +++ b/recipes/fs-existssync-valid-args/tests/input/case9-already-wrapped.js @@ -0,0 +1,12 @@ +const fs = require("node:fs"); + +// These should NOT be modified - already wrapped with String() +const exists1 = fs.existsSync(String(123)); +const exists2 = fs.existsSync(String(null)); + +// These should NOT be modified - already using Buffer +const exists3 = fs.existsSync(Buffer.from('/path')); + +// These should NOT be modified - already using new URL +const exists4 = fs.existsSync(new URL('file:///path')); +