Skip to content

Commit c4dd3ba

Browse files
add first draft
1 parent 4349931 commit c4dd3ba

File tree

17 files changed

+211
-23
lines changed

17 files changed

+211
-23
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
File renamed without changes.

recipes/fs-truncate-fs-depreciation/codemod.yml renamed to recipes/fs-truncate-fd-depreciation/codemod.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
schema_version: "1.0"
2-
name: nodejs/create-require-from-path
2+
name: nodejs/fs-truncate-fd-depreciation
33
version: 0.0.1
44
description: Handle DEP0081 via transforming `truncate` to `ftruncateSync` when using a file descriptor.
55
author: Augustin Mauroy

recipes/fs-truncate-fs-depreciation/package.json renamed to recipes/fs-truncate-fd-depreciation/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@nodejs/fs-truncate-fs-depreciation",
2+
"name": "@nodejs/fs-truncate-fd-depreciation",
33
"version": "0.0.1",
44
"description": "Handle DEP0081 via transforming `truncate` to `ftruncateSync` when using a file descriptor.",
55
"type": "module",
@@ -9,12 +9,12 @@
99
"repository": {
1010
"type": "git",
1111
"url": "git+https://github.com/nodejs/userland-migrations.git",
12-
"directory": "recipes/fs-truncate-fs-depreciation",
12+
"directory": "recipes/fs-truncate-fd-depreciation",
1313
"bugs": "https://github.com/nodejs/userland-migrations/issues"
1414
},
1515
"author": "Augustin Mauroy",
1616
"license": "MIT",
17-
"homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/fs-truncate-fs-depreciation/README.md",
17+
"homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/fs-truncate-fd-depreciation/README.md",
1818
"devDependencies": {
1919
"@codemod.com/jssg-types": "^1.0.3"
2020
},
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement";
2+
import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call";
3+
import type { SgRoot, Edit, SgNode } from "@codemod.com/jssg-types/main";
4+
import type Js from "@codemod.com/jssg-types/langs/javascript";
5+
6+
/**
7+
* Transform function that converts deprecated fs.truncate calls to fs.ftruncate.
8+
*
9+
* See DEP0081: https://nodejs.org/api/deprecations.html#DEP0081
10+
*
11+
* Handles:
12+
* 1. fs.truncate(fd, len, callback) -> fs.ftruncate(fd, len, callback)
13+
* 2. fs.truncateSync(fd, len) -> fs.ftruncateSync(fd, len)
14+
* 3. truncate(fd, len, callback) -> ftruncate(fd, len, callback) (destructured imports)
15+
* 4. truncateSync(fd, len) -> ftruncateSync(fd, len) (destructured imports)
16+
* 5. Import/require statement updates to replace truncate/truncateSync with ftruncate/ftruncateSync
17+
*/
18+
export default function transform(root: SgRoot<Js>): string | null {
19+
const rootNode = root.root();
20+
const edits: Edit[] = [];
21+
let hasChanges = false;
22+
23+
// Track what imports need to be updated
24+
let usedTruncate = false;
25+
let usedTruncateSync = false;
26+
27+
// Find all truncate and truncateSync calls
28+
const truncateCalls = rootNode.findAll({
29+
rule: {
30+
any: [
31+
{ pattern: "fs.truncate($FD, $LEN, $CALLBACK)" },
32+
{ pattern: "fs.truncate($FD, $LEN)" },
33+
{ pattern: "truncate($FD, $LEN, $CALLBACK)" },
34+
{ pattern: "truncate($FD, $LEN)" }
35+
]
36+
}
37+
});
38+
39+
const truncateSyncCalls = rootNode.findAll({
40+
rule: {
41+
any: [
42+
{ pattern: "fs.truncateSync($FD, $LEN)" },
43+
{ pattern: "truncateSync($FD, $LEN)" }
44+
]
45+
}
46+
});
47+
48+
// Transform truncate calls
49+
for (const call of truncateCalls) {
50+
const fdMatch = call.getMatch("FD");
51+
const lenMatch = call.getMatch("LEN");
52+
const callbackMatch = call.getMatch("CALLBACK");
53+
54+
if (!fdMatch || !lenMatch) continue;
55+
56+
const fd = fdMatch.text();
57+
const len = lenMatch.text();
58+
const callback = callbackMatch?.text();
59+
const callText = call.text();
60+
61+
// Check if this looks like a file descriptor (numeric or variable from open)
62+
if (isLikelyFileDescriptor(fd, rootNode)) {
63+
if (callText.includes("fs.truncate(")) {
64+
const newCallText = callback
65+
? `fs.ftruncate(${fd}, ${len}, ${callback})`
66+
: `fs.ftruncate(${fd}, ${len})`;
67+
edits.push(call.replace(newCallText));
68+
} else {
69+
// destructured call like truncate(...)
70+
const newCallText = callback
71+
? `ftruncate(${fd}, ${len}, ${callback})`
72+
: `ftruncate(${fd}, ${len})`;
73+
edits.push(call.replace(newCallText));
74+
usedTruncate = true;
75+
}
76+
hasChanges = true;
77+
}
78+
}
79+
80+
// Transform truncateSync calls
81+
for (const call of truncateSyncCalls) {
82+
const fdMatch = call.getMatch("FD");
83+
const lenMatch = call.getMatch("LEN");
84+
85+
if (!fdMatch || !lenMatch) continue;
86+
87+
const fd = fdMatch.text();
88+
const len = lenMatch.text();
89+
const callText = call.text();
90+
91+
// Check if this looks like a file descriptor
92+
if (isLikelyFileDescriptor(fd, rootNode)) {
93+
if (callText.includes("fs.truncateSync(")) {
94+
const newCallText = `fs.ftruncateSync(${fd}, ${len})`;
95+
edits.push(call.replace(newCallText));
96+
} else {
97+
// destructured call like truncateSync(...)
98+
const newCallText = `ftruncateSync(${fd}, ${len})`;
99+
edits.push(call.replace(newCallText));
100+
usedTruncateSync = true;
101+
}
102+
hasChanges = true;
103+
}
104+
}
105+
106+
// Update imports/requires if we have destructured calls that were transformed
107+
if (usedTruncate || usedTruncateSync) {
108+
// @ts-ignore - ast-grep types are not fully compatible with JSSG types
109+
const importStatements = getNodeImportStatements(root, 'fs');
110+
111+
// Update import statements
112+
for (const importNode of importStatements) {
113+
const namedImports = importNode.find({ rule: { kind: 'named_imports' } });
114+
if (!namedImports) continue;
115+
116+
let importText = importNode.text();
117+
let updated = false;
118+
119+
if (usedTruncate && importText.includes("truncate") && !importText.includes("ftruncate")) {
120+
// Replace truncate with ftruncate in imports
121+
importText = importText.replace(/\btruncate\b/g, "ftruncate");
122+
updated = true;
123+
}
124+
125+
if (usedTruncateSync && importText.includes("truncateSync") && !importText.includes("ftruncateSync")) {
126+
// Replace truncateSync with ftruncateSync in imports
127+
importText = importText.replace(/\btruncateSync\b/g, "ftruncateSync");
128+
updated = true;
129+
}
130+
131+
if (updated) {
132+
edits.push(importNode.replace(importText));
133+
hasChanges = true;
134+
}
135+
}
136+
137+
// @ts-ignore - ast-grep types are not fully compatible with JSSG types
138+
const requireStatements = getNodeRequireCalls(root, 'fs');
139+
140+
// Update require statements
141+
for (const requireNode of requireStatements) {
142+
let requireText = requireNode.text();
143+
let updated = false;
144+
145+
if (usedTruncate && requireText.includes("truncate") && !requireText.includes("ftruncate")) {
146+
// Replace truncate with ftruncate in requires
147+
requireText = requireText.replace(/\btruncate\b/g, "ftruncate");
148+
updated = true;
149+
}
150+
151+
if (usedTruncateSync && requireText.includes("truncateSync") && !requireText.includes("ftruncateSync")) {
152+
// Replace truncateSync with ftruncateSync in requires
153+
requireText = requireText.replace(/\btruncateSync\b/g, "ftruncateSync");
154+
updated = true;
155+
}
156+
157+
if (updated) {
158+
edits.push(requireNode.replace(requireText));
159+
hasChanges = true;
160+
}
161+
}
162+
}
163+
164+
if (!hasChanges) return null;
165+
166+
return rootNode.commitEdits(edits);
167+
}
168+
169+
/**
170+
* Helper function to determine if a parameter is likely a file descriptor
171+
* rather than a file path string.
172+
* @todo(@AugustinMauroy): use more AST than regex for this function.
173+
* @param param The parameter to check (e.g., 'fd').
174+
* @param rootNode The root node of the AST to search within.
175+
*/
176+
function isLikelyFileDescriptor(param: string, rootNode: SgNode<Js>): boolean {
177+
// Check if it's a numeric literal
178+
if (/^\d+$/.test(param.trim())) return true;
179+
180+
// Simple check: if the parameter appears in an `openSync` assignment, it's likely a file descriptor
181+
const sourceText = rootNode.text();
182+
183+
// Look for patterns like "const fd = openSync(...)" or "const fd = fs.openSync(...)"
184+
const openSyncPattern = new RegExp(`(?:const|let|var)\\s+${param}\\s*=\\s*(?:fs\\.)?openSync\\s*\\(`, 'g');
185+
186+
if (openSyncPattern.test(sourceText)) return true;
187+
188+
// Look for patterns where the parameter is used in an open callback
189+
// This handles cases like: open('file', (err, fd) => { truncate(fd, ...) })
190+
const callbackPattern = new RegExp(`\\(\\s*(?:err|error)\\s*,\\s*${param}\\s*\\)\\s*=>`, 'g');
191+
192+
if (callbackPattern.test(sourceText)) return true;
193+
194+
195+
// Look for function callback patterns
196+
const functionCallbackPattern = new RegExp(`function\\s*\\(\\s*(?:err|error)\\s*,\\s*${param}\\s*\\)`, 'g');
197+
198+
if (functionCallbackPattern.test(sourceText)) return true;
199+
200+
// Conservative approach: if we can't determine it's a file descriptor,
201+
// assume it's a file path to avoid breaking valid path-based truncate calls
202+
return false;
203+
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)