From 5034ce6f5e34116374aae42e1cc3c7a1bd4cce06 Mon Sep 17 00:00:00 2001 From: lluisemper <58423269+lluisemper@users.noreply.github.com> Date: Thu, 28 Aug 2025 22:36:48 +0200 Subject: [PATCH 1/4] feat: migrate SlowBuffer to Buffer.allocUnsafeSlow() Add codemod to handle deprecated SlowBuffer API migration covering CommonJS, ES6 imports, and usage patterns Closes: https://github.com/nodejs/userland-migrations/issues/125#issuecomment-3207799076 --- package-lock.json | 24 ++ .../README.md | 27 ++ .../codemod.yaml | 23 ++ .../package.json | 24 ++ .../src/workflow.ts | 330 ++++++++++++++++++ .../tests/expected/file-01.js | 5 + .../tests/expected/file-02.js | 3 + .../tests/expected/file-03.mjs | 2 + .../tests/expected/file-04.js | 4 + .../tests/expected/file-05.js | 8 + .../tests/expected/file-06.js | 10 + .../tests/expected/file-07.js | 19 + .../tests/expected/file-08.js | 10 + .../tests/expected/file-09.mjs | 10 + .../tests/expected/file-10.js | 14 + .../tests/expected/file-11.js | 10 + .../tests/expected/file-12.mjs | 19 + .../tests/expected/file-13.mjs | 3 + .../tests/input/file-01.js | 5 + .../tests/input/file-02.js | 3 + .../tests/input/file-03.mjs | 2 + .../tests/input/file-04.js | 4 + .../tests/input/file-05.js | 8 + .../tests/input/file-06.js | 10 + .../tests/input/file-07.js | 19 + .../tests/input/file-08.js | 10 + .../tests/input/file-09.mjs | 10 + .../tests/input/file-10.js | 14 + .../tests/input/file-11.js | 10 + .../tests/input/file-12.mjs | 19 + .../tests/input/file-13.mjs | 3 + .../workflow.yaml | 24 ++ 32 files changed, 686 insertions(+) create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/README.md create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/codemod.yaml create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/package.json create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-01.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-02.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-03.mjs create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-04.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-05.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-06.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-07.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-08.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-09.mjs create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-10.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-11.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-12.mjs create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-13.mjs create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-01.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-02.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-03.mjs create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-04.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-05.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-06.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-07.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-08.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-09.mjs create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-10.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-11.js create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-12.mjs create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-13.mjs create mode 100644 recipes/slow-buffer-to-buffer-alloc-unsafe-slow/workflow.yaml diff --git a/package-lock.json b/package-lock.json index 4c7e22d5..19275c8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -196,6 +196,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -212,6 +213,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -228,6 +230,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -244,6 +247,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -260,6 +264,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -276,6 +281,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -292,6 +298,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -308,6 +315,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -324,6 +332,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1427,6 +1436,10 @@ "resolved": "recipes/rmdir", "link": true }, + "node_modules/@nodejs/slow-buffer-to-buffer-alloc-unsafe-slow": { + "resolved": "recipes/slow-buffer-to-buffer-alloc-unsafe-slow", + "link": true + }, "node_modules/@nodejs/tmpdir-to-tmpdir": { "resolved": "recipes/tmpdir-to-tmpdir", "link": true @@ -4116,6 +4129,17 @@ "@codemod.com/jssg-types": "^1.0.3" } }, + "recipes/slow-buffer-to-buffer-alloc-unsafe-slow": { + "name": "@nodejs/slow-buffer-to-buffer-alloc-unsafe-slow", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.3" + } + }, "recipes/tmpdir-to-tmpdir": { "name": "@nodejs/tmpdir-to-tmpdir", "version": "1.0.0", diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/README.md b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/README.md new file mode 100644 index 00000000..9f1889dd --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/README.md @@ -0,0 +1,27 @@ +# SlowBuffer to Buffer.allocUnsafeSlow Codemod + +This codemod migrates deprecated `SlowBuffer` usage to `Buffer.allocUnsafeSlow()` to handle Node.js [DEP0030](https://nodejs.org/api/deprecations.html#DEP0030). + +## What it does + +This codemod transforms: + +1. `SlowBuffer` constructor calls to `Buffer.allocUnsafeSlow()` +2. Direct `SlowBuffer` calls to `Buffer.allocUnsafeSlow()` +3. Import/require statements be synced with new function + +## Example + +**Before:** + +```javascript +import { SlowBuffer } from "buffer"; +const buf = new SlowBuffer(1024); +``` + +**After:** + +```javascript +import { Buffer } from "buffer"; +const buf = Buffer.allocUnsafeSlow(1024); +``` diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/codemod.yaml b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/codemod.yaml new file mode 100644 index 00000000..16d36501 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/codemod.yaml @@ -0,0 +1,23 @@ +schema_version: "1.0" +name: "@nodejs/slow-buffer-to-buffer-alloc-unsafe-slow" +version: "1.0.0" +description: Handle DEP0030 via transforming SlowBuffer usage to Buffer.allocUnsafeSlow(). +author: lluisemper(Lluis Semper Lloret) +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - buffer + - slowbuffer + +registry: + access: public + visibility: public diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/package.json b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/package.json new file mode 100644 index 00000000..cd4666cd --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/slow-buffer-to-buffer-alloc-unsafe-slow", + "version": "1.0.0", + "description": "Handle DEP0030 via transforming SlowBuffer usage to Buffer.allocUnsafeSlow().", + "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/slow-buffer-to-buffer-alloc-unsafe-slow", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "lluisemper(Lluis Semper Lloret)", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/README.md", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.3" + } +} diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts new file mode 100644 index 00000000..520a1675 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts @@ -0,0 +1,330 @@ +import type { Edit, SgRoot, SgNode } from "@codemod.com/jssg-types/main"; +import { + getNodeImportStatements, + getNodeImportCalls, +} 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"; + +type StatementType = "import-dynamic" | "import-static" | "require"; + +const nodeGetterMap = { + "import-dynamic": getNodeImportCalls, + require: getNodeRequireCalls, +} as const; + +/** + * Main entry point that orchestrates all SlowBuffer → Buffer transformations + * @param root - The AST root node to transform + * @returns The transformed code as a string, or null if no changes were made + */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + + // Process transformations in order: + // 1. CommonJS require statements + processStatements(root, edits, "require"); + // 2. ESM import statements + processStatements(root, edits, "import-static"); + // 3. Dynamic import statements + processStatements(root, edits, "import-dynamic"); + // 4. Usage patterns (constructor/function calls) + processSlowBufferUsage(rootNode, edits); + + if (!edits.length) return null; + return rootNode.commitEdits(edits); +} + +/** + * Unified function to process different types of import/require statements + * @param root - The AST root to search for statements + * @param edits - Array to collect edit operations + * @param type - The type of statement to process + */ +function processStatements(root: SgRoot, edits: Edit[], type: StatementType): void { + if (type === "import-static") { + // ESM imports have a different structure - handle them separately + for (const imp of root.root().findAll({ rule: { kind: "import_clause" } })) { + const specifiers = imp.findAll({ rule: { kind: "import_specifier" } }); + + // Collect all imported names (not aliases) to check for conflicts + const importedNames = new Set(); + for (const spec of specifiers) { + const imported = spec.child(0); // imported name + if (imported) { + importedNames.add(imported.text()); + } + } + + for (const spec of specifiers) { + const imported = spec.child(0); // imported name + if (!imported) continue; + + if (imported.text() === "SlowBuffer") { + // Check if this specifier has an alias + const alias = spec.child(2); // "as alias" part + + if (alias) { + // Has alias: SlowBuffer as SomeAlias -> Buffer as SomeAlias + edits.push(imported.replace("Buffer")); + } else if (importedNames.has("Buffer")) { + // No alias but Buffer already imported: remove SlowBuffer entirely using AST + removeSlowBufferFromImport(imp, edits); + } else { + // No alias and no Buffer conflict: SlowBuffer -> Buffer + edits.push(imported.replace("Buffer")); + } + } + } + } + return; + } + + const statements = nodeGetterMap[type](root, "buffer"); + + for (const statement of statements) { + const nameField = statement.field("name"); + if (!nameField) continue; + + // Handle different name field patterns + switch (nameField.kind()) { + case "identifier": + if (nameField.text() === "SlowBuffer") { + if (type === "require") { + // Handle direct assignment: const SlowBuffer = require('buffer').SlowBuffer + const valueField = statement.field("value"); + if (valueField && valueField.kind() === "member_expression") { + const property = valueField.field("property"); + if (property && property.text() === "SlowBuffer") { + // Transform both the variable name and the property access + edits.push(nameField.replace("Buffer")); + edits.push(property.replace("Buffer")); + } + } + } else { + // Handle dynamic import: const SlowBuffer = await import('buffer') + edits.push(nameField.replace("Buffer")); + } + } + break; + case "object_pattern": + // Handle destructuring: const { SlowBuffer, ... } = require('buffer') or import('buffer') + transformObjectPattern(nameField, edits); + break; + } + } +} + +/** + * Transforms object destructuring patterns containing SlowBuffer references + * Handles complex cases like: + * - { SlowBuffer } → { Buffer } + * - { SlowBuffer, Buffer } → { Buffer } (removes duplicate) + * - { SlowBuffer: alias } → { Buffer: alias } + * - { SlowBuffer as alias } → { Buffer as alias } (ESM) + * @param patternNode - AST node representing the destructuring pattern + * @param edits - Array to collect edit operations + */ +function transformObjectPattern(patternNode: SgNode, edits: Edit[]): void { + // Collect existing identifiers to detect conflicts + const identifiers = patternNode + .findAll({ + rule: { kind: "shorthand_property_identifier_pattern" }, + }) + .map((n) => n.text()); + + // Handle shorthand SlowBuffer references: { SlowBuffer } + for (const node of patternNode.findAll({ + rule: { kind: "shorthand_property_identifier_pattern", regex: "^SlowBuffer$" }, + })) { + if (identifiers.includes("Buffer")) { + // Buffer already exists, remove SlowBuffer using pure AST manipulation + removeSlowBufferFromObjectPattern(patternNode, edits); + } else { + // No conflict, just replace SlowBuffer with Buffer + edits.push(node.replace("Buffer")); + } + } + + // Handle aliased patterns: { SlowBuffer: alias } or { SlowBuffer as alias } + for (const node of patternNode.findAll({ + rule: { kind: "property_identifier", regex: "^SlowBuffer$" }, + })) { + // Replace the property key SlowBuffer with Buffer + edits.push(node.replace("Buffer")); + } + + // Handle ESM-style aliased patterns: { SlowBuffer as alias } + // These are handled as import_specifier in ESM contexts, but we check here too for completeness + for (const node of patternNode.findAll({ + rule: { + kind: "import_specifier", + has: { + field: "name", + kind: "identifier", + regex: "^SlowBuffer$", + }, + }, + })) { + const nameField = node.field("name"); + if (nameField) { + edits.push(nameField.replace("Buffer")); + } + } +} + +/** + * Helper function to remove SlowBuffer from import statements using AST manipulation + * @param importClause - The import_clause node containing the SlowBuffer specifier + * @param edits - Array to collect edit operations + */ +function removeSlowBufferFromImport(importClause: SgNode, edits: Edit[]): void { + // Find all import specifiers + const allSpecifiers = importClause.findAll({ rule: { kind: "import_specifier" } }); + + // Filter out SlowBuffer specifiers + const remainingSpecifiers = allSpecifiers.filter((spec) => { + const imported = spec.child(0); + return imported && imported.text() !== "SlowBuffer"; + }); + + if (remainingSpecifiers.length === 0) { + // No remaining specifiers, this would result in empty import + // Should not happen in practice since we check for Buffer conflict + return; + } + + // Reconstruct the named imports with remaining specifiers + const namedImports = importClause.find({ rule: { kind: "named_imports" } }); + if (namedImports) { + const newSpecifiersText = remainingSpecifiers.map((spec) => spec.text()).join(", "); + edits.push(namedImports.replace(`{ ${newSpecifiersText} }`)); + } +} + +/** + * Helper function to remove SlowBuffer from object patterns using pure AST manipulation + * Handles patterns like { SlowBuffer, Buffer, other } → { Buffer, other } + * @param patternNode - The object_pattern node containing SlowBuffer + * @param edits - Array to collect edit operations + */ +function removeSlowBufferFromObjectPattern(patternNode: SgNode, edits: Edit[]): void { + // Find all shorthand property identifiers in the pattern + const allProperties = patternNode.findAll({ + rule: { kind: "shorthand_property_identifier_pattern" }, + }); + + // Filter out SlowBuffer properties + const remainingProperties = allProperties.filter((prop) => prop.text() !== "SlowBuffer"); + + // Also collect any other pattern types (like pair_pattern for aliases) + const otherPatterns = patternNode.findAll({ + rule: { kind: "pair_pattern" }, + }); + + // Combine all remaining elements + const allRemainingElements = [...remainingProperties, ...otherPatterns]; + + if (allRemainingElements.length === 0) { + // This would result in empty pattern {}, which shouldn't happen + // since we know Buffer exists + return; + } + + // Reconstruct the object pattern with remaining elements + const newPatternText = allRemainingElements.map((elem) => elem.text()).join(", "); + edits.push(patternNode.replace(`{ ${newPatternText} }`)); +} + +/** + * Transforms actual SlowBuffer usage (constructor calls and function calls) + * Uses resolveBindingPath to determine how SlowBuffer was imported, then transforms: + * - new SlowBuffer(100) → Buffer.allocUnsafeSlow(100) + * - new alias(100) → alias.allocUnsafeSlow(100) (when SlowBuffer imported as alias) + * - SlowBuffer(100) → Buffer.allocUnsafeSlow(100) + * @param rootNode - The AST root node to search for usage patterns + * @param edits - Array to collect edit operations + */ +function processSlowBufferUsage(rootNode: SgNode, edits: Edit[]): void { + const root = rootNode.getRoot(); + const importStatements = getNodeImportStatements(root, "buffer"); + const requireStatements = getNodeRequireCalls(root, "buffer"); + const dynamicImportStatements = getNodeImportCalls(root, "buffer"); + + for (const importNode of [ + ...importStatements, + ...requireStatements, + ...dynamicImportStatements, + ]) { + try { + const binding = resolveBindingPath(importNode, "$.SlowBuffer"); + if (!binding) continue; + + const slowBufferCalls = rootNode.findAll({ + rule: { + any: [ + { kind: "new_expression", pattern: `new ${binding}($$$ARGS)` }, + { kind: "call_expression", pattern: `${binding}($$$ARGS)` }, + ], + }, + }); + + for (const match of slowBufferCalls) { + try { + const argsMatches = match.getMultipleMatches("ARGS"); + const argsField = match.field("arguments"); + const originalArgsText = argsField ? argsField.text().slice(1, -1) : ""; + const argsText = + argsMatches.length > 0 ? argsMatches.map((a) => a.text()).join(", ") : originalArgsText; + + const replacement = + binding === "SlowBuffer" + ? `Buffer.allocUnsafeSlow(${argsText})` + : `${binding}.allocUnsafeSlow(${argsText})`; + + edits.push(match.replace(replacement)); + } catch { + const args = match.field("arguments"); + if (args) { + const argsText = args.text().slice(1, -1); + const replacement = + binding === "SlowBuffer" + ? `Buffer.allocUnsafeSlow(${argsText})` + : `${binding}.allocUnsafeSlow(${argsText})`; + edits.push(match.replace(replacement)); + } + } + } + } catch { + continue; + } + } + + const directSlowBufferCalls = rootNode.findAll({ + rule: { + any: [ + { kind: "new_expression", pattern: "new SlowBuffer($$$ARGS)" }, + { kind: "call_expression", pattern: "SlowBuffer($$$ARGS)" }, + ], + }, + }); + + for (const match of directSlowBufferCalls) { + try { + const argsMatches = match.getMultipleMatches("ARGS"); + const argsField = match.field("arguments"); + const originalArgsText = argsField ? argsField.text().slice(1, -1) : ""; + const argsText = + argsMatches.length > 0 ? argsMatches.map((a) => a.text()).join(", ") : originalArgsText; + + edits.push(match.replace(`Buffer.allocUnsafeSlow(${argsText})`)); + } catch { + const args = match.field("arguments"); + if (args) { + const argsText = args.text().slice(1, -1); + edits.push(match.replace(`Buffer.allocUnsafeSlow(${argsText})`)); + } + } + } +} diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-01.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-01.js new file mode 100644 index 00000000..76202afd --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-01.js @@ -0,0 +1,5 @@ +const Buffer = require('buffer').Buffer; + +// Using SlowBuffer constructor +const buf1 = Buffer.allocUnsafeSlow(1024); +const buf2 = Buffer.allocUnsafeSlow(512); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-02.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-02.js new file mode 100644 index 00000000..3ece8ba1 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-02.js @@ -0,0 +1,3 @@ +// Direct import +const { Buffer } = require('buffer'); +const buf3 = Buffer.allocUnsafeSlow(256); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-03.mjs b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-03.mjs new file mode 100644 index 00000000..0a6fd3d3 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-03.mjs @@ -0,0 +1,2 @@ +import { Buffer } from 'buffer'; +const buf = Buffer.allocUnsafeSlow(1024); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-04.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-04.js new file mode 100644 index 00000000..fecf6ab2 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-04.js @@ -0,0 +1,4 @@ +// Mixed with existing Buffer import +const { Buffer } = require('buffer'); +const buf1 = Buffer.allocUnsafeSlow(100); +const buf2 = Buffer.alloc(200); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-05.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-05.js new file mode 100644 index 00000000..7540c016 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-05.js @@ -0,0 +1,8 @@ +// Multiple SlowBuffer calls +const { Buffer } = require('buffer'); + +function createBuffers() { + const buf1 = Buffer.allocUnsafeSlow(1024); + const buf2 = Buffer.allocUnsafeSlow(512); + return [buf1, buf2]; +} \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-06.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-06.js new file mode 100644 index 00000000..c61c98e2 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-06.js @@ -0,0 +1,10 @@ +// Complex destructuring patterns with SlowBuffer +const { Buffer, constants } = require('buffer'); +const { Buffer: SB } = require('buffer'); +const { Buffer: SlowBuf, Buffer: Buf } = require('buffer'); + +// Various usage patterns +const buf1 = Buffer.allocUnsafeSlow(1024); +const buf2 = Buffer.allocUnsafeSlow(512); +const buf3 = SB.allocUnsafeSlow(256); +const buf4 = SlowBuf.allocUnsafeSlow(128); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-07.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-07.js new file mode 100644 index 00000000..7763d97c --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-07.js @@ -0,0 +1,19 @@ +// Multiple arguments and expressions +const { Buffer } = require('buffer'); + +// SlowBuffer with complex expressions +const size = 1024; +const buf1 = Buffer.allocUnsafeSlow(size * 2); +const buf2 = Buffer.allocUnsafeSlow(Math.max(100, size)); +const buf3 = Buffer.allocUnsafeSlow(size + 512); +const buf4 = Buffer.allocUnsafeSlow(parseInt('256')); + +// SlowBuffer in nested contexts +function createBuffer(size) { + return Buffer.allocUnsafeSlow(size); +} + +const buffers = [ + Buffer.allocUnsafeSlow(64), + Buffer.allocUnsafeSlow(128), +]; \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-08.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-08.js new file mode 100644 index 00000000..21c3228e --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-08.js @@ -0,0 +1,10 @@ +// Edge case: Just SlowBuffer in destructuring +const { Buffer } = require('buffer'); + +// Edge case: SlowBuffer at different positions +const { Buffer, constants } = require('buffer'); +const { constants, Buffer } = require('buffer'); + +// Edge case: Multiple SlowBuffer references (should only add Buffer once) +const buf1 = Buffer.allocUnsafeSlow(100); +const buf2 = Buffer.allocUnsafeSlow(200); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-09.mjs b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-09.mjs new file mode 100644 index 00000000..2eaabc65 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-09.mjs @@ -0,0 +1,10 @@ +// ESM import edge cases +import { Buffer } from 'buffer'; +import { Buffer as SB, Buffer as B } from 'buffer'; +import { constants, Buffer } from 'buffer'; + +// Various usage patterns with imports +const buf1 = Buffer.allocUnsafeSlow(1024); +const buf2 = Buffer.allocUnsafeSlow(512); +const buf3 = SB.allocUnsafeSlow(256); +const buf4 = SB.allocUnsafeSlow(128); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-10.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-10.js new file mode 100644 index 00000000..38ddd7a7 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-10.js @@ -0,0 +1,14 @@ +// Comments and whitespace edge cases +const { Buffer } = require('buffer'); + +// Using SlowBuffer constructor - should be updated +const buf1 = Buffer.allocUnsafeSlow(1024); // inline comment about SlowBuffer +/* + * Multi-line comment mentioning SlowBuffer + * This should not be changed by the codemod + */ +const buf2 = Buffer.allocUnsafeSlow(512); + +// Edge case: SlowBuffer in string (should not be changed) +const message = "This mentions SlowBuffer but should not change"; +const code = 'new SlowBuffer(100)'; // This is in a string \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-11.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-11.js new file mode 100644 index 00000000..55792504 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-11.js @@ -0,0 +1,10 @@ +// No arguments edge case +const { Buffer } = require('buffer'); + +// SlowBuffer with no arguments (edge case) +const buf1 = Buffer.allocUnsafeSlow(); +const buf2 = Buffer.allocUnsafeSlow(); + +// SlowBuffer with empty parentheses and whitespace +const buf3 = Buffer.allocUnsafeSlow( ); +const buf4 = Buffer.allocUnsafeSlow( ); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-12.mjs b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-12.mjs new file mode 100644 index 00000000..c145b6a8 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-12.mjs @@ -0,0 +1,19 @@ +// Dynamic imports with SlowBuffer +const { Buffer } = await import('buffer'); +const { Buffer: SB, Buffer } = await import('buffer'); +const buffer = await import('buffer'); + +// Usage patterns with dynamic imports +const buf1 = Buffer.allocUnsafeSlow(1024); +const buf2 = Buffer.allocUnsafeSlow(512); +const buf3 = SB.allocUnsafeSlow(256); +const buf4 = SB.allocUnsafeSlow(128); + +// Mixed with regular imports +import { constants } from 'buffer'; +const { Buffer: DynamicSB } = await import('buffer'); +const buf5 = DynamicSB.allocUnsafeSlow(64); + +// Direct usage from buffer module (these patterns are handled by member access, not dynamic imports) +const buf6 = buffer.SlowBuffer.allocUnsafeSlow(32); +const buf7 = buffer.SlowBuffer.allocUnsafeSlow(16); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-13.mjs b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-13.mjs new file mode 100644 index 00000000..b8eee059 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/expected/file-13.mjs @@ -0,0 +1,3 @@ +// Simple dynamic import test case +const { Buffer: SB, Buffer } = await import('buffer'); +const buf1 = SB.allocUnsafeSlow(100); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-01.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-01.js new file mode 100644 index 00000000..f6730456 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-01.js @@ -0,0 +1,5 @@ +const SlowBuffer = require('buffer').SlowBuffer; + +// Using SlowBuffer constructor +const buf1 = new SlowBuffer(1024); +const buf2 = SlowBuffer(512); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-02.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-02.js new file mode 100644 index 00000000..1d935d2c --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-02.js @@ -0,0 +1,3 @@ +// Direct import +const { SlowBuffer } = require('buffer'); +const buf3 = new SlowBuffer(256); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-03.mjs b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-03.mjs new file mode 100644 index 00000000..aaf8a3ca --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-03.mjs @@ -0,0 +1,2 @@ +import { SlowBuffer } from 'buffer'; +const buf = new SlowBuffer(1024); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-04.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-04.js new file mode 100644 index 00000000..144f5999 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-04.js @@ -0,0 +1,4 @@ +// Mixed with existing Buffer import +const { Buffer, SlowBuffer } = require('buffer'); +const buf1 = new SlowBuffer(100); +const buf2 = Buffer.alloc(200); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-05.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-05.js new file mode 100644 index 00000000..c51ea61e --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-05.js @@ -0,0 +1,8 @@ +// Multiple SlowBuffer calls +const { SlowBuffer } = require('buffer'); + +function createBuffers() { + const buf1 = new SlowBuffer(1024); + const buf2 = SlowBuffer(512); + return [buf1, buf2]; +} \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-06.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-06.js new file mode 100644 index 00000000..94db1d99 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-06.js @@ -0,0 +1,10 @@ +// Complex destructuring patterns with SlowBuffer +const { SlowBuffer, Buffer, constants } = require('buffer'); +const { SlowBuffer: SB } = require('buffer'); +const { SlowBuffer: SlowBuf, Buffer: Buf } = require('buffer'); + +// Various usage patterns +const buf1 = new SlowBuffer(1024); +const buf2 = SlowBuffer(512); +const buf3 = new SB(256); +const buf4 = SlowBuf(128); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-07.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-07.js new file mode 100644 index 00000000..74fa0fcc --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-07.js @@ -0,0 +1,19 @@ +// Multiple arguments and expressions +const { SlowBuffer } = require('buffer'); + +// SlowBuffer with complex expressions +const size = 1024; +const buf1 = new SlowBuffer(size * 2); +const buf2 = SlowBuffer(Math.max(100, size)); +const buf3 = new SlowBuffer(size + 512); +const buf4 = SlowBuffer(parseInt('256')); + +// SlowBuffer in nested contexts +function createBuffer(size) { + return new SlowBuffer(size); +} + +const buffers = [ + new SlowBuffer(64), + SlowBuffer(128), +]; \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-08.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-08.js new file mode 100644 index 00000000..d66edf3a --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-08.js @@ -0,0 +1,10 @@ +// Edge case: Just SlowBuffer in destructuring +const { SlowBuffer } = require('buffer'); + +// Edge case: SlowBuffer at different positions +const { Buffer, SlowBuffer, constants } = require('buffer'); +const { constants, SlowBuffer, Buffer } = require('buffer'); + +// Edge case: Multiple SlowBuffer references (should only add Buffer once) +const buf1 = new SlowBuffer(100); +const buf2 = SlowBuffer(200); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-09.mjs b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-09.mjs new file mode 100644 index 00000000..416e6856 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-09.mjs @@ -0,0 +1,10 @@ +// ESM import edge cases +import { SlowBuffer, Buffer } from 'buffer'; +import { SlowBuffer as SB, Buffer as B } from 'buffer'; +import { constants, SlowBuffer } from 'buffer'; + +// Various usage patterns with imports +const buf1 = new SlowBuffer(1024); +const buf2 = SlowBuffer(512); +const buf3 = new SB(256); +const buf4 = SB(128); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-10.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-10.js new file mode 100644 index 00000000..fd841995 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-10.js @@ -0,0 +1,14 @@ +// Comments and whitespace edge cases +const { SlowBuffer } = require('buffer'); + +// Using SlowBuffer constructor - should be updated +const buf1 = new SlowBuffer(1024); // inline comment about SlowBuffer +/* + * Multi-line comment mentioning SlowBuffer + * This should not be changed by the codemod + */ +const buf2 = SlowBuffer(512); + +// Edge case: SlowBuffer in string (should not be changed) +const message = "This mentions SlowBuffer but should not change"; +const code = 'new SlowBuffer(100)'; // This is in a string \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-11.js b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-11.js new file mode 100644 index 00000000..ef12d850 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-11.js @@ -0,0 +1,10 @@ +// No arguments edge case +const { SlowBuffer } = require('buffer'); + +// SlowBuffer with no arguments (edge case) +const buf1 = new SlowBuffer(); +const buf2 = SlowBuffer(); + +// SlowBuffer with empty parentheses and whitespace +const buf3 = new SlowBuffer( ); +const buf4 = SlowBuffer( ); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-12.mjs b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-12.mjs new file mode 100644 index 00000000..c716986a --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-12.mjs @@ -0,0 +1,19 @@ +// Dynamic imports with SlowBuffer +const { SlowBuffer } = await import('buffer'); +const { SlowBuffer: SB, Buffer } = await import('buffer'); +const buffer = await import('buffer'); + +// Usage patterns with dynamic imports +const buf1 = new SlowBuffer(1024); +const buf2 = SlowBuffer(512); +const buf3 = new SB(256); +const buf4 = SB(128); + +// Mixed with regular imports +import { constants } from 'buffer'; +const { SlowBuffer: DynamicSB } = await import('buffer'); +const buf5 = DynamicSB(64); + +// Direct usage from buffer module (these patterns are handled by member access, not dynamic imports) +const buf6 = new buffer.SlowBuffer(32); +const buf7 = buffer.SlowBuffer(16); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-13.mjs b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-13.mjs new file mode 100644 index 00000000..ceaac706 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/tests/input/file-13.mjs @@ -0,0 +1,3 @@ +// Simple dynamic import test case +const { SlowBuffer: SB, Buffer } = await import('buffer'); +const buf1 = new SB(100); \ No newline at end of file diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/workflow.yaml b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/workflow.yaml new file mode 100644 index 00000000..64b66b92 --- /dev/null +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/workflow.yaml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json + +version: "1.0" +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: Handle DEP0030 via transforming SlowBuffer usage to Buffer.allocUnsafeSlow().. + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript From 22763e324b610c9fc3af7d604862728c8a3547a4 Mon Sep 17 00:00:00 2001 From: lluisemper <58423269+lluisemper@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:32:45 +0200 Subject: [PATCH 2/4] feat: migrate SlowBuffer to Buffer.allocUnsafeSlow() Add codemod to handle deprecated SlowBuffer API migration covering CommonJS, ES6 imports, and usage patterns Closes: #125 --- .../src/workflow.ts | 374 ++++++++---------- 1 file changed, 166 insertions(+), 208 deletions(-) diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts index 520a1675..c0e9ac43 100644 --- a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts @@ -5,6 +5,7 @@ import { } 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 { updateBinding } from "@nodejs/codemod-utils/ast-grep/update-binding"; type StatementType = "import-dynamic" | "import-static" | "require"; @@ -44,40 +45,7 @@ export default function transform(root: SgRoot): string | null { */ function processStatements(root: SgRoot, edits: Edit[], type: StatementType): void { if (type === "import-static") { - // ESM imports have a different structure - handle them separately - for (const imp of root.root().findAll({ rule: { kind: "import_clause" } })) { - const specifiers = imp.findAll({ rule: { kind: "import_specifier" } }); - - // Collect all imported names (not aliases) to check for conflicts - const importedNames = new Set(); - for (const spec of specifiers) { - const imported = spec.child(0); // imported name - if (imported) { - importedNames.add(imported.text()); - } - } - - for (const spec of specifiers) { - const imported = spec.child(0); // imported name - if (!imported) continue; - - if (imported.text() === "SlowBuffer") { - // Check if this specifier has an alias - const alias = spec.child(2); // "as alias" part - - if (alias) { - // Has alias: SlowBuffer as SomeAlias -> Buffer as SomeAlias - edits.push(imported.replace("Buffer")); - } else if (importedNames.has("Buffer")) { - // No alias but Buffer already imported: remove SlowBuffer entirely using AST - removeSlowBufferFromImport(imp, edits); - } else { - // No alias and no Buffer conflict: SlowBuffer -> Buffer - edits.push(imported.replace("Buffer")); - } - } - } - } + processESMImports(root, edits); return; } @@ -87,176 +55,202 @@ function processStatements(root: SgRoot, edits: Edit[], type: StatementType): vo const nameField = statement.field("name"); if (!nameField) continue; - // Handle different name field patterns - switch (nameField.kind()) { - case "identifier": - if (nameField.text() === "SlowBuffer") { - if (type === "require") { - // Handle direct assignment: const SlowBuffer = require('buffer').SlowBuffer - const valueField = statement.field("value"); - if (valueField && valueField.kind() === "member_expression") { - const property = valueField.field("property"); - if (property && property.text() === "SlowBuffer") { - // Transform both the variable name and the property access - edits.push(nameField.replace("Buffer")); - edits.push(property.replace("Buffer")); - } - } - } else { - // Handle dynamic import: const SlowBuffer = await import('buffer') - edits.push(nameField.replace("Buffer")); - } - } - break; - case "object_pattern": - // Handle destructuring: const { SlowBuffer, ... } = require('buffer') or import('buffer') - transformObjectPattern(nameField, edits); - break; + if (nameField.kind() === "identifier") { + processIdentifierPattern(statement, nameField, edits, type); + } else if (nameField.kind() === "object_pattern") { + processObjectPattern(statement, nameField, edits); } } } /** - * Transforms object destructuring patterns containing SlowBuffer references - * Handles complex cases like: - * - { SlowBuffer } → { Buffer } - * - { SlowBuffer, Buffer } → { Buffer } (removes duplicate) - * - { SlowBuffer: alias } → { Buffer: alias } - * - { SlowBuffer as alias } → { Buffer as alias } (ESM) - * @param patternNode - AST node representing the destructuring pattern - * @param edits - Array to collect edit operations + * Handle ESM import statements */ -function transformObjectPattern(patternNode: SgNode, edits: Edit[]): void { - // Collect existing identifiers to detect conflicts - const identifiers = patternNode - .findAll({ - rule: { kind: "shorthand_property_identifier_pattern" }, - }) - .map((n) => n.text()); - - // Handle shorthand SlowBuffer references: { SlowBuffer } - for (const node of patternNode.findAll({ - rule: { kind: "shorthand_property_identifier_pattern", regex: "^SlowBuffer$" }, - })) { - if (identifiers.includes("Buffer")) { - // Buffer already exists, remove SlowBuffer using pure AST manipulation - removeSlowBufferFromObjectPattern(patternNode, edits); - } else { - // No conflict, just replace SlowBuffer with Buffer - edits.push(node.replace("Buffer")); +function processESMImports(root: SgRoot, edits: Edit[]): void { + const importStatements = getNodeImportStatements(root, "buffer"); + + for (const statement of importStatements) { + const allSpecifiers = statement.findAll({ rule: { kind: "import_specifier" } }); + + const hasBuffer = allSpecifiers.some((spec) => spec.child(0)?.text() === "Buffer"); + const slowBufferSpecs = allSpecifiers.filter((spec) => spec.child(0)?.text() === "SlowBuffer"); + + for (const spec of slowBufferSpecs) { + const imported = spec.child(0); + const alias = spec.child(2); // "as alias" part + + if (alias) { + // SlowBuffer as SomeAlias -> Buffer as SomeAlias + edits.push(imported.replace("Buffer")); + } else if (hasBuffer) { + // Remove SlowBuffer when Buffer already exists + applyUpdateBinding(statement, edits, { old: "SlowBuffer" }); + } else { + // SlowBuffer -> Buffer + applyUpdateBinding(statement, edits, { old: "SlowBuffer", new: "Buffer" }); + } } } +} - // Handle aliased patterns: { SlowBuffer: alias } or { SlowBuffer as alias } - for (const node of patternNode.findAll({ - rule: { kind: "property_identifier", regex: "^SlowBuffer$" }, - })) { - // Replace the property key SlowBuffer with Buffer - edits.push(node.replace("Buffer")); +/** + * Handle identifier patterns like: const SlowBuffer = require('buffer') + */ +function processIdentifierPattern( + statement: SgNode, + nameField: SgNode, + edits: Edit[], + type: StatementType, +): void { + if (nameField.text() !== "SlowBuffer") return; + + if (type === "require") { + // Handle: const SlowBuffer = require('buffer').SlowBuffer + const valueField = statement.field("value"); + if (valueField?.kind() === "member_expression") { + const property = valueField.field("property"); + if (property?.text() === "SlowBuffer") { + edits.push(nameField.replace("Buffer")); + edits.push(property.replace("Buffer")); + return; + } + } } - // Handle ESM-style aliased patterns: { SlowBuffer as alias } - // These are handled as import_specifier in ESM contexts, but we check here too for completeness - for (const node of patternNode.findAll({ + // Handle: const SlowBuffer = await import('buffer') + edits.push(nameField.replace("Buffer")); +} + +/** + * Handle object destructuring patterns + */ +function processObjectPattern(statement: SgNode, nameField: SgNode, edits: Edit[]): void { + // Check for aliased patterns: { SlowBuffer: alias } + const aliasedPatterns = nameField.findAll({ rule: { - kind: "import_specifier", + kind: "pair_pattern", has: { - field: "name", - kind: "identifier", + field: "key", + kind: "property_identifier", regex: "^SlowBuffer$", }, }, - })) { - const nameField = node.field("name"); - if (nameField) { - edits.push(nameField.replace("Buffer")); + }); + + if (aliasedPatterns.length > 0) { + // Just replace the key: { SlowBuffer: SB } -> { Buffer: SB } + for (const pattern of aliasedPatterns) { + const key = pattern.field("key"); + if (key?.text() === "SlowBuffer") { + edits.push(key.replace("Buffer")); + } } + return; + } + + // Check for shorthand patterns: { SlowBuffer } + const hasSlowBufferShorthand = nameField + .findAll({ rule: { kind: "shorthand_property_identifier_pattern" } }) + .some((n) => n.text() === "SlowBuffer"); + + if (!hasSlowBufferShorthand) return; + + const hasBufferShorthand = nameField + .findAll({ rule: { kind: "shorthand_property_identifier_pattern" } }) + .some((n) => n.text() === "Buffer"); + + const parentDeclaration = findParentDeclaration(statement); + if (!parentDeclaration) return; + + if (hasBufferShorthand) { + // Remove SlowBuffer when Buffer exists + applyUpdateBinding(parentDeclaration, edits, { old: "SlowBuffer" }); + } else { + // Replace SlowBuffer with Buffer + applyUpdateBinding(parentDeclaration, edits, { old: "SlowBuffer", new: "Buffer" }); } } /** - * Helper function to remove SlowBuffer from import statements using AST manipulation - * @param importClause - The import_clause node containing the SlowBuffer specifier - * @param edits - Array to collect edit operations + * Apply updateBinding result to edits array */ -function removeSlowBufferFromImport(importClause: SgNode, edits: Edit[]): void { - // Find all import specifiers - const allSpecifiers = importClause.findAll({ rule: { kind: "import_specifier" } }); - - // Filter out SlowBuffer specifiers - const remainingSpecifiers = allSpecifiers.filter((spec) => { - const imported = spec.child(0); - return imported && imported.text() !== "SlowBuffer"; - }); - - if (remainingSpecifiers.length === 0) { - // No remaining specifiers, this would result in empty import - // Should not happen in practice since we check for Buffer conflict - return; +function applyUpdateBinding( + node: SgNode, + edits: Edit[], + options: { old?: string; new?: string }, +): void { + const result = updateBinding(node, options); + if (result?.edit) { + edits.push(result.edit); + } else if (result?.lineToRemove) { + edits.push(node.replace("")); } +} - // Reconstruct the named imports with remaining specifiers - const namedImports = importClause.find({ rule: { kind: "named_imports" } }); - if (namedImports) { - const newSpecifiersText = remainingSpecifiers.map((spec) => spec.text()).join(", "); - edits.push(namedImports.replace(`{ ${newSpecifiersText} }`)); +/** + * Find the parent lexical_declaration node for updateBinding + */ +function findParentDeclaration(node: SgNode): SgNode | null { + let current = node; + while (current) { + const kind = current.kind(); + if (kind === "lexical_declaration" || kind === "variable_declaration") { + return current; + } + current = current.parent(); + if (!current) break; } + return null; } /** - * Helper function to remove SlowBuffer from object patterns using pure AST manipulation - * Handles patterns like { SlowBuffer, Buffer, other } → { Buffer, other } - * @param patternNode - The object_pattern node containing SlowBuffer - * @param edits - Array to collect edit operations + * Extract arguments from a call expression */ -function removeSlowBufferFromObjectPattern(patternNode: SgNode, edits: Edit[]): void { - // Find all shorthand property identifiers in the pattern - const allProperties = patternNode.findAll({ - rule: { kind: "shorthand_property_identifier_pattern" }, - }); - - // Filter out SlowBuffer properties - const remainingProperties = allProperties.filter((prop) => prop.text() !== "SlowBuffer"); - - // Also collect any other pattern types (like pair_pattern for aliases) - const otherPatterns = patternNode.findAll({ - rule: { kind: "pair_pattern" }, - }); - - // Combine all remaining elements - const allRemainingElements = [...remainingProperties, ...otherPatterns]; +function extractArgs(match: SgNode): string { + try { + const argsMatches = match.getMultipleMatches("ARGS"); + if (argsMatches.length > 0) { + return argsMatches.map((a) => a.text()).join(", "); + } + } catch { + // Fall through to field-based extraction + } - if (allRemainingElements.length === 0) { - // This would result in empty pattern {}, which shouldn't happen - // since we know Buffer exists - return; + const argsField = match.field("arguments"); + if (argsField) { + const text = argsField.text(); + return text.slice(1, -1); // Remove parentheses } - // Reconstruct the object pattern with remaining elements - const newPatternText = allRemainingElements.map((elem) => elem.text()).join(", "); - edits.push(patternNode.replace(`{ ${newPatternText} }`)); + return ""; } /** - * Transforms actual SlowBuffer usage (constructor calls and function calls) - * Uses resolveBindingPath to determine how SlowBuffer was imported, then transforms: - * - new SlowBuffer(100) → Buffer.allocUnsafeSlow(100) - * - new alias(100) → alias.allocUnsafeSlow(100) (when SlowBuffer imported as alias) - * - SlowBuffer(100) → Buffer.allocUnsafeSlow(100) - * @param rootNode - The AST root node to search for usage patterns - * @param edits - Array to collect edit operations + * Transform SlowBuffer usage to Buffer.allocUnsafeSlow + */ +function transformSlowBufferCall(match: SgNode, binding: string, edits: Edit[]): void { + const args = extractArgs(match); + const replacement = + binding === "SlowBuffer" + ? `Buffer.allocUnsafeSlow(${args})` + : `${binding}.allocUnsafeSlow(${args})`; + + edits.push(match.replace(replacement)); +} + +/** + * Process SlowBuffer constructor and function calls */ function processSlowBufferUsage(rootNode: SgNode, edits: Edit[]): void { const root = rootNode.getRoot(); - const importStatements = getNodeImportStatements(root, "buffer"); - const requireStatements = getNodeRequireCalls(root, "buffer"); - const dynamicImportStatements = getNodeImportCalls(root, "buffer"); - - for (const importNode of [ - ...importStatements, - ...requireStatements, - ...dynamicImportStatements, - ]) { + const allStatements = [ + ...getNodeImportStatements(root, "buffer"), + ...getNodeRequireCalls(root, "buffer"), + ...getNodeImportCalls(root, "buffer"), + ]; + + // Process bound SlowBuffer calls (from imports/requires) + for (const importNode of allStatements) { try { const binding = resolveBindingPath(importNode, "$.SlowBuffer"); if (!binding) continue; @@ -271,37 +265,15 @@ function processSlowBufferUsage(rootNode: SgNode, edits: Edit[]): void { }); for (const match of slowBufferCalls) { - try { - const argsMatches = match.getMultipleMatches("ARGS"); - const argsField = match.field("arguments"); - const originalArgsText = argsField ? argsField.text().slice(1, -1) : ""; - const argsText = - argsMatches.length > 0 ? argsMatches.map((a) => a.text()).join(", ") : originalArgsText; - - const replacement = - binding === "SlowBuffer" - ? `Buffer.allocUnsafeSlow(${argsText})` - : `${binding}.allocUnsafeSlow(${argsText})`; - - edits.push(match.replace(replacement)); - } catch { - const args = match.field("arguments"); - if (args) { - const argsText = args.text().slice(1, -1); - const replacement = - binding === "SlowBuffer" - ? `Buffer.allocUnsafeSlow(${argsText})` - : `${binding}.allocUnsafeSlow(${argsText})`; - edits.push(match.replace(replacement)); - } - } + transformSlowBufferCall(match, binding, edits); } } catch { - continue; + // Skip if binding resolution fails } } - const directSlowBufferCalls = rootNode.findAll({ + // Process direct SlowBuffer calls (unbound) + const directCalls = rootNode.findAll({ rule: { any: [ { kind: "new_expression", pattern: "new SlowBuffer($$$ARGS)" }, @@ -310,21 +282,7 @@ function processSlowBufferUsage(rootNode: SgNode, edits: Edit[]): void { }, }); - for (const match of directSlowBufferCalls) { - try { - const argsMatches = match.getMultipleMatches("ARGS"); - const argsField = match.field("arguments"); - const originalArgsText = argsField ? argsField.text().slice(1, -1) : ""; - const argsText = - argsMatches.length > 0 ? argsMatches.map((a) => a.text()).join(", ") : originalArgsText; - - edits.push(match.replace(`Buffer.allocUnsafeSlow(${argsText})`)); - } catch { - const args = match.field("arguments"); - if (args) { - const argsText = args.text().slice(1, -1); - edits.push(match.replace(`Buffer.allocUnsafeSlow(${argsText})`)); - } - } + for (const match of directCalls) { + transformSlowBufferCall(match, "SlowBuffer", edits); } } From 513bc940ef66fed97b6fdaf483a1cb07b1981bee Mon Sep 17 00:00:00 2001 From: lluisemper <58423269+lluisemper@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:10:34 +0100 Subject: [PATCH 3/4] Updated types for CI to pass --- .../src/workflow.ts | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts index c0e9ac43..e56349a8 100644 --- a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts @@ -1,4 +1,5 @@ import type { Edit, SgRoot, SgNode } from "@codemod.com/jssg-types/main"; +import type JS from "@codemod.com/jssg-types/langs/javascript"; import { getNodeImportStatements, getNodeImportCalls, @@ -19,9 +20,9 @@ const nodeGetterMap = { * @param root - The AST root node to transform * @returns The transformed code as a string, or null if no changes were made */ -export default function transform(root: SgRoot): string | null { +export default function transform(root: SgRoot): string | null { const rootNode = root.root(); - const edits: Edit[] = []; + const edits: Edit[] = []; // Process transformations in order: // 1. CommonJS require statements @@ -43,7 +44,7 @@ export default function transform(root: SgRoot): string | null { * @param edits - Array to collect edit operations * @param type - The type of statement to process */ -function processStatements(root: SgRoot, edits: Edit[], type: StatementType): void { +function processStatements(root: SgRoot, edits: Edit[], type: StatementType): void { if (type === "import-static") { processESMImports(root, edits); return; @@ -66,7 +67,7 @@ function processStatements(root: SgRoot, edits: Edit[], type: StatementType): vo /** * Handle ESM import statements */ -function processESMImports(root: SgRoot, edits: Edit[]): void { +function processESMImports(root: SgRoot, edits: Edit[]): void { const importStatements = getNodeImportStatements(root, "buffer"); for (const statement of importStatements) { @@ -97,9 +98,9 @@ function processESMImports(root: SgRoot, edits: Edit[]): void { * Handle identifier patterns like: const SlowBuffer = require('buffer') */ function processIdentifierPattern( - statement: SgNode, - nameField: SgNode, - edits: Edit[], + statement: SgNode, + nameField: SgNode, + edits: Edit[], type: StatementType, ): void { if (nameField.text() !== "SlowBuffer") return; @@ -124,7 +125,11 @@ function processIdentifierPattern( /** * Handle object destructuring patterns */ -function processObjectPattern(statement: SgNode, nameField: SgNode, edits: Edit[]): void { +function processObjectPattern( + statement: SgNode, + nameField: SgNode, + edits: Edit[], +): void { // Check for aliased patterns: { SlowBuffer: alias } const aliasedPatterns = nameField.findAll({ rule: { @@ -175,8 +180,8 @@ function processObjectPattern(statement: SgNode, nameField: SgNode, edits: Edit[ * Apply updateBinding result to edits array */ function applyUpdateBinding( - node: SgNode, - edits: Edit[], + node: SgNode, + edits: Edit[], options: { old?: string; new?: string }, ): void { const result = updateBinding(node, options); @@ -190,7 +195,7 @@ function applyUpdateBinding( /** * Find the parent lexical_declaration node for updateBinding */ -function findParentDeclaration(node: SgNode): SgNode | null { +function findParentDeclaration(node: SgNode): SgNode | null { let current = node; while (current) { const kind = current.kind(); @@ -206,7 +211,7 @@ function findParentDeclaration(node: SgNode): SgNode | null { /** * Extract arguments from a call expression */ -function extractArgs(match: SgNode): string { +function extractArgs(match: SgNode): string { try { const argsMatches = match.getMultipleMatches("ARGS"); if (argsMatches.length > 0) { @@ -228,7 +233,7 @@ function extractArgs(match: SgNode): string { /** * Transform SlowBuffer usage to Buffer.allocUnsafeSlow */ -function transformSlowBufferCall(match: SgNode, binding: string, edits: Edit[]): void { +function transformSlowBufferCall(match: SgNode, binding: string, edits: Edit[]): void { const args = extractArgs(match); const replacement = binding === "SlowBuffer" @@ -241,7 +246,7 @@ function transformSlowBufferCall(match: SgNode, binding: string, edits: Edit[]): /** * Process SlowBuffer constructor and function calls */ -function processSlowBufferUsage(rootNode: SgNode, edits: Edit[]): void { +function processSlowBufferUsage(rootNode: SgNode, edits: Edit[]): void { const root = rootNode.getRoot(); const allStatements = [ ...getNodeImportStatements(root, "buffer"), From 2ae608d3f3d0531214629309fd08f25f46260ab9 Mon Sep 17 00:00:00 2001 From: lluisemper <58423269+lluisemper@users.noreply.github.com> Date: Sun, 9 Nov 2025 17:47:41 +0100 Subject: [PATCH 4/4] fix pipeline type-check --- .../src/workflow.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts index e56349a8..1fb29b64 100644 --- a/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts +++ b/recipes/slow-buffer-to-buffer-alloc-unsafe-slow/src/workflow.ts @@ -22,7 +22,7 @@ const nodeGetterMap = { */ export default function transform(root: SgRoot): string | null { const rootNode = root.root(); - const edits: Edit[] = []; + const edits: Edit[] = []; // Process transformations in order: // 1. CommonJS require statements @@ -44,7 +44,7 @@ export default function transform(root: SgRoot): string | null { * @param edits - Array to collect edit operations * @param type - The type of statement to process */ -function processStatements(root: SgRoot, edits: Edit[], type: StatementType): void { +function processStatements(root: SgRoot, edits: Edit[], type: StatementType): void { if (type === "import-static") { processESMImports(root, edits); return; @@ -67,7 +67,7 @@ function processStatements(root: SgRoot, edits: Edit[], type: StatementT /** * Handle ESM import statements */ -function processESMImports(root: SgRoot, edits: Edit[]): void { +function processESMImports(root: SgRoot, edits: Edit[]): void { const importStatements = getNodeImportStatements(root, "buffer"); for (const statement of importStatements) { @@ -100,7 +100,7 @@ function processESMImports(root: SgRoot, edits: Edit[]): void { function processIdentifierPattern( statement: SgNode, nameField: SgNode, - edits: Edit[], + edits: Edit[], type: StatementType, ): void { if (nameField.text() !== "SlowBuffer") return; @@ -125,11 +125,7 @@ function processIdentifierPattern( /** * Handle object destructuring patterns */ -function processObjectPattern( - statement: SgNode, - nameField: SgNode, - edits: Edit[], -): void { +function processObjectPattern(statement: SgNode, nameField: SgNode, edits: Edit[]): void { // Check for aliased patterns: { SlowBuffer: alias } const aliasedPatterns = nameField.findAll({ rule: { @@ -181,7 +177,7 @@ function processObjectPattern( */ function applyUpdateBinding( node: SgNode, - edits: Edit[], + edits: Edit[], options: { old?: string; new?: string }, ): void { const result = updateBinding(node, options); @@ -233,7 +229,7 @@ function extractArgs(match: SgNode): string { /** * Transform SlowBuffer usage to Buffer.allocUnsafeSlow */ -function transformSlowBufferCall(match: SgNode, binding: string, edits: Edit[]): void { +function transformSlowBufferCall(match: SgNode, binding: string, edits: Edit[]): void { const args = extractArgs(match); const replacement = binding === "SlowBuffer" @@ -246,7 +242,7 @@ function transformSlowBufferCall(match: SgNode, binding: string, edits: Edit /** * Process SlowBuffer constructor and function calls */ -function processSlowBufferUsage(rootNode: SgNode, edits: Edit[]): void { +function processSlowBufferUsage(rootNode: SgNode, edits: Edit[]): void { const root = rootNode.getRoot(); const allStatements = [ ...getNodeImportStatements(root, "buffer"),