From a1b0505ff92ffcf03442f2f12cdf32e2641deb14 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Thu, 14 Aug 2025 23:27:04 +0100 Subject: [PATCH 01/13] feat(util/update-binding): add new utility to update binding in the file --- utils/src/ast-grep/remove-binding.ts | 172 +-------- utils/src/ast-grep/update-binding.test.ts | 437 ++++++++++++++++++++++ utils/src/ast-grep/update-binding.ts | 251 +++++++++++++ 3 files changed, 692 insertions(+), 168 deletions(-) create mode 100644 utils/src/ast-grep/update-binding.test.ts create mode 100644 utils/src/ast-grep/update-binding.ts diff --git a/utils/src/ast-grep/remove-binding.ts b/utils/src/ast-grep/remove-binding.ts index 04c1f44e..1c86a596 100644 --- a/utils/src/ast-grep/remove-binding.ts +++ b/utils/src/ast-grep/remove-binding.ts @@ -1,12 +1,5 @@ import type { SgNode, Edit, Range, Kinds, TypesMap } from "@codemod.com/jssg-types/main"; - -const requireKinds = ["lexical_declaration", "variable_declarator"]; -const importKinds = ["import_statement", "import_clause"]; - -type RemoveBindingReturnType = { - edit?: Edit; - lineToRemove?: Range; -}; +import { updateBinding } from "./update-binding.ts"; /** * Removes a specific binding from an import or require statement. @@ -40,165 +33,8 @@ type RemoveBindingReturnType = { * // Returns: undefined (no destructured binding found) * ``` */ -export function removeBinding( - node: SgNode>, - binding: string, -): RemoveBindingReturnType | undefined { - const nodeKind = node.kind().toString(); - - const identifier = node.find({ - rule: { - any: [ - { - kind: "identifier", - inside: { - kind: "variable_declarator", - }, - }, - { - kind: "identifier", - inside: { - kind: "import_clause", - }, - }, - ], - }, - }); - - if (identifier && identifier.text() === binding) { - return { - lineToRemove: node.range(), - }; - } - - if (requireKinds.includes(nodeKind)) { - return handleNamedRequireBindings(node, binding); - } - - if (importKinds.includes(nodeKind)) { - return handleNamedImportBindings(node, binding); - } -} - -function handleNamedImportBindings( - node: SgNode>, - binding: string, -): RemoveBindingReturnType | undefined { - const namespaceImport = node.find({ - rule: { - kind: "identifier", - inside: { - kind: "namespace_import", - }, - }, - }); - - if (Boolean(namespaceImport) && namespaceImport.text() === binding) { - return { - lineToRemove: node.range(), - }; - } - - const namedImports = node.findAll({ - rule: { - kind: "import_specifier", - // ignore imports with alias (renamed imports) - not: { - has: { - field: "alias", - kind: "identifier", - }, - }, - }, +export function removeBinding(node: SgNode>, binding: string) { + return updateBinding(node, binding, { + newBinding: undefined, }); - - for (const namedImport of namedImports) { - const text = namedImport.text(); - if (text === binding) { - if (namedImports.length === 1) { - return { - lineToRemove: node.range(), - }; - } - - const namedImportsNode = node.find({ - rule: { - kind: "named_imports", - }, - }); - const restNamedImports = namedImports.map((d) => d.text()).filter((d) => d !== binding); - - return { - edit: namedImportsNode.replace(`{ ${restNamedImports.join(", ")} }`), - }; - } - } - - const renamedImports = node.findAll({ - rule: { - has: { - field: "alias", - kind: "identifier", - }, - }, - }); - - for (const renamedImport of renamedImports) { - if (renamedImport.text() === binding) { - if (renamedImports.length === 1 && namedImports.length === 0) { - return { - lineToRemove: node.range(), - }; - } - - const namedImportsNode = node.find({ - rule: { - kind: "named_imports", - }, - }); - - const aliasStatement = renamedImports.map((alias) => alias.parent()); - - const restNamedImports = [...namedImports, ...aliasStatement] - .map((d) => d.text()) - .filter((d) => d !== renamedImport.parent().text()); - - return { - edit: namedImportsNode.replace(`{ ${restNamedImports.join(", ")} }`), - }; - } - } -} - -function handleNamedRequireBindings( - node: SgNode>, - binding: string, -): RemoveBindingReturnType | undefined { - const objectPattern = node.find({ - rule: { - kind: "object_pattern", - }, - }); - - if (!objectPattern) return; - - const declarations = node.findAll({ - rule: { - kind: "shorthand_property_identifier_pattern", - }, - }); - - if (declarations.length === 1) { - return { - lineToRemove: node.range(), - }; - } - - if (declarations.length > 1) { - const restDeclarations = declarations.map((d) => d.text()).filter((d) => d !== binding); - - return { - edit: objectPattern.replace(`{ ${restDeclarations.join(", ")} }`), - }; - } } diff --git a/utils/src/ast-grep/update-binding.test.ts b/utils/src/ast-grep/update-binding.test.ts new file mode 100644 index 00000000..57338bc9 --- /dev/null +++ b/utils/src/ast-grep/update-binding.test.ts @@ -0,0 +1,437 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import astGrep from "@ast-grep/napi"; +import dedent from "dedent"; +import { updateBinding } from "./update-binding.ts"; + +describe("update-binding", () => { + it("should update only the specified named import while preserving other named imports", () => { + const code = dedent` + const { types, diff } = require('node:util'); + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const requireStatement = node.find({ + rule: { + kind: "lexical_declaration", + }, + }); + + const change = updateBinding(requireStatement!, "types", { newBinding: "newTypes" }); + const sourceCode = node.commitEdits([change?.edit!]); + + assert.notEqual(change, null); + assert.strictEqual(change?.lineToRemove, undefined); + assert.strictEqual(sourceCode, "const { newTypes, diff } = require('node:util');"); + }); + + it("should update the specified named import", () => { + const code = dedent` + const { types } = require('node:util'); + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const requireStatement = node.find({ + rule: { + kind: "lexical_declaration", + }, + }); + + const change = updateBinding(requireStatement!, "types", { newBinding: "newTypes" }); + const sourceCode = node.commitEdits([change?.edit!]); + + assert.notEqual(change, undefined); + assert.strictEqual(change?.lineToRemove, undefined); + assert.strictEqual(sourceCode, "const { newTypes } = require('node:util');"); + }); + + it("should remove the entire require statement when the only imported binding is removed", () => { + const code = dedent` + const util = require('node:util'); + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const requireStatement = node.find({ + rule: { + kind: "lexical_declaration", + }, + }); + + const change = updateBinding(requireStatement!, "util"); + + assert.notEqual(change, null); + assert.strictEqual(change?.edit, undefined); + assert.deepEqual(change?.lineToRemove, { + start: { line: 0, column: 0, index: 0 }, + end: { line: 0, column: 34, index: 34 }, + }); + }); + + it("should update only the specified named import while preserving other named imports", () => { + const code = dedent` + import { types, diff } = from 'node:util'; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const requireStatement = node.find({ + rule: { + kind: "import_statement", + }, + }); + + const change = updateBinding(requireStatement!, "types", { newBinding: "newTypes" }); + const sourceCode = node.commitEdits([change?.edit!]); + + assert.notEqual(change, null); + assert.strictEqual(change?.lineToRemove, undefined); + assert.strictEqual(sourceCode, "import { newTypes, diff } = from 'node:util';"); + }); + + it("should remove the specified named import while preserving other named imports", () => { + const code = dedent` + import { types, diff } = from 'node:util'; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const requireStatement = node.find({ + rule: { + kind: "import_statement", + }, + }); + + const change = updateBinding(requireStatement!, "types"); + const sourceCode = node.commitEdits([change?.edit!]); + + assert.notEqual(change, null); + assert.strictEqual(change?.lineToRemove, undefined); + assert.strictEqual(sourceCode, "import { diff } = from 'node:util';"); + }); + + it("should remove the entire import statement when the only imported binding is removed", () => { + const code = dedent` + import util from 'node:util'; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const importStatement = node.find({ + rule: { + kind: "import_statement", + }, + }); + + const change = updateBinding(importStatement!, "util"); + + assert.notEqual(change, null); + assert.deepEqual(change?.lineToRemove, { + start: { line: 0, column: 0, index: 0 }, + end: { line: 0, column: 29, index: 29 }, + }); + assert.strictEqual(change?.edit, undefined); + }); + + it("should remove the entire import statement when removing the only named import", () => { + const code = dedent` + import { types } from 'node:util'; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const importStatement = node.find({ + rule: { + kind: "import_statement", + }, + }); + + const change = updateBinding(importStatement!, "types"); + + assert.notEqual(change, null); + assert.deepEqual(change?.lineToRemove, { + start: { line: 0, column: 0, index: 0 }, + end: { line: 0, column: 34, index: 34 }, + }); + assert.strictEqual(change?.edit, undefined); + }); + + it("should remove the entire import line when only one aliased binding is imported and it matches the alias", () => { + const code = dedent` + import { types as utilTypes } from 'node:util'; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const importStatement = node.find({ + rule: { + kind: "import_statement", + }, + }); + + const change = updateBinding(importStatement!, "utilTypes"); + + assert.notEqual(change, null); + assert.deepEqual(change?.lineToRemove, { + start: { line: 0, column: 0, index: 0 }, + end: { line: 0, column: 47, index: 47 }, + }); + assert.strictEqual(change?.edit, undefined); + }); + + it("should update the entire import line when only one aliased binding is imported and it matches the alias", () => { + const code = dedent` + import { types as utilTypes } from 'node:util'; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const importStatement = node.find({ + rule: { + kind: "import_statement", + }, + }); + + const change = updateBinding(importStatement!, "utilTypes", { newBinding: "newTypes" }); + const sourceCode = node.commitEdits([change?.edit!]); + + assert.notEqual(change, null); + assert.strictEqual(change?.lineToRemove, undefined); + assert.strictEqual(sourceCode, "import { newTypes as utilTypes } from 'node:util';"); + }); + + it("should remove only the aliased import binding when it matches the provided alias", () => { + const code = dedent` + import { types as utilTypes, diff } from 'node:util'; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const importStatement = node.find({ + rule: { + kind: "import_statement", + }, + }); + + const change = updateBinding(importStatement!, "utilTypes"); + const sourceCode = node.commitEdits([change?.edit!]); + + assert.notEqual(change, null); + assert.strictEqual(change?.lineToRemove, undefined); + assert.strictEqual(sourceCode, "import { diff } from 'node:util';"); + }); + + it("should remove the entire require statement when the only imported binding is removed", () => { + const code = dedent` + const util = require('node:util'); + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const requireStatement = node.find({ + rule: { + kind: "lexical_declaration", + }, + }); + + const change = updateBinding(requireStatement!, "util"); + + assert.notEqual(change, null); + assert.strictEqual(change?.edit, undefined); + assert.deepEqual(change?.lineToRemove, { + start: { line: 0, column: 0, index: 0 }, + end: { line: 0, column: 34, index: 34 }, + }); + }); + + it("should return undefined when the binding does not match the imported name", () => { + const code = dedent` + const util = require('node:util'); + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const requireStatement = node.find({ + rule: { + kind: "lexical_declaration", + }, + }); + + // line 12 it was imported as util, and here is passed types to be removed + const change = updateBinding(requireStatement!, "types"); + + assert.equal(change, undefined); + }); + + it("should remove the entire require statement when removing the only named import", () => { + const code = dedent` + const { types } = require('node:util'); + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const importStatement = node.find({ + rule: { + kind: "lexical_declaration", + }, + }); + + const change = updateBinding(importStatement!, "types"); + + assert.notEqual(change, null); + assert.strictEqual(change?.edit, undefined); + assert.deepEqual(change?.lineToRemove, { + start: { line: 0, column: 0, index: 0 }, + end: { line: 0, column: 39, index: 39 }, + }); + }); + + it("should update the destructured variable", () => { + const code = dedent` + const { mainModule } = process; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const requireStatement = node.find({ + rule: { + kind: "lexical_declaration", + }, + }); + + const change = updateBinding(requireStatement!, "mainModule", { newBinding: "newMainModule" }); + const sourceCode = node.commitEdits([change?.edit!]); + + assert.notEqual(change, null); + assert.strictEqual(change?.lineToRemove, undefined); + assert.strictEqual(sourceCode, "const { newMainModule } = process;"); + }); + + it("should remove the entire import statement when the only namespace import is removed", () => { + const code = dedent` + import * as util from 'node:util'; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const importStatement = node.find({ + rule: { + kind: "import_statement", + }, + }); + + const change = updateBinding(importStatement!, "util"); + + assert.notEqual(change, null); + assert.deepEqual(change?.lineToRemove, { + start: { line: 0, column: 0, index: 0 }, + end: { line: 0, column: 34, index: 34 }, + }); + assert.strictEqual(change?.edit, undefined); + }); + + it("should update the namespace binding when newBinding is passed", () => { + const code = dedent` + import * as util from 'node:util'; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const importStatement = node.find({ + rule: { + kind: "import_statement", + }, + }); + + const change = updateBinding(importStatement!, "util", { newBinding: "newUtil" }); + const sourceCode = node.commitEdits([change?.edit!]); + + assert.notEqual(change, null); + assert.strictEqual(change?.lineToRemove, undefined); + assert.strictEqual(sourceCode, "import * as newUtil from 'node:util';"); + }); + + it("should return undefined when trying to update a binding that does not exist in the import statement", () => { + const code = dedent` + import { types, diff } from 'node:util'; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const importStatement = node.find({ + rule: { + kind: "import_statement", + }, + }); + + const change = updateBinding(importStatement!, "none", { newBinding: "newNone" }); + + assert.equal(change, undefined); + }); + + it("should remove only the aliased import binding when it matches the provided alias", () => { + const code = dedent` + import { types as utilTypes, diff } from 'node:util'; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const importStatement = node.find({ + rule: { + kind: "import_statement", + }, + }); + + const change = updateBinding(importStatement!, "utilTypes"); + const sourceCode = node.commitEdits([change?.edit!]); + + assert.notEqual(change, null); + assert.strictEqual(change?.lineToRemove, undefined); + assert.strictEqual(sourceCode, "import { diff } from 'node:util';"); + }); + + it("should update only the aliased import binding when it matches the provided alias among multiple aliased imports", () => { + const code = dedent` + import { types as utilTypes, diff as utilDiffs } from 'node:util'; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root(); + + const importStatement = node.find({ + rule: { + kind: "import_statement", + }, + }); + + const change = updateBinding(importStatement!, "utilTypes", { newBinding: "newTypes" }); + const sourceCode = node.commitEdits([change?.edit!]); + + assert.notEqual(change, null); + assert.strictEqual(change?.lineToRemove, undefined); + assert.strictEqual( + sourceCode, + "import { newTypes as utilTypes, diff as utilDiffs } from 'node:util';", + ); + }); +}); diff --git a/utils/src/ast-grep/update-binding.ts b/utils/src/ast-grep/update-binding.ts new file mode 100644 index 00000000..c5a7c8ee --- /dev/null +++ b/utils/src/ast-grep/update-binding.ts @@ -0,0 +1,251 @@ +import type { SgNode, Edit, Range, Kinds, TypesMap } from "@codemod.com/jssg-types/main"; + +const requireKinds = ["lexical_declaration", "variable_declarator"]; +const importKinds = ["import_statement", "import_clause"]; + +type UpdateBindingReturnType = { + edit?: Edit; + lineToRemove?: Range; +}; + +type UpdateBindingOptions = { + newBinding: string | undefined; +}; + +/** + * Update a specific binding from an import or require statement. + * + * Analyzes the provided AST node to find and remove a specific binding from destructured imports. + * If the binding is the only one in the statement, the entire import line is marked for removal. + * If there are multiple bindings, only the specified binding is removed from the destructuring pattern. + * + * @param node - The AST node representing the import or require statement + * @param binding - The name of the binding to remove (e.g., "isNativeError") + * @returns An object containing either an edit operation or a line range to remove, or undefined if no binding found + * + * @example + * ```typescript + * // Given an import: const {types, isNativeError} = require("node:util") + * // And binding: "isNativeError" + * // Returns: an edit object that transforms to: const {types} = require("node:util") + * ``` + * + * @example + * ```typescript + * // Given an import: const {isNativeError} = require("node:util") + * // And binding: "isNativeError" + * // Returns: {lineToRemove: Range} to remove the entire line + * ``` + * + * @example + * ```typescript + * // Given an import: const util = require("node:util") + * // And binding: "isNativeError" + * // Returns: undefined (no destructured binding found) + * ``` + */ +export function updateBinding( + node: SgNode>, + binding: string, + options?: UpdateBindingOptions, +): UpdateBindingReturnType { + const nodeKind = node.kind().toString(); + + const identifier = node.find({ + rule: { + any: [ + { + kind: "identifier", + inside: { + kind: "variable_declarator", + }, + }, + { + kind: "identifier", + inside: { + kind: "import_clause", + }, + }, + ], + }, + }); + + if (!options?.newBinding && identifier && identifier.text() === binding) { + return { + lineToRemove: node.range(), + }; + } + + if (requireKinds.includes(nodeKind)) { + return handleNamedRequireBindings(node, binding, options); + } + + if (importKinds.includes(nodeKind)) { + return handleNamedImportBindings(node, binding, options); + } +} + +function handleNamedImportBindings( + node: SgNode>, + binding: string, + options: UpdateBindingOptions, +): UpdateBindingReturnType { + const namespaceImport = node.find({ + rule: { + kind: "identifier", + inside: { + kind: "namespace_import", + }, + }, + }); + + if (Boolean(namespaceImport) && namespaceImport.text() === binding) { + if (options?.newBinding) { + return { + edit: namespaceImport.replace(options.newBinding), + }; + } + + return { + lineToRemove: node.range(), + }; + } + + const namedImports = node.findAll({ + rule: { + kind: "import_specifier", + // ignore imports with alias (renamed imports) + not: { + has: { + field: "alias", + kind: "identifier", + }, + }, + }, + }); + + for (const namedImport of namedImports) { + const text = namedImport.text(); + if (text === binding) { + if (!options?.newBinding && namedImports.length === 1) { + return { + lineToRemove: node.range(), + }; + } + + const namedImportsNode = node.find({ + rule: { + kind: "named_imports", + }, + }); + + return { + edit: namedImportsNode.replace(updateObjectPattern(namedImports, binding, options)), + }; + } + } + + const renamedImports = node.findAll({ + rule: { + has: { + field: "alias", + kind: "identifier", + }, + }, + }); + + for (const renamedImport of renamedImports) { + if (renamedImport.text() === binding) { + if (!options?.newBinding && renamedImports.length === 1 && namedImports.length === 0) { + return { + lineToRemove: node.range(), + }; + } + + const namedImportsNode = node.find({ + rule: { + kind: "named_imports", + }, + }); + + if (options?.newBinding) { + for (const renamedImport of renamedImports) { + if (renamedImport.text() === binding) { + const importName = renamedImport.parent().find({ + rule: { + has: { + field: "name", + kind: "identifier", + }, + }, + }); + return { + edit: importName.replace(options.newBinding), + }; + } + } + } else { + const aliasStatement = renamedImports.map((alias) => alias.parent()); + const newNamedImports = [...namedImports, ...aliasStatement] + .map((d) => d.text()) + .filter((d) => d !== renamedImport.parent().text()); + + return { + edit: namedImportsNode.replace(`{ ${newNamedImports.join(", ")} }`), + }; + } + } + } +} + +function handleNamedRequireBindings( + node: SgNode>, + binding: string, + options: UpdateBindingOptions, +): UpdateBindingReturnType { + const objectPattern = node.find({ + rule: { + kind: "object_pattern", + }, + }); + + if (!objectPattern) return; + + const declarations = node.findAll({ + rule: { + kind: "shorthand_property_identifier_pattern", + }, + }); + + if (!options?.newBinding && declarations.length === 1) { + return { + lineToRemove: node.range(), + }; + } + + return { + edit: objectPattern.replace(updateObjectPattern(declarations, binding, options)), + }; +} + +function updateObjectPattern( + previous: SgNode>[], + binding: string, + options: UpdateBindingOptions, +): string { + let newObjectPattern: string[]; + + if (options?.newBinding) { + newObjectPattern = previous.map((d) => { + const text = d.text(); + if (text === binding) { + return options.newBinding; + } + return text; + }); + } else { + newObjectPattern = previous.map((d) => d.text()).filter((d) => d !== binding); + } + + return `{ ${newObjectPattern.join(", ")} }`; +} From 164d78c7a8e4c830acfc2a1b6123e536fc94e5ee Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Thu, 14 Aug 2025 23:32:13 +0100 Subject: [PATCH 02/13] chore(util/update-binding): update tsdocs --- utils/src/ast-grep/update-binding.ts | 31 +++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/utils/src/ast-grep/update-binding.ts b/utils/src/ast-grep/update-binding.ts index c5a7c8ee..82c76ce4 100644 --- a/utils/src/ast-grep/update-binding.ts +++ b/utils/src/ast-grep/update-binding.ts @@ -13,27 +13,44 @@ type UpdateBindingOptions = { }; /** - * Update a specific binding from an import or require statement. + * Update or remove a specific binding from an import or require statement. * - * Analyzes the provided AST node to find and remove a specific binding from destructured imports. - * If the binding is the only one in the statement, the entire import line is marked for removal. - * If there are multiple bindings, only the specified binding is removed from the destructuring pattern. + * Analyzes the provided AST node to find and update a specific binding from destructured imports. + * If `newBinding` is provided in options, the binding will be replaced with the new name. + * If `newBinding` is not provided, the binding will be removed. + * If the binding is the only one in the statement and no replacement is provided, the entire import line is marked for removal. * * @param node - The AST node representing the import or require statement - * @param binding - The name of the binding to remove (e.g., "isNativeError") + * @param binding - The name of the binding to update or remove (e.g., "isNativeError") + * @param options - Optional configuration object + * @param options.newBinding - The new binding name to replace the old one. If not provided, the binding is removed. * @returns An object containing either an edit operation or a line range to remove, or undefined if no binding found * * @example * ```typescript * // Given an import: const {types, isNativeError} = require("node:util") - * // And binding: "isNativeError" + * // And binding: "isNativeError", options: {newBinding: "isError"} + * // Returns: an edit object that transforms to: const {types, isError} = require("node:util") + * ``` + * + * @example + * ```typescript + * // Given an import: const {types, isNativeError} = require("node:util") + * // And binding: "isNativeError", options: undefined * // Returns: an edit object that transforms to: const {types} = require("node:util") * ``` * * @example * ```typescript * // Given an import: const {isNativeError} = require("node:util") - * // And binding: "isNativeError" + * // And binding: "isNativeError", options: {newBinding: "isError"} + * // Returns: an edit object that transforms to: const {isError} = require("node:util") + * ``` + * + * @example + * ```typescript + * // Given an import: const {isNativeError} = require("node:util") + * // And binding: "isNativeError", options: undefined * // Returns: {lineToRemove: Range} to remove the entire line * ``` * From ad6abbadfa8c492960b63342f7960cd275ec1066 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Fri, 26 Sep 2025 22:11:13 +0100 Subject: [PATCH 03/13] remove log --- utils/src/ast-grep/update-binding.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/src/ast-grep/update-binding.ts b/utils/src/ast-grep/update-binding.ts index 453134e9..6f9a684b 100644 --- a/utils/src/ast-grep/update-binding.ts +++ b/utils/src/ast-grep/update-binding.ts @@ -286,7 +286,6 @@ function updateObjectPattern( continue; } - console.log({ newObjectPattern, oldBinding }); newObjectPattern.push(oldBinding.text()); } From a2f205ac097ee7439f4c6e198be2353dbcd2d61c Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sat, 27 Sep 2025 22:42:16 +0100 Subject: [PATCH 04/13] add more scenarios to update binding --- utils/src/ast-grep/update-binding.test.ts | 264 ++++++++++++++-------- utils/src/ast-grep/update-binding.ts | 69 +++++- 2 files changed, 241 insertions(+), 92 deletions(-) diff --git a/utils/src/ast-grep/update-binding.test.ts b/utils/src/ast-grep/update-binding.test.ts index 57338bc9..f6c2cf83 100644 --- a/utils/src/ast-grep/update-binding.test.ts +++ b/utils/src/ast-grep/update-binding.test.ts @@ -1,69 +1,81 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; -import astGrep from "@ast-grep/napi"; -import dedent from "dedent"; -import { updateBinding } from "./update-binding.ts"; - -describe("update-binding", () => { - it("should update only the specified named import while preserving other named imports", () => { +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import astGrep from '@ast-grep/napi'; +import dedent from 'dedent'; +import { updateBinding } from './update-binding.ts'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; +import type { SgNode } from '@codemod.com/jssg-types/main'; + +describe('update-binding', () => { + it('should update only the specified named import while preserving other named imports', () => { const code = dedent` const { types, diff } = require('node:util'); `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const requireStatement = node.find({ rule: { - kind: "lexical_declaration", + kind: 'lexical_declaration', }, }); - const change = updateBinding(requireStatement!, "types", { newBinding: "newTypes" }); + const change = updateBinding(requireStatement!, 'types', { + newBinding: 'newTypes', + }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); assert.strictEqual(change?.lineToRemove, undefined); - assert.strictEqual(sourceCode, "const { newTypes, diff } = require('node:util');"); + assert.strictEqual( + sourceCode, + "const { newTypes, diff } = require('node:util');", + ); }); - it("should update the specified named import", () => { + it('should update the specified named import', () => { const code = dedent` const { types } = require('node:util'); `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const requireStatement = node.find({ rule: { - kind: "lexical_declaration", + kind: 'lexical_declaration', }, }); - const change = updateBinding(requireStatement!, "types", { newBinding: "newTypes" }); + const change = updateBinding(requireStatement!, 'types', { + newBinding: 'newTypes', + }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, undefined); assert.strictEqual(change?.lineToRemove, undefined); - assert.strictEqual(sourceCode, "const { newTypes } = require('node:util');"); + assert.strictEqual( + sourceCode, + "const { newTypes } = require('node:util');", + ); }); - it("should remove the entire require statement when the only imported binding is removed", () => { + it('should remove the entire require statement when the only imported binding is removed', () => { const code = dedent` const util = require('node:util'); `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const requireStatement = node.find({ rule: { - kind: "lexical_declaration", + kind: 'lexical_declaration', }, }); - const change = updateBinding(requireStatement!, "util"); + const change = updateBinding(requireStatement!, 'util'); assert.notEqual(change, null); assert.strictEqual(change?.edit, undefined); @@ -73,43 +85,48 @@ describe("update-binding", () => { }); }); - it("should update only the specified named import while preserving other named imports", () => { + it('should update only the specified named import while preserving other named imports', () => { const code = dedent` import { types, diff } = from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const requireStatement = node.find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const change = updateBinding(requireStatement!, "types", { newBinding: "newTypes" }); + const change = updateBinding(requireStatement!, 'types', { + newBinding: 'newTypes', + }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); assert.strictEqual(change?.lineToRemove, undefined); - assert.strictEqual(sourceCode, "import { newTypes, diff } = from 'node:util';"); + assert.strictEqual( + sourceCode, + "import { newTypes, diff } = from 'node:util';", + ); }); - it("should remove the specified named import while preserving other named imports", () => { + it('should remove the specified named import while preserving other named imports', () => { const code = dedent` import { types, diff } = from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const requireStatement = node.find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const change = updateBinding(requireStatement!, "types"); + const change = updateBinding(requireStatement!, 'types'); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); @@ -117,21 +134,21 @@ describe("update-binding", () => { assert.strictEqual(sourceCode, "import { diff } = from 'node:util';"); }); - it("should remove the entire import statement when the only imported binding is removed", () => { + it('should remove the entire import statement when the only imported binding is removed', () => { const code = dedent` import util from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const change = updateBinding(importStatement!, "util"); + const change = updateBinding(importStatement!, 'util'); assert.notEqual(change, null); assert.deepEqual(change?.lineToRemove, { @@ -141,21 +158,21 @@ describe("update-binding", () => { assert.strictEqual(change?.edit, undefined); }); - it("should remove the entire import statement when removing the only named import", () => { + it('should remove the entire import statement when removing the only named import', () => { const code = dedent` import { types } from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const change = updateBinding(importStatement!, "types"); + const change = updateBinding(importStatement!, 'types'); assert.notEqual(change, null); assert.deepEqual(change?.lineToRemove, { @@ -165,21 +182,21 @@ describe("update-binding", () => { assert.strictEqual(change?.edit, undefined); }); - it("should remove the entire import line when only one aliased binding is imported and it matches the alias", () => { + it('should remove the entire import line when only one aliased binding is imported and it matches the alias', () => { const code = dedent` import { types as utilTypes } from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const change = updateBinding(importStatement!, "utilTypes"); + const change = updateBinding(importStatement!, 'utilTypes'); assert.notEqual(change, null); assert.deepEqual(change?.lineToRemove, { @@ -189,43 +206,48 @@ describe("update-binding", () => { assert.strictEqual(change?.edit, undefined); }); - it("should update the entire import line when only one aliased binding is imported and it matches the alias", () => { + it('should update the entire import line when only one aliased binding is imported and it matches the alias', () => { const code = dedent` import { types as utilTypes } from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const change = updateBinding(importStatement!, "utilTypes", { newBinding: "newTypes" }); + const change = updateBinding(importStatement!, 'utilTypes', { + newBinding: 'newTypes', + }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); assert.strictEqual(change?.lineToRemove, undefined); - assert.strictEqual(sourceCode, "import { newTypes as utilTypes } from 'node:util';"); + assert.strictEqual( + sourceCode, + "import { newTypes as utilTypes } from 'node:util';", + ); }); - it("should remove only the aliased import binding when it matches the provided alias", () => { + it('should remove only the aliased import binding when it matches the provided alias', () => { const code = dedent` import { types as utilTypes, diff } from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const change = updateBinding(importStatement!, "utilTypes"); + const change = updateBinding(importStatement!, 'utilTypes'); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); @@ -233,21 +255,21 @@ describe("update-binding", () => { assert.strictEqual(sourceCode, "import { diff } from 'node:util';"); }); - it("should remove the entire require statement when the only imported binding is removed", () => { + it('should remove the entire require statement when the only imported binding is removed', () => { const code = dedent` const util = require('node:util'); `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const requireStatement = node.find({ rule: { - kind: "lexical_declaration", + kind: 'lexical_declaration', }, }); - const change = updateBinding(requireStatement!, "util"); + const change = updateBinding(requireStatement!, 'util'); assert.notEqual(change, null); assert.strictEqual(change?.edit, undefined); @@ -257,41 +279,41 @@ describe("update-binding", () => { }); }); - it("should return undefined when the binding does not match the imported name", () => { + it('should return undefined when the binding does not match the imported name', () => { const code = dedent` const util = require('node:util'); `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const requireStatement = node.find({ rule: { - kind: "lexical_declaration", + kind: 'lexical_declaration', }, }); // line 12 it was imported as util, and here is passed types to be removed - const change = updateBinding(requireStatement!, "types"); + const change = updateBinding(requireStatement!, 'types'); assert.equal(change, undefined); }); - it("should remove the entire require statement when removing the only named import", () => { + it('should remove the entire require statement when removing the only named import', () => { const code = dedent` const { types } = require('node:util'); `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: "lexical_declaration", + kind: 'lexical_declaration', }, }); - const change = updateBinding(importStatement!, "types"); + const change = updateBinding(importStatement!, 'types'); assert.notEqual(change, null); assert.strictEqual(change?.edit, undefined); @@ -301,43 +323,45 @@ describe("update-binding", () => { }); }); - it("should update the destructured variable", () => { + it('should update the destructured variable', () => { const code = dedent` const { mainModule } = process; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const requireStatement = node.find({ rule: { - kind: "lexical_declaration", + kind: 'lexical_declaration', }, }); - const change = updateBinding(requireStatement!, "mainModule", { newBinding: "newMainModule" }); + const change = updateBinding(requireStatement!, 'mainModule', { + newBinding: 'newMainModule', + }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); assert.strictEqual(change?.lineToRemove, undefined); - assert.strictEqual(sourceCode, "const { newMainModule } = process;"); + assert.strictEqual(sourceCode, 'const { newMainModule } = process;'); }); - it("should remove the entire import statement when the only namespace import is removed", () => { + it('should remove the entire import statement when the only namespace import is removed', () => { const code = dedent` import * as util from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const change = updateBinding(importStatement!, "util"); + const change = updateBinding(importStatement!, 'util'); assert.notEqual(change, null); assert.deepEqual(change?.lineToRemove, { @@ -347,21 +371,23 @@ describe("update-binding", () => { assert.strictEqual(change?.edit, undefined); }); - it("should update the namespace binding when newBinding is passed", () => { + it('should update the namespace binding when newBinding is passed', () => { const code = dedent` import * as util from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const change = updateBinding(importStatement!, "util", { newBinding: "newUtil" }); + const change = updateBinding(importStatement!, 'util', { + newBinding: 'newUtil', + }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); @@ -369,40 +395,42 @@ describe("update-binding", () => { assert.strictEqual(sourceCode, "import * as newUtil from 'node:util';"); }); - it("should return undefined when trying to update a binding that does not exist in the import statement", () => { + it('should return undefined when trying to update a binding that does not exist in the import statement', () => { const code = dedent` import { types, diff } from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const change = updateBinding(importStatement!, "none", { newBinding: "newNone" }); + const change = updateBinding(importStatement!, 'none', { + newBinding: 'newNone', + }); assert.equal(change, undefined); }); - it("should remove only the aliased import binding when it matches the provided alias", () => { + it('should remove only the aliased import binding when it matches the provided alias', () => { const code = dedent` import { types as utilTypes, diff } from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const change = updateBinding(importStatement!, "utilTypes"); + const change = updateBinding(importStatement!, 'utilTypes'); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); @@ -410,21 +438,23 @@ describe("update-binding", () => { assert.strictEqual(sourceCode, "import { diff } from 'node:util';"); }); - it("should update only the aliased import binding when it matches the provided alias among multiple aliased imports", () => { + it('should update only the aliased import binding when it matches the provided alias among multiple aliased imports', () => { const code = dedent` import { types as utilTypes, diff as utilDiffs } from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root(); + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const change = updateBinding(importStatement!, "utilTypes", { newBinding: "newTypes" }); + const change = updateBinding(importStatement!, 'utilTypes', { + newBinding: 'newTypes', + }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); @@ -434,4 +464,62 @@ describe("update-binding", () => { "import { newTypes as utilTypes, diff as utilDiffs } from 'node:util';", ); }); + + it('Should update destructured property access from require statement to named import', () => { + const code = dedent` + const SlowBuffer = require("buffer").SlowBuffer; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root() as SgNode; + + const requireStatement = node.find({ + rule: { + kind: 'lexical_declaration', + }, + }); + + const change = updateBinding(requireStatement!, 'SlowBuffer', { + newBinding: 'Buffer', + }); + + assert.notEqual(change, undefined); + assert.strictEqual(change?.lineToRemove, undefined); + + const sourceCode = node.commitEdits([change?.edit!]); + + assert.strictEqual(sourceCode, `const { Buffer } = require("buffer");`); + }); + + it('Should remove entire require when property access exists require statement', () => { + const code = dedent` + const SlowBuffer = require("buffer").SlowBuffer; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root() as SgNode; + + const requireStatement = node.find({ + rule: { + kind: 'lexical_declaration', + }, + }); + + const change = updateBinding(requireStatement!, 'SlowBuffer'); + + assert.notEqual(change, undefined); + assert.strictEqual(change.edit, undefined); + assert.deepEqual(change?.lineToRemove, { + end: { + column: 48, + index: 48, + line: 0, + }, + start: { + column: 0, + index: 0, + line: 0, + }, + }); + }); }); diff --git a/utils/src/ast-grep/update-binding.ts b/utils/src/ast-grep/update-binding.ts index 6f9a684b..9fa437bb 100644 --- a/utils/src/ast-grep/update-binding.ts +++ b/utils/src/ast-grep/update-binding.ts @@ -69,13 +69,24 @@ export function updateBinding( ): UpdateBindingReturnType { const nodeKind = node.kind().toString(); - const identifier = node.find({ + const namespaceImport = node.find({ rule: { any: [ { kind: 'identifier', inside: { kind: 'variable_declarator', + // this `not rule` ensures that expressions like `require("something").NamedImport` are ignored + // because we only want the namespace to be returned here + not: { + has: { + field: 'value', + kind: 'member_expression', + }, + }, + inside: { + kind: 'lexical_declaration', + }, }, }, { @@ -88,7 +99,11 @@ export function updateBinding( }, }); - if (!options?.newBinding && identifier && identifier.text() === binding) { + if ( + !options?.newBinding && + namespaceImport && + namespaceImport.text() === binding + ) { return { lineToRemove: node.range(), }; @@ -219,6 +234,52 @@ function handleNamedRequireBindings( binding: string, options: UpdateBindingOptions, ): UpdateBindingReturnType { + const requireWithMemberExpression = node.find({ + rule: { + kind: 'variable_declarator', + all: [ + { + has: { + field: 'name', + kind: 'identifier', + pattern: binding, + }, + }, + { + has: { + field: 'value', + kind: 'member_expression', + has: { + field: 'property', + kind: 'property_identifier', + }, + }, + }, + ], + }, + }); + + if (requireWithMemberExpression) { + if (!options?.newBinding) { + return { + lineToRemove: node.range(), + }; + } + + const reqNode = node.find({ + rule: { + kind: 'call_expression', + pattern: 'require($ARGS)', + }, + }); + + return { + edit: node.replace( + `const { ${options.newBinding} } = ${reqNode.text()};`, + ), + }; + } + const objectPattern = node.find({ rule: { kind: 'object_pattern', @@ -247,7 +308,7 @@ function handleNamedRequireBindings( function updateObjectPattern( previouses: SgNode[], binding: string, - options: UpdateBindingOptions, + options?: UpdateBindingOptions, ): Edit { let newObjectPattern: string[] = []; @@ -280,7 +341,7 @@ function updateObjectPattern( for (const oldBinding of oldBindings) { if (oldBinding.text() === binding) { - if (options.newBinding) { + if (options?.newBinding) { newObjectPattern.push(options.newBinding); } continue; From 26c0a2475500df2ead8a3b39a28e2a1350a45667 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sat, 27 Sep 2025 23:01:19 +0100 Subject: [PATCH 05/13] if import already exists not add it --- utils/src/ast-grep/update-binding.test.ts | 30 +++++++++++++++++++++-- utils/src/ast-grep/update-binding.ts | 12 ++++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/utils/src/ast-grep/update-binding.test.ts b/utils/src/ast-grep/update-binding.test.ts index f6c2cf83..3e6d28ee 100644 --- a/utils/src/ast-grep/update-binding.test.ts +++ b/utils/src/ast-grep/update-binding.test.ts @@ -30,7 +30,7 @@ describe('update-binding', () => { assert.strictEqual(change?.lineToRemove, undefined); assert.strictEqual( sourceCode, - "const { newTypes, diff } = require('node:util');", + "const { diff, newTypes } = require('node:util');", ); }); @@ -108,7 +108,7 @@ describe('update-binding', () => { assert.strictEqual(change?.lineToRemove, undefined); assert.strictEqual( sourceCode, - "import { newTypes, diff } = from 'node:util';", + "import { diff, newTypes } = from 'node:util';", ); }); @@ -522,4 +522,30 @@ describe('update-binding', () => { }, }); }); + + it('If named import already exists it just needs to remove the old reference', () => { + const code = dedent` + const { SlowBuffer, Buffer } = require("buffer"); + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root() as SgNode; + + const requireStatement = node.find({ + rule: { + kind: 'lexical_declaration', + }, + }); + + const change = updateBinding(requireStatement!, 'SlowBuffer', { + newBinding: 'Buffer', + }); + + assert.notEqual(change, undefined); + assert.strictEqual(change?.lineToRemove, undefined); + + const sourceCode = node.commitEdits([change?.edit!]); + + assert.strictEqual(sourceCode, `const { Buffer } = require("buffer");`); + }); }); diff --git a/utils/src/ast-grep/update-binding.ts b/utils/src/ast-grep/update-binding.ts index 9fa437bb..182f4fd1 100644 --- a/utils/src/ast-grep/update-binding.ts +++ b/utils/src/ast-grep/update-binding.ts @@ -339,16 +339,22 @@ function updateObjectPattern( }, }); + let needAddNewBinding = true; for (const oldBinding of oldBindings) { if (oldBinding.text() === binding) { - if (options?.newBinding) { - newObjectPattern.push(options.newBinding); - } continue; } + if (oldBinding.text() === options?.newBinding) { + needAddNewBinding = false; + } + newObjectPattern.push(oldBinding.text()); } + if (options?.newBinding && needAddNewBinding) { + newObjectPattern.push(options.newBinding); + } + return parentNode.replace(`{ ${newObjectPattern.join(', ')} }`); } From e02715fcbb001a9b7b28593af351a7453d043959 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sun, 28 Sep 2025 18:00:42 +0100 Subject: [PATCH 06/13] make move old binding to options and make it optional --- utils/src/ast-grep/remove-binding.ts | 5 +- utils/src/ast-grep/update-binding.test.ts | 127 ++++++++++++++++------ utils/src/ast-grep/update-binding.ts | 71 ++++++------ 3 files changed, 133 insertions(+), 70 deletions(-) diff --git a/utils/src/ast-grep/remove-binding.ts b/utils/src/ast-grep/remove-binding.ts index 2d0b16c5..3cea6711 100644 --- a/utils/src/ast-grep/remove-binding.ts +++ b/utils/src/ast-grep/remove-binding.ts @@ -38,7 +38,8 @@ export function removeBinding( node: SgNode | SgNode>, binding: string, ) { - return updateBinding(node, binding, { - newBinding: undefined, + return updateBinding(node, { + old: binding, + new: undefined, }); } diff --git a/utils/src/ast-grep/update-binding.test.ts b/utils/src/ast-grep/update-binding.test.ts index 3e6d28ee..e46819e4 100644 --- a/utils/src/ast-grep/update-binding.test.ts +++ b/utils/src/ast-grep/update-binding.test.ts @@ -21,8 +21,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(requireStatement!, 'types', { - newBinding: 'newTypes', + const change = updateBinding(requireStatement!, { + old: 'types', + new: 'newTypes', }); const sourceCode = node.commitEdits([change?.edit!]); @@ -48,8 +49,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(requireStatement!, 'types', { - newBinding: 'newTypes', + const change = updateBinding(requireStatement!, { + old: 'types', + new: 'newTypes', }); const sourceCode = node.commitEdits([change?.edit!]); @@ -75,7 +77,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(requireStatement!, 'util'); + const change = updateBinding(requireStatement!, { + old: 'util', + }); assert.notEqual(change, null); assert.strictEqual(change?.edit, undefined); @@ -99,8 +103,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(requireStatement!, 'types', { - newBinding: 'newTypes', + const change = updateBinding(requireStatement!, { + old: 'types', + new: 'newTypes', }); const sourceCode = node.commitEdits([change?.edit!]); @@ -126,7 +131,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(requireStatement!, 'types'); + const change = updateBinding(requireStatement!, { + old: 'types', + }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); @@ -148,7 +155,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(importStatement!, 'util'); + const change = updateBinding(importStatement!, { + old: 'util', + }); assert.notEqual(change, null); assert.deepEqual(change?.lineToRemove, { @@ -172,7 +181,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(importStatement!, 'types'); + const change = updateBinding(importStatement!, { + old: 'types', + }); assert.notEqual(change, null); assert.deepEqual(change?.lineToRemove, { @@ -196,7 +207,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(importStatement!, 'utilTypes'); + const change = updateBinding(importStatement!, { + old: 'utilTypes', + }); assert.notEqual(change, null); assert.deepEqual(change?.lineToRemove, { @@ -220,8 +233,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(importStatement!, 'utilTypes', { - newBinding: 'newTypes', + const change = updateBinding(importStatement!, { + old: 'utilTypes', + new: 'newTypes', }); const sourceCode = node.commitEdits([change?.edit!]); @@ -247,7 +261,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(importStatement!, 'utilTypes'); + const change = updateBinding(importStatement!, { + old: 'utilTypes', + }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); @@ -269,7 +285,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(requireStatement!, 'util'); + const change = updateBinding(requireStatement!, { + old: 'util', + }); assert.notEqual(change, null); assert.strictEqual(change?.edit, undefined); @@ -294,7 +312,9 @@ describe('update-binding', () => { }); // line 12 it was imported as util, and here is passed types to be removed - const change = updateBinding(requireStatement!, 'types'); + const change = updateBinding(requireStatement!, { + old: 'types', + }); assert.equal(change, undefined); }); @@ -313,7 +333,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(importStatement!, 'types'); + const change = updateBinding(importStatement!, { + old: 'types', + }); assert.notEqual(change, null); assert.strictEqual(change?.edit, undefined); @@ -337,8 +359,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(requireStatement!, 'mainModule', { - newBinding: 'newMainModule', + const change = updateBinding(requireStatement!, { + old: 'mainModule', + new: 'newMainModule', }); const sourceCode = node.commitEdits([change?.edit!]); @@ -361,7 +384,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(importStatement!, 'util'); + const change = updateBinding(importStatement!, { + old: 'util', + }); assert.notEqual(change, null); assert.deepEqual(change?.lineToRemove, { @@ -385,8 +410,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(importStatement!, 'util', { - newBinding: 'newUtil', + const change = updateBinding(importStatement!, { + old: 'util', + new: 'newUtil', }); const sourceCode = node.commitEdits([change?.edit!]); @@ -409,8 +435,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(importStatement!, 'none', { - newBinding: 'newNone', + const change = updateBinding(importStatement!, { + old: 'none', + new: 'newNone', }); assert.equal(change, undefined); @@ -430,7 +457,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(importStatement!, 'utilTypes'); + const change = updateBinding(importStatement!, { + old: 'utilTypes', + }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); @@ -452,8 +481,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(importStatement!, 'utilTypes', { - newBinding: 'newTypes', + const change = updateBinding(importStatement!, { + old: 'utilTypes', + new: 'newTypes', }); const sourceCode = node.commitEdits([change?.edit!]); @@ -479,8 +509,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(requireStatement!, 'SlowBuffer', { - newBinding: 'Buffer', + const change = updateBinding(requireStatement!, { + old: 'SlowBuffer', + new: 'Buffer', }); assert.notEqual(change, undefined); @@ -505,7 +536,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(requireStatement!, 'SlowBuffer'); + const change = updateBinding(requireStatement!, { + old: 'SlowBuffer', + }); assert.notEqual(change, undefined); assert.strictEqual(change.edit, undefined); @@ -537,8 +570,9 @@ describe('update-binding', () => { }, }); - const change = updateBinding(requireStatement!, 'SlowBuffer', { - newBinding: 'Buffer', + const change = updateBinding(requireStatement!, { + old: 'SlowBuffer', + new: 'Buffer', }); assert.notEqual(change, undefined); @@ -548,4 +582,33 @@ describe('update-binding', () => { assert.strictEqual(sourceCode, `const { Buffer } = require("buffer");`); }); + + it('When oldBinding is not passed, should create new binding in require', () => { + const code = dedent` + const { SlowBuffer } = require("buffer"); + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root() as SgNode; + + const requireStatement = node.find({ + rule: { + kind: 'lexical_declaration', + }, + }); + + const change = updateBinding(requireStatement!, { + new: 'Buffer', + }); + + assert.notEqual(change, undefined); + assert.strictEqual(change?.lineToRemove, undefined); + + const sourceCode = node.commitEdits([change?.edit!]); + + assert.strictEqual( + sourceCode, + `const { SlowBuffer, Buffer } = require("buffer");`, + ); + }); }); diff --git a/utils/src/ast-grep/update-binding.ts b/utils/src/ast-grep/update-binding.ts index 182f4fd1..846d77d4 100644 --- a/utils/src/ast-grep/update-binding.ts +++ b/utils/src/ast-grep/update-binding.ts @@ -10,7 +10,8 @@ type UpdateBindingReturnType = { }; type UpdateBindingOptions = { - newBinding: string | undefined; + old?: string; + new?: string; }; /** @@ -64,7 +65,6 @@ type UpdateBindingOptions = { */ export function updateBinding( node: SgNode | SgNode>, - binding: string, options?: UpdateBindingOptions, ): UpdateBindingReturnType { const nodeKind = node.kind().toString(); @@ -100,9 +100,9 @@ export function updateBinding( }); if ( - !options?.newBinding && + !options?.new && namespaceImport && - namespaceImport.text() === binding + namespaceImport.text() === options?.old ) { return { lineToRemove: node.range(), @@ -110,17 +110,16 @@ export function updateBinding( } if (requireKinds.includes(nodeKind)) { - return handleNamedRequireBindings(node, binding, options); + return handleNamedRequireBindings(node, options); } if (importKinds.includes(nodeKind)) { - return handleNamedImportBindings(node, binding, options); + return handleNamedImportBindings(node, options); } } function handleNamedImportBindings( node: SgNode, - binding: string, options: UpdateBindingOptions, ): UpdateBindingReturnType { const namespaceImport = node.find({ @@ -132,10 +131,10 @@ function handleNamedImportBindings( }, }); - if (Boolean(namespaceImport) && namespaceImport.text() === binding) { - if (options?.newBinding) { + if (Boolean(namespaceImport) && namespaceImport.text() === options.old) { + if (options?.new) { return { - edit: namespaceImport.replace(options.newBinding), + edit: namespaceImport.replace(options.new), }; } @@ -159,15 +158,15 @@ function handleNamedImportBindings( for (const namedImport of namedImports) { const text = namedImport.text(); - if (text === binding) { - if (!options?.newBinding && namedImports.length === 1) { + if (text === options.old) { + if (!options?.new && namedImports.length === 1) { return { lineToRemove: node.range(), }; } return { - edit: updateObjectPattern(namedImports, binding, options), + edit: updateObjectPattern(namedImports, options.old, options.new), }; } } @@ -182,9 +181,9 @@ function handleNamedImportBindings( }); for (const renamedImport of renamedImports) { - if (renamedImport.text() === binding) { + if (renamedImport.text() === options.old) { if ( - !options?.newBinding && + !options?.new && renamedImports.length === 1 && namedImports.length === 0 ) { @@ -199,9 +198,9 @@ function handleNamedImportBindings( }, }); - if (options?.newBinding) { + if (options?.new) { for (const renamedImport of renamedImports) { - if (renamedImport.text() === binding) { + if (renamedImport.text() === options.old) { const importName = renamedImport.parent().find({ rule: { has: { @@ -211,7 +210,7 @@ function handleNamedImportBindings( }, }); return { - edit: importName.replace(options.newBinding), + edit: importName.replace(options.new), }; } } @@ -231,7 +230,6 @@ function handleNamedImportBindings( function handleNamedRequireBindings( node: SgNode, - binding: string, options: UpdateBindingOptions, ): UpdateBindingReturnType { const requireWithMemberExpression = node.find({ @@ -242,7 +240,7 @@ function handleNamedRequireBindings( has: { field: 'name', kind: 'identifier', - pattern: binding, + pattern: options.old, }, }, { @@ -260,7 +258,7 @@ function handleNamedRequireBindings( }); if (requireWithMemberExpression) { - if (!options?.newBinding) { + if (!options?.new) { return { lineToRemove: node.range(), }; @@ -274,9 +272,7 @@ function handleNamedRequireBindings( }); return { - edit: node.replace( - `const { ${options.newBinding} } = ${reqNode.text()};`, - ), + edit: node.replace(`const { ${options.new} } = ${reqNode.text()};`), }; } @@ -294,33 +290,36 @@ function handleNamedRequireBindings( }, }); - if (!options?.newBinding && declarations.length === 1) { + if (!options?.new && declarations.length === 1) { return { lineToRemove: node.range(), }; } return { - edit: updateObjectPattern(declarations, binding, options), + edit: updateObjectPattern(declarations, options.old, options.new), }; } function updateObjectPattern( previouses: SgNode[], - binding: string, - options?: UpdateBindingOptions, + oldBinding?: string, + newBinding?: string, ): Edit { let newObjectPattern: string[] = []; let parentNode; for (const previous of previouses) { - if (previous.text() === binding) { + if (!oldBinding) { + parentNode = previous.parent(); + } + if (previous.text() === oldBinding) { parentNode = previous.parent(); break; } } - const oldBindings = parentNode.findAll({ + const bindings = parentNode.findAll({ rule: { any: [ { @@ -340,20 +339,20 @@ function updateObjectPattern( }); let needAddNewBinding = true; - for (const oldBinding of oldBindings) { - if (oldBinding.text() === binding) { + for (const binding of bindings) { + if (binding.text() === oldBinding) { continue; } - if (oldBinding.text() === options?.newBinding) { + if (binding.text() === newBinding) { needAddNewBinding = false; } - newObjectPattern.push(oldBinding.text()); + newObjectPattern.push(binding.text()); } - if (options?.newBinding && needAddNewBinding) { - newObjectPattern.push(options.newBinding); + if (newBinding && needAddNewBinding) { + newObjectPattern.push(newBinding); } return parentNode.replace(`{ ${newObjectPattern.join(', ')} }`); From 6686d9c50ae299a581507bdf74335a11c29be0fa Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sun, 28 Sep 2025 18:12:50 +0100 Subject: [PATCH 07/13] update ts docs --- utils/src/ast-grep/update-binding.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/src/ast-grep/update-binding.ts b/utils/src/ast-grep/update-binding.ts index 846d77d4..7f076841 100644 --- a/utils/src/ast-grep/update-binding.ts +++ b/utils/src/ast-grep/update-binding.ts @@ -23,9 +23,9 @@ type UpdateBindingOptions = { * If the binding is the only one in the statement and no replacement is provided, the entire import line is marked for removal. * * @param node - The AST node representing the import or require statement - * @param binding - The name of the binding to update or remove (e.g., "isNativeError") * @param options - Optional configuration object - * @param options.newBinding - The new binding name to replace the old one. If not provided, the binding is removed. + * @param options.old - The name of the binding to update or remove (e.g., "isNativeError") + * @param options.new - The new binding name to replace the old one. If not provided, the binding is removed. * @returns An object containing either an edit operation or a line range to remove, or undefined if no binding found * * @example From 3ed5ca53d8814b988ddbf8c6dc6f00d8ee0d5eb2 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sun, 28 Sep 2025 19:32:28 +0100 Subject: [PATCH 08/13] code style --- utils/src/ast-grep/remove-binding.test.ts | 178 ++++++++++++---------- 1 file changed, 97 insertions(+), 81 deletions(-) diff --git a/utils/src/ast-grep/remove-binding.test.ts b/utils/src/ast-grep/remove-binding.test.ts index 8d6e3aba..6a942fa5 100644 --- a/utils/src/ast-grep/remove-binding.test.ts +++ b/utils/src/ast-grep/remove-binding.test.ts @@ -1,27 +1,27 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import astGrep from '@ast-grep/napi'; -import dedent from 'dedent'; -import { removeBinding } from './remove-binding.ts'; -import type Js from '@codemod.com/jssg-types/langs/javascript'; -import type { SgNode } from '@codemod.com/jssg-types/main'; - -describe('remove-binding', () => { - it('should remove the entire require statement when the only imported binding is removed', () => { +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import astGrep from "@ast-grep/napi"; +import dedent from "dedent"; +import { removeBinding } from "./remove-binding.ts"; +import type Js from "@codemod.com/jssg-types/langs/javascript"; +import type { SgNode } from "@codemod.com/jssg-types/main"; + +describe("remove-binding", () => { + it("should remove the entire require statement when the only imported binding is removed", () => { const code = dedent` const util = require('node:util'); `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); - const change = removeBinding(requireStatement!, 'util'); + const change = removeBinding(requireStatement!, "util"); assert.notEqual(change, null); assert.strictEqual(change?.edit, undefined); @@ -31,41 +31,41 @@ describe('remove-binding', () => { }); }); - it('should return undefined when the binding does not match the imported name', () => { + it("should return undefined when the binding does not match the imported name", () => { const code = dedent` const util = require('node:util'); `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); // line 12 it was imported as util, and here is passed types to be removed - const change = removeBinding(requireStatement!, 'types'); + const change = removeBinding(requireStatement!, "types"); assert.equal(change, undefined); }); - it('should remove the entire require statement when removing the only named import', () => { + it("should remove the entire require statement when removing the only named import", () => { const code = dedent` const { types } = require('node:util'); `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); - const change = removeBinding(importStatement!, 'types'); + const change = removeBinding(importStatement!, "types"); assert.notEqual(change, null); assert.strictEqual(change?.edit, undefined); @@ -75,21 +75,21 @@ describe('remove-binding', () => { }); }); - it('should remove only the specified named import while preserving other named imports', () => { + it("should remove only the specified named import while preserving other named imports", () => { const code = dedent` const { types, diff } = require('node:util'); `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); - const change = removeBinding(requireStatement!, 'types'); + const change = removeBinding(requireStatement!, "types"); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); @@ -97,21 +97,21 @@ describe('remove-binding', () => { assert.strictEqual(sourceCode, "const { diff } = require('node:util');"); }); - it('should remove the entire line when removing the only destructured variable', () => { + it("should remove the entire line when removing the only destructured variable", () => { const code = dedent` const { mainModule } = process; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); - const change = removeBinding(requireStatement!, 'mainModule'); + const change = removeBinding(requireStatement!, "mainModule"); assert.notEqual(change, null); assert.strictEqual(change?.edit, undefined); @@ -121,21 +121,21 @@ describe('remove-binding', () => { }); }); - it('should remove the entire import statement when the only imported binding is removed', () => { + it("should remove the entire import statement when the only imported binding is removed", () => { const code = dedent` import util from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); - const change = removeBinding(importStatement!, 'util'); + const change = removeBinding(importStatement!, "util"); assert.notEqual(change, null); assert.deepEqual(change?.lineToRemove, { @@ -145,41 +145,41 @@ describe('remove-binding', () => { assert.strictEqual(change?.edit, undefined); }); - it('should return undefined when trying to remove a non-existent binding from an import statement', () => { + it("should return undefined when trying to remove a non-existent binding from an import statement", () => { const code = dedent` import util from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); // line 12 it was imported as util, and here is passed types to be removed - const change = removeBinding(importStatement!, 'types'); + const change = removeBinding(importStatement!, "types"); assert.equal(change, undefined); }); - it('should remove the entire import statement when the only namespace import is removed', () => { + it("should remove the entire import statement when the only namespace import is removed", () => { const code = dedent` import * as util from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); - const change = removeBinding(importStatement!, 'util'); + const change = removeBinding(importStatement!, "util"); assert.notEqual(change, null); assert.deepEqual(change?.lineToRemove, { @@ -189,40 +189,40 @@ describe('remove-binding', () => { assert.strictEqual(change?.edit, undefined); }); - it('should not remove the import statement when the namespace identifier does not match', () => { + it("should not remove the import statement when the namespace identifier does not match", () => { const code = dedent` import * as util from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); - const change = removeBinding(importStatement!, 'types'); + const change = removeBinding(importStatement!, "types"); assert.equal(change, undefined); }); - it('should remove the entire import statement when the only named import is removed', () => { + it("should remove the entire import statement when the only named import is removed", () => { const code = dedent` import { types } from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); - const change = removeBinding(importStatement!, 'types'); + const change = removeBinding(importStatement!, "types"); assert.notEqual(change, null); assert.deepEqual(change?.lineToRemove, { @@ -232,21 +232,21 @@ describe('remove-binding', () => { assert.strictEqual(change?.edit, undefined); }); - it('should remove a specific named import from an import statement with multiple imports', () => { + it("should remove a specific named import from an import statement with multiple imports", () => { const code = dedent` import { types, diff } from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); - const change = removeBinding(importStatement!, 'types'); + const change = removeBinding(importStatement!, "types"); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); @@ -254,40 +254,40 @@ describe('remove-binding', () => { assert.strictEqual(sourceCode, "import { diff } from 'node:util';"); }); - it('should return undefined when trying to remove a binding that does not exist in the import statement', () => { + it("should return undefined when trying to remove a binding that does not exist in the import statement", () => { const code = dedent` import { types, diff } from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); - const change = removeBinding(importStatement!, 'none'); + const change = removeBinding(importStatement!, "none"); assert.equal(change, undefined); }); - it('should remove the entire import line when only one aliased binding is imported and it matches the alias', () => { + it("should remove the entire import line when only one aliased binding is imported and it matches the alias", () => { const code = dedent` import { types as utilTypes } from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); - const change = removeBinding(importStatement!, 'utilTypes'); + const change = removeBinding(importStatement!, "utilTypes"); assert.notEqual(change, null); assert.deepEqual(change?.lineToRemove, { @@ -297,21 +297,21 @@ describe('remove-binding', () => { assert.strictEqual(change?.edit, undefined); }); - it('should remove only the aliased import binding when it matches the provided alias', () => { + it("should remove only the aliased import binding when it matches the provided alias", () => { const code = dedent` import { types as utilTypes, diff } from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); - const change = removeBinding(importStatement!, 'utilTypes'); + const change = removeBinding(importStatement!, "utilTypes"); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); @@ -319,53 +319,69 @@ describe('remove-binding', () => { assert.strictEqual(sourceCode, "import { diff } from 'node:util';"); }); - it('should remove only the aliased import binding when it matches the provided alias among multiple aliased imports', () => { + it("should remove only the aliased import binding when it matches the provided alias among multiple aliased imports", () => { const code = dedent` import { types as utilTypes, diff as utilDiffs } from 'node:util'; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); - const change = removeBinding(importStatement!, 'utilTypes'); + const change = removeBinding(importStatement!, "utilTypes"); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); assert.strictEqual(change?.lineToRemove, undefined); - assert.strictEqual( - sourceCode, - "import { diff as utilDiffs } from 'node:util';", - ); + assert.strictEqual(sourceCode, "import { diff as utilDiffs } from 'node:util';"); }); - it('should remove only the specific import binding from nested destructuring when multiple bindings exist', () => { + it("should remove only the specific import binding from nested destructuring when multiple bindings exist", () => { const code = dedent` const { types: { isNativeError, isMap } } = require("util"); `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const node = rootNode.root() as SgNode + const node = rootNode.root() as SgNode; const importStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); - const change = removeBinding(importStatement!, 'isNativeError'); + const change = removeBinding(importStatement!, "isNativeError"); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); assert.strictEqual(change?.lineToRemove, undefined); - assert.strictEqual( - sourceCode, - `const { types: { isMap } } = require("util");`, - ); + assert.strictEqual(sourceCode, `const { types: { isMap } } = require("util");`); + }); + + it("should remove only the specific import binding from nested destructuring when multiple bindings exist", () => { + const code = dedent` + const Buffer = require("buffer").Buffer; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root() as SgNode; + + const importStatement = node.find({ + rule: { + kind: "lexical_declaration", + }, + }); + + const change = removeBinding(importStatement!, "isNativeError"); + const sourceCode = node.commitEdits([change?.edit!]); + + assert.notEqual(change, null); + assert.strictEqual(change?.lineToRemove, undefined); + assert.strictEqual(sourceCode, `const { types: { isMap } } = require("util");`); }); }); From 41c26d7d5d98ea44e76c23de3ccf5fe7be7254a7 Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sun, 28 Sep 2025 19:40:50 +0100 Subject: [PATCH 09/13] add two more test cases --- utils/src/ast-grep/remove-binding.test.ts | 54 ++++++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/utils/src/ast-grep/remove-binding.test.ts b/utils/src/ast-grep/remove-binding.test.ts index 6a942fa5..895f2959 100644 --- a/utils/src/ast-grep/remove-binding.test.ts +++ b/utils/src/ast-grep/remove-binding.test.ts @@ -363,7 +363,7 @@ describe("remove-binding", () => { assert.strictEqual(sourceCode, `const { types: { isMap } } = require("util");`); }); - it("should remove only the specific import binding from nested destructuring when multiple bindings exist", () => { + it("Should remove the line in member expression scenarios", () => { const code = dedent` const Buffer = require("buffer").Buffer; `; @@ -377,11 +377,53 @@ describe("remove-binding", () => { }, }); - const change = removeBinding(importStatement!, "isNativeError"); - const sourceCode = node.commitEdits([change?.edit!]); + const change = removeBinding(importStatement!, "Buffer"); - assert.notEqual(change, null); - assert.strictEqual(change?.lineToRemove, undefined); - assert.strictEqual(sourceCode, `const { types: { isMap } } = require("util");`); + assert.notEqual(change, undefined); + assert.strictEqual(change?.edit, undefined); + assert.deepEqual(change?.lineToRemove, { + end: { + column: 40, + index: 40, + line: 0, + }, + start: { + column: 0, + index: 0, + line: 0, + }, + }); + }); + + it("Should remove the line when the accessed property is different from the identifier", () => { + const code = dedent` + const Buffer = require("buffer").SlowBuffer + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root() as SgNode; + + const importStatement = node.find({ + rule: { + kind: "lexical_declaration", + }, + }); + + const change = removeBinding(importStatement!, "Buffer"); + + assert.notEqual(change, undefined); + assert.strictEqual(change?.edit, undefined); + assert.deepEqual(change?.lineToRemove, { + end: { + column: 43, + index: 43, + line: 0, + }, + start: { + column: 0, + index: 0, + line: 0, + }, + }); }); }); From 7c50a840ef31dbd77e56a651cf9b4c062203ecef Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Sun, 28 Sep 2025 21:58:17 +0100 Subject: [PATCH 10/13] remove unecessary generic type on test file --- utils/src/ast-grep/remove-binding.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/src/ast-grep/remove-binding.test.ts b/utils/src/ast-grep/remove-binding.test.ts index 895f2959..e4d0b73a 100644 --- a/utils/src/ast-grep/remove-binding.test.ts +++ b/utils/src/ast-grep/remove-binding.test.ts @@ -12,7 +12,7 @@ describe("remove-binding", () => { const util = require('node:util'); `; - const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const node = rootNode.root() as SgNode; const requireStatement = node.find({ From cfe70eafdde205b8a63fe61036dcd62fd7e8f4fc Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Mon, 29 Sep 2025 11:08:15 +0100 Subject: [PATCH 11/13] fix: augustin suggestions Co-authored-by: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> --- utils/src/ast-grep/update-binding.ts | 98 +++++++++++++--------------- 1 file changed, 45 insertions(+), 53 deletions(-) diff --git a/utils/src/ast-grep/update-binding.ts b/utils/src/ast-grep/update-binding.ts index 7f076841..058bc978 100644 --- a/utils/src/ast-grep/update-binding.ts +++ b/utils/src/ast-grep/update-binding.ts @@ -1,8 +1,8 @@ -import type { SgNode, Edit, Range, Kinds } from '@codemod.com/jssg-types/main'; -import type Js from '@codemod.com/jssg-types/langs/javascript'; +import type { SgNode, Edit, Range, Kinds } from "@codemod.com/jssg-types/main"; +import type Js from "@codemod.com/jssg-types/langs/javascript"; -const requireKinds = ['lexical_declaration', 'variable_declarator']; -const importKinds = ['import_statement', 'import_clause']; +const requireKinds = ["lexical_declaration", "variable_declarator"]; +const importKinds = ["import_statement", "import_clause"]; type UpdateBindingReturnType = { edit?: Edit; @@ -64,7 +64,7 @@ type UpdateBindingOptions = { * ``` */ export function updateBinding( - node: SgNode | SgNode>, + node: SgNode, options?: UpdateBindingOptions, ): UpdateBindingReturnType { const nodeKind = node.kind().toString(); @@ -73,37 +73,33 @@ export function updateBinding( rule: { any: [ { - kind: 'identifier', + kind: "identifier", inside: { - kind: 'variable_declarator', + kind: "variable_declarator", // this `not rule` ensures that expressions like `require("something").NamedImport` are ignored // because we only want the namespace to be returned here not: { has: { - field: 'value', - kind: 'member_expression', + field: "value", + kind: "member_expression", }, }, inside: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }, }, { - kind: 'identifier', + kind: "identifier", inside: { - kind: 'import_clause', + kind: "import_clause", }, }, ], }, }); - if ( - !options?.new && - namespaceImport && - namespaceImport.text() === options?.old - ) { + if (!options?.new && namespaceImport && namespaceImport.text() === options?.old) { return { lineToRemove: node.range(), }; @@ -124,9 +120,9 @@ function handleNamedImportBindings( ): UpdateBindingReturnType { const namespaceImport = node.find({ rule: { - kind: 'identifier', + kind: "identifier", inside: { - kind: 'namespace_import', + kind: "namespace_import", }, }, }); @@ -145,12 +141,12 @@ function handleNamedImportBindings( const namedImports = node.findAll({ rule: { - kind: 'import_specifier', + kind: "import_specifier", // ignore imports with alias (renamed imports) not: { has: { - field: 'alias', - kind: 'identifier', + field: "alias", + kind: "identifier", }, }, }, @@ -171,22 +167,18 @@ function handleNamedImportBindings( } } - const renamedImports = node.findAll({ + const aliasedImports = node.findAll({ rule: { has: { - field: 'alias', - kind: 'identifier', + field: "alias", + kind: "identifier", }, }, }); - for (const renamedImport of renamedImports) { + for (const renamedImport of aliasedImports) { if (renamedImport.text() === options.old) { - if ( - !options?.new && - renamedImports.length === 1 && - namedImports.length === 0 - ) { + if (!options?.new && aliasedImports.length === 1 && namedImports.length === 0) { return { lineToRemove: node.range(), }; @@ -194,18 +186,18 @@ function handleNamedImportBindings( const namedImportsNode = node.find({ rule: { - kind: 'named_imports', + kind: "named_imports", }, }); if (options?.new) { - for (const renamedImport of renamedImports) { + for (const renamedImport of aliasedImports) { if (renamedImport.text() === options.old) { const importName = renamedImport.parent().find({ rule: { has: { - field: 'name', - kind: 'identifier', + field: "name", + kind: "identifier", }, }, }); @@ -215,13 +207,13 @@ function handleNamedImportBindings( } } } else { - const aliasStatement = renamedImports.map((alias) => alias.parent()); + const aliasStatement = aliasedImports.map((alias) => alias.parent()); const newNamedImports = [...namedImports, ...aliasStatement] .map((d) => d.text()) .filter((d) => d !== renamedImport.parent().text()); return { - edit: namedImportsNode.replace(`{ ${newNamedImports.join(', ')} }`), + edit: namedImportsNode.replace(`{ ${newNamedImports.join(", ")} }`), }; } } @@ -234,22 +226,22 @@ function handleNamedRequireBindings( ): UpdateBindingReturnType { const requireWithMemberExpression = node.find({ rule: { - kind: 'variable_declarator', + kind: "variable_declarator", all: [ { has: { - field: 'name', - kind: 'identifier', + field: "name", + kind: "identifier", pattern: options.old, }, }, { has: { - field: 'value', - kind: 'member_expression', + field: "value", + kind: "member_expression", has: { - field: 'property', - kind: 'property_identifier', + field: "property", + kind: "property_identifier", }, }, }, @@ -266,8 +258,8 @@ function handleNamedRequireBindings( const reqNode = node.find({ rule: { - kind: 'call_expression', - pattern: 'require($ARGS)', + kind: "call_expression", + pattern: "require($ARGS)", }, }); @@ -278,7 +270,7 @@ function handleNamedRequireBindings( const objectPattern = node.find({ rule: { - kind: 'object_pattern', + kind: "object_pattern", }, }); @@ -286,7 +278,7 @@ function handleNamedRequireBindings( const declarations = node.findAll({ rule: { - kind: 'shorthand_property_identifier_pattern', + kind: "shorthand_property_identifier_pattern", }, }); @@ -323,14 +315,14 @@ function updateObjectPattern( rule: { any: [ { - kind: 'shorthand_property_identifier_pattern', + kind: "shorthand_property_identifier_pattern", }, { - kind: 'import_specifier', + kind: "import_specifier", not: { has: { - field: 'alias', - kind: 'identifier', + field: "alias", + kind: "identifier", }, }, }, @@ -355,5 +347,5 @@ function updateObjectPattern( newObjectPattern.push(newBinding); } - return parentNode.replace(`{ ${newObjectPattern.join(', ')} }`); + return parentNode.replace(`{ ${newObjectPattern.join(", ")} }`); } From 2e1e2c397fd39720981f9a2d5c9f5d5aed28f8ba Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Mon, 29 Sep 2025 14:13:47 +0100 Subject: [PATCH 12/13] docs: updateBinding Co-authored-by: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> --- utils/README.md | 24 ++- utils/src/ast-grep/update-binding.test.ts | 205 ++++++++++------------ 2 files changed, 116 insertions(+), 113 deletions(-) diff --git a/utils/README.md b/utils/README.md index b5d97ea5..3dd54623 100644 --- a/utils/README.md +++ b/utils/README.md @@ -72,7 +72,7 @@ import { resolveBindingPath } from '@nodejs/codemod-utils'; #### `removeBinding(node, binding)` -Removes a specific binding from destructured imports/requires, or removes the entire statement if it's the only binding. +Removes a specific binding from imports/requires, or removes the entire statement if it's the only binding. ```typescript import { removeBinding } from '@nodejs/codemod-utils'; @@ -82,6 +82,26 @@ import { removeBinding } from '@nodejs/codemod-utils'; // Given: const { isNativeError } = require('node:util'); // removeBinding(node, 'isNativeError') → Returns line range to remove entire statement + +// Given: const util = require('node:util'); +// removeBinding(node, 'util') → Returns line range to remove entire statement +``` + +#### `updateBinding(node, { old, new })` + +Updates a specific binding from imports/requires. It can be used to replace, add, or remove bindings. + +```typescript +import { updateBinding } from '@nodejs/codemod-utils'; + +// Given: const { isNativeError } = require('node:util'); +// updateBinding(node, {old: 'isNativeError', new: 'types'}) → Edit to: const { types } = require('node:util'); + +// Given: const { isNativeError } = require('node:util'); +// updateBinding(node, {old: undefined, new: 'types'}) → Edit to: const { isNativeError, types } = require('node:util'); + +// Given: const { isNativeError, types } = require('node:util'); +// updateBinding(node, {old: isNativeError, new: undefined}) → Works exactly as removeBinding util: const { types } = require('node:util'); ``` ### Code Manipulation @@ -230,5 +250,3 @@ const { types: t } = require('util'); // → t.isNativeError import { types } from 'node:util'; // → types.isNativeError const util = require('node:util'); // → util.types.isNativeError ``` - -This unified approach ensures your codemods work correctly regardless of how developers import Node.js modules in their projects. diff --git a/utils/src/ast-grep/update-binding.test.ts b/utils/src/ast-grep/update-binding.test.ts index e46819e4..0303823f 100644 --- a/utils/src/ast-grep/update-binding.test.ts +++ b/utils/src/ast-grep/update-binding.test.ts @@ -1,13 +1,13 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import astGrep from '@ast-grep/napi'; -import dedent from 'dedent'; -import { updateBinding } from './update-binding.ts'; -import type Js from '@codemod.com/jssg-types/langs/javascript'; -import type { SgNode } from '@codemod.com/jssg-types/main'; - -describe('update-binding', () => { - it('should update only the specified named import while preserving other named imports', () => { +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import astGrep from "@ast-grep/napi"; +import dedent from "dedent"; +import { updateBinding } from "./update-binding.ts"; +import type Js from "@codemod.com/jssg-types/langs/javascript"; +import type { SgNode } from "@codemod.com/jssg-types/main"; + +describe("update-binding", () => { + it("should update only the specified named import while preserving other named imports", () => { const code = dedent` const { types, diff } = require('node:util'); `; @@ -17,25 +17,22 @@ describe('update-binding', () => { const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); const change = updateBinding(requireStatement!, { - old: 'types', - new: 'newTypes', + old: "types", + new: "newTypes", }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); assert.strictEqual(change?.lineToRemove, undefined); - assert.strictEqual( - sourceCode, - "const { diff, newTypes } = require('node:util');", - ); + assert.strictEqual(sourceCode, "const { diff, newTypes } = require('node:util');"); }); - it('should update the specified named import', () => { + it("should update the specified named import", () => { const code = dedent` const { types } = require('node:util'); `; @@ -45,25 +42,22 @@ describe('update-binding', () => { const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); const change = updateBinding(requireStatement!, { - old: 'types', - new: 'newTypes', + old: "types", + new: "newTypes", }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, undefined); assert.strictEqual(change?.lineToRemove, undefined); - assert.strictEqual( - sourceCode, - "const { newTypes } = require('node:util');", - ); + assert.strictEqual(sourceCode, "const { newTypes } = require('node:util');"); }); - it('should remove the entire require statement when the only imported binding is removed', () => { + it("should remove the entire require statement when the only imported binding is removed", () => { const code = dedent` const util = require('node:util'); `; @@ -73,12 +67,12 @@ describe('update-binding', () => { const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); const change = updateBinding(requireStatement!, { - old: 'util', + old: "util", }); assert.notEqual(change, null); @@ -89,7 +83,7 @@ describe('update-binding', () => { }); }); - it('should update only the specified named import while preserving other named imports', () => { + it("should update only the specified named import while preserving other named imports", () => { const code = dedent` import { types, diff } = from 'node:util'; `; @@ -99,25 +93,22 @@ describe('update-binding', () => { const requireStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); const change = updateBinding(requireStatement!, { - old: 'types', - new: 'newTypes', + old: "types", + new: "newTypes", }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); assert.strictEqual(change?.lineToRemove, undefined); - assert.strictEqual( - sourceCode, - "import { diff, newTypes } = from 'node:util';", - ); + assert.strictEqual(sourceCode, "import { diff, newTypes } = from 'node:util';"); }); - it('should remove the specified named import while preserving other named imports', () => { + it("should remove the specified named import while preserving other named imports", () => { const code = dedent` import { types, diff } = from 'node:util'; `; @@ -127,12 +118,12 @@ describe('update-binding', () => { const requireStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); const change = updateBinding(requireStatement!, { - old: 'types', + old: "types", }); const sourceCode = node.commitEdits([change?.edit!]); @@ -141,7 +132,7 @@ describe('update-binding', () => { assert.strictEqual(sourceCode, "import { diff } = from 'node:util';"); }); - it('should remove the entire import statement when the only imported binding is removed', () => { + it("should remove the entire import statement when the only imported binding is removed", () => { const code = dedent` import util from 'node:util'; `; @@ -151,12 +142,12 @@ describe('update-binding', () => { const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); const change = updateBinding(importStatement!, { - old: 'util', + old: "util", }); assert.notEqual(change, null); @@ -167,7 +158,7 @@ describe('update-binding', () => { assert.strictEqual(change?.edit, undefined); }); - it('should remove the entire import statement when removing the only named import', () => { + it("should remove the entire import statement when removing the only named import", () => { const code = dedent` import { types } from 'node:util'; `; @@ -177,12 +168,12 @@ describe('update-binding', () => { const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); const change = updateBinding(importStatement!, { - old: 'types', + old: "types", }); assert.notEqual(change, null); @@ -193,7 +184,7 @@ describe('update-binding', () => { assert.strictEqual(change?.edit, undefined); }); - it('should remove the entire import line when only one aliased binding is imported and it matches the alias', () => { + it("should remove the entire import line when only one aliased binding is imported and it matches the alias", () => { const code = dedent` import { types as utilTypes } from 'node:util'; `; @@ -203,12 +194,12 @@ describe('update-binding', () => { const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); const change = updateBinding(importStatement!, { - old: 'utilTypes', + old: "utilTypes", }); assert.notEqual(change, null); @@ -219,7 +210,7 @@ describe('update-binding', () => { assert.strictEqual(change?.edit, undefined); }); - it('should update the entire import line when only one aliased binding is imported and it matches the alias', () => { + it("should update the entire import line when only one aliased binding is imported and it matches the alias", () => { const code = dedent` import { types as utilTypes } from 'node:util'; `; @@ -229,25 +220,22 @@ describe('update-binding', () => { const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); const change = updateBinding(importStatement!, { - old: 'utilTypes', - new: 'newTypes', + old: "utilTypes", + new: "newTypes", }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); assert.strictEqual(change?.lineToRemove, undefined); - assert.strictEqual( - sourceCode, - "import { newTypes as utilTypes } from 'node:util';", - ); + assert.strictEqual(sourceCode, "import { newTypes as utilTypes } from 'node:util';"); }); - it('should remove only the aliased import binding when it matches the provided alias', () => { + it("should remove only the aliased import binding when it matches the provided alias", () => { const code = dedent` import { types as utilTypes, diff } from 'node:util'; `; @@ -257,12 +245,12 @@ describe('update-binding', () => { const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); const change = updateBinding(importStatement!, { - old: 'utilTypes', + old: "utilTypes", }); const sourceCode = node.commitEdits([change?.edit!]); @@ -271,7 +259,7 @@ describe('update-binding', () => { assert.strictEqual(sourceCode, "import { diff } from 'node:util';"); }); - it('should remove the entire require statement when the only imported binding is removed', () => { + it("should remove the entire require statement when the only imported binding is removed", () => { const code = dedent` const util = require('node:util'); `; @@ -281,12 +269,12 @@ describe('update-binding', () => { const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); const change = updateBinding(requireStatement!, { - old: 'util', + old: "util", }); assert.notEqual(change, null); @@ -297,7 +285,7 @@ describe('update-binding', () => { }); }); - it('should return undefined when the binding does not match the imported name', () => { + it("should return undefined when the binding does not match the imported name", () => { const code = dedent` const util = require('node:util'); `; @@ -307,19 +295,19 @@ describe('update-binding', () => { const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); // line 12 it was imported as util, and here is passed types to be removed const change = updateBinding(requireStatement!, { - old: 'types', + old: "types", }); assert.equal(change, undefined); }); - it('should remove the entire require statement when removing the only named import', () => { + it("should remove the entire require statement when removing the only named import", () => { const code = dedent` const { types } = require('node:util'); `; @@ -329,12 +317,12 @@ describe('update-binding', () => { const importStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); const change = updateBinding(importStatement!, { - old: 'types', + old: "types", }); assert.notEqual(change, null); @@ -345,7 +333,7 @@ describe('update-binding', () => { }); }); - it('should update the destructured variable', () => { + it("should update the destructured variable", () => { const code = dedent` const { mainModule } = process; `; @@ -355,22 +343,22 @@ describe('update-binding', () => { const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); const change = updateBinding(requireStatement!, { - old: 'mainModule', - new: 'newMainModule', + old: "mainModule", + new: "newMainModule", }); const sourceCode = node.commitEdits([change?.edit!]); assert.notEqual(change, null); assert.strictEqual(change?.lineToRemove, undefined); - assert.strictEqual(sourceCode, 'const { newMainModule } = process;'); + assert.strictEqual(sourceCode, "const { newMainModule } = process;"); }); - it('should remove the entire import statement when the only namespace import is removed', () => { + it("should remove the entire import statement when the only namespace import is removed", () => { const code = dedent` import * as util from 'node:util'; `; @@ -380,12 +368,12 @@ describe('update-binding', () => { const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); const change = updateBinding(importStatement!, { - old: 'util', + old: "util", }); assert.notEqual(change, null); @@ -396,7 +384,7 @@ describe('update-binding', () => { assert.strictEqual(change?.edit, undefined); }); - it('should update the namespace binding when newBinding is passed', () => { + it("should update the namespace binding when newBinding is passed", () => { const code = dedent` import * as util from 'node:util'; `; @@ -406,13 +394,13 @@ describe('update-binding', () => { const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); const change = updateBinding(importStatement!, { - old: 'util', - new: 'newUtil', + old: "util", + new: "newUtil", }); const sourceCode = node.commitEdits([change?.edit!]); @@ -421,7 +409,7 @@ describe('update-binding', () => { assert.strictEqual(sourceCode, "import * as newUtil from 'node:util';"); }); - it('should return undefined when trying to update a binding that does not exist in the import statement', () => { + it("should return undefined when trying to update a binding that does not exist in the import statement", () => { const code = dedent` import { types, diff } from 'node:util'; `; @@ -431,19 +419,19 @@ describe('update-binding', () => { const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); const change = updateBinding(importStatement!, { - old: 'none', - new: 'newNone', + old: "none", + new: "newNone", }); assert.equal(change, undefined); }); - it('should remove only the aliased import binding when it matches the provided alias', () => { + it("should remove only the aliased import binding when it matches the provided alias", () => { const code = dedent` import { types as utilTypes, diff } from 'node:util'; `; @@ -453,12 +441,12 @@ describe('update-binding', () => { const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); const change = updateBinding(importStatement!, { - old: 'utilTypes', + old: "utilTypes", }); const sourceCode = node.commitEdits([change?.edit!]); @@ -467,7 +455,7 @@ describe('update-binding', () => { assert.strictEqual(sourceCode, "import { diff } from 'node:util';"); }); - it('should update only the aliased import binding when it matches the provided alias among multiple aliased imports', () => { + it("should update only the aliased import binding when it matches the provided alias among multiple aliased imports", () => { const code = dedent` import { types as utilTypes, diff as utilDiffs } from 'node:util'; `; @@ -477,13 +465,13 @@ describe('update-binding', () => { const importStatement = node.find({ rule: { - kind: 'import_statement', + kind: "import_statement", }, }); const change = updateBinding(importStatement!, { - old: 'utilTypes', - new: 'newTypes', + old: "utilTypes", + new: "newTypes", }); const sourceCode = node.commitEdits([change?.edit!]); @@ -495,9 +483,9 @@ describe('update-binding', () => { ); }); - it('Should update destructured property access from require statement to named import', () => { + it("Should update destructured property access from require statement to named import", () => { const code = dedent` - const SlowBuffer = require("buffer").SlowBuffer; + const Test = require("buffer").SlowBuffer; `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); @@ -505,13 +493,13 @@ describe('update-binding', () => { const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); const change = updateBinding(requireStatement!, { - old: 'SlowBuffer', - new: 'Buffer', + old: "Test", + new: "Buffer", }); assert.notEqual(change, undefined); @@ -522,7 +510,7 @@ describe('update-binding', () => { assert.strictEqual(sourceCode, `const { Buffer } = require("buffer");`); }); - it('Should remove entire require when property access exists require statement', () => { + it("Should remove entire require when property access exists require statement", () => { const code = dedent` const SlowBuffer = require("buffer").SlowBuffer; `; @@ -532,12 +520,12 @@ describe('update-binding', () => { const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); const change = updateBinding(requireStatement!, { - old: 'SlowBuffer', + old: "SlowBuffer", }); assert.notEqual(change, undefined); @@ -556,7 +544,7 @@ describe('update-binding', () => { }); }); - it('If named import already exists it just needs to remove the old reference', () => { + it("If named import already exists it just needs to remove the old reference", () => { const code = dedent` const { SlowBuffer, Buffer } = require("buffer"); `; @@ -566,13 +554,13 @@ describe('update-binding', () => { const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); const change = updateBinding(requireStatement!, { - old: 'SlowBuffer', - new: 'Buffer', + old: "SlowBuffer", + new: "Buffer", }); assert.notEqual(change, undefined); @@ -583,7 +571,7 @@ describe('update-binding', () => { assert.strictEqual(sourceCode, `const { Buffer } = require("buffer");`); }); - it('When oldBinding is not passed, should create new binding in require', () => { + it("When oldBinding is not passed, should create new binding in require", () => { const code = dedent` const { SlowBuffer } = require("buffer"); `; @@ -593,12 +581,12 @@ describe('update-binding', () => { const requireStatement = node.find({ rule: { - kind: 'lexical_declaration', + kind: "lexical_declaration", }, }); const change = updateBinding(requireStatement!, { - new: 'Buffer', + new: "Buffer", }); assert.notEqual(change, undefined); @@ -606,9 +594,6 @@ describe('update-binding', () => { const sourceCode = node.commitEdits([change?.edit!]); - assert.strictEqual( - sourceCode, - `const { SlowBuffer, Buffer } = require("buffer");`, - ); + assert.strictEqual(sourceCode, `const { SlowBuffer, Buffer } = require("buffer");`); }); }); From ca1b7cfea511b91a47812f82277f42e13022823b Mon Sep 17 00:00:00 2001 From: Bruno Rodrigues Date: Tue, 30 Sep 2025 10:08:34 +0100 Subject: [PATCH 13/13] bring readme.md text back --- utils/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/utils/README.md b/utils/README.md index 3dd54623..d644eea8 100644 --- a/utils/README.md +++ b/utils/README.md @@ -250,3 +250,5 @@ const { types: t } = require('util'); // → t.isNativeError import { types } from 'node:util'; // → types.isNativeError const util = require('node:util'); // → util.types.isNativeError ``` + +This unified approach ensures your codemods work correctly regardless of how developers import Node.js modules in their projects.