Skip to content

Commit ca0bd07

Browse files
update
1 parent 0cd173d commit ca0bd07

File tree

5 files changed

+124
-103
lines changed

5 files changed

+124
-103
lines changed

recipes/fs-truncate-fd-deprecation/src/workflow.ts

Lines changed: 80 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement";
22
import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call";
3+
import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path";
34
import type { SgRoot, Edit, SgNode } from "@codemod.com/jssg-types/main";
45
import type Js from "@codemod.com/jssg-types/langs/javascript";
56

@@ -19,95 +20,98 @@ export default function transform(root: SgRoot<Js>): string | null {
1920
const rootNode = root.root();
2021
const edits: Edit[] = [];
2122

22-
// Track what imports need to be updated
23+
// Bindings we care about and their replacements for truncate -> ftruncate
24+
const checks = [
25+
{
26+
path: "$.truncate",
27+
prop: "truncate",
28+
replaceFn: (name: string) => name.replace(/truncate$/, "ftruncate"),
29+
isSync: false
30+
},
31+
{
32+
path: "$.truncateSync",
33+
prop: "truncateSync",
34+
replaceFn: (name: string) => name.replace(/truncateSync$/, "ftruncateSync"),
35+
isSync: true
36+
},
37+
{
38+
path: "$.promises.truncate",
39+
prop: "truncate",
40+
replaceFn: (name: string) => name.replace(/truncate$/, "ftruncate"),
41+
isSync: false
42+
},
43+
];
44+
45+
// Gather fs import/require statements to resolve local binding names
46+
const stmtNodes = [
47+
...getNodeRequireCalls(root, "fs"),
48+
...getNodeImportStatements(root, "fs"),
49+
];
50+
2351
let usedTruncate = false;
2452
let usedTruncateSync = false;
2553

26-
// Find fs.truncate and fs.truncateSync calls (these are always safe to transform)
27-
const fsTruncateCalls = rootNode.findAll({
28-
rule: {
29-
any: [
30-
{ pattern: "fs.truncate($FD, $LEN, $CALLBACK)" },
31-
{ pattern: "fs.truncate($FD, $LEN)" },
32-
{ pattern: "fs.truncateSync($FD, $LEN)" }
33-
]
34-
}
35-
});
54+
for (const stmt of stmtNodes) {
55+
for (const check of checks) {
56+
const local = resolveBindingPath(stmt, check.path);
57+
if (!local) continue;
3658

37-
// Transform fs.truncate calls
38-
for (const call of fsTruncateCalls) {
39-
const fdMatch = call.getMatch("FD");
40-
const lenMatch = call.getMatch("LEN");
41-
const callbackMatch = call.getMatch("CALLBACK");
42-
43-
if (!fdMatch || !lenMatch) continue;
44-
45-
const fd = fdMatch.text();
46-
const len = lenMatch.text();
47-
const callback = callbackMatch?.text();
48-
const callText = call.text();
49-
50-
let newCallText: string;
51-
if (callText.includes("fs.truncateSync(")) {
52-
newCallText = `fs.ftruncateSync(${fd}, ${len})`;
53-
} else {
54-
newCallText = callback
55-
? `fs.ftruncate(${fd}, ${len}, ${callback})`
56-
: `fs.ftruncate(${fd}, ${len})`;
57-
}
59+
// property name to look for on fs (e.g. 'truncate' or 'truncateSync')
60+
const propName = check.prop;
5861

59-
edits.push(call.replace(newCallText));
60-
}
62+
// Find call sites for the resolved local binding and for fs.<prop>
63+
const calls = rootNode.findAll({
64+
rule: {
65+
any: [
66+
{ pattern: `${local}($FD, $LEN, $CALLBACK)` },
67+
{ pattern: `${local}($FD, $LEN)` },
68+
{ pattern: `fs.${propName}($FD, $LEN, $CALLBACK)` },
69+
{ pattern: `fs.${propName}($FD, $LEN)` },
70+
],
71+
},
72+
});
6173

62-
// Find destructured truncate/truncateSync calls (need scope analysis)
63-
const destructuredCalls = rootNode.findAll({
64-
rule: {
65-
any: [
66-
{ pattern: "truncate($FD, $LEN, $CALLBACK)" },
67-
{ pattern: "truncate($FD, $LEN)" },
68-
{ pattern: "truncateSync($FD, $LEN)" }
69-
]
70-
}
71-
});
74+
let transformedAny = false;
75+
for (const call of calls) {
76+
const fdMatch = call.getMatch("FD");
77+
if (!fdMatch) continue;
78+
const fdText = fdMatch.text();
7279

73-
// Transform destructured calls only if they're from fs imports/requires
74-
for (const call of destructuredCalls) {
75-
if (isFromFsModule(call, root)) {
76-
const fdMatch = call.getMatch("FD");
77-
const lenMatch = call.getMatch("LEN");
78-
const callbackMatch = call.getMatch("CALLBACK");
79-
80-
if (!fdMatch || !lenMatch) continue;
81-
82-
const fd = fdMatch.text();
83-
const len = lenMatch.text();
84-
const callback = callbackMatch?.text();
85-
const callText = call.text();
86-
87-
// Check if this looks like a file descriptor
88-
if (isLikelyFileDescriptor(fd, rootNode)) {
89-
let newCallText: string;
90-
91-
if (callText.includes("truncateSync(")) {
92-
newCallText = `ftruncateSync(${fd}, ${len})`;
93-
usedTruncateSync = true;
94-
} else {
95-
newCallText = callback
96-
? `ftruncate(${fd}, ${len}, ${callback})`
97-
: `ftruncate(${fd}, ${len})`;
98-
usedTruncate = true;
99-
}
80+
// only transform when first arg is likely a file descriptor
81+
if (!isLikelyFileDescriptor(fdText, rootNode)) continue;
82+
83+
// Replace callee name (handles local alias and fs.<prop> usages)
84+
let newCallText = call.text();
85+
86+
// Replace occurrences of the local binding (e.g. "truncate" or alias)
87+
88+
const escapedLocal = local.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
89+
newCallText = newCallText.replace(new RegExp(`\\b${escapedLocal}\\b`), check.replaceFn(local));
90+
91+
// Also replace fs.truncate / fs.truncateSync usages
92+
newCallText = newCallText.replace(new RegExp(`\\bfs\\.${propName}\\b`, "g"), `fs.${check.replaceFn(propName)}`);
10093

10194
edits.push(call.replace(newCallText));
95+
transformedAny = true;
96+
if (check.isSync) usedTruncateSync = true; else usedTruncate = true;
97+
}
98+
99+
// Update import/destructure to include/rename to ftruncate/ftruncateSync where necessary
100+
const namedNode = stmt.find({ rule: { kind: "object_pattern" } }) || stmt.find({ rule: { kind: "named_imports" } });
101+
if (transformedAny && namedNode?.text().includes(propName)) {
102+
const original = namedNode.text();
103+
const newText = original.replace(new RegExp(`\\b${propName}\\b`, "g"), check.replaceFn(propName));
104+
if (newText !== original) {
105+
edits.push(namedNode.replace(newText));
106+
}
102107
}
103108
}
104109
}
105110

106-
// Update imports/requires if we have destructured calls that were transformed
107-
if (usedTruncate || usedTruncateSync) {
108-
updateImportsAndRequires(root, usedTruncate, usedTruncateSync, edits);
109-
}
110-
if (edits.length === 0) return null;
111+
// Update import/require statements to reflect renamed bindings
112+
updateImportsAndRequires(root, usedTruncate, usedTruncateSync, edits);
113+
114+
if (!edits.length) return null;
111115

112116
return rootNode.commitEdits(edits);
113117
}
@@ -116,9 +120,7 @@ export default function transform(root: SgRoot<Js>): string | null {
116120
* Update import and require statements to replace truncate functions with ftruncate
117121
*/
118122
function updateImportsAndRequires(root: SgRoot<Js>, usedTruncate: boolean, usedTruncateSync: boolean, edits: Edit[]): void {
119-
// @ts-ignore - ast-grep types are not fully compatible with JSSG types
120123
const importStatements = getNodeImportStatements(root, 'fs');
121-
// @ts-ignore - ast-grep types are not fully compatible with JSSG types
122124
const requireStatements = getNodeRequireCalls(root, 'fs');
123125

124126
// Update import and require statements
@@ -142,31 +144,6 @@ function updateImportsAndRequires(root: SgRoot<Js>, usedTruncate: boolean, usedT
142144
}
143145
}
144146

145-
/**
146-
* Check if a call expression is from a destructured fs import/require
147-
*/
148-
function isFromFsModule(call: SgNode<Js>, root: SgRoot<Js>): boolean {
149-
// @ts-ignore - ast-grep types are not fully compatible with JSSG types
150-
const importStatements = getNodeImportStatements(root, 'fs');
151-
// @ts-ignore - ast-grep types are not fully compatible with JSSG types
152-
const requireStatements = getNodeRequireCalls(root, 'fs');
153-
154-
// Get the function name being called (truncate or truncateSync)
155-
const callExpression = call.child(0);
156-
const functionName = callExpression?.text();
157-
if (!functionName) return false;
158-
159-
// Check if this function name appears in any fs import/require destructuring
160-
for (const statement of [...importStatements, ...requireStatements]) {
161-
const text = statement.text();
162-
if (text.includes("{") && text.includes(functionName)) {
163-
return true;
164-
}
165-
}
166-
167-
return false;
168-
}
169-
170147
/**
171148
* Helper function to determine if a parameter is likely a file descriptor
172149
* rather than a file path string.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const myFS = await import('node:fs');
2+
3+
// fd usage should be transformed
4+
const fd = myFS.openSync('file.txt', 'w');
5+
myFS.ftruncateSync(fd, 10);
6+
myFS.closeSync(fd);
7+
8+
// path usage should not be transformed
9+
myFS.truncate('other.txt', 5, (err) => {
10+
if (err) throw err;
11+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import('node:fs').then(fs => {
2+
// fd usage should be transformed
3+
const fd = fs.openSync('file.txt', 'w');
4+
fs.ftruncateSync(fd, 10);
5+
fs.closeSync(fd);
6+
7+
// path usage should not be transformed
8+
fs.truncate('other.txt', 5, (err) => {
9+
if (err) throw err;
10+
});
11+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const myFS = await import('node:fs');
2+
3+
// fd usage should be transformed
4+
const fd = myFS.openSync('file.txt', 'w');
5+
myFS.ftruncateSync(fd, 10);
6+
myFS.closeSync(fd);
7+
8+
// path usage should not be transformed
9+
myFS.truncate('other.txt', 5, (err) => {
10+
if (err) throw err;
11+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import('node:fs').then(fs => {
2+
// fd usage should be transformed
3+
const fd = fs.openSync('file.txt', 'w');
4+
fs.ftruncateSync(fd, 10);
5+
fs.closeSync(fd);
6+
7+
// path usage should not be transformed
8+
fs.truncate('other.txt', 5, (err) => {
9+
if (err) throw err;
10+
});
11+
});

0 commit comments

Comments
 (0)