Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions recipes/fs-existssync-valid-args/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# 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](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
- 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' }));
```

18 changes: 18 additions & 0 deletions recipes/fs-existssync-valid-args/codemod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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

registry:
access: public
visibility: public

25 changes: 25 additions & 0 deletions recipes/fs-existssync-valid-args/package.json
Original file line number Diff line number Diff line change
@@ -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": "*"
}
}

200 changes: 200 additions & 0 deletions recipes/fs-existssync-valid-args/src/workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
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 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<JS>): string | null {
const rootNode = root.root();
const edits: Edit[] = [];

// Collect all fs import/require statements
const allStatementNodes = [
...getNodeImportStatements(root, "fs"),
...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
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) 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: SgNode<JS>, 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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const fs = require("node:fs");

const exists = fs.existsSync(String(123));

Original file line number Diff line number Diff line change
@@ -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);
}

4 changes: 4 additions & 0 deletions recipes/fs-existssync-valid-args/tests/expected/case3-null.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const fs = require("node:fs");

const fileExists = fs.existsSync(String(null || ''));

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { existsSync } from "node:fs";

const exists = existsSync(String({ path: '/some/file' }));

Original file line number Diff line number Diff line change
@@ -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`);

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import fs from "node:fs";

const exists = fs.existsSync(String(123));
const exists2 = fs.existsSync(String({ path: '/file' }));

Original file line number Diff line number Diff line change
@@ -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]));

Original file line number Diff line number Diff line change
@@ -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));

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const fs = require("node:fs");

const exists = fs.existsSync(123);

Loading