Skip to content

feat(DEP0162): add codemod for fs write coercion#416

Open
syhstanley wants to merge 13 commits intonodejs:mainfrom
syhstanley:feat/fs-write-coercion-codemod
Open

feat(DEP0162): add codemod for fs write coercion#416
syhstanley wants to merge 13 commits intonodejs:mainfrom
syhstanley:feat/fs-write-coercion-codemod

Conversation

@syhstanley
Copy link
Copy Markdown
Contributor

@syhstanley syhstanley commented Mar 29, 2026

Summary

Adds a codemod for DEP0162 — implicit coercion of objects in fs.write() and fs.writeFileSync().

  • Wraps the data parameter with String() for fs.writeFile(), fs.writeFileSync(), fs.appendFile(), fs.appendFileSync(), fs.write(), and their fs/promises variants
  • Skips already-safe types: string literals, template literals, Buffer.from(), new Uint8Array(), .toString(), String() wrappers
  • Handles both CommonJS require('fs') and ESM import { writeFile } from 'node:fs/promises'
  • Handles destructured imports

Test plan

Closes #412

🤖 Generated with Claude Code

stanleyshen2003 and others added 2 commits March 30, 2026 03:42
Add explicit String() conversion for objects passed as the data
parameter to fs.writeFile(), fs.writeFileSync(), fs.appendFile(),
fs.appendFileSync(), fs.write(), and their promises variants.

