Skip to content

Commit d2d54d6

Browse files
committed
feat: migrate SlowBuffer to Buffer.allocUnsafeSlow()
Add codemod to handle deprecated SlowBuffer API migration covering CommonJS, ES6 imports, and usage patterns Closes: #125 (comment)
1 parent b1e9a9d commit d2d54d6

27 files changed

+594
-0
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# SlowBuffer to Buffer.allocUnsafeSlow Codemod
2+
3+
This codemod migrates deprecated `SlowBuffer` usage to `Buffer.allocUnsafeSlow()` to handle Node.js DEP0030.
4+
5+
## What it does
6+
7+
This codemod transforms:
8+
9+
1. `SlowBuffer` constructor calls to `Buffer.allocUnsafeSlow()`
10+
2. Direct `SlowBuffer` calls to `Buffer.allocUnsafeSlow()`
11+
3. Import/require statements to use `Buffer` instead of `SlowBuffer`
12+
13+
## Examples
14+
15+
### Before
16+
17+
```javascript
18+
const SlowBuffer = require("buffer").SlowBuffer;
19+
20+
// Using SlowBuffer constructor
21+
const buf1 = new SlowBuffer(1024);
22+
const buf2 = SlowBuffer(512);
23+
24+
// Direct import
25+
const { SlowBuffer } = require("buffer");
26+
const buf3 = new SlowBuffer(256);
27+
```
28+
29+
### After
30+
31+
```javascript
32+
const Buffer = require("buffer").Buffer;
33+
34+
// Using Buffer.allocUnsafeSlow()
35+
const buf1 = Buffer.allocUnsafeSlow(1024);
36+
const buf2 = Buffer.allocUnsafeSlow(512);
37+
38+
// Direct import
39+
const { Buffer } = require("buffer");
40+
const buf3 = Buffer.allocUnsafeSlow(256);
41+
```
42+
43+
### ESM Support
44+
45+
```javascript
46+
// Before
47+
import { SlowBuffer } from "buffer";
48+
const buf = new SlowBuffer(1024);
49+
50+
// After
51+
import { Buffer } from "buffer";
52+
const buf = Buffer.allocUnsafeSlow(1024);
53+
```
54+
55+
## References
56+
57+
- [Node.js DEP0030: SlowBuffer](https://nodejs.org/api/deprecations.html#DEP0030)
58+
- [nodejs/node#58220](https://github.com/nodejs/node/pull/58220)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
schema_version: "1.0"
2+
name: "@nodejs/slow-buffer-to-buffer-alloc-unsafe-slow"
3+
version: "1.0.0"
4+
description: Handle DEP0030 via transforming SlowBuffer usage to Buffer.allocUnsafeSlow().
5+
author: lluisemper(Lluis Semper Lloret)
6+
license: MIT
7+
workflow: workflow.yaml
8+
category: migration
9+
10+
targets:
11+
languages:
12+
- javascript
13+
- typescript
14+
15+
keywords:
16+
- transformation
17+
- migration
18+
- buffer
19+
- slowbuffer
20+
21+
registry:
22+
access: public
23+
visibility: public
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "@nodejs/slow-buffer-to-buffer-alloc-unsafe-slow",
3+
"version": "1.0.0",
4+
"description": "Handle DEP0030 via transforming SlowBuffer usage to Buffer.allocUnsafeSlow().",
5+
"type": "module",
6+
"scripts": {
7+
"test": "npx codemod jssg test -l typescript ./src/workflow.ts ./"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/nodejs/userland-migrations.git",
12+
"directory": "recipes/slow-buffer-to-buffer-alloc-unsafe-slow",
13+
"bugs": "https://github.com/nodejs/userland-migrations/issues"
14+
},
15+
"author": "lluisemper(Lluis Semper Lloret)",
16+
"license": "MIT",
17+
"homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/README.md",
18+
"dependencies": {
19+
"@nodejs/codemod-utils": "*"
20+
},
21+
"devDependencies": {
22+
"@codemod.com/jssg-types": "^1.0.3"
23+
}
24+
}
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import type { Edit, SgRoot, SgNode, Kinds } from "@codemod.com/jssg-types/main";
2+
import type Js from "@codemod.com/jssg-types/langs/javascript";
3+
import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement";
4+
import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call";
5+
6+
export default function transform(root: SgRoot): string | null {
7+
const rootNode = root.root();
8+
const edits: Edit[] = [];
9+
let hasChanges = false;
10+
11+
// Process require statements for 'buffer' module
12+
hasChanges = processRequireStatements(root, edits) || hasChanges;
13+
14+
// Process ESM import statements for 'buffer' module
15+
hasChanges = processImportStatements(root, edits) || hasChanges;
16+
17+
// Process SlowBuffer usage patterns (constructor calls, direct calls)
18+
hasChanges = processSlowBufferUsage(rootNode, edits) || hasChanges;
19+
20+
// Process comments mentioning SlowBuffer
21+
hasChanges = processSlowBufferComments(rootNode, edits) || hasChanges;
22+
23+
return hasChanges ? rootNode.commitEdits(edits) : null;
24+
}
25+
26+
/**
27+
* Process require statements for the 'buffer' module and transform SlowBuffer references
28+
*/
29+
function processRequireStatements(root: SgRoot, edits: Edit[]): boolean {
30+
let hasChanges = false;
31+
const requireStatements = getNodeRequireCalls(root, "buffer");
32+
33+
for (const statement of requireStatements) {
34+
// Get the name field to check what type of pattern we're dealing with
35+
const nameField = statement.field("name");
36+
if (!nameField) continue;
37+
38+
// Handle direct variable assignment: const SlowBuffer = require('buffer').SlowBuffer
39+
if (nameField.kind() === "identifier" && nameField.text() === "SlowBuffer") {
40+
const valueField = statement.field("value");
41+
if (valueField && valueField.kind() === "member_expression") {
42+
const property = valueField.field("property");
43+
if (property && property.text() === "SlowBuffer") {
44+
// Replace the identifier name
45+
edits.push(nameField.replace("Buffer"));
46+
// Replace the property in the member expression
47+
edits.push(property.replace("Buffer"));
48+
hasChanges = true;
49+
}
50+
}
51+
}
52+
53+
// Handle destructuring patterns: const { SlowBuffer, ... } = require('buffer')
54+
if (nameField.kind() === "object_pattern") {
55+
const originalText = nameField.text();
56+
if (originalText.includes("SlowBuffer")) {
57+
let newText = transformDestructuringPattern(originalText);
58+
if (newText !== originalText) {
59+
edits.push(nameField.replace(newText));
60+
hasChanges = true;
61+
}
62+
}
63+
}
64+
}
65+
66+
return hasChanges;
67+
}
68+
69+
/**
70+
* Process ESM import statements for the 'buffer' module and transform SlowBuffer references
71+
*/
72+
function processImportStatements(root: SgRoot, edits: Edit[]): boolean {
73+
let hasChanges = false;
74+
const importStatements = getNodeImportStatements(root, "buffer");
75+
76+
for (const statement of importStatements) {
77+
// Try to find named_imports directly in the statement
78+
const namedImports = statement.find({ rule: { kind: "named_imports" } });
79+
if (namedImports) {
80+
const originalText = namedImports.text();
81+
if (originalText.includes("SlowBuffer")) {
82+
let newText = transformDestructuringPattern(originalText);
83+
if (newText !== originalText) {
84+
edits.push(namedImports.replace(newText));
85+
hasChanges = true;
86+
}
87+
}
88+
}
89+
}
90+
91+
return hasChanges;
92+
}
93+
94+
/**
95+
* Transform destructuring patterns by replacing SlowBuffer with Buffer
96+
* Handles cases like { SlowBuffer }, { Buffer, SlowBuffer }, { SlowBuffer, Buffer }, etc.
97+
* Also handles aliased patterns like { SlowBuffer: SB }, { SlowBuffer: SB, Buffer }
98+
* For ESM imports: { SlowBuffer as SB } -> { Buffer as SB }
99+
* Preserves original formatting as much as possible
100+
*/
101+
function transformDestructuringPattern(originalText: string): string {
102+
// For simple case { SlowBuffer }, replace with { Buffer } preserving whitespace
103+
if (originalText.includes("SlowBuffer") && !originalText.includes(",") && !originalText.includes(":") && !originalText.includes(" as ")) {
104+
return originalText.replace(/SlowBuffer/g, "Buffer");
105+
}
106+
107+
let newText = originalText;
108+
109+
// Handle aliased patterns (both CommonJS : and ESM as syntax)
110+
// { SlowBuffer: SB } -> { Buffer: SB }
111+
newText = newText.replace(/SlowBuffer(\s*:\s*\w+)/g, "Buffer$1");
112+
// { SlowBuffer as SB } -> { Buffer as SB }
113+
newText = newText.replace(/SlowBuffer(\s+as\s+\w+)/g, "Buffer$1");
114+
115+
// If Buffer is already present in this specific pattern, just remove SlowBuffer
116+
if (originalText.includes("Buffer") && originalText.includes("SlowBuffer")) {
117+
// Remove non-aliased SlowBuffer references very carefully to preserve spacing
118+
newText = newText
119+
.replace(/,\s*SlowBuffer(?!\s*[:as])/g, "") // Remove SlowBuffer with leading comma
120+
.replace(/SlowBuffer\s*,\s*/g, "") // Remove SlowBuffer with trailing comma and space
121+
.replace(/SlowBuffer(?!\s*[:as])/g, ""); // Remove standalone SlowBuffer (not followed by : or as)
122+
123+
// Clean up any double spaces after opening brace
124+
newText = newText.replace(/{\s{2,}/g, "{ ");
125+
// Clean up any double commas but preserve spacing
126+
newText = newText.replace(/,\s*,/g, ",");
127+
}
128+
// If Buffer is not present, replace first SlowBuffer with Buffer, remove others
129+
else if (originalText.includes("SlowBuffer")) {
130+
// Replace the first non-aliased SlowBuffer with Buffer
131+
newText = newText.replace(/SlowBuffer(?!\s*[:as])/, "Buffer");
132+
// Remove any remaining non-aliased SlowBuffer references
133+
newText = newText
134+
.replace(/,\s*SlowBuffer\s*(?![\s:as])/g, "")
135+
.replace(/SlowBuffer\s*,\s*/g, "")
136+
.replace(/SlowBuffer\s*(?![\s:as])/g, "");
137+
// Clean up commas
138+
newText = newText.replace(/,\s*,/g, ",");
139+
}
140+
141+
return newText;
142+
}
143+
144+
/**
145+
* Process SlowBuffer usage patterns (constructor calls and direct function calls)
146+
*/
147+
function processSlowBufferUsage(rootNode: SgNode, edits: Edit[]): boolean {
148+
let hasChanges = false;
149+
150+
// Collect all SlowBuffer aliases from destructuring patterns and imports
151+
const slowBufferAliases = new Set<string>();
152+
153+
// Find all object patterns that include SlowBuffer aliases (CommonJS require)
154+
const objectPatterns = rootNode.findAll({ rule: { kind: "object_pattern" } });
155+
for (const pattern of objectPatterns) {
156+
const text = pattern.text();
157+
// Match patterns like { SlowBuffer: SB } to extract alias "SB"
158+
const aliasMatches = text.matchAll(/SlowBuffer\s*:\s*(\w+)/g);
159+
for (const match of aliasMatches) {
160+
slowBufferAliases.add(match[1]);
161+
}
162+
}
163+
164+
// Find all named_imports that include SlowBuffer aliases (ESM imports)
165+
const namedImports = rootNode.findAll({ rule: { kind: "named_imports" } });
166+
for (const importNode of namedImports) {
167+
const text = importNode.text();
168+
// Match patterns like { SlowBuffer as SB } to extract alias "SB"
169+
const aliasMatches = text.matchAll(/SlowBuffer\s+as\s+(\w+)/g);
170+
for (const match of aliasMatches) {
171+
slowBufferAliases.add(match[1]);
172+
}
173+
}
174+
175+
// Handle constructor patterns: new SlowBuffer(size) and new SB(size) for aliases
176+
const constructorCalls = rootNode.findAll({
177+
rule: {
178+
kind: "new_expression",
179+
has: {
180+
field: "constructor",
181+
kind: "identifier",
182+
regex: "^SlowBuffer$",
183+
},
184+
},
185+
});
186+
187+
for (const constructorCall of constructorCalls) {
188+
const args = constructorCall.field("arguments");
189+
if (args) {
190+
// Extract the arguments text (without parentheses)
191+
const argsText = args.text().slice(1, -1); // Remove ( and )
192+
edits.push(constructorCall.replace(`Buffer.allocUnsafeSlow(${argsText})`));
193+
hasChanges = true;
194+
}
195+
}
196+
197+
// Handle constructor calls with aliases
198+
for (const alias of slowBufferAliases) {
199+
const aliasConstructorCalls = rootNode.findAll({
200+
rule: {
201+
kind: "new_expression",
202+
has: {
203+
field: "constructor",
204+
kind: "identifier",
205+
regex: `^${alias}$`,
206+
},
207+
},
208+
});
209+
210+
for (const constructorCall of aliasConstructorCalls) {
211+
const args = constructorCall.field("arguments");
212+
if (args) {
213+
const argsText = args.text().slice(1, -1);
214+
edits.push(constructorCall.replace(`${alias}.allocUnsafeSlow(${argsText})`));
215+
hasChanges = true;
216+
}
217+
}
218+
}
219+
220+
// Handle direct function calls: SlowBuffer(size)
221+
const directCalls = rootNode.findAll({
222+
rule: {
223+
kind: "call_expression",
224+
has: {
225+
field: "function",
226+
kind: "identifier",
227+
regex: "^SlowBuffer$",
228+
},
229+
},
230+
});
231+
232+
for (const directCall of directCalls) {
233+
const args = directCall.field("arguments");
234+
if (args) {
235+
// Extract the arguments text (without parentheses)
236+
const argsText = args.text().slice(1, -1); // Remove ( and )
237+
edits.push(directCall.replace(`Buffer.allocUnsafeSlow(${argsText})`));
238+
hasChanges = true;
239+
}
240+
}
241+
242+
// Handle direct function calls with aliases
243+
for (const alias of slowBufferAliases) {
244+
const aliasDirectCalls = rootNode.findAll({
245+
rule: {
246+
kind: "call_expression",
247+
has: {
248+
field: "function",
249+
kind: "identifier",
250+
regex: `^${alias}$`,
251+
},
252+
},
253+
});
254+
255+
for (const directCall of aliasDirectCalls) {
256+
const args = directCall.field("arguments");
257+
if (args) {
258+
const argsText = args.text().slice(1, -1);
259+
edits.push(directCall.replace(`${alias}.allocUnsafeSlow(${argsText})`));
260+
hasChanges = true;
261+
}
262+
}
263+
}
264+
265+
return hasChanges;
266+
}
267+
268+
/**
269+
* Process comments mentioning SlowBuffer and update them
270+
*/
271+
function processSlowBufferComments(rootNode: SgNode, edits: Edit[]): boolean {
272+
let hasChanges = false;
273+
274+
// Handle specific comment patterns
275+
const comments = rootNode.findAll({
276+
rule: {
277+
kind: "comment",
278+
regex: ".*SlowBuffer.*",
279+
},
280+
});
281+
282+
for (const comment of comments) {
283+
const originalText = comment.text();
284+
if (originalText.includes("Using SlowBuffer constructor")) {
285+
edits.push(comment.replace("// Using Buffer.allocUnsafeSlow()"));
286+
hasChanges = true;
287+
}
288+
}
289+
290+
return hasChanges;
291+
}

0 commit comments

Comments
 (0)