UNPKG

lively.ast

Version:

Parsing JS code into ASTs and tools to query and transform these trees.

705 lines (579 loc) 24.6 kB
/*global beforeEach, afterEach, describe, it*/ import { expect } from "mocha-es6"; import * as query from "../lib/query.js"; import { parse } from "../lib/parser.js"; import { arr, obj, chain, fun } from "lively.lang"; describe('query', function() { describe("toplevel", () => { it("declsAndRefsInTopLevelScope", function() { var code = "var x = 3;\n function baz(y) { var zork; return xxx + zork + x + y; }\nvar y = 4, z;\nbar = 'foo';" var parsed = parse(code); var declsAndRefs = query.topLevelDeclsAndRefs(parsed); var varDecls = declsAndRefs.varDecls; var varIds = chain(declsAndRefs.varDecls).pluck('declarations').flatten().pluck("id").pluck("name").value(); expect(["x", "y", "z"]).deep.equals(varIds, "var ids"); var funcIds = chain(declsAndRefs.funcDecls).pluck('id').pluck('name').value(); expect(["baz"]).deep.equals(funcIds, "funIds: " + obj.inspect(funcIds)); var refs = declsAndRefs.refs; var refIds = chain(refs).pluck('name').value(); expect(["bar", "xxx", "x"]).deep.equals(refIds, "ref ids"); }); it("recognizeFunctionDeclaration", function() { var code = "this.addScript(function run(n) { if (n > 0) run(n-1); show('done'); });", result = query.topLevelDeclsAndRefs(code), expected = ["show"]; expect(expected).deep.equals(result.undeclaredNames); }); it("recognizeArrowFunctionDeclaration", function() { var code = "this.addScript((n, run) => { if (n > 0) run(n-1); show('done'); });", result = query.topLevelDeclsAndRefs(code), expected = ["show"]; expect(expected).deep.equals(result.undeclaredNames); }); it("recognizeClassDeclaration", function() { var code = "class Foo {\n" + " constructor(name) { this.name = name; }\n" + "}\n"+ "new Foo();", result = query.topLevelDeclsAndRefs(code), expected = []; expect(expected).deep.equals(result.undeclaredNames); }); it("finds this references", function() { var code = "this.foo = this.bar;", result = query.topLevelDeclsAndRefs(code), expected = [{start: 0}, {start: 0}]; expect(result.thisRefs).to.containSubset(expected); }); }); describe("toplevel func decls", () => { it("tested-action", () => { var code = "var baz = function zork() { function barf() {} }\nfunction foo() { function bar() {}; }", parsed = parse(code), funcDecls = query.topLevelFuncDecls(parsed); expect(funcDecls).to.deep.equal([{node: parsed.body[1], path: ["body", 1]}]); }); }); describe("scoping", () => { it("scopes", function() { var code = "var x = {y: 3}; function foo(y) { var foo = 3, baz = 5; x = 99; bar = 2; bar; Object.bar = 3; this.x = this.foo + 23; }"; var parsed = parse(code); var scope = query.scopes(parsed); var expected = { node: parsed, varDecls: [{declarations: [{id: {name: 'x'}}]}], funcDecls: [{id: {name: 'foo'}}], params: [], refs: [], thisRefs: [], subScopes: [{ node: parsed.body[1], varDecls: [{declarations: [{id: {name: 'foo'}}, {id: {name: 'baz'}}]}], funcDecls: [], params: [{name: "y"}], refs: [{name: "x"}, {name: "bar"}, {name: "bar"}, {name: "Object"}], thisRefs: [{type: "ThisExpression"}, {type: "ThisExpression"}] }] } expect(scope).to.containSubset(expected) // top level scope var varNames = chain(scope.varDecls).pluck('declarations').flatten().value(); expect(1).equals(varNames.length, 'root scope vars'); var funcNames = chain(scope.funcDecls).pluck('id').pluck('name').value(); expect(1).equals(scope.funcDecls.length, 'root scope funcs'); expect(0).equals(scope.params.length, 'root scope params'); expect(0).equals(scope.refs.length, 'root scope refs'); // sub scope expect(1).equals(scope.subScopes.length, 'subscope length'); var subScope = scope.subScopes[0]; var varNames = chain(subScope.varDecls).pluck('declarations').flatten().value(); expect(2).equals(varNames.length, 'subscope vars'); expect(0).equals(subScope.funcDecls.length, 'subscope funcs'); expect(4).equals(subScope.refs.length, 'subscope refs'); expect(1).equals(subScope.params.length, 'subscope params'); }); }); describe("finding globals", () => { it("findGlobalVars", function() { var code = "var margin = {top: 20, right: 20, bottom: 30, left: 40},\n" + " width = 960 - margin.left - margin.right,\n" + " height = 500 - margin.top - margin.bottom;\n" + "function blup() {}\n" + "foo + String(baz) + foo + height;\n" var result = query.findGlobalVarRefs(code); var expected = [{start:169,end:172, name:"foo", type:"Identifier"}, {start:182,end:185, name:"baz", type:"Identifier"}, {start:189,end:192, name:"foo", type:"Identifier"}]; expect(result).to.containSubset(expected); }); }); describe("finding stuff from a source location", () => { it("findNodesIncludingLines", function() { var code = "var x = {\n f: function(a) {\n return 23;\n }\n}\n", expected1 = ["Program", "VariableDeclaration", "VariableDeclarator", "ObjectExpression", "Property", "FunctionExpression", "BlockStatement", "ReturnStatement", "Literal"], nodes1 = query.findNodesIncludingLines(null, code, [3]); expect(expected1).deep.equals(nodes1.map(n => n.type)); var expected2 = ["Program", "VariableDeclaration", "VariableDeclarator", "ObjectExpression"], nodes2 = query.findNodesIncludingLines(null, code, [3,5]); expect(expected2).deep.equals(nodes2.map(n => n.type)); }); describe("find scopes", function() { it("findScopeAtIndex", function() { var src = fun.extractBody(function() { var x = { f: function(a) { return function(a) { return a + 1}; }, f2: function() {} } }); var index = 35; // on first return var parsed = parse(src, {addSource: true}); var result = query.scopesAtIndex(parsed, index); var scopes = query.scopes(parsed); var expected = [scopes, scopes.subScopes[0]] expect(expected).deep.equals(result); }); it("findScopeAtIndexWhenIndexPointsToFuncDecl", function() { var src = 'var x = "fooo"; function bar() { var z = "baz" }'; var parsed = parse(src, {addSource: true}); var scopes = query.scopes(parsed); var index = 26; // on bar var result = query.scopeAtIndex(parsed, index); expect(scopes).deep.equals(result); var index = 34; // inside bar body var result = query.scopeAtIndex(parsed, index); expect(scopes.subScopes[0]).deep.equals(result); }); it("findScopeAtIndexWhenIndexPointsToArg", function() { var src = 'var x = "fooo"; function bar(zork) { var z = zork + "baz"; }'; var parsed = parse(src, {addSource: true}); var scopes = query.scopes(parsed); var index = 31; // on zork var result = query.scopeAtIndex(parsed, index); expect(scopes.subScopes[0]).deep.equals(result); }); }); describe("finding references and declarations", function() { it("findDeclarationClosestToIndex", function() { var src = `var x = 3, yyy = 4;\nvar z = function() { yyy + yyy + (function(yyy) { yyy+1 })(); }`, index = 48, // second yyy of addition parsed = parse(src), result = query.findDeclarationClosestToIndex(parsed, "yyy", index); expect(result).to.containSubset({end:14,name:"yyy",start:11,type:"Identifier"}); }); it("findDeclarationClosestToIndex 2", function() { var src = `var x = 3, yyy = 4;\nvar z = function() { yyy + yyy + (function(yyy) { yyy+1 })(); }`, index = 73, // yyy of function parsed = parse(src), result = query.findDeclarationClosestToIndex(parsed, "yyy", index); expect(result).to.containSubset({end:66,name:"yyy",start:63,type:"Identifier"}); }); it("findReferencesAndDeclsInScope find vars", function() { var parsed = parse("var x = 3, y = 4;\nvar z = function() { y + y + (function(y) { y+1 })(); }"); expect(query.findReferencesAndDeclsInScope(query.scopes(parsed), "y")) .to.containSubset([{end:12,name:"y",start:11,type:"Identifier"}, {end:40,name:"y",start:39,type:"Identifier"}, {end:44,name:"y",start:43,type:"Identifier"}]); }); it("findReferencesAndDeclsInScope finds this", function() { var parsed = parse("this.bar = 23; var x = function() { this.foo(this.zork, function() { this.bark }); };"), scope = query.scopes(parsed).subScopes[0]; expect(query.findReferencesAndDeclsInScope(scope, "this")) .to.containSubset([{end: 40,start: 36, type: "ThisExpression"}, {end: 49,start: 45, type: "ThisExpression"}]); }); }); }); describe("statementOf", () => { function itFindsTheStatment(src, getTarget, getExpected) { return it(src, () => { var parsed = parse(src), found = query.statementOf(parsed, getTarget(parsed)), expected = getExpected(parsed); // expect(expected).to.equal(found, `node not found\nexpected: ${JSON.stringify(expected, null, 2)}\nactual: ${JSON.stringify(found, null, 2)}`); expect(JSON.stringify(expected, null, 2)).to.equal(JSON.stringify(found, null, 2)); }); } itFindsTheStatment( 'var x = 3; function foo() { var y = 3; return y + 2 }; x + foo();', ast => ast.body[1].body.body[1].argument.left, ast => ast.body[1].body.body[1]); itFindsTheStatment( 'var x = 1; x;', ast => ast.body[1], ast => ast.body[1]); itFindsTheStatment( 'switch (123) { case 123: debugger; }', ast => ast.body[0].cases[0].consequent[0], ast => ast.body[0].cases[0].consequent[0]); itFindsTheStatment( 'if (true) { var a = 1; }', ast => ast.body[0].consequent.body[0].declarations[0], ast => ast.body[0].consequent.body[0]); itFindsTheStatment( 'if (true) var a = 1;', ast => ast.body[0].consequent.declarations[0].id, ast => ast.body[0].consequent); itFindsTheStatment( 'if (true) var a = 2;', ast => ast.body[0].test, ast => ast.body[0]); itFindsTheStatment( 'export default class Foo {}', ast => ast.body[0].declaration.id, ast => ast.body[0].declaration); itFindsTheStatment( 'a;', ast => ast.body[0].expression, ast => ast.body[0]); it("finds path to statement", () => { var parsed = parse('var x = 3; function foo() { var y = 3; return y + 2 }; x + foo();'), found = query.statementOf(parsed, parsed.body[1].body.body[1].argument.left, {asPath: true}), expected = ["body", 1,"body", "body", 1]; expect(expected).to.deep.equal(found); }); }); describe("es6 compat", () => { describe("patterns", function() { describe("obj destructuring", function() { describe("params", function() { it("simple", () => expect(query.topLevelDeclsAndRefs("({x}) => { x }").undeclaredNames).deep.equals([])); it("default init", () => expect(query.topLevelDeclsAndRefs("({x} = {}) => { x }").undeclaredNames).deep.equals([])); it("array", () => expect(query.topLevelDeclsAndRefs("([a, b, ...rest]) => { (a + b).concat(rest); }").undeclaredNames).deep.equals([])); it("default init array", () => expect(query.topLevelDeclsAndRefs("([a, b, ...rest] = []) => { (a + b).concat(rest); }").undeclaredNames).deep.equals([])); it("alias", function() { var code = "({x: y}) => y", result = query.topLevelDeclsAndRefs(code), expected = []; expect(expected).deep.equals(result.undeclaredNames); }); it("nested", function() { var code = "({x: {a}}) => a", result = query.topLevelDeclsAndRefs(code), expected = []; expect(expected).deep.equals(result.undeclaredNames); }); }); describe("vars", function() { it("simple", function() { var code = "var {x, y} = {x: 3, y: 4};" var parsed = parse(code); var scopes = query.scopes(parsed); expect(["x", "y"]).deep.equals(query._declaredVarNames(scopes)); }); it("nested", function() { var code = "var {x, y: [{z}]} = {x: 3, y: [{z: 4}]};" var parsed = parse(code); var scopes = query.scopes(parsed); expect(["x", "z"]).deep.equals(query._declaredVarNames(scopes)); }); it("let", function() { var code = "let {x, y} = {x: 3, y: 4};" var parsed = parse(code); var scopes = query.scopes(parsed); expect(["x", "y"]).deep.equals(query._declaredVarNames(scopes)); }); }); }); describe("arr destructuring", function() { describe("params", function() { it("simple", function() { var code = "([x,{y}]) => x + y", result = query.topLevelDeclsAndRefs(code), expected = []; expect(expected).deep.equals(result.undeclaredNames); }); }); describe("vars", function() { it("simple", function() { var code = "var {x} = {x: 3};", parsed = parse(code), scopes = query.scopes(parsed); expect(["x"]).deep.equals(query._declaredVarNames(scopes)); }); }); describe("...", function() { it("as param", function() { var code = "(a, ...b) => a + b[0];", result = query.topLevelDeclsAndRefs(code), expected = []; expect(expected).deep.equals(result.undeclaredNames); }); it("as assignment", function() { var code = "var [head, ...inner] = [1,2,3,4,5];", parsed = parse(code), scopes = query.scopes(parsed); expect(["head", "inner"]).deep.equals(query._declaredVarNames(scopes)); }); }); }); it("finds default params", () => { var code = "function x(y = 2) { return y; }", parsed = parse(code), scopes = query.scopes(parsed).subScopes[0]; expect(["x", "y"]).deep.equals(query._declaredVarNames(scopes)); }); }); describe("templateStrings", function() { it("simple", function() { var code = "var x = `foo`;", parsed = parse(code), scopes = query.scopes(parsed); expect(["x"]).deep.equals(query._declaredVarNames(scopes)); }); it("with expressions", function() { var code = "var x = `foo ${y}`;", result = query.topLevelDeclsAndRefs(code); expect(["y"]).deep.equals(result.undeclaredNames); }); }); describe("es6 modules", function() { it('recognizes export declarations', function() { var code = 'export var x = 42; export function y() {}; export default function z() {};', parsed = parse(code), scopes = query.scopes(parsed); expect(["y", "z", "x"]).deep.equals(query._declaredVarNames(scopes)); }); it('recognizes import declarations', function() { var code = "import foo from 'bar';\n" + "import { baz } from 'zork';\n" + "import { qux as corge } from 'quux';\n", // rk not yet supported by acorn as of 2016-01-28: // + "import { * as grault } from 'garply';\n", parsed = parse(code), scopes = query.scopes(parsed); expect(["foo", "baz", "corge"/*, "grault"*/]).deep.equals(query._declaredVarNames(scopes)); }); it('ignores export froms', function() { var code = "export { foo } from 'bar';\n", parsed = parse(code); expect([]).deep.equals(query.findGlobalVarRefs(parsed)); }); }); describe("es6 classes", function() { it('recognizes super call', function() { var code = "class Foo extends Bar {\n" + " m() { return super.m() + 2; }\n" + "};\n", parsed = parse(code), toplevel = query.topLevelDeclsAndRefs(parsed); expect(toplevel.undeclaredNames).deep.equals(["Bar"]); }); }); describe("async", function() { it('recognizes async function', function() { var code = "async function foo() { return 23 }\nvar x = await foo();", parsed = parse(code), toplevel = query.topLevelDeclsAndRefs(parsed); expect(toplevel.declaredNames).deep.equals(["foo", "x"]); }); }); }); describe("helper", function() { var objExpr = parse("({x: 23, y: [{z: 4}]});").body[0].expression; expect(arr.pluck(query.helpers.objPropertiesAsList(objExpr, [], true), "key")) .eql([["x"], ["y", 0, "z"]]); expect(arr.pluck(query.helpers.objPropertiesAsList(objExpr, [], false), "key")) .eql([["x"], ["y"], ["y", 0, "z"]]); var objExpr = parse("var {x, y: [{z}]} = {x: 23, y: [{z: 4}]};").body[0].declarations[0].id; expect(arr.pluck(query.helpers.objPropertiesAsList(objExpr, [], true), "key")) .eql([["x"], ["y", 0, "z"]]); expect(arr.pluck(query.helpers.objPropertiesAsList(objExpr, [], false), "key")) .eql([["x"], ["y"], ["y", 0, "z"]]); }); describe("imports", () => { function im(src) { const scope = query.scopes(parse(src)); return query.imports(scope); } it("of named vars", async () => { const result = im("import { y as yyy } from './file2.js';"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ fromModule: "./file2.js", imported: 'y', local: "yyy" }); }); it("default", async () => { const result = im("import z from './file2.js';"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ fromModule: "./file2.js", imported: 'default', local: 'z' }); }); it("*", async () => { const result = im("import * as file2 from './file2.js';"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ fromModule: "./file2.js", imported: "*", local: "file2" }); }); }); describe("exports", () => { function ex(src) { const scope = query.scopes(parse(src)); return query.exports(scope, true); } it("of ids", async () => { const result = ex("var x = 23; export { x }"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ exported: "x", local: "x", type: "id", decl: {type: "VariableDeclarator", start: 4, end: 10}, declId: {type: "Identifier", start: 4, end: 5, name: "x"} }); }); it("as of ids", async () => { const result = ex("var x = 23; export { x as y }"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ exported: "y", local: "x", type: "id", decl: {type: "VariableDeclarator", start: 4, end: 10}, declId: {type: "Identifier", start: 4, end: 5, name: "x"} }); }); it("of referenced function", async () => { const result = ex("function x() {}; export { x }"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ exported: "x", local: "x", type: "id", decl: {type: "FunctionDeclaration", start: 0, end: 15}, declId: {type: "Identifier", start: 9, end: 10, name: "x"} }); }); it("of var decls", async () => { const result = ex("export var x = 23;"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ exported: "x", local: "x", type: "var", decl: {type: "VariableDeclarator", start: 11, end: 17}, declId: {type: "Identifier", start: 11, end: 12, name: "x"} }); }); it("of multiple var decls", async () => { const result = ex("export var x = 23, y = 42;"); expect(result).to.have.length(2); expect(result).to.containSubset([{ exported: "x", local: "x", type: "var", decl: {type: "VariableDeclarator", start: 11, end: 17}, declId: {type: "Identifier", start: 11, end: 12, name: "x"} },{ exported: "y", local: "y", type: "var", decl: {type: "VariableDeclarator", start: 19, end: 25}, declId: {type: "Identifier", start: 19, end: 20, name: "y"} }]); }); it("* from", async () => { const result = ex("export * from './file1.js'"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ exported: "*", local: null, fromModule: "./file1.js" }); }); it("named from", async () => { const result = ex("export { x } from './file1.js';"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ exported: "x", fromModule: "./file1.js", local: null }); }); it("named as from", async () => { const result = ex("export { x as y } from './file1.js';"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ exported: "y", imported: "x", fromModule: "./file1.js", local: null }); }); it("functions", async () => { const result = ex("export function bar() {}"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ exported: "bar", local: "bar", type: "function", decl: {type: "FunctionDeclaration", start: 7, end: 24}, declId: {type: "Identifier", start: 16, end: 19, name: "bar"} }); }); it("default function", async () => { const result = ex("export default async function foo() {}"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ exported: "default", local: "foo", type: "function", decl: {type: "FunctionDeclaration", start: 15, end: 38}, declId: {type: "Identifier", start: 30, end: 33, name: "foo"} }); }); it("default id", async () => { const result = ex("var x = 23; export default x;"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ exported: "default", local: "x", type: "id", decl: {type: "VariableDeclarator", start: 4, end: 10}, declId: {type: "Identifier", start: 4, end: 5, name: "x"} }); }); it("default expr", async () => { const result = ex("export default 12;"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ exported: "default", type: "expr", decl: {type: "Literal", start: 15, end: 17}, declId: {type: "Literal", start: 15, end: 17, value: 12} }); }); it("class", async () => { const result = ex("export class Baz {}"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ exported: "Baz", local: "Baz", type: "class", decl: {type: "ClassDeclaration", start: 7, end: 19}, declId: {type: "Identifier", start: 13, end: 16, name: "Baz"} }); }); it("default class", async () => { const result = ex("export default class Baz {}"); expect(result).to.have.length(1); expect(result[0]).to.containSubset({ exported: "default", local: "Baz", type: "class", decl: {type: "ClassDeclaration", start: 15, end: 27}, declId: {type: "Identifier", start: 21, end: 24, name: "Baz"} }); }); }); });