diff --git a/utils/src/ast-grep/resolve-binding-path.test.ts b/utils/src/ast-grep/resolve-binding-path.test.ts index 2d73335b..f424f49a 100644 --- a/utils/src/ast-grep/resolve-binding-path.test.ts +++ b/utils/src/ast-grep/resolve-binding-path.test.ts @@ -2,6 +2,8 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import astGrep from "@ast-grep/napi"; import dedent from "dedent"; +import type Js from "@codemod.com/jssg-types/langs/javascript"; +import type { SgNode } from "@codemod.com/jssg-types/main"; import { resolveBindingPath } from "./resolve-binding-path.ts"; @@ -12,7 +14,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const importStatement = rootNode.root().find({ + const importStatement = (rootNode.root() as SgNode).find({ rule: { kind: "lexical_declaration", }, @@ -23,13 +25,30 @@ describe("resolve-binding-path", () => { assert.strictEqual(bindingPath, "util.types.isNativeError"); }); + it("should be able to resolve binding path from namespace ESM import", () => { + const code = dedent` + import foo from "node:foo" + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const importStatement = (rootNode.root() as SgNode).find({ + rule: { + kind: "import_statement", + }, + }); + + const bindingPath = resolveBindingPath(importStatement!, "$.bar"); + + assert.strictEqual(bindingPath, "foo.bar"); + }); + it("should be able to solve binding path when destructuring happen", () => { const code = dedent` const { types } = require('node:util'); `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const importStatement = rootNode.root().find({ + const importStatement = (rootNode.root() as SgNode).find({ rule: { kind: "variable_declarator", }, @@ -46,7 +65,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const requireStatement = rootNode.root().find({ + const requireStatement = (rootNode.root() as SgNode).find({ rule: { kind: "variable_declarator", }, @@ -63,7 +82,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const importStatement = rootNode.root().find({ + const importStatement = (rootNode.root() as SgNode).find({ rule: { kind: "variable_declarator", }, @@ -80,7 +99,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const functionDeclaration = rootNode.root().find({ + const functionDeclaration = (rootNode.root() as SgNode).find({ rule: { kind: "function_declaration", }, @@ -95,7 +114,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const importStatement = rootNode.root().find({ + const importStatement = (rootNode.root() as SgNode).find({ rule: { kind: "import_statement", }, @@ -112,7 +131,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const importStatement = rootNode.root().find({ + const importStatement = (rootNode.root() as SgNode).find({ rule: { kind: "import_statement", }, @@ -129,7 +148,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const importStatement = rootNode.root().find({ + const importStatement = (rootNode.root() as SgNode).find({ rule: { kind: "import_statement", }, @@ -146,7 +165,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const importStatement = rootNode.root().find({ + const importStatement = (rootNode.root() as SgNode).find({ rule: { kind: "import_statement", }, @@ -163,7 +182,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const requireStatement = rootNode.root().find({ + const requireStatement = (rootNode.root() as SgNode).find({ rule: { kind: "variable_declarator", }, @@ -180,7 +199,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const importStatement = rootNode.root().find({ + const importStatement = (rootNode.root() as SgNode).find({ rule: { kind: "lexical_declaration", }, @@ -197,7 +216,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const importStatement = rootNode.root().find({ + const importStatement = (rootNode.root() as SgNode).find({ rule: { kind: "import_statement", }, @@ -214,7 +233,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const requireStatement = rootNode.root().find({ + const requireStatement = (rootNode.root() as SgNode).find({ rule: { kind: "variable_declarator", }, @@ -231,7 +250,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const requireStatement = rootNode.root().find({ + const requireStatement = (rootNode.root() as SgNode).find({ rule: { kind: "variable_declarator", }, @@ -248,7 +267,7 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const requireStatement = rootNode.root().find({ + const requireStatement = (rootNode.root() as SgNode).find({ rule: { kind: "variable_declarator", }, @@ -265,7 +284,8 @@ describe("resolve-binding-path", () => { `; const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); - const requireStatement = rootNode.root().find({ + + const requireStatement = (rootNode.root() as SgNode).find({ rule: { kind: "variable_declarator", }, @@ -275,4 +295,89 @@ describe("resolve-binding-path", () => { assert.strictEqual(bindingPath, "types.isNativeError"); }); + + it("should resolve correctly when have member-expression", () => { + const code = dedent` + const SlowBuffer = require('buffer').SlowBuffer; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const requireStatement = (rootNode.root() as SgNode).find({ + rule: { + kind: "variable_declarator", + }, + }); + + const bindingPath = resolveBindingPath(requireStatement!, "$.SlowBuffer"); + + assert.strictEqual(bindingPath, "SlowBuffer"); + }); + + it("should resolve correctly when there are multiple property accesses", () => { + const code = dedent` + const variable = require('buffer').a.b; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const requireStatement = (rootNode.root() as SgNode).find({ + rule: { + kind: "variable_declarator", + }, + }); + + const bindingPath = resolveBindingPath(requireStatement!, "$.a.b"); + + assert.strictEqual(bindingPath, "variable"); + }); + + it("should resolve correctly when there are multiple property accesses but not the entire path", () => { + const code = dedent` + const variable = require('buffer').a.b; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const requireStatement = (rootNode.root() as SgNode).find({ + rule: { + kind: "variable_declarator", + }, + }); + + const bindingPath = resolveBindingPath(requireStatement!, "$.a.b.c.d.e"); + + assert.strictEqual(bindingPath, "variable.c.d.e"); + }); + + it("should resolve correctly when there are multiple property accesses and destructuring", () => { + const code = dedent` + const { c: { d } } = require('buffer').a.b; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const requireStatement = (rootNode.root() as SgNode).find({ + rule: { + kind: "variable_declarator", + }, + }); + + const bindingPath = resolveBindingPath(requireStatement!, "$.a.b.c.d.e"); + + assert.strictEqual(bindingPath, "d.e"); + }); + + it("should resolve as undefined when property accesses is different than path to solve", () => { + const code = dedent` + const c = require('buffer').a.g.c; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const requireStatement = (rootNode.root() as SgNode).find({ + rule: { + kind: "variable_declarator", + }, + }); + + const bindingPath = resolveBindingPath(requireStatement!, "$.a.b.c.d.e"); + + assert.strictEqual(bindingPath, undefined); + }); }); diff --git a/utils/src/ast-grep/resolve-binding-path.ts b/utils/src/ast-grep/resolve-binding-path.ts index 55d9c4a4..f9a1bbf1 100644 --- a/utils/src/ast-grep/resolve-binding-path.ts +++ b/utils/src/ast-grep/resolve-binding-path.ts @@ -68,6 +68,31 @@ function resolveBindingPathRequire(node: SgNode, path: string) { }); } + const propertyAccesses = activeNode.findAll({ + rule: { + kind: "property_identifier", + inside: { + kind: "member_expression", + }, + }, + }); + + if (propertyAccesses.length) { + const pathArr = path.split("."); + let newPath = ["$"]; + let i = 0; + + for (; i < propertyAccesses.length; i++) { + // pathArr[i+1] to skip the first element (which is $) that was used for binding replacement + if (propertyAccesses[i]?.text() !== pathArr[i + 1]) { + return undefined; + } + } + + // Get the remaining path that was not used in propertyAccesses + path = newPath.concat(pathArr.splice(i + 1)).join("."); + } + activeNode = activeNode.child(0); if (activeNode?.kind() === "identifier") {