reference-following support via adapted scope.js and scopetools.js from defs.js project

This commit is contained in:
Olov Lassus
2014-08-05 23:59:08 +02:00
parent 471cf7334b
commit 08c2b5a40a
9 changed files with 435 additions and 1 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ echo "beginning ng-annotate defs-build"
rm -rf es5
mkdir es5
declare -a files=(generate-sourcemap.js lut.js ng-annotate.js ng-annotate-main.js nginject-comments.js run-tests.js)
declare -a files=(generate-sourcemap.js lut.js ng-annotate.js ng-annotate-main.js nginject-comments.js run-tests.js scope.js scopetools.js)
DEFS="../node_modules/.bin/defs"
if [[ ! -f "$DEFS" ]]; then DEFS="../../../../node_modules/.bin/defs" ; fi
if [[ ! -f "$DEFS" ]]; then DEFS="defs" ; fi
+4
View File
@@ -1,3 +1,7 @@
// lut.js
// MIT licensed, see LICENSE file
// Copyright (c) 2013-2014 Olov Lassus <olov.lassus@gmail.com>
"use strict";
const assert = require("assert");
+39
View File
@@ -11,9 +11,11 @@ const is = require("simple-is");
const alter = require("alter");
const traverse = require("ordered-ast-traverse");
const EOL = require("os").EOL;
const assert = require("assert");
const ngInjectComments = require("./nginject-comments");
const generateSourcemap = require("./generate-sourcemap");
const Lut = require("./lut");
const scopeTools = require("./scopetools");
const chainedRouteProvider = 1;
const chainedUrlRouterProvider = 2;
@@ -354,6 +356,14 @@ function judgeSuspects(ctx) {
}
target = jumpOverIife(target);
const followedTarget = followReference(target);
if (followedTarget) {
if (followedTarget.$once) {
continue;
}
followedTarget.$once = true;
target = followedTarget;
}
if (mode === "rebuild" && isAnnotatedArray(target)) {
replaceArray(target, fragments, quot);
@@ -368,6 +378,33 @@ function judgeSuspects(ctx) {
}
}
function followReference(node) {
if (!scopeTools.isReference(node)) {
return null;
}
const scope = node.$scope.lookup(node.name);
if (!scope) {
return null;
}
const parent = scope.getNode(node.name).$parent;
const kind = scope.getKind(node.name);
const ptype = parent.type;
if (is.someof(kind, ["const", "let", "var"])) {
assert(ptype === "VariableDeclarator");
return parent.init;
} else if (kind === "fun") {
assert(ptype === "FunctionDeclaration" || ptype === "FunctionExpression")
return parent;
}
// other kinds should not be handled ("param", "caught")
return null;
}
function judgeInjectArraySuspect(node, ctx) {
// /*@ngInject*/ var foo = function($scope) {} and
// /*@ngInject*/ function foo($scope) {} and
@@ -523,6 +560,8 @@ module.exports = function ngAnnotate(src, options) {
const lut = new Lut(ast, src);
scopeTools.setupScopeAndReferences(ast);
const ctx = {
mode: mode,
quot: quot,
+4
View File
@@ -1,3 +1,7 @@
// nginject-comments.js
// MIT licensed, see LICENSE file
// Copyright (c) 2013-2014 Olov Lassus <olov.lassus@gmail.com>
"use strict";
const is = require("simple-is");
+158
View File
@@ -0,0 +1,158 @@
// scope.js
// MIT licensed, see LICENSE file
// Copyright (c) 2013-2014 Olov Lassus <olov.lassus@gmail.com>
"use strict";
const assert = require("assert");
const stringmap = require("stringmap");
const stringset = require("stringset");
const is = require("simple-is");
const fmt = require("simple-fmt");
function Scope(args) {
assert(is.someof(args.kind, ["hoist", "block", "catch-block"]));
assert(is.object(args.node));
assert(args.parent === null || is.object(args.parent));
// kind === "hoist": function scopes, program scope, injected globals
// kind === "block": ES6 block scopes
// kind === "catch-block": catch block scopes
this.kind = args.kind;
// the AST node the block corresponds to
this.node = args.node;
// parent scope
this.parent = args.parent;
// children scopes for easier traversal (populated internally)
this.children = [];
// scope declarations. decls[variable_name] = {
// kind: "fun" for functions,
// "param" for function parameters,
// "caught" for catch parameter
// "var",
// "const",
// "let"
// node: the AST node the declaration corresponds to
// from: source code index from which it is visible at earliest
// (only stored for "const", "let" [and "var"] nodes)
// }
this.decls = stringmap();
// names of all variables declared outside this hoist scope but
// referenced in this scope (immediately or in child).
// only stored on hoist scopes for efficiency
// (because we currently generate lots of empty block scopes)
this.propagates = (this.kind === "hoist" ? stringset() : null);
// scopes register themselves with their parents for easier traversal
if (this.parent) {
this.parent.children.push(this);
}
}
Scope.prototype.print = function(indent) {
indent = indent || 0;
const scope = this;
const names = this.decls.keys().map(function(name) {
return fmt("{0} [{1}]", name, scope.decls.get(name).kind);
}).join(", ");
const propagates = this.propagates ? this.propagates.items().join(", ") : "";
console.log(fmt("{0}{1}: {2}. propagates: {3}", fmt.repeat(" ", indent), this.node.type, names, propagates));
this.children.forEach(function(c) {
c.print(indent + 2);
});
};
Scope.prototype.add = function(name, kind, node, referableFromPos) {
assert(is.someof(kind, ["fun", "param", "var", "caught", "const", "let"]));
function isConstLet(kind) {
return is.someof(kind, ["const", "let"]);
}
let scope = this;
// search nearest hoist-scope for fun, param and var's
// const, let and caught variables go directly in the scope (which may be hoist, block or catch-block)
if (is.someof(kind, ["fun", "param", "var"])) {
while (scope.kind !== "hoist") {
// if (scope.decls.has(name) && isConstLet(scope.decls.get(name).kind)) { // could be caught
// return error(getline(node), "{0} is already declared", name);
// }
scope = scope.parent;
}
}
// name exists in scope and either new or existing kind is const|let => error
// if (scope.decls.has(name) && (isConstLet(scope.decls.get(name).kind) || isConstLet(kind))) {
// return error(getline(node), "{0} is already declared", name);
// }
const declaration = {
kind: kind,
node: node,
};
if (referableFromPos) {
assert(is.someof(kind, ["var", "const", "let"]));
declaration.from = referableFromPos;
}
scope.decls.set(name, declaration);
};
Scope.prototype.getKind = function(name) {
assert(is.string(name));
const decl = this.decls.get(name);
return decl ? decl.kind : null;
};
Scope.prototype.getNode = function(name) {
assert(is.string(name));
const decl = this.decls.get(name);
return decl ? decl.node : null;
};
Scope.prototype.getFromPos = function(name) {
assert(is.string(name));
const decl = this.decls.get(name);
return decl ? decl.from : null;
};
Scope.prototype.hasOwn = function(name) {
return this.decls.has(name);
};
Scope.prototype.remove = function(name) {
return this.decls.remove(name);
};
Scope.prototype.doesPropagate = function(name) {
return this.propagates.has(name);
};
Scope.prototype.markPropagates = function(name) {
this.propagates.add(name);
};
Scope.prototype.closestHoistScope = function() {
let scope = this;
while (scope.kind !== "hoist") {
scope = scope.parent;
}
return scope;
};
Scope.prototype.lookup = function(name) {
for (let scope = this; scope; scope = scope.parent) {
if (scope.decls.has(name)) {
return scope;
} else if (scope.kind === "hoist") {
scope.propagates.add(name);
}
}
return null;
};
module.exports = Scope;
+180
View File
@@ -0,0 +1,180 @@
// scopetools.js
// MIT licensed, see LICENSE file
// Copyright (c) 2013-2014 Olov Lassus <olov.lassus@gmail.com>
"use strict";
const assert = require("assert");
const traverse = require("ordered-ast-traverse");
const Scope = require("./scope");
const is = require("simple-is");
module.exports = {
setupScopeAndReferences: setupScopeAndReferences,
isReference: isReference,
};
function setupScopeAndReferences(root) {
traverse(root, {pre: createScopes});
createTopScope(root.$scope);
}
function createScopes(node, parent) {
node.$parent = parent;
node.$scope = parent ? parent.$scope : null; // may be overridden
if (isNonFunctionBlock(node, parent)) {
// A block node is a scope unless parent is a function
node.$scope = new Scope({
kind: "block",
node: node,
parent: parent.$scope,
});
} else if (node.type === "VariableDeclaration") {
// Variable declarations names goes in current scope
node.declarations.forEach(function(declarator) {
const name = declarator.id.name;
node.$scope.add(name, node.kind, declarator.id, declarator.range[1]);
});
} else if (isFunction(node)) {
// Function is a scope, with params in it
// There's no block-scope under it
node.$scope = new Scope({
kind: "hoist",
node: node,
parent: parent.$scope,
});
// function has a name
if (node.id) {
if (node.type === "FunctionDeclaration") {
// Function name goes in parent scope for declared functions
parent.$scope.add(node.id.name, "fun", node.id, null);
} else if (node.type === "FunctionExpression") {
// Function name goes in function's scope for named function expressions
node.$scope.add(node.id.name, "fun", node.id, null);
} else {
assert(false);
}
}
node.params.forEach(function(param) {
node.$scope.add(param.name, "param", param, null);
});
} else if (isForWithConstLet(node) || isForInOfWithConstLet(node)) {
// For(In/Of) loop with const|let declaration is a scope, with declaration in it
// There may be a block-scope under it
node.$scope = new Scope({
kind: "block",
node: node,
parent: parent.$scope,
});
} else if (node.type === "CatchClause") {
const identifier = node.param;
node.$scope = new Scope({
kind: "catch-block",
node: node,
parent: parent.$scope,
});
node.$scope.add(identifier.name, "caught", identifier, null);
// All hoist-scope keeps track of which variables that are propagated through,
// i.e. an reference inside the scope points to a declaration outside the scope.
// This is used to mark "taint" the name since adding a new variable in the scope,
// with a propagated name, would change the meaning of the existing references.
//
// catch(e) is special because even though e is a variable in its own scope,
// we want to make sure that catch(e){let e} is never transformed to
// catch(e){var e} (but rather var e$0). For that reason we taint the use of e
// in the closest hoist-scope, i.e. where var e$0 belongs.
node.$scope.closestHoistScope().markPropagates(identifier.name);
} else if (node.type === "Program") {
// Top-level program is a scope
// There's no block-scope under it
node.$scope = new Scope({
kind: "hoist",
node: node,
parent: null,
});
}
}
function createTopScope(programScope) {
function inject(obj) {
for (let name in obj) {
const writeable = obj[name];
const kind = (writeable ? "var" : "const");
if (topScope.hasOwn(name)) {
topScope.remove(name);
}
topScope.add(name, kind, {loc: {start: {line: -1}}}, -1);
}
}
const topScope = new Scope({
kind: "hoist",
node: {},
parent: null,
});
const complementary = {
undefined: false,
Infinity: false,
console: false,
};
inject(complementary);
// inject(jshint_vars.reservedVars);
// inject(jshint_vars.ecmaIdentifiers);
// link it in
programScope.parent = topScope;
topScope.children.push(programScope);
return topScope;
}
function isConstLet(kind) {
return kind === "const" || kind === "let";
}
function isNonFunctionBlock(node, parent) {
return node.type === "BlockStatement" && parent.type !== "FunctionDeclaration" && parent.type !== "FunctionExpression";
}
function isForWithConstLet(node) {
return node.type === "ForStatement" && node.init && node.init.type === "VariableDeclaration" && isConstLet(node.init.kind);
}
function isForInOfWithConstLet(node) {
return isForInOf(node) && node.left.type === "VariableDeclaration" && isConstLet(node.left.kind);
}
function isForInOf(node) {
return node.type === "ForInStatement" || node.type === "ForOfStatement";
}
function isFunction(node) {
return node.type === "FunctionDeclaration" || node.type === "FunctionExpression";
}
function isReference(node) {
const parent = node.$parent;
return node.$refToScope ||
node.type === "Identifier" &&
!(parent.type === "VariableDeclarator" && parent.id === node) && // var|let|const $
!(parent.type === "MemberExpression" && parent.computed === false && parent.property === node) && // obj.$
!(parent.type === "Property" && parent.key === node) && // {$: ...}
!(parent.type === "LabeledStatement" && parent.label === node) && // $: ...
!(parent.type === "CatchClause" && parent.param === node) && // catch($)
!(isFunction(parent) && parent.id === node) && // function $(..
!(isFunction(parent) && is.someof(node, parent.params)) && // function f($)..
true;
}
+15
View File
@@ -428,3 +428,18 @@ var x = /*@ngInject*/ (function() {
return function($a) {
};
})();
// reference support
function MyCtrl1(a, b) {
}
if (true) {
// proper scope analysis including shadowing
let MyCtrl1 = function(c) {
}
angular.module("MyMod").directive("foo", MyCtrl1);
}
angular.module("MyMod").controller("bar", MyCtrl1);
function MyCtrl2(z) {
}
funcall(/*@ngInject*/ MyCtrl2); // explicit annotation on reference flows back to definition
+17
View File
@@ -446,3 +446,20 @@ var x = /*@ngInject*/ (function() {
return ["$a", function($a) {
}];
})();
// reference support
function MyCtrl1(a, b) {
}
MyCtrl1.$inject = ["a", "b"];
if (true) {
// proper scope analysis including shadowing
let MyCtrl1 = ["c", function(c) {
}]
angular.module("MyMod").directive("foo", MyCtrl1);
}
angular.module("MyMod").controller("bar", MyCtrl1);
function MyCtrl2(z) {
}
MyCtrl2.$inject = ["z"];
funcall(/*@ngInject*/ MyCtrl2); // explicit annotation on reference flows back to definition
+17
View File
@@ -446,3 +446,20 @@ var x = /*@ngInject*/ (function() {
return ['$a', function($a) {
}];
})();
// reference support
function MyCtrl1(a, b) {
}
MyCtrl1.$inject = ['a', 'b'];
if (true) {
// proper scope analysis including shadowing
let MyCtrl1 = ['c', function(c) {
}]
angular.module("MyMod").directive("foo", MyCtrl1);
}
angular.module("MyMod").controller("bar", MyCtrl1);
function MyCtrl2(z) {
}
MyCtrl2.$inject = ['z'];
funcall(/*@ngInject*/ MyCtrl2); // explicit annotation on reference flows back to definition