mirror of
https://github.com/bluetech/ng-annotate-patched.git
synced 2026-07-02 00:17:42 +08:00
926 lines
30 KiB
JavaScript
926 lines
30 KiB
JavaScript
// ng-annotate-main.js
|
|
// MIT licensed, see LICENSE file
|
|
// Copyright (c) 2013-2014 Olov Lassus <olov.lassus@gmail.com>
|
|
|
|
"use strict";
|
|
const fmt = require("simple-fmt");
|
|
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 ngInject = require("./nginject");
|
|
const generateSourcemap = require("./generate-sourcemap");
|
|
const Lut = require("./lut");
|
|
const scopeTools = require("./scopetools");
|
|
const stringmap = require("stringmap");
|
|
let parser = null; // will be lazy-loaded to esprima or acorn
|
|
|
|
const chainedRouteProvider = 1;
|
|
const chainedUrlRouterProvider = 2;
|
|
const chainedStateProvider = 3;
|
|
const chainedRegular = 4;
|
|
|
|
function match(node, ctx, matchPlugins) {
|
|
const isMethodCall = (
|
|
node.type === "CallExpression" &&
|
|
node.callee.type === "MemberExpression" &&
|
|
node.callee.computed === false
|
|
);
|
|
|
|
const matchMethodCalls = (isMethodCall &&
|
|
(matchRegular(node, ctx) || matchNgRoute(node) || matchNgUi(node) || matchHttpProvider(node)));
|
|
|
|
return matchMethodCalls ||
|
|
(matchPlugins && matchPlugins(node)) ||
|
|
matchDirectiveReturnObject(node) ||
|
|
matchProviderGet(node);
|
|
}
|
|
|
|
function matchDirectiveReturnObject(node) {
|
|
// only matches inside directives
|
|
// return { .. controller: function($scope, $timeout), ...}
|
|
|
|
return limit("directive", node.type === "ReturnStatement" &&
|
|
node.argument && node.argument.type === "ObjectExpression" &&
|
|
matchProp("controller", node.argument.properties));
|
|
}
|
|
|
|
function limit(name, node) {
|
|
if (node) {
|
|
node.$limitToMethodName = name;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
function matchProviderGet(node) {
|
|
// only matches inside providers
|
|
// (this|self|that).$get = function($scope, $timeout)
|
|
// { ... $get: function($scope, $timeout), ...}
|
|
let memberExpr;
|
|
let self;
|
|
return limit("provider", (node.type === "AssignmentExpression" && (memberExpr = node.left).type === "MemberExpression" &&
|
|
memberExpr.property.name === "$get" &&
|
|
((self = memberExpr.object).type === "ThisExpression" || (self.type === "Identifier" && is.someof(self.name, ["self", "that"]))) &&
|
|
node.right) ||
|
|
(node.type === "ObjectExpression" && matchProp("$get", node.properties)));
|
|
}
|
|
|
|
function matchNgRoute(node) {
|
|
// $routeProvider.when("path", {
|
|
// ...
|
|
// controller: function($scope) {},
|
|
// resolve: {f: function($scope) {}, ..}
|
|
// })
|
|
|
|
// we already know that node is a (non-computed) method call
|
|
const callee = node.callee;
|
|
const obj = callee.object; // identifier or expression
|
|
if (!(obj.$chained === chainedRouteProvider || (obj.type === "Identifier" && obj.name === "$routeProvider"))) {
|
|
return false;
|
|
}
|
|
node.$chained = chainedRouteProvider;
|
|
|
|
const method = callee.property; // identifier
|
|
if (method.name !== "when") {
|
|
return false;
|
|
}
|
|
|
|
const args = node.arguments;
|
|
if (args.length !== 2) {
|
|
return false;
|
|
}
|
|
const configArg = last(args)
|
|
if (configArg.type !== "ObjectExpression") {
|
|
return false;
|
|
}
|
|
|
|
const props = configArg.properties;
|
|
const res = [
|
|
matchProp("controller", props)
|
|
];
|
|
// {resolve: ..}
|
|
res.push.apply(res, matchResolve(props));
|
|
|
|
const filteredRes = res.filter(Boolean);
|
|
return (filteredRes.length === 0 ? false : filteredRes);
|
|
}
|
|
|
|
function matchNgUi(node) {
|
|
// $stateProvider.state("myState", {
|
|
// ...
|
|
// controller: function($scope)
|
|
// controllerProvider: function($scope)
|
|
// templateProvider: function($scope)
|
|
// onEnter: function($scope)
|
|
// onExit: function($scope)
|
|
// });
|
|
// $stateProvider.state("myState", {... resolve: {f: function($scope) {}, ..} ..})
|
|
// $stateProvider.state("myState", {... views: {... somename: {... controller: fn, controllerProvider: fn, templateProvider: fn, resolve: {f: fn}}}})
|
|
//
|
|
// stateHelperProvider.setNestedState({ sameasregularstate, children: [sameasregularstate, ..]})
|
|
// stateHelperProvider.setNestedState({ sameasregularstate, children: [sameasregularstate, ..]}, true)
|
|
//
|
|
// $urlRouterProvider.when(.., function($scope) {})
|
|
//
|
|
// $modal.open({.. controller: fn, resolve: {f: function($scope) {}, ..}});
|
|
|
|
// we already know that node is a (non-computed) method call
|
|
const callee = node.callee;
|
|
const obj = callee.object; // identifier or expression
|
|
const method = callee.property; // identifier
|
|
const args = node.arguments;
|
|
|
|
// shortcut for $modal.open({.. controller: fn, resolve: {f: function($scope) {}, ..}});
|
|
if (obj.type === "Identifier" && obj.name === "$modal" && method.name === "open" &&
|
|
args.length === 1 && args[0].type === "ObjectExpression") {
|
|
const props = args[0].properties;
|
|
const res = [matchProp("controller", props)];
|
|
res.push.apply(res, matchResolve(props));
|
|
return res.filter(Boolean);
|
|
}
|
|
|
|
// shortcut for $urlRouterProvider.when(.., function($scope) {})
|
|
if (obj.$chained === chainedUrlRouterProvider || (obj.type === "Identifier" && obj.name === "$urlRouterProvider")) {
|
|
node.$chained = chainedUrlRouterProvider;
|
|
|
|
if (method.name === "when" && args.length >= 1) {
|
|
return last(args);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// everything below is for $stateProvider and stateHelperProvider alone
|
|
if (!(obj.$chained === chainedStateProvider || (obj.type === "Identifier" && is.someof(obj.name, ["$stateProvider", "stateHelperProvider"])))) {
|
|
return false;
|
|
}
|
|
node.$chained = chainedStateProvider;
|
|
|
|
if (is.noneof(method.name, ["state", "setNestedState"])) {
|
|
return false;
|
|
}
|
|
|
|
// $stateProvider.state({ ... }) and $stateProvider.state("name", { ... })
|
|
// stateHelperProvider.setNestedState({ .. }) and stateHelperProvider.setNestedState({ .. }, true)
|
|
if (!(args.length >= 1 && args.length <= 2)) {
|
|
return false;
|
|
}
|
|
|
|
const configArg = (method.name === "state" ? last(args) : args[0]);
|
|
|
|
const res = [];
|
|
|
|
recursiveMatch(configArg);
|
|
|
|
const filteredRes = res.filter(Boolean);
|
|
return (filteredRes.length === 0 ? false : filteredRes);
|
|
|
|
|
|
function recursiveMatch(objectExpressionNode) {
|
|
if (!objectExpressionNode || objectExpressionNode.type !== "ObjectExpression") {
|
|
return false;
|
|
}
|
|
|
|
const properties = objectExpressionNode.properties;
|
|
|
|
matchStateProps(properties, res);
|
|
|
|
const childrenArrayExpression = matchProp("children", properties);
|
|
const children = childrenArrayExpression && childrenArrayExpression.elements;
|
|
|
|
if (!children) {
|
|
return;
|
|
}
|
|
children.forEach(recursiveMatch);
|
|
}
|
|
|
|
function matchStateProps(props, res) {
|
|
const simple = [
|
|
matchProp("controller", props),
|
|
matchProp("controllerProvider", props),
|
|
matchProp("templateProvider", props),
|
|
matchProp("onEnter", props),
|
|
matchProp("onExit", props),
|
|
];
|
|
res.push.apply(res, simple);
|
|
|
|
// {resolve: ..}
|
|
res.push.apply(res, matchResolve(props));
|
|
|
|
// {view: ...}
|
|
const viewObject = matchProp("views", props);
|
|
if (viewObject && viewObject.type === "ObjectExpression") {
|
|
viewObject.properties.forEach(function(prop) {
|
|
if (prop.value.type === "ObjectExpression") {
|
|
res.push(matchProp("controller", prop.value.properties));
|
|
res.push(matchProp("controllerProvider", prop.value.properties));
|
|
res.push(matchProp("templateProvider", prop.value.properties));
|
|
res.push.apply(res, matchResolve(prop.value.properties));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function matchHttpProvider(node) {
|
|
// $httpProvider.interceptors.push(function($scope) {});
|
|
// $httpProvider.responseInterceptors.push(function($scope) {});
|
|
|
|
// we already know that node is a (non-computed) method call
|
|
const callee = node.callee;
|
|
const obj = callee.object; // identifier or expression
|
|
const method = callee.property; // identifier
|
|
|
|
return (method.name === "push" &&
|
|
obj.type === "MemberExpression" && !obj.computed &&
|
|
obj.object.name === "$httpProvider" && is.someof(obj.property.name, ["interceptors", "responseInterceptors"]) &&
|
|
node.arguments.length >= 1 && node.arguments);
|
|
}
|
|
|
|
function matchRegular(node, ctx) {
|
|
// we already know that node is a (non-computed) method call
|
|
const callee = node.callee;
|
|
const obj = callee.object; // identifier or expression
|
|
const method = callee.property; // identifier
|
|
|
|
// short-cut implicit config special case:
|
|
// angular.module("MyMod", function(a) {})
|
|
if (obj.name === "angular" && method.name === "module") {
|
|
const args = node.arguments;
|
|
if (args.length >= 2) {
|
|
node.$chained = chainedRegular;
|
|
return last(args);
|
|
}
|
|
}
|
|
|
|
const matchAngularModule = (obj.$chained === chainedRegular || isReDef(obj, ctx) || isLongDef(obj)) &&
|
|
is.someof(method.name, ["provider", "value", "constant", "bootstrap", "config", "factory", "directive", "filter", "run", "controller", "service", "decorator", "animation", "invoke"]);
|
|
if (!matchAngularModule) {
|
|
return false;
|
|
}
|
|
node.$chained = chainedRegular;
|
|
|
|
if (is.someof(method.name, ["value", "constant", "bootstrap"])) {
|
|
return false; // affects matchAngularModule because of chaining
|
|
}
|
|
|
|
const args = node.arguments;
|
|
const target = (is.someof(method.name, ["config", "run"]) ?
|
|
args.length === 1 && args[0] :
|
|
args.length === 2 && args[0].type === "Literal" && is.string(args[0].value) && args[1]);
|
|
|
|
target.$methodName = method.name;
|
|
|
|
if (ctx.rename && args.length === 2 && target) {
|
|
// for eventual rename purposes
|
|
const somethingNameLiteral = args[0];
|
|
return [somethingNameLiteral, target];
|
|
}
|
|
return target;
|
|
}
|
|
|
|
// matches with default regexp
|
|
// *.controller("MyCtrl", function($scope, $timeout) {});
|
|
// *.*.controller("MyCtrl", function($scope, $timeout) {});
|
|
// matches with --regexp "^require(.*)$"
|
|
// require("app-module").controller("MyCtrl", function($scope) {});
|
|
function isReDef(node, ctx) {
|
|
return ctx.re.test(ctx.srcForRange(node.range));
|
|
}
|
|
|
|
// Long form: angular.module(*).controller("MyCtrl", function($scope, $timeout) {});
|
|
function isLongDef(node) {
|
|
return node.callee &&
|
|
node.callee.object && node.callee.object.name === "angular" &&
|
|
node.callee.property && node.callee.property.name === "module";
|
|
}
|
|
|
|
function last(arr) {
|
|
return arr[arr.length - 1];
|
|
}
|
|
|
|
function matchProp(name, props) {
|
|
for (let i = 0; i < props.length; i++) {
|
|
const prop = props[i];
|
|
if ((prop.key.type === "Identifier" && prop.key.name === name) ||
|
|
(prop.key.type === "Literal" && prop.key.value === name)) {
|
|
return prop.value; // FunctionExpression or ArrayExpression
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function matchResolve(props) {
|
|
const resolveObject = matchProp("resolve", props);
|
|
if (resolveObject && resolveObject.type === "ObjectExpression") {
|
|
return resolveObject.properties.map(function(prop) {
|
|
return prop.value;
|
|
});
|
|
}
|
|
return [];
|
|
};
|
|
|
|
function renamedString(ctx, originalString) {
|
|
if (ctx.rename) {
|
|
return ctx.rename.get(originalString) || originalString;
|
|
}
|
|
return originalString;
|
|
}
|
|
|
|
function stringify(ctx, arr, quot) {
|
|
return "[" + arr.map(function(arg) {
|
|
return quot + renamedString(ctx, arg.name) + quot;
|
|
}).join(", ") + "]";
|
|
}
|
|
|
|
function parseExpressionOfType(str, type) {
|
|
const node = parser(str).body[0].expression;
|
|
assert(node.type === type);
|
|
return node;
|
|
}
|
|
|
|
// stand-in for not having a jsshaper-style ref's
|
|
function replaceNodeWith(node, newNode) {
|
|
let done = false;
|
|
const parent = node.$parent;
|
|
const keys = Object.keys(parent);
|
|
keys.forEach(function(key) {
|
|
if (parent[key] === node) {
|
|
parent[key] = newNode;
|
|
done = true;
|
|
}
|
|
});
|
|
|
|
if (done) {
|
|
return;
|
|
}
|
|
|
|
// second pass, now check arrays
|
|
keys.forEach(function(key) {
|
|
if (Array.isArray(parent[key])) {
|
|
const arr = parent[key];
|
|
for (let i = 0; i < arr.length; i++) {
|
|
if (arr[i] === node) {
|
|
arr[i] = newNode;
|
|
done = true;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
assert(done);
|
|
}
|
|
|
|
function insertArray(ctx, functionExpression, fragments, quot) {
|
|
const range = functionExpression.range;
|
|
|
|
const args = stringify(ctx, functionExpression.params, quot);
|
|
|
|
const arrayExpression = parseExpressionOfType(args, "ArrayExpression");
|
|
const parent = functionExpression.$parent;
|
|
replaceNodeWith(functionExpression, arrayExpression);
|
|
arrayExpression.$parent = parent;
|
|
arrayExpression.elements.push(functionExpression)
|
|
functionExpression.$parent = arrayExpression;
|
|
|
|
fragments.push({
|
|
start: range[0],
|
|
end: range[0],
|
|
str: args.slice(0, -1) + ", ",
|
|
});
|
|
fragments.push({
|
|
start: range[1],
|
|
end: range[1],
|
|
str: "]",
|
|
});
|
|
}
|
|
|
|
function replaceArray(ctx, array, fragments, quot) {
|
|
const functionExpression = last(array.elements);
|
|
|
|
if (functionExpression.params.length === 0) {
|
|
return removeArray(array, fragments);
|
|
}
|
|
|
|
const args = stringify(ctx, functionExpression.params, quot);
|
|
fragments.push({
|
|
start: array.range[0],
|
|
end: functionExpression.range[0],
|
|
str: args.slice(0, -1) + ", ",
|
|
});
|
|
}
|
|
|
|
function removeArray(array, fragments) {
|
|
const functionExpression = last(array.elements);
|
|
|
|
fragments.push({
|
|
start: array.range[0],
|
|
end: functionExpression.range[0],
|
|
str: "",
|
|
});
|
|
fragments.push({
|
|
start: functionExpression.range[1],
|
|
end: array.range[1],
|
|
str: "",
|
|
});
|
|
}
|
|
|
|
function renameProviderDeclarationSite(ctx, literalNode, fragments) {
|
|
fragments.push({
|
|
start: literalNode.range[0] + 1,
|
|
end: literalNode.range[1] - 1,
|
|
str: renamedString(ctx, literalNode.value),
|
|
});
|
|
}
|
|
|
|
function judgeSuspects(ctx) {
|
|
const mode = ctx.mode;
|
|
const fragments = ctx.fragments;
|
|
const quot = ctx.quot;
|
|
|
|
const suspects = makeUnique(ctx.suspects, 1);
|
|
|
|
for (let n = 0; n < 42; n++) {
|
|
// could be while(true), above is just a safety-net
|
|
// in practice it will loop just a couple of times
|
|
propagateModuleContextAndMethodName(suspects);
|
|
if (!setChainedAndMethodNameThroughIifesAndReferences(suspects)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// create final suspects by jumping, following, uniq'ing
|
|
const finalSuspects = makeUnique(suspects.map(function(target) {
|
|
const jumped = jumpOverIife(target);
|
|
const jumpedAndFollowed = followReference(jumped) || jumped;
|
|
|
|
if (target.$limitToMethodName && findOuterMethodName(target) !== target.$limitToMethodName) {
|
|
return null;
|
|
}
|
|
|
|
return jumpedAndFollowed;
|
|
}).filter(Boolean), 2);
|
|
|
|
finalSuspects.forEach(function(target) {
|
|
if (target.$chained !== chainedRegular) {
|
|
return;
|
|
}
|
|
|
|
if (mode === "rebuild" && isAnnotatedArray(target)) {
|
|
replaceArray(ctx, target, fragments, quot);
|
|
} else if (mode === "remove" && isAnnotatedArray(target)) {
|
|
removeArray(target, fragments);
|
|
} else if (is.someof(mode, ["add", "rebuild"]) && isFunctionExpressionWithArgs(target)) {
|
|
insertArray(ctx, target, fragments, quot);
|
|
} else if (isGenericProviderName(target)) {
|
|
renameProviderDeclarationSite(ctx, target, fragments);
|
|
} else {
|
|
// if it's not array or function-expression, then it's a candidate for foo.$inject = [..]
|
|
judgeInjectArraySuspect(target, ctx);
|
|
}
|
|
});
|
|
|
|
|
|
function propagateModuleContextAndMethodName(suspects) {
|
|
suspects.forEach(function(target) {
|
|
if (target.$chained !== chainedRegular && isInsideModuleContext(target)) {
|
|
target.$chained = chainedRegular;
|
|
}
|
|
|
|
if (!target.$methodName) {
|
|
const methodName = findOuterMethodName(target);
|
|
if (methodName) {
|
|
target.$methodName = methodName;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function findOuterMethodName(node) {
|
|
for (; node && !node.$methodName; node = node.$parent) {
|
|
}
|
|
return node ? node.$methodName : null;
|
|
}
|
|
|
|
function setChainedAndMethodNameThroughIifesAndReferences(suspects) {
|
|
let modified = false;
|
|
suspects.forEach(function(target) {
|
|
const jumped = jumpOverIife(target);
|
|
if (jumped !== target) { // we did skip an IIFE
|
|
if (target.$chained === chainedRegular && jumped.$chained !== chainedRegular) {
|
|
modified = true;
|
|
jumped.$chained = chainedRegular;
|
|
}
|
|
if (target.$methodName && !jumped.$methodName) {
|
|
modified = true;
|
|
jumped.$methodName = target.$methodName;
|
|
}
|
|
}
|
|
|
|
const jumpedAndFollowed = followReference(jumped) || jumped;
|
|
if (jumpedAndFollowed !== jumped) { // we did follow a reference
|
|
if (jumped.$chained === chainedRegular && jumpedAndFollowed.$chained !== chainedRegular) {
|
|
modified = true;
|
|
jumpedAndFollowed.$chained = chainedRegular;
|
|
}
|
|
if (jumped.$methodName && !jumpedAndFollowed.$methodName) {
|
|
modified = true;
|
|
jumpedAndFollowed.$methodName = jumped.$methodName;
|
|
}
|
|
}
|
|
});
|
|
return modified;
|
|
}
|
|
|
|
function isInsideModuleContext(node) {
|
|
let $parent = node.$parent;
|
|
for (; $parent && $parent.$chained !== chainedRegular; $parent = $parent.$parent) {
|
|
}
|
|
return Boolean($parent);
|
|
}
|
|
|
|
function makeUnique(suspects, val) {
|
|
return suspects.filter(function(target) {
|
|
if (target.$seen === val) {
|
|
return false;
|
|
}
|
|
target.$seen = val;
|
|
return true;
|
|
});
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// O(srclength) so should only be used for debugging purposes, else replace with lut
|
|
function posToLine(pos, src) {
|
|
if (pos >= src.length) {
|
|
pos = src.length - 1;
|
|
}
|
|
|
|
if (pos <= -1) {
|
|
return -1;
|
|
}
|
|
|
|
let line = 1;
|
|
for (let i = 0; i < pos; i++) {
|
|
if (src[i] === "\n") {
|
|
++line;
|
|
}
|
|
}
|
|
|
|
return line;
|
|
}
|
|
|
|
function judgeInjectArraySuspect(node, ctx) {
|
|
// /*@ngInject*/ var foo = function($scope) {} and
|
|
// /*@ngInject*/ function foo($scope) {} and
|
|
// /*@ngInject*/ foo.bar[0] = function($scope) {}
|
|
|
|
// suspect must be inside of a block or at the top-level (i.e. inside of node.$parent.body[])
|
|
if (!node.$parent || is.noneof(node.$parent.type, ["Program", "BlockStatement"])) {
|
|
return;
|
|
}
|
|
|
|
let d0 = null;
|
|
const nr0 = node.range[0];
|
|
const nr1 = node.range[1];
|
|
if (node.type === "VariableDeclaration" && node.declarations.length === 1 &&
|
|
(d0 = node.declarations[0]).init && ctx.isFunctionExpressionWithArgs(d0.init)) {
|
|
const isSemicolonTerminated = (ctx.src[nr1 - 1] === ";");
|
|
addRemoveInjectArray(d0.init.params, nr0, isSemicolonTerminated ? nr1 : d0.init.range[1], d0.id.name);
|
|
} else if (ctx.isFunctionDeclarationWithArgs(node)) {
|
|
addRemoveInjectArray(node.params, nr0, nr1, node.id.name);
|
|
} else if (node.type === "ExpressionStatement" && node.expression.type === "AssignmentExpression" &&
|
|
ctx.isFunctionExpressionWithArgs(node.expression.right)) {
|
|
const isSemicolonTerminated = (ctx.src[nr1 - 1] === ";");
|
|
const name = ctx.srcForRange(node.expression.left.range);
|
|
addRemoveInjectArray(node.expression.right.params, nr0, isSemicolonTerminated ? nr1 : node.expression.right.range[1], name);
|
|
}
|
|
|
|
function getIndent(pos) {
|
|
const src = ctx.src;
|
|
const lineStart = src.lastIndexOf("\n", pos - 1) + 1;
|
|
let i = lineStart;
|
|
for (; src[i] === " " || src[i] === "\t"; i++) {
|
|
}
|
|
return src.slice(lineStart, i);
|
|
}
|
|
|
|
function addRemoveInjectArray(params, posAtFunctionDeclaration, posAfterFunctionDeclaration, name) {
|
|
// if an existing something.$inject = [..] exists then is will always be recycled when rebuilding
|
|
|
|
const indent = getIndent(posAfterFunctionDeclaration);
|
|
|
|
let foundSuspectInBody = false;
|
|
let existingExpressionStatementWithArray = null;
|
|
let troublesomeReturn = false;
|
|
node.$parent.body.forEach(function(bnode) {
|
|
if (bnode === node) {
|
|
foundSuspectInBody = true;
|
|
}
|
|
|
|
if (hasInjectArray(bnode)) {
|
|
if (existingExpressionStatementWithArray) {
|
|
throw fmt("conflicting inject arrays at line {0} and {1}",
|
|
posToLine(existingExpressionStatementWithArray.range[0], ctx.src),
|
|
posToLine(bnode.range[0], ctx.src));
|
|
}
|
|
existingExpressionStatementWithArray = bnode;
|
|
}
|
|
|
|
// there's a return statement before our function
|
|
if (!foundSuspectInBody && bnode.type === "ReturnStatement") {
|
|
troublesomeReturn = bnode;
|
|
}
|
|
});
|
|
assert(foundSuspectInBody);
|
|
|
|
if (troublesomeReturn && !existingExpressionStatementWithArray) {
|
|
posAfterFunctionDeclaration = skipPrevNewline(troublesomeReturn.range[0]);
|
|
}
|
|
|
|
function hasInjectArray(node) {
|
|
let lvalue;
|
|
let assignment;
|
|
return (node && node.type === "ExpressionStatement" && (assignment = node.expression).type === "AssignmentExpression" &&
|
|
assignment.operator === "=" &&
|
|
(lvalue = assignment.left).type === "MemberExpression" &&
|
|
((lvalue.computed === false && ctx.srcForRange(lvalue.object.range) === name && lvalue.property.name === "$inject") ||
|
|
(lvalue.computed === true && ctx.srcForRange(lvalue.object.range) === name && lvalue.property.type === "Literal" && lvalue.property.value === "$inject")));
|
|
}
|
|
|
|
function skipNewline(pos) {
|
|
if (ctx.src[pos] === "\n") {
|
|
return pos + 1;
|
|
} else if (ctx.src.slice(pos, pos + 2) === "\r\n") {
|
|
return pos + 2;
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
function skipPrevNewline(pos) {
|
|
let prevLF = ctx.src.lastIndexOf("\n", pos);
|
|
if (prevLF === -1) {
|
|
return pos;
|
|
}
|
|
if (prevLF >= 1 && ctx.src[prevLF] === "\r") {
|
|
--prevLF;
|
|
}
|
|
|
|
if (/\S/g.test(ctx.src.slice(prevLF, pos - 1))) {
|
|
return pos;
|
|
}
|
|
|
|
return prevLF;
|
|
}
|
|
|
|
const str = fmt("{0}{1}{2}.$inject = {3};", EOL, indent, name, ctx.stringify(ctx, params, ctx.quot));
|
|
|
|
if (ctx.mode === "rebuild" && existingExpressionStatementWithArray) {
|
|
ctx.fragments.push({
|
|
start: existingExpressionStatementWithArray.range[0],
|
|
end: existingExpressionStatementWithArray.range[1],
|
|
str: str,
|
|
});
|
|
} else if (ctx.mode === "remove" && existingExpressionStatementWithArray) {
|
|
ctx.fragments.push({
|
|
start: skipPrevNewline(existingExpressionStatementWithArray.range[0]),
|
|
end: existingExpressionStatementWithArray.range[1],
|
|
str: "",
|
|
});
|
|
} else if (is.someof(ctx.mode, ["add", "rebuild"]) && !existingExpressionStatementWithArray) {
|
|
ctx.fragments.push({
|
|
start: posAfterFunctionDeclaration,
|
|
end: posAfterFunctionDeclaration,
|
|
str: str,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function jumpOverIife(node) {
|
|
let outerfn;
|
|
let outerbody;
|
|
if (node.type === "CallExpression" &&
|
|
(outerfn = node.callee).type === "FunctionExpression" &&
|
|
(outerbody = outerfn.body.body).length === 1 &&
|
|
outerbody[0].type === "ReturnStatement" && outerbody[0].argument) {
|
|
return outerbody[0].argument;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
function addModuleContextDependentSuspect(target, ctx) {
|
|
ctx.suspects.push(target);
|
|
}
|
|
|
|
function addModuleContextIndependentSuspect(target, ctx) {
|
|
target.$chained = chainedRegular;
|
|
ctx.suspects.push(target);
|
|
}
|
|
|
|
function isAnnotatedArray(node) {
|
|
return node.type === "ArrayExpression" && node.elements.length >= 1 && last(node.elements).type === "FunctionExpression";
|
|
}
|
|
function isFunctionExpressionWithArgs(node) {
|
|
return node.type === "FunctionExpression" && node.params.length >= 1;
|
|
}
|
|
function isFunctionDeclarationWithArgs(node) {
|
|
return node.type === "FunctionDeclaration" && node.params.length >= 1;
|
|
}
|
|
function isGenericProviderName(node) {
|
|
return node.type === "Literal" && is.string(node.value);
|
|
}
|
|
|
|
module.exports = function ngAnnotate(src, options) {
|
|
const mode = (options.add && options.remove ? "rebuild" :
|
|
options.remove ? "remove" :
|
|
options.add ? "add" : null);
|
|
|
|
if (!mode) {
|
|
return {src: src};
|
|
}
|
|
|
|
const quot = options.single_quotes ? "'" : '"';
|
|
const re = (options.regexp ? new RegExp(options.regexp) : /^[a-zA-Z0-9_\$\.\s]+$/);
|
|
const rename = new stringmap();
|
|
if (options.rename) {
|
|
options.rename.forEach(function(value) {
|
|
rename.set(value.from, value.to);
|
|
});
|
|
}
|
|
let ast;
|
|
const stats = {};
|
|
|
|
// [{type: "Block"|"Line", value: str, range: [from,to]}, ..]
|
|
let comments = [];
|
|
|
|
stats.parser_require_t0 = Date.now();
|
|
if (!options.es6) {
|
|
parser = require("esprima").parse;
|
|
} else {
|
|
parser = require("acorn").parse;
|
|
}
|
|
stats.parser_require_t1 = Date.now();
|
|
|
|
try {
|
|
stats.parser_parse_t0 = Date.now();
|
|
|
|
if (!options.es6) {
|
|
// esprima
|
|
ast = parser(src, {
|
|
range: true,
|
|
comment: true,
|
|
});
|
|
|
|
// Fix Program node range (https://code.google.com/p/esprima/issues/detail?id=541)
|
|
ast.range[0] = 0;
|
|
|
|
// detach comments from ast
|
|
comments = ast.comments;
|
|
ast.comments = null;
|
|
|
|
} else {
|
|
// acorn
|
|
ast = parser(src, {
|
|
ecmaVersion: 6,
|
|
locations: true,
|
|
ranges: true,
|
|
onComment: comments,
|
|
});
|
|
}
|
|
|
|
stats.parser_parse_t1 = Date.now();
|
|
} catch(e) {
|
|
return {
|
|
errors: ["error: couldn't process source due to parse error", e.message],
|
|
};
|
|
}
|
|
|
|
// append a dummy-node to ast so that lut.findNodeFromPos(lastPos) returns something
|
|
ast.body.push({
|
|
type: "DebuggerStatement",
|
|
range: [ast.range[1], ast.range[1]],
|
|
});
|
|
|
|
// all source modifications are built up as operations in the
|
|
// fragments array, later sent to alter in one shot
|
|
const fragments = [];
|
|
|
|
// suspects is built up with suspect nodes by match.
|
|
// A suspect node will get annotations added / removed if it
|
|
// fulfills the arrayexpression or functionexpression look,
|
|
// and if it is in the correct context (inside an angular
|
|
// module definition)
|
|
const suspects = [];
|
|
|
|
const lut = new Lut(ast, src);
|
|
|
|
scopeTools.setupScopeAndReferences(ast);
|
|
|
|
const ctx = {
|
|
mode: mode,
|
|
quot: quot,
|
|
src: src,
|
|
srcForRange: function(range) {
|
|
return src.slice(range[0], range[1]);
|
|
},
|
|
re: re,
|
|
rename: rename,
|
|
comments: comments,
|
|
fragments: fragments,
|
|
suspects: suspects,
|
|
lut: lut,
|
|
isFunctionExpressionWithArgs: isFunctionExpressionWithArgs,
|
|
isFunctionDeclarationWithArgs: isFunctionDeclarationWithArgs,
|
|
isAnnotatedArray: isAnnotatedArray,
|
|
addModuleContextDependentSuspect: addModuleContextDependentSuspect,
|
|
addModuleContextIndependentSuspect: addModuleContextIndependentSuspect,
|
|
stringify: stringify,
|
|
};
|
|
|
|
const plugins = options.plugin || [];
|
|
function matchPlugins(node, isMethodCall) {
|
|
for (let i = 0; i < plugins.length; i++) {
|
|
const res = plugins[i].match(node, isMethodCall);
|
|
if (res) {
|
|
return res;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
const matchPluginsOrNull = (plugins.length === 0 ? null : matchPlugins);
|
|
|
|
ngInject.inspectComments(ctx);
|
|
plugins.forEach(function(plugin) {
|
|
plugin.init(ctx);
|
|
});
|
|
|
|
traverse(ast, {pre: function(node) {
|
|
if (node.type === "CallExpression") {
|
|
ngInject.inspectCallExpression(node, ctx);
|
|
}
|
|
|
|
}, post: function(node) {
|
|
let targets = match(node, ctx, matchPluginsOrNull);
|
|
if (!targets) {
|
|
return;
|
|
}
|
|
if (!is.array(targets)) {
|
|
targets = [targets];
|
|
}
|
|
|
|
for (let i = 0; i < targets.length; i++) {
|
|
addModuleContextDependentSuspect(targets[i], ctx);
|
|
}
|
|
}});
|
|
|
|
try {
|
|
judgeSuspects(ctx);
|
|
} catch(e) {
|
|
return {
|
|
errors: ["error: " + e],
|
|
};
|
|
}
|
|
|
|
const out = alter(src, fragments);
|
|
const result = {
|
|
src: out,
|
|
_stats: stats,
|
|
};
|
|
|
|
if (options.sourcemap) {
|
|
stats.sourcemap_t0 = Date.now();
|
|
result.map = generateSourcemap(src, fragments, options.inFile, options.sourceroot);
|
|
stats.sourcemap_t1 = Date.now();
|
|
}
|
|
|
|
return result;
|
|
}
|