ng-annotate
Version:
add, remove and rebuild angularjs dependency injection annotations
207 lines (176 loc) • 6.64 kB
JavaScript
// nginject.js
// MIT licensed, see LICENSE file
// Copyright (c) 2013-2016 Olov Lassus <olov.lassus@gmail.com>
;
var is = require("simple-is");
module.exports = {
inspectComments: inspectComments,
inspectNode: inspectNode,
};
function inspectNode(node, ctx) {
if (node.type === "CallExpression") {
inspectCallExpression(node, ctx);
} else if (node.type === "FunctionExpression" || node.type === "FunctionDeclaration") {
inspectFunction(node, ctx);
}
}
function inspectCallExpression(node, ctx) {
var name = node.callee.name;
if (node.callee.type === "Identifier" && (name === "ngInject" || name === "ngNoInject") && node.arguments.length === 1) {
var block = (name === "ngNoInject");
addSuspect(node.arguments[0], ctx, block);
}
}
var ngAnnotatePrologueDirectives = ["ngInject", "ngNoInject"];
function inspectFunction(node, ctx) {
var str = matchPrologueDirectives(ngAnnotatePrologueDirectives, node);
if (!str) {
return;
}
var block = (str === "ngNoInject");
// now add the correct suspect
// for function declarations, it is always the function declaration node itself
if (node.type === "FunctionDeclaration") {
addSuspect(node, ctx, block);
return;
}
// node is a function expression below
// case 1: a function expression which is the rhs of a variable declarator, such as
// var f1 = function(a) {
// "ngInject"
// };
// in this case we can mark the declarator, same as saying var /*@ngInject*/ f1 = function(a) ..
// or /*@ngInject*/ var f1 = function(a) ..
// f1.$inject = ["a"]; will be added (or rebuilt/removed)
if (node.$parent.type === "VariableDeclarator") {
addSuspect(node.$parent, ctx, block);
return;
}
// case 2: an anonymous function expression, such as
// g(function(a) {
// "ngInject"
// });
//
// the suspect is now its parent annotated array (if any), otherwise itself
// there is a risk of false suspects here, in case the parent annotated array has nothing to do
// with annotations. the risk should be very low and hopefully easy to workaround
//
// added/rebuilt/removed => g(["a", function(a) {
// "ngInject"
// }]);
var maybeArrayExpression = node.$parent;
if (ctx.isAnnotatedArray(maybeArrayExpression)) {
addSuspect(maybeArrayExpression, ctx, block);
} else {
addSuspect(node, ctx, block);
}
}
function matchPrologueDirectives(prologueDirectives, node) {
var body = node.body.body;
var found = null;
for (var i = 0; i < body.length; i++) {
if (body[i].type !== "ExpressionStatement") {
break;
}
var expr = body[i].expression;
var isStringLiteral = (expr.type === "Literal" && typeof expr.value === "string");
if (!isStringLiteral) {
break;
}
if (prologueDirectives.indexOf(expr.value) >= 0) {
found = expr.value;
break;
}
}
return found;
}
function inspectComments(ctx) {
var comments = ctx.comments;
for (var i = 0; i < comments.length; i++) {
var comment = comments[i];
var yesPos = comment.value.indexOf("@ngInject");
var noPos = (yesPos === -1 ? comment.value.indexOf("@ngNoInject") : -1);
if (yesPos === -1 && noPos === -1) {
continue;
}
var target = ctx.lut.findNodeFromPos(comment.range[1]);
if (!target) {
continue;
}
addSuspect(target, ctx, noPos >= 0);
}
}
function isStringArray(node) {
if (node.type !== "ArrayExpression") {
return false;
}
return node.elements.length >= 1 && node.elements.every(function(n) {
return n.type === "Literal" && is.string(n.value);
});
}
function findNextStatement(node) {
var body = node.$parent.body;
for (var i = 0; i < body.length; i++) {
if (body[i] === node) {
return body[i + 1] || null;
}
}
return null;
}
function addSuspect(target, ctx, block) {
if (target.type === "ExpressionStatement" && target.expression.type === "AssignmentExpression" && isStringArray(target.expression.right)) {
// /*@ngInject*/
// FooBar.$inject = ["$a", "$b"];
// function FooBar($a, $b) {}
var adjustedTarget = findNextStatement(target);
if (adjustedTarget) {
return addSuspect(adjustedTarget, ctx, block);
}
}
if (target.type === "ObjectExpression") {
// /*@ngInject*/ {f1: function(a), .., {f2: function(b)}}
addObjectExpression(target, ctx);
} else if (target.type === "AssignmentExpression" && target.right.type === "ObjectExpression") {
// /*@ngInject*/ f(x.y = {f1: function(a), .., {f2: function(b)}})
addObjectExpression(target.right, ctx);
} else if (target.type === "ExpressionStatement" && target.expression.type === "AssignmentExpression" && target.expression.right.type === "ObjectExpression") {
// /*@ngInject*/ x.y = {f1: function(a), .., {f2: function(b)}}
addObjectExpression(target.expression.right, ctx);
} else if (target.type === "VariableDeclaration" && target.declarations.length === 1 && target.declarations[0].init && target.declarations[0].init.type === "ObjectExpression") {
// /*@ngInject*/ var x = {f1: function(a), .., {f2: function(b)}}
addObjectExpression(target.declarations[0].init, ctx);
} else if (target.type === "Property") {
// {/*@ngInject*/ justthisone: function(a), ..}
target.value.$limitToMethodName = "*never*";
addOrBlock(target.value, ctx);
} else {
// /*@ngInject*/ function(a) {}
target.$limitToMethodName = "*never*";
addOrBlock(target, ctx);
}
function addObjectExpression(node, ctx) {
nestedObjectValues(node).forEach(function(n) {
n.$limitToMethodName = "*never*";
addOrBlock(n, ctx);
});
}
function addOrBlock(node, ctx) {
if (block) {
ctx.blocked.push(node);
} else {
ctx.addModuleContextIndependentSuspect(node, ctx)
}
}
}
function nestedObjectValues(node, res) {
res = res || [];
node.properties.forEach(function(prop) {
var v = prop.value;
if (is.someof(v.type, ["FunctionExpression", "ArrayExpression"])) {
res.push(v);
} else if (v.type === "ObjectExpression") {
nestedObjectValues(v, res);
}
});
return res;
}