import traverse, { NodePath } from "../lib"; import { parse } from "@babel/parser"; import * as t from "@babel/types"; function getPath(code, options): NodePath { const ast = typeof code === "string" ? parse(code, options) : createNode(code); let path; traverse(ast, { Program: function (_path) { path = _path; _path.stop(); }, }); return path; } function getIdentifierPath(code) { const ast = parse(code); let nodePath; traverse(ast, { Identifier: function (path) { nodePath = path; path.stop(); }, }); return nodePath; } function createNode(node) { const ast = t.file(t.program(Array.isArray(node) ? node : [node])); // This puts the path into the cache internally // We afterwards traverse ast, as we need to start traversing // at the File node and not the Program node NodePath.get({ hub: { buildError: (_, msg) => new Error(msg), }, parentPath: null, parent: ast, container: ast, key: "program", }).setContext(); return ast; } describe("scope", () => { describe("binding paths", () => { it("function declaration id", function () { expect( getPath("function foo() {}").scope.getBinding("foo").path.type, ).toBe("FunctionDeclaration"); }); it("function expression id", function () { expect( getPath("(function foo() {})") .get("body")[0] .get("expression") .scope.getBinding("foo").path.type, ).toBe("FunctionExpression"); }); it("function param", function () { expect( getPath("(function (foo) {})") .get("body")[0] .get("expression") .scope.getBinding("foo").path.type, ).toBe("Identifier"); }); describe("function parameter expression", function () { it("should not have visibility of declarations inside function body", () => { expect( getPath( `var a = "outside"; (function foo(b = a) { let a = "inside" })`, ) .get("body.1.expression.params.0") .scope.getBinding("a").path.node.init.value, ).toBe("outside"); }); it("should have visibility on parameter bindings", () => { expect( getPath(`var a = "outside"; (function foo(b = a, a = "inside") {})`) .get("body.1.expression.params.0") .scope.getBinding("a").path.node.right.value, ).toBe("inside"); }); }); describe("import declaration", () => { it.each([ [ "import default", "import foo from 'foo';(foo)=>{}", "foo", "ImportDefaultSpecifier", ], [ "import named default", "import { default as foo } from 'foo';(foo)=>{}", "foo", "ImportSpecifier", ], [ "import named", "import { foo } from 'foo';(foo)=>{}", "foo", "ImportSpecifier", ], [ "import named aliased", "import { _foo as foo } from 'foo';(foo)=>{}", "foo", "ImportSpecifier", ], [ "import namespace", "import * as foo from 'foo';(foo)=>{}", "foo", "ImportNamespaceSpecifier", ], ])("%s", (testTitle, source, bindingName, bindingNodeType) => { expect( getPath(source, { sourceType: "module" }).scope.getBinding( bindingName, ).path.type, ).toBe(bindingNodeType); }); }); describe("export declaration", () => { it.each([ [ "export default function", "export default function foo(foo) {}", "foo", "FunctionDeclaration", ], [ "export default class", "export default class foo extends function foo () {} {}", "foo", "ClassDeclaration", ], [ "export named default", "export const foo = function foo(foo) {};", "foo", "VariableDeclarator", ], [ "export named default", "export const [ { foo } ] = function foo(foo) {};", "foo", "VariableDeclarator", ], ])("%s", (testTitle, source, bindingName, bindingNodeType) => { expect( getPath(source, { sourceType: "module" }).scope.getBinding( bindingName, ).path.type, ).toBe(bindingNodeType); }); }); describe("computed method key", () => { describe("should not have visibility of declarations inside method body", () => { it("when path is computed key", () => { expect( getPath(`var a = "outside"; ({ [a]() { let a = "inside" } })`) .get("body.1.expression.properties.0.key") .scope.getBinding("a").path.node.init.value, ).toBe("outside"); expect( getPath( `var a = "outside"; class foo { [a]() { let a = "inside" } }`, ) .get("body.1.body.body.0.key") .scope.getBinding("a").path.node.init.value, ).toBe("outside"); }); it("when path is in nested scope which is computed key", () => { expect( getPath(`var a = "outside"; ({ [() => a]() { let a = "inside" } })`) .get("body.1.expression.properties.0.key.body") .scope.getBinding("a").path.node.init.value, ).toBe("outside"); expect( getPath( `var a = "outside"; class foo { [() => a]() { let a = "inside" } }`, ) .get("body.1.body.body.0.key.body") .scope.getBinding("a").path.node.init.value, ).toBe("outside"); }); it("when path is in nested scope within computed key", () => { expect( getPath( `var a = "outside"; ({ [(() => a)() + ""]() { let a = "inside" } })`, ) .get("body.1.expression.properties.0.key.left.callee.body") .scope.getBinding("a").path.node.init.value, ).toBe("outside"); expect( getPath( `var a = "outside"; class foo { [(() => a)() + ""]() { let a = "inside" } }`, ) .get("body.1.body.body.0.key.left.callee.body") .scope.getBinding("a").path.node.init.value, ).toBe("outside"); }); it("when path is in nested within another computed key", () => { expect( getPath( `var a = "outside"; ({ get [ { get [a]() { let a = "inside"; return a; } }.outside ]() { let a = "middle"; return a; } })`, ) .get("body.1.expression.properties.0.key.object.properties.0.key") .scope.getBinding("a").path.node.init.value, ).toBe("outside"); expect( getPath( `var a = "outside"; class foo { static get [ class { static get [a]() { let a = "inside"; return a; } }.outside ]() { let a = "middle"; return a; } }`, ) .get("body.1.body.body.0.key.object.body.body.0.key") .scope.getBinding("a").path.node.init.value, ).toBe("outside"); }); }); it("should not have visibility on parameter bindings", () => { expect( getPath(`var a = "outside"; ({ [a](a = "inside") {} })`) .get("body.1.expression.properties.0.key") .scope.getBinding("a").path.node.init.value, ).toBe("outside"); expect( getPath(`var a = "outside"; class foo { [a](a = "inside") {} }`) .get("body.1.body.body.0.key") .scope.getBinding("a").path.node.init.value, ).toBe("outside"); }); }); it("variable declaration", function () { expect(getPath("var foo = null;").scope.getBinding("foo").path.type).toBe( "VariableDeclarator", ); expect( getPath("var { foo } = null;").scope.getBinding("foo").path.type, ).toBe("VariableDeclarator"); expect( getPath("var [ foo ] = null;").scope.getBinding("foo").path.type, ).toBe("VariableDeclarator"); expect( getPath("var { bar: [ foo ] } = null;").scope.getBinding("foo").path .type, ).toBe("VariableDeclarator"); }); it("declare var", function () { expect( getPath("declare var foo;", { plugins: ["flow"] }).scope.getBinding( "foo", ).path.type, ).toBe("DeclareVariable"); }); it("declare function", function () { expect( getPath("declare function foo(): void;", { plugins: ["flow"], }).scope.getBinding("foo").path.type, ).toBe("DeclareFunction"); }); it("declare module", function () { expect( getPath("declare module foo {};", { plugins: ["flow"], }).scope.getBinding("foo").path.type, ).toBe("DeclareModule"); }); it("declare type alias", function () { expect( getPath("declare type foo = string;", { plugins: ["flow"], }).scope.getBinding("foo").path.type, ).toBe("DeclareTypeAlias"); }); it("declare opaque type", function () { expect( getPath("declare opaque type foo;", { plugins: ["flow"], }).scope.getBinding("foo").path.type, ).toBe("DeclareOpaqueType"); }); it("declare interface", function () { expect( getPath("declare interface Foo {};", { plugins: ["flow"], }).scope.getBinding("Foo").path.type, ).toBe("DeclareInterface"); }); it("type alias", function () { expect( getPath("type foo = string;", { plugins: ["flow"], }).scope.getBinding("foo").path.type, ).toBe("TypeAlias"); }); it("opaque type alias", function () { expect( getPath("opaque type foo = string;", { plugins: ["flow"], }).scope.getBinding("foo").path.type, ).toBe("OpaqueType"); }); it("interface", function () { expect( getPath("interface Foo {};", { plugins: ["flow"], }).scope.getBinding("Foo").path.type, ).toBe("InterfaceDeclaration"); }); it("import type", function () { expect( getPath("import type {Foo} from 'foo';", { plugins: ["flow"], }).scope.getBinding("Foo").path.type, ).toBe("ImportSpecifier"); }); it("variable constantness", function () { expect(getPath("var a = 1;").scope.getBinding("a").constant).toBe(true); expect(getPath("var a = 1; a = 2;").scope.getBinding("a").constant).toBe( false, ); expect(getPath("var a = 1, a = 2;").scope.getBinding("a").constant).toBe( false, ); expect( getPath("var a = 1; var a = 2;").scope.getBinding("a").constant, ).toBe(false); }); it("purity", function () { expect( getPath("({ x: 1, foo() { return 1 } })") .get("body")[0] .get("expression") .isPure(), ).toBeTruthy(); expect( getPath("class X { get foo() { return 1 } }") .get("body")[0] .get("expression") .isPure(), ).toBeFalsy(); expect( getPath("`${a}`").get("body")[0].get("expression").isPure(), ).toBeFalsy(); expect( getPath("let a = 1; `${a}`").get("body")[1].get("expression").isPure(), ).toBeTruthy(); expect( getPath("let a = 1; `${a++}`") .get("body")[1] .get("expression") .isPure(), ).toBeFalsy(); expect( getPath("tagged`foo`").get("body")[0].get("expression").isPure(), ).toBeFalsy(); expect( getPath("String.raw`foo`").get("body")[0].get("expression").isPure(), ).toBeTruthy(); }); test("label", function () { expect(getPath("foo: { }").scope.getBinding("foo")).toBeUndefined(); expect(getPath("foo: { }").scope.getLabel("foo").type).toBe( "LabeledStatement", ); expect(getPath("foo: { }").scope.getLabel("toString")).toBeUndefined(); expect( getPath( ` foo: { } `, ).scope.generateUid("foo"), ).toBe("_foo"); }); test("generateUid collision check with labels", function () { expect( getPath( ` _foo: { } `, ).scope.generateUid("foo"), ).toBe("_foo2"); expect( getPath( ` _foo: { } _foo1: { } _foo2: { } `, ).scope.generateUid("foo"), ).toBe("_foo3"); }); describe("reference paths", () => { it("param referenced in function body", function () { const path = getIdentifierPath("function square(n) { return n * n}"); const referencePaths = path.context.scope.bindings.n.referencePaths; expect(referencePaths).toHaveLength(2); expect(referencePaths[0].node.loc.start).toEqual({ line: 1, column: 28, }); expect(referencePaths[1].node.loc.start).toEqual({ line: 1, column: 32, }); }); it("id referenced in function body", () => { const path = getIdentifierPath("(function n(m) { return n })"); const { referencePaths, identifier } = path.scope.getOwnBinding("n"); expect(identifier.start).toMatchInlineSnapshot(`10`); expect(referencePaths).toHaveLength(1); expect(referencePaths[0].node.start).toMatchInlineSnapshot(`24`); }); it("id referenced in param initializer - function expression", () => { const path = getIdentifierPath("(function n(m = n) {})"); const { referencePaths, identifier } = path.scope.getOwnBinding("n"); expect(identifier.start).toMatchInlineSnapshot(`10`); expect(referencePaths).toHaveLength(1); expect(referencePaths[0].node.start).toMatchInlineSnapshot(`16`); }); it("id referenced in param initializer - function declaration", () => { const path = getIdentifierPath("function n(m = n) {}"); const { referencePaths, identifier } = path.scope.getBinding("n"); expect(identifier.start).toMatchInlineSnapshot(`9`); expect(referencePaths).toHaveLength(1); expect(referencePaths[0].node.start).toMatchInlineSnapshot(`15`); }); it("param referenced in function body with id collision", () => { const path = getIdentifierPath("(function n(n) { return n })"); const { referencePaths, identifier } = path.scope.getOwnBinding("n"); expect(identifier.start).toMatchInlineSnapshot(`12`); expect(referencePaths).toHaveLength(1); expect(referencePaths[0].node.start).toMatchInlineSnapshot(`24`); }); it("param referenced in param initializer with id collision", () => { const path = getIdentifierPath("(function n(n, m = n) {})"); const { referencePaths, identifier } = path.scope.getOwnBinding("n"); expect(identifier.start).toMatchInlineSnapshot(`12`); expect(referencePaths).toHaveLength(1); expect(referencePaths[0].node.start).toMatchInlineSnapshot(`19`); }); }); describe("after crawl", () => { it("modified function identifier available in function scope", () => { const path = getPath("(function f(f) {})") .get("body")[0] .get("expression"); path.get("id").replaceWith(t.identifier("g")); path.scope.crawl(); const binding = path.scope.getBinding("g"); expect(binding.kind).toBe("local"); }); it("modified function param available in function scope", () => { const path = getPath("(function f(f) {})") .get("body")[0] .get("expression"); path.get("params")[0].replaceWith(t.identifier("g")); path.scope.crawl(); const binding = path.scope.getBinding("g"); expect(binding.kind).toBe("param"); }); it("modified class identifier available in class expression scope", () => { const path = getPath("(class c {})").get("body")[0].get("expression"); path.get("id").replaceWith(t.identifier("g")); path.scope.crawl(); const binding = path.scope.getBinding("g"); expect(binding.kind).toBe("local"); }); it("modified class identifier available in class declaration scope", () => { const path = getPath("class c {}").get("body")[0]; path.get("id").replaceWith(t.identifier("g")); path.scope.crawl(); const binding = path.scope.getBinding("g"); expect(binding.kind).toBe("let"); }); }); it("class identifier available in class scope after crawl", function () { const path = getPath("class a { build() { return new a(); } }"); path.scope.crawl(); let referencePaths = path.scope.bindings.a.referencePaths; expect(referencePaths).toHaveLength(1); referencePaths = path.get("body[0]").scope.bindings.a.referencePaths; expect(referencePaths).toHaveLength(1); expect(path.scope.bindings.a).toBe(path.get("body[0]").scope.bindings.a); }); it("references after re-crawling", function () { const path = getPath("function Foo() { var _jsx; }"); path.scope.crawl(); path.scope.crawl(); expect(path.scope.references._jsx).toBeTruthy(); }); test("generateUid collision check after re-crawling", function () { const path = getPath("function Foo() { var _jsx; }"); path.scope.crawl(); path.scope.crawl(); expect(path.scope.generateUid("jsx")).toBe("_jsx2"); }); test("generateUid collision check after re-crawling (function expression local id)", function () { const path = getPath("var fn = function _name(){}"); path.scope.crawl(); path.scope.crawl(); expect(path.scope.generateUid("name")).toBe("_name2"); }); test("generateUid collision check after re-crawling (function params)", function () { const path = getPath("[].map(_unicorn => [_unicorn])"); path.scope.crawl(); path.scope.crawl(); expect(path.scope.generateUid("unicorn")).toBe("_unicorn2"); }); test("generateUid collision check after re-crawling (catch clause)", function () { const path = getPath("try {} catch (_err) {}"); path.scope.crawl(); path.scope.crawl(); expect(path.scope.generateUid("err")).toBe("_err2"); }); test("generateUid collision check after re-crawling (class expression local id)", function () { const path = getPath("var C = class _Cls{}"); path.scope.crawl(); path.scope.crawl(); expect(path.scope.generateUid("Cls")).toBe("_Cls2"); }); it("re-exports are not references", () => { const path = getPath("export { x } from 'y'", { sourceType: "module", }); expect(path.scope.hasGlobal("x")).toBe(false); }); }); describe("duplicate bindings", () => { /* * These tests do not use the parser as the parser has * its own scope tracking and we want to test the scope tracking * of traverse here and see if it handles duplicate bindings correctly */ describe("catch", () => { // try {} catch (e) { let e; } const createTryCatch = function (kind) { return t.tryStatement( t.blockStatement([]), t.catchClause( t.identifier("e"), t.blockStatement([ t.variableDeclaration(kind, [ t.variableDeclarator(t.identifier("e"), t.stringLiteral("1")), ]), ]), ), ); }; it("let", () => { const ast = createTryCatch("let"); expect(() => getPath(ast)).toThrowErrorMatchingSnapshot(); }); it("const", () => { const ast = createTryCatch("const"); expect(() => getPath(ast)).toThrowErrorMatchingSnapshot(); }); it("var", () => { const ast = createTryCatch("var"); expect(() => getPath(ast)).not.toThrow(); }); }); ["let", "const"].forEach(name => { it(`${name} and function in sub scope`, () => { const ast = [ t.variableDeclaration(name, [ t.variableDeclarator(t.identifier("foo")), ]), t.blockStatement([ t.functionDeclaration( t.identifier("foo"), [], t.blockStatement([]), ), ]), ]; expect(() => getPath(ast)).not.toThrow(); }); }); describe("global", () => { // node1, node2, success // every line will run 2 tests `node1;node2;` and `node2;node1;` // unless node1 === node2 const cases = [ ["const", "let", false], ["const", "const", false], ["const", "function", false], ["const", "class", false], ["const", "var", false], ["let", "let", false], ["let", "class", false], ["let", "function", false], ["let", "var", false], ["var", "class", false], ["var", "function", true], ["var", "var", true], ["class", "function", false], ["class", "class", false], ["function", "function", true], ]; const createNode = function (kind) { switch (kind) { case "let": case "const": case "var": return t.variableDeclaration(kind, [ t.variableDeclarator(t.identifier("foo")), ]); case "class": return t.classDeclaration( t.identifier("foo"), null, t.classBody([]), ); case "function": return t.functionDeclaration( t.identifier("foo"), [], t.blockStatement([]), ); } }; const createAST = function (kind1, kind2) { return [createNode(kind1), createNode(kind2)]; }; for (const [kind1, kind2, success] of cases) { if (success) { it(`${kind1}/${kind2} should succeed`, () => { const ast = createAST(kind1, kind2); expect(() => getPath(ast)).not.toThrow(); }); } else { it(`${kind1}/${kind2} should fail`, () => { const ast = createAST(kind1, kind2); expect(() => getPath(ast)).toThrowErrorMatchingSnapshot(); }); } if (kind1 !== kind2) { // todo: remove the if allowed if (kind1 === "const" && (kind2 === "function" || kind2 === "var")) { continue; } if (success) { it(`${kind2}/${kind1} should succeed`, () => { const ast = createAST(kind2, kind1); expect(() => getPath(ast)).not.toThrow(); }); } else { it(`${kind2}/${kind1} should fail`, () => { const ast = createAST(kind2, kind1); expect(() => getPath(ast)).toThrowErrorMatchingSnapshot(); }); } } } }); }); describe("own bindings", () => { // Var declarations should be declared in the nearest FunctionParent ancestry describe("var declarations should be registered", () => { it("in program", () => { const program = getPath("var foo;"); expect(program.scope.hasOwnBinding("foo")).toBe(true); }); it("in function declaration", () => { const functionDeclaration = getPath("function f() { var foo; }").get( "body.0.expression", ); expect(functionDeclaration.scope.hasOwnBinding("foo")).toBe(true); }); it("in function expression", () => { const functionExpression = getPath("(function () { var foo; })").get( "body.0.expression", ); expect(functionExpression.scope.hasOwnBinding("foo")).toBe(true); }); it("in arrow expression", () => { const arrowExpression = getPath("() => { var foo; }").get("body.0.expression"); expect(arrowExpression.scope.hasOwnBinding("foo")).toBe(true); }); it("in object method", () => { const objectMethod = getPath("({ method() { var foo; } })").get( "body.0.expression.properties.0", ); expect(objectMethod.scope.hasOwnBinding("foo")).toBe(true); }); it("in class method", () => { const classMethod = getPath("(class { method() { var foo; } })").get( "body.0.expression.body.body.0", ); expect(classMethod.scope.hasOwnBinding("foo")).toBe(true); }); it("in class private method", () => { const classMethod = getPath("(class { #method() { var foo; } })").get( "body.0.expression.body.body.0", ); expect(classMethod.scope.hasOwnBinding("foo")).toBe(true); }); it("in static block", () => { const staticBlock = getPath("(class { static { var foo; } })", { plugins: ["classStaticBlock"], }).get("body.0.expression.body.body.0"); expect(staticBlock.scope.hasOwnBinding("foo")).toBe(true); }); }); describe("var declarations should not be registered", () => { it("in block statement", () => { const blockStatement = getPath("{ var foo; }").get("body.0"); expect(blockStatement.scope.hasOwnBinding("foo")).toBe(false); }); it("in catch clause", () => { const catchClause = getPath("try {} catch { var foo; }").get( "body.0.handler", ); expect(catchClause.scope.hasOwnBinding("foo")).toBe(false); }); it("in for-init statement", () => { const forStatement = getPath("for (var foo;;);").get("body.0"); expect(forStatement.scope.hasOwnBinding("foo")).toBe(false); }); it("in for-in statement", () => { const forStatement = getPath("for (var foo in x);").get("body.0"); expect(forStatement.scope.hasOwnBinding("foo")).toBe(false); }); it("in for-of statement", () => { const forStatement = getPath("for (var foo of x);").get("body.0"); expect(forStatement.scope.hasOwnBinding("foo")).toBe(false); }); it("in switch statement", () => { const switchStatement = getPath("switch (0) { case 0: var foo; }").get( "body.0", ); expect(switchStatement.scope.hasOwnBinding("foo")).toBe(false); }); it("in while statement", () => { const whileStatement = getPath("while (0) \n var foo;").get("body.0"); expect(whileStatement.scope.hasOwnBinding("foo")).toBe(false); }); it("in do-while statement", () => { const doWhileStatement = getPath("do \n var foo \n while(0);").get( "body.0", ); expect(doWhileStatement.scope.hasOwnBinding("foo")).toBe(false); }); }); // Lexical declarations should be registered in the nearest BlockParent ancestry describe("let declarations should be registered", () => { it("in program", () => { const program = getPath("let foo;"); expect(program.scope.hasOwnBinding("foo")).toBe(true); }); it("in function declaration", () => { const functionDeclaration = getPath("function f() { let foo; }").get( "body.0.expression", ); expect(functionDeclaration.scope.hasOwnBinding("foo")).toBe(true); }); it("in function expression", () => { const functionExpression = getPath("(function () { let foo; })").get( "body.0.expression", ); expect(functionExpression.scope.hasOwnBinding("foo")).toBe(true); }); it("in arrow expression", () => { const arrowExpression = getPath("() => { let foo; }").get("body.0.expression"); expect(arrowExpression.scope.hasOwnBinding("foo")).toBe(true); }); it("in object method", () => { const objectMethod = getPath("({ method() { let foo; } })").get( "body.0.expression.properties.0", ); expect(objectMethod.scope.hasOwnBinding("foo")).toBe(true); }); it("in class method", () => { const classMethod = getPath("(class { method() { let foo; } })").get( "body.0.expression.body.body.0", ); expect(classMethod.scope.hasOwnBinding("foo")).toBe(true); }); it("in class private method", () => { const classMethod = getPath("(class { #method() { let foo; } })").get( "body.0.expression.body.body.0", ); expect(classMethod.scope.hasOwnBinding("foo")).toBe(true); }); it("in static block", () => { const staticBlock = getPath("(class { static { let foo; } })", { plugins: ["classStaticBlock"], }).get("body.0.expression.body.body.0"); expect(staticBlock.scope.hasOwnBinding("foo")).toBe(true); }); it("in block statement", () => { const blockStatement = getPath("{ let foo; }").get("body.0"); expect(blockStatement.scope.hasOwnBinding("foo")).toBe(true); }); it("in catch clause", () => { const catchClause = getPath("try {} catch { let foo; }").get( "body.0.handler", ); expect(catchClause.scope.hasOwnBinding("foo")).toBe(true); }); it("in for-init statement", () => { const forStatement = getPath("for (let foo;;);").get("body.0"); expect(forStatement.scope.hasOwnBinding("foo")).toBe(true); }); it("in for-in statement", () => { const forStatement = getPath("for (let foo in x);").get("body.0"); expect(forStatement.scope.hasOwnBinding("foo")).toBe(true); }); it("in for-of statement", () => { const forStatement = getPath("for (let foo of x);").get("body.0"); expect(forStatement.scope.hasOwnBinding("foo")).toBe(true); }); it("in switch statement", () => { const switchStatement = getPath("switch (0) { case 0: let foo; }").get( "body.0", ); expect(switchStatement.scope.hasOwnBinding("foo")).toBe(true); }); }); }); });