function judgeInjectArraySuspect(node, ctx) { if (node.type === "VariableDeclaration") { // suspect can only be a VariableDeclaration (statement) in case of // explicitly marked via /*@ngInject*/, not via references because // references follow to VariableDeclarator (child) // /*@ngInject*/ var foo = function($scope) {} and if (node.declarations.length !== 1) { // more than one declarator => exit return; } // one declarator => jump over declaration into declarator // rest of code will treat it as any (referenced) declarator node = node.declarations[0]; } // onode is a top-level node (inside function block), later verified // node is inner match, descent in multiple steps var onode = null; var declaratorName = null; if (node.type === "VariableDeclarator") { onode = node.$parent; declaratorName = node.id.name; node = node.init; // var foo = ___; } else { onode = node; } // suspect must be inside of a block or at the top-level (i.e. inside of node.$parent.body[]) if (!node || !onode.$parent || is.noneof(onode.$parent.type, ["Program", "BlockStatement"])) { return; } var insertPos = { pos: onode.range[1], loc: onode.loc.end }; var isSemicolonTerminated = (ctx.src[insertPos.pos - 1] === ";"); node = jumpOverIife(node); if (ctx.isFunctionExpressionWithArgs(node)) { // var x = 1, y = function(a,b) {}, z; assert(declaratorName); addRemoveInjectArray( node.params, isSemicolonTerminated ? insertPos : { pos: node.range[1], loc: node.loc.end }, declaratorName); } else if (ctx.isFunctionDeclarationWithArgs(node)) { // /*@ngInject*/ function foo($scope) {} addRemoveInjectArray( node.params, insertPos, node.id.name); } else if (node.type === "ExpressionStatement" && node.expression.type === "AssignmentExpression" && ctx.isFunctionExpressionWithArgs(node.expression.right)) { // /*@ngInject*/ foo.bar[0] = function($scope) {} var name = ctx.srcForRange(node.expression.left.range); addRemoveInjectArray( node.expression.right.params, isSemicolonTerminated ? insertPos : { pos: node.expression.right.range[1], loc: node.expression.right.loc.end }, name); } else if (node = followReference(node)) { // node was a reference and followed node now is either a // FunctionDeclaration or a VariableDeclarator // => recurse judgeInjectArraySuspect(node, ctx); } function getIndent(pos) { var src = ctx.src; var lineStart = src.lastIndexOf("\n", pos - 1) + 1; var i = lineStart; for (; src[i] === " " || src[i] === "\t"; i++) { } return src.slice(lineStart, i); } function addRemoveInjectArray(params, posAfterFunctionDeclaration, name) { // if an existing something.$inject = [..] exists then is will always be recycled when rebuilding var indent = getIndent(posAfterFunctionDeclaration.pos); var foundSuspectInBody = false; var existingExpressionStatementWithArray = null; var troublesomeReturn = false; onode.$parent.body.forEach(function(bnode) { if (bnode === onode) { 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 (onode.type === "FunctionDeclaration") { troublesomeReturn = firstNonPrologueStatement(onode.$parent.body); } if (troublesomeReturn && !existingExpressionStatementWithArray) { posAfterFunctionDeclaration = skipPrevNewline(troublesomeReturn.range[0], troublesomeReturn.loc.start); } function hasInjectArray(node) { var lvalue; var 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 skipPrevNewline(pos, loc) { var prevLF = ctx.src.lastIndexOf("\n", pos); if (prevLF === -1) { return { pos: pos, loc: loc }; } if (prevLF >= 1 && ctx.src[prevLF - 1] === "\r") { --prevLF; } if (/\S/g.test(ctx.src.slice(prevLF, pos - 1))) { // any non-whitespace chars between prev newline and pos? return { pos: pos, loc: loc }; } return { pos: prevLF, loc: { line: loc.line - 1, column: prevLF - ctx.src.lastIndexOf("\n", prevLF) - 1, } }; } if (ctx.mode === "rebuild" && existingExpressionStatementWithArray) { var strNoWhitespace = fmt("{2}.$inject = {3};", null, null, name, ctx.stringify(ctx, params, ctx.quot)); ctx.fragments.push({ start: existingExpressionStatementWithArray.range[0], end: existingExpressionStatementWithArray.range[1], str: strNoWhitespace, loc: { start: existingExpressionStatementWithArray.loc.start, end: existingExpressionStatementWithArray.loc.end } }); } else if (ctx.mode === "remove" && existingExpressionStatementWithArray) { var start = skipPrevNewline(existingExpressionStatementWithArray.range[0], existingExpressionStatementWithArray.loc.start); ctx.fragments.push({ start: start.pos, end: existingExpressionStatementWithArray.range[1], str: "", loc: { start: start.loc, end: existingExpressionStatementWithArray.loc.end } }); } else if (is.someof(ctx.mode, ["add", "rebuild"]) && !existingExpressionStatementWithArray) { var str = fmt("{0}{1}{2}.$inject = {3};", EOL, indent, name, ctx.stringify(ctx, params, ctx.quot)); ctx.fragments.push({ start: posAfterFunctionDeclaration.pos, end: posAfterFunctionDeclaration.pos, str: str, loc: { start: posAfterFunctionDeclaration.loc, end: posAfterFunctionDeclaration.loc } }); } } }
function isNonFunctionBlock(node) { return node.type === "BlockStatement" && is.noneof(node.$parent.type, ["FunctionDeclaration", "FunctionExpression"]); }
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", {... params: {params: {simple: function($scope) {}, inValue: { value: 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 see matchMaterialShowModalOpen // we already know that node is a (non-computed) method call var callee = node.callee; var obj = callee.object; // identifier or expression var method = callee.property; // identifier var args = node.arguments; // 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; } var configArg = (method.name === "state" ? last(args) : args[0]); var res = []; recursiveMatch(configArg); var filteredRes = res.filter(Boolean); return (filteredRes.length === 0 ? false : filteredRes); function recursiveMatch(objectExpressionNode) { if (!objectExpressionNode || objectExpressionNode.type !== "ObjectExpression") { return false; } var properties = objectExpressionNode.properties; matchStateProps(properties, res); var childrenArrayExpression = matchProp("children", properties); var children = childrenArrayExpression && childrenArrayExpression.elements; if (!children) { return; } children.forEach(recursiveMatch); } function matchStateProps(props, res) { var 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)); // {params: {simple: function($scope) {}, inValue: { value: function($scope) {} }} var a = matchProp("params", props); if (a && a.type === "ObjectExpression") { a.properties.forEach(function(prop) { if (prop.value.type === "ObjectExpression") { res.push(matchProp("value", prop.value.properties)); } else { res.push(prop.value); } }); } // {view: ...} var 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 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(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, }); } } }