Closes nodejs#412

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add missing README.md (required per CONTRIBUTING.md)
- Add yaml-language-server schema comment to workflow.yaml
- Sort include file patterns alphabetically
- Fix isSafeType: remove dead template literal check, tighten String() match
- Add isLikelyBufferOverload guard for fs.write() buffer overload
- Support float numeric literals in isSafeType
- Normalize test file naming (file-02 → file-2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@AugustinMauroy AugustinMauroy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not too bad but:

for you info you have to use this structure of testing

Codemod leverages a before ("input") + after ("expected") snapshot comparison. Codemod supports 2 options:

  • 👍 Single-file fixtures
    tests/
      some-test-case-description/
        input.ts
        expected.ts
      another-test-case-description/
        input.ts
        expected.ts
    
  • 👎 Directory snapshot fixtures
    tests/
      input/
        some-test-case-description.ts
        another-test-case-description.ts
      expected
        some-test-case-description.ts
        another-test-case-description.ts
    

Use the Single-file fixtures option.

Comment thread recipes/fs-write-coercion/src/workflow.ts
Comment thread recipes/fs-write-coercion/src/workflow.ts Outdated
@AugustinMauroy AugustinMauroy added the awaiting author Reviewer has requested something from the author label Mar 31, 2026
stanleyshen2003 and others added 2 commits April 1, 2026 16:45
- Migrate tests to single-file fixture structure (input.ts + expected.ts per case)
- Use getModuleDependencies utility instead of separate import/require helpers
- Remove unnecessary escapeRegex() wrapper for identifier matching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@syhstanley
Copy link
Copy Markdown
Contributor Author

Thanks for your review! Please check the new commits.

Change test fixture path from ./ to ./tests to match single-file fixture structure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@AugustinMauroy AugustinMauroy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGMT !

@AugustinMauroy AugustinMauroy added awaiting reviewer Author has responded and needs action from the reviewer and removed awaiting author Reviewer has requested something from the author labels Apr 1, 2026
Comment thread recipes/fs-write-coercion/src/workflow.ts Outdated
stanleyshen2003 and others added 2 commits April 15, 2026 21:59
Simplify filter logic by using the isNamed() method instead of directly
comparing kind() against punctuation characters. This is more semantic and
aligns with AST best practices.

Fixes review feedback from brunocroh.
Comment thread recipes/fs-write-coercion/src/workflow.ts Outdated
@AugustinMauroy
Copy link
Copy Markdown
Member

your test are failing + lint need to be fixed

…codemod

- Remove unused `Edit` and `SgNode` imports that were causing linter warnings
- Fix syntax error on line 133: replace semicolon with closing parenthesis in filter() chain

This resolves the CI linting failures and allows tests to pass cleanly.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@syhstanley
Copy link
Copy Markdown
Contributor Author

Fixed

@JakobJingleheimer JakobJingleheimer changed the title feat: add codemod for DEP0162 fs write coercion feat(DEP0162): add codemod for fs write coercion Apr 23, 2026
has: {
field: 'function',
any: [
{ kind: 'identifier', regex: `^${local}$` },
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{ kind: 'identifier', regex: `^${local}$` },
{ kind: 'identifier', pattern: local },

if (!local) continue;

// Find all call expressions for this binding
const calls = rootNode.findAll({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const calls = rootNode.findAll({
const calls = rootNode.findAll<'call_expression'>({


for (const call of calls) {
// Get the arguments node
const argsNode = call.find({ rule: { kind: 'arguments' } });
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const argsNode = call.find({ rule: { kind: 'arguments' } });
const argsNode = call.field('arguments');

Comment on lines +19 to +55

/**
* Check if a text expression is already a safe type that doesn't need String() wrapping.
* Safe types: string literals, template literals, Buffer/TypedArray expressions,
* already-wrapped String() or .toString() calls.
*/
function isSafeType(text: string): boolean {
const trimmed = text.trim();

// String literals and template literals (', ", `)
if (/^['"`]/.test(trimmed)) return true;

// Already has .toString()
if (trimmed.endsWith('.toString()')) return true;

// Already wrapped in String() — exact match to avoid false positives like Stringify()
if (/^String\(/.test(trimmed) && trimmed.endsWith(')')) return true;

// Buffer.from(), Buffer.alloc(), etc.
if (/^Buffer\.\w+\(/.test(trimmed)) return true;

// new Uint8Array, new Int8Array, etc.
if (
/^new\s+(Uint8Array|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|DataView)\b/.test(
trimmed,
)
)
return true;

// Numeric literal (integers and floats)
if (/^\d+(\.\d+)?$/.test(trimmed)) return true;

// null or undefined
if (trimmed === 'null' || trimmed === 'undefined') return true;

return false;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* Check if a text expression is already a safe type that doesn't need String() wrapping.
* Safe types: string literals, template literals, Buffer/TypedArray expressions,
* already-wrapped String() or .toString() calls.
*/
function isSafeType(text: string): boolean {
const trimmed = text.trim();
// String literals and template literals (', ", `)
if (/^['"`]/.test(trimmed)) return true;
// Already has .toString()
if (trimmed.endsWith('.toString()')) return true;
// Already wrapped in String() — exact match to avoid false positives like Stringify()
if (/^String\(/.test(trimmed) && trimmed.endsWith(')')) return true;
// Buffer.from(), Buffer.alloc(), etc.
if (/^Buffer\.\w+\(/.test(trimmed)) return true;
// new Uint8Array, new Int8Array, etc.
if (
/^new\s+(Uint8Array|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|DataView)\b/.test(
trimmed,
)
)
return true;
// Numeric literal (integers and floats)
if (/^\d+(\.\d+)?$/.test(trimmed)) return true;
// null or undefined
if (trimmed === 'null' || trimmed === 'undefined') return true;
return false;
}
const TypedArrayRegex = `^(Uint8Array|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|DataView)$`;
/**
* Check if a text expression is already a safe type that doesn't need String() wrapping.
* Safe types: string literals, template literals, Buffer/TypedArray expressions,
* already-wrapped String() or .toString() calls.
*/
function isSafeType(node: SgNode<Js, Kinds<Js>>): boolean {
const safe = node.find({
constraints: {
METHOD: {
regex: TypedArrayRegex,
},
},
rule: {
any: [
{ kind: 'number' },
{ kind: 'string' },
{ kind: 'string_fragment' },
{ pattern: '$ANY.toString()' },
{ pattern: '$METHOD' },
{ pattern: 'Buffer.from($ANY)' },
{ pattern: 'String($ANY)' },
{ pattern: 'null' },
{ pattern: 'undefined' },
],
},
});
return Boolean(safe);
}

@JakobJingleheimer JakobJingleheimer added dep:EoL Migrate a deprecation for an EoL version of node dep:19.x Migrate a deprecation for node 19.x labels Apr 25, 2026
@brunocroh brunocroh added awaiting author Reviewer has requested something from the author and removed awaiting reviewer Author has responded and needs action from the reviewer labels Apr 26, 2026
stanleyshen2003 and others added 2 commits April 27, 2026 18:07
This fixes the TS2304 error where Edit was used but not imported.

Co-Authored-By: 員工克勞德 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting author Reviewer has requested something from the author dep:EoL Migrate a deprecation for an EoL version of node dep:19.x Migrate a deprecation for node 19.x ⚠️ fully-AI-generated

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fs.write() and fs.writeFileSync() coercion to string

5 participants