ng-annotate-main.js 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  1. // ng-annotate-main.js
  2. // MIT licensed, see LICENSE file
  3. // Copyright (c) 2013-2015 Olov Lassus <olov.lassus@gmail.com>
  4. "use strict";
  5. const fmt = require("simple-fmt");
  6. const is = require("simple-is");
  7. const alter = require("alter");
  8. const traverse = require("ordered-ast-traverse");
  9. const EOL = require("os").EOL;
  10. const assert = require("assert");
  11. const ngInject = require("./nginject");
  12. const generateSourcemap = require("./generate-sourcemap");
  13. const Lut = require("./lut");
  14. const scopeTools = require("./scopetools");
  15. const stringmap = require("stringmap");
  16. const require_acorn_t0 = Date.now();
  17. const parser = require("acorn").parse;
  18. const require_acorn_t1 = Date.now();
  19. const chainedRouteProvider = 1;
  20. const chainedUrlRouterProvider = 2;
  21. const chainedStateProvider = 3;
  22. const chainedRegular = 4;
  23. function match(node, ctx, matchPlugins) {
  24. const isMethodCall = (
  25. node.type === "CallExpression" &&
  26. node.callee.type === "MemberExpression" &&
  27. node.callee.computed === false
  28. );
  29. // matchInjectorInvoke must happen before matchRegular
  30. // to prevent false positive ($injector.invoke() outside module)
  31. // matchProvide must happen before matchRegular
  32. // to prevent regular from matching it as a short-form
  33. const matchMethodCalls = (isMethodCall &&
  34. (matchInjectorInvoke(node) || matchProvide(node, ctx) || matchRegular(node, ctx) || matchNgRoute(node) || matchMaterialShowModalOpen(node) || matchNgUi(node) || matchHttpProvider(node)));
  35. return matchMethodCalls ||
  36. (matchPlugins && matchPlugins(node)) ||
  37. matchDirectiveReturnObject(node) ||
  38. matchProviderGet(node);
  39. }
  40. function matchMaterialShowModalOpen(node) {
  41. // $mdDialog.show({.. controller: fn, resolve: {f: function($scope) {}, ..}});
  42. // $mdToast.show({.. controller: fn, resolve: {f: function($scope) {}, ..}});
  43. // $mdBottomSheet.show({.. controller: fn, resolve: {f: function($scope) {}, ..}});
  44. // $modal.open({.. controller: fn, resolve: {f: function($scope) {}, ..}});
  45. // we already know that node is a (non-computed) method call
  46. const callee = node.callee;
  47. const obj = callee.object; // identifier or expression
  48. const method = callee.property; // identifier
  49. const args = node.arguments;
  50. if (obj.type === "Identifier" &&
  51. ((obj.name === "$modal" && method.name === "open") || (is.someof(obj.name, ["$mdDialog", "$mdToast", "$mdBottomSheet"]) && method.name === "show")) &&
  52. args.length === 1 && args[0].type === "ObjectExpression") {
  53. const props = args[0].properties;
  54. const res = [matchProp("controller", props)];
  55. res.push.apply(res, matchResolve(props));
  56. return res.filter(Boolean);
  57. }
  58. return false;
  59. }
  60. function matchDirectiveReturnObject(node) {
  61. // only matches inside directives
  62. // return { .. controller: function($scope, $timeout), ...}
  63. return limit("directive", node.type === "ReturnStatement" &&
  64. node.argument && node.argument.type === "ObjectExpression" &&
  65. matchProp("controller", node.argument.properties));
  66. }
  67. function limit(name, node) {
  68. if (node && !node.$limitToMethodName) {
  69. node.$limitToMethodName = name;
  70. }
  71. return node;
  72. }
  73. function matchProviderGet(node) {
  74. // only matches inside providers
  75. // (this|self|that).$get = function($scope, $timeout)
  76. // { ... $get: function($scope, $timeout), ...}
  77. let memberExpr;
  78. let self;
  79. return limit("provider", (node.type === "AssignmentExpression" && (memberExpr = node.left).type === "MemberExpression" &&
  80. memberExpr.property.name === "$get" &&
  81. ((self = memberExpr.object).type === "ThisExpression" || (self.type === "Identifier" && is.someof(self.name, ["self", "that"]))) &&
  82. node.right) ||
  83. (node.type === "ObjectExpression" && matchProp("$get", node.properties)));
  84. }
  85. function matchNgRoute(node) {
  86. // $routeProvider.when("path", {
  87. // ...
  88. // controller: function($scope) {},
  89. // resolve: {f: function($scope) {}, ..}
  90. // })
  91. // we already know that node is a (non-computed) method call
  92. const callee = node.callee;
  93. const obj = callee.object; // identifier or expression
  94. if (!(obj.$chained === chainedRouteProvider || (obj.type === "Identifier" && obj.name === "$routeProvider"))) {
  95. return false;
  96. }
  97. node.$chained = chainedRouteProvider;
  98. const method = callee.property; // identifier
  99. if (method.name !== "when") {
  100. return false;
  101. }
  102. const args = node.arguments;
  103. if (args.length !== 2) {
  104. return false;
  105. }
  106. const configArg = last(args)
  107. if (configArg.type !== "ObjectExpression") {
  108. return false;
  109. }
  110. const props = configArg.properties;
  111. const res = [
  112. matchProp("controller", props)
  113. ];
  114. // {resolve: ..}
  115. res.push.apply(res, matchResolve(props));
  116. const filteredRes = res.filter(Boolean);
  117. return (filteredRes.length === 0 ? false : filteredRes);
  118. }
  119. function matchNgUi(node) {
  120. // $stateProvider.state("myState", {
  121. // ...
  122. // controller: function($scope)
  123. // controllerProvider: function($scope)
  124. // templateProvider: function($scope)
  125. // onEnter: function($scope)
  126. // onExit: function($scope)
  127. // });
  128. // $stateProvider.state("myState", {... resolve: {f: function($scope) {}, ..} ..})
  129. // $stateProvider.state("myState", {... views: {... somename: {... controller: fn, controllerProvider: fn, templateProvider: fn, resolve: {f: fn}}}})
  130. //
  131. // stateHelperProvider.setNestedState({ sameasregularstate, children: [sameasregularstate, ..]})
  132. // stateHelperProvider.setNestedState({ sameasregularstate, children: [sameasregularstate, ..]}, true)
  133. //
  134. // $urlRouterProvider.when(.., function($scope) {})
  135. //
  136. // $modal.open see matchMaterialShowModalOpen
  137. // we already know that node is a (non-computed) method call
  138. const callee = node.callee;
  139. const obj = callee.object; // identifier or expression
  140. const method = callee.property; // identifier
  141. const args = node.arguments;
  142. // shortcut for $urlRouterProvider.when(.., function($scope) {})
  143. if (obj.$chained === chainedUrlRouterProvider || (obj.type === "Identifier" && obj.name === "$urlRouterProvider")) {
  144. node.$chained = chainedUrlRouterProvider;
  145. if (method.name === "when" && args.length >= 1) {
  146. return last(args);
  147. }
  148. return false;
  149. }
  150. // everything below is for $stateProvider and stateHelperProvider alone
  151. if (!(obj.$chained === chainedStateProvider || (obj.type === "Identifier" && is.someof(obj.name, ["$stateProvider", "stateHelperProvider"])))) {
  152. return false;
  153. }
  154. node.$chained = chainedStateProvider;
  155. if (is.noneof(method.name, ["state", "setNestedState"])) {
  156. return false;
  157. }
  158. // $stateProvider.state({ ... }) and $stateProvider.state("name", { ... })
  159. // stateHelperProvider.setNestedState({ .. }) and stateHelperProvider.setNestedState({ .. }, true)
  160. if (!(args.length >= 1 && args.length <= 2)) {
  161. return false;
  162. }
  163. const configArg = (method.name === "state" ? last(args) : args[0]);
  164. const res = [];
  165. recursiveMatch(configArg);
  166. const filteredRes = res.filter(Boolean);
  167. return (filteredRes.length === 0 ? false : filteredRes);
  168. function recursiveMatch(objectExpressionNode) {
  169. if (!objectExpressionNode || objectExpressionNode.type !== "ObjectExpression") {
  170. return false;
  171. }
  172. const properties = objectExpressionNode.properties;
  173. matchStateProps(properties, res);
  174. const childrenArrayExpression = matchProp("children", properties);
  175. const children = childrenArrayExpression && childrenArrayExpression.elements;
  176. if (!children) {
  177. return;
  178. }
  179. children.forEach(recursiveMatch);
  180. }
  181. function matchStateProps(props, res) {
  182. const simple = [
  183. matchProp("controller", props),
  184. matchProp("controllerProvider", props),
  185. matchProp("templateProvider", props),
  186. matchProp("onEnter", props),
  187. matchProp("onExit", props),
  188. ];
  189. res.push.apply(res, simple);
  190. // {resolve: ..}
  191. res.push.apply(res, matchResolve(props));
  192. // {view: ...}
  193. const viewObject = matchProp("views", props);
  194. if (viewObject && viewObject.type === "ObjectExpression") {
  195. viewObject.properties.forEach(function(prop) {
  196. if (prop.value.type === "ObjectExpression") {
  197. res.push(matchProp("controller", prop.value.properties));
  198. res.push(matchProp("controllerProvider", prop.value.properties));
  199. res.push(matchProp("templateProvider", prop.value.properties));
  200. res.push.apply(res, matchResolve(prop.value.properties));
  201. }
  202. });
  203. }
  204. }
  205. }
  206. function matchInjectorInvoke(node) {
  207. // $injector.invoke(function($compile) { ... });
  208. // we already know that node is a (non-computed) method call
  209. const callee = node.callee;
  210. const obj = callee.object; // identifier or expression
  211. const method = callee.property; // identifier
  212. return method.name === "invoke" &&
  213. obj.type === "Identifier" && obj.name === "$injector" &&
  214. node.arguments.length >= 1 && node.arguments;
  215. }
  216. function matchHttpProvider(node) {
  217. // $httpProvider.interceptors.push(function($scope) {});
  218. // $httpProvider.responseInterceptors.push(function($scope) {});
  219. // we already know that node is a (non-computed) method call
  220. const callee = node.callee;
  221. const obj = callee.object; // identifier or expression
  222. const method = callee.property; // identifier
  223. return (method.name === "push" &&
  224. obj.type === "MemberExpression" && !obj.computed &&
  225. obj.object.name === "$httpProvider" && is.someof(obj.property.name, ["interceptors", "responseInterceptors"]) &&
  226. node.arguments.length >= 1 && node.arguments);
  227. }
  228. function matchProvide(node, ctx) {
  229. // $provide.decorator("foo", function($scope) {});
  230. // $provide.service("foo", function($scope) {});
  231. // $provide.factory("foo", function($scope) {});
  232. // $provide.provider("foo", function($scope) {});
  233. // we already know that node is a (non-computed) method call
  234. const callee = node.callee;
  235. const obj = callee.object; // identifier or expression
  236. const method = callee.property; // identifier
  237. const args = node.arguments;
  238. const target = obj.type === "Identifier" && obj.name === "$provide" &&
  239. is.someof(method.name, ["decorator", "service", "factory", "provider"]) &&
  240. args.length === 2 && args[1];
  241. if (target) {
  242. target.$methodName = method.name;
  243. if (ctx.rename) {
  244. // for eventual rename purposes
  245. return args;
  246. }
  247. }
  248. return target;
  249. }
  250. function matchRegular(node, ctx) {
  251. // we already know that node is a (non-computed) method call
  252. const callee = node.callee;
  253. const obj = callee.object; // identifier or expression
  254. const method = callee.property; // identifier
  255. // short-cut implicit config special case:
  256. // angular.module("MyMod", function(a) {})
  257. if (obj.name === "angular" && method.name === "module") {
  258. const args = node.arguments;
  259. if (args.length >= 2) {
  260. node.$chained = chainedRegular;
  261. return last(args);
  262. }
  263. }
  264. const matchAngularModule = (obj.$chained === chainedRegular || isReDef(obj, ctx) || isLongDef(obj)) &&
  265. is.someof(method.name, ["provider", "value", "constant", "bootstrap", "config", "factory", "directive", "filter", "run", "controller", "service", "animation", "invoke"]);
  266. if (!matchAngularModule) {
  267. return false;
  268. }
  269. node.$chained = chainedRegular;
  270. if (is.someof(method.name, ["value", "constant", "bootstrap"])) {
  271. return false; // affects matchAngularModule because of chaining
  272. }
  273. const args = node.arguments;
  274. const target = (is.someof(method.name, ["config", "run"]) ?
  275. args.length === 1 && args[0] :
  276. args.length === 2 && args[0].type === "Literal" && is.string(args[0].value) && args[1]);
  277. if (target) {
  278. target.$methodName = method.name;
  279. }
  280. if (ctx.rename && args.length === 2 && target) {
  281. // for eventual rename purposes
  282. const somethingNameLiteral = args[0];
  283. return [somethingNameLiteral, target];
  284. }
  285. return target;
  286. }
  287. // matches with default regexp
  288. // *.controller("MyCtrl", function($scope, $timeout) {});
  289. // *.*.controller("MyCtrl", function($scope, $timeout) {});
  290. // matches with --regexp "^require(.*)$"
  291. // require("app-module").controller("MyCtrl", function($scope) {});
  292. function isReDef(node, ctx) {
  293. return ctx.re.test(ctx.srcForRange(node.range));
  294. }
  295. // Long form: angular.module(*).controller("MyCtrl", function($scope, $timeout) {});
  296. function isLongDef(node) {
  297. return node.callee &&
  298. node.callee.object && node.callee.object.name === "angular" &&
  299. node.callee.property && node.callee.property.name === "module";
  300. }
  301. function last(arr) {
  302. return arr[arr.length - 1];
  303. }
  304. function matchProp(name, props) {
  305. for (let i = 0; i < props.length; i++) {
  306. const prop = props[i];
  307. if ((prop.key.type === "Identifier" && prop.key.name === name) ||
  308. (prop.key.type === "Literal" && prop.key.value === name)) {
  309. return prop.value; // FunctionExpression or ArrayExpression
  310. }
  311. }
  312. return null;
  313. }
  314. function matchResolve(props) {
  315. const resolveObject = matchProp("resolve", props);
  316. if (resolveObject && resolveObject.type === "ObjectExpression") {
  317. return resolveObject.properties.map(function(prop) {
  318. return prop.value;
  319. });
  320. }
  321. return [];
  322. };
  323. function renamedString(ctx, originalString) {
  324. if (ctx.rename) {
  325. return ctx.rename.get(originalString) || originalString;
  326. }
  327. return originalString;
  328. }
  329. function stringify(ctx, arr, quot) {
  330. return "[" + arr.map(function(arg) {
  331. return quot + renamedString(ctx, arg.name) + quot;
  332. }).join(", ") + "]";
  333. }
  334. function parseExpressionOfType(str, type) {
  335. const node = parser(str).body[0].expression;
  336. assert(node.type === type);
  337. return node;
  338. }
  339. // stand-in for not having a jsshaper-style ref's
  340. function replaceNodeWith(node, newNode) {
  341. let done = false;
  342. const parent = node.$parent;
  343. const keys = Object.keys(parent);
  344. keys.forEach(function(key) {
  345. if (parent[key] === node) {
  346. parent[key] = newNode;
  347. done = true;
  348. }
  349. });
  350. if (done) {
  351. return;
  352. }
  353. // second pass, now check arrays
  354. keys.forEach(function(key) {
  355. if (Array.isArray(parent[key])) {
  356. const arr = parent[key];
  357. for (let i = 0; i < arr.length; i++) {
  358. if (arr[i] === node) {
  359. arr[i] = newNode;
  360. done = true;
  361. }
  362. }
  363. }
  364. });
  365. assert(done);
  366. }
  367. function insertArray(ctx, functionExpression, fragments, quot) {
  368. const args = stringify(ctx, functionExpression.params, quot);
  369. fragments.push({
  370. start: functionExpression.range[0],
  371. end: functionExpression.range[0],
  372. str: args.slice(0, -1) + ", ",
  373. loc: {
  374. start: functionExpression.loc.start,
  375. end: functionExpression.loc.start
  376. }
  377. });
  378. fragments.push({
  379. start: functionExpression.range[1],
  380. end: functionExpression.range[1],
  381. str: "]",
  382. loc: {
  383. start: functionExpression.loc.end,
  384. end: functionExpression.loc.end
  385. }
  386. });
  387. }
  388. function replaceArray(ctx, array, fragments, quot) {
  389. const functionExpression = last(array.elements);
  390. if (functionExpression.params.length === 0) {
  391. return removeArray(array, fragments);
  392. }
  393. const args = stringify(ctx, functionExpression.params, quot);
  394. fragments.push({
  395. start: array.range[0],
  396. end: functionExpression.range[0],
  397. str: args.slice(0, -1) + ", ",
  398. loc: {
  399. start: array.loc.start,
  400. end: functionExpression.loc.start
  401. }
  402. });
  403. }
  404. function removeArray(array, fragments) {
  405. const functionExpression = last(array.elements);
  406. fragments.push({
  407. start: array.range[0],
  408. end: functionExpression.range[0],
  409. str: "",
  410. loc: {
  411. start: array.loc.start,
  412. end: functionExpression.loc.start
  413. }
  414. });
  415. fragments.push({
  416. start: functionExpression.range[1],
  417. end: array.range[1],
  418. str: "",
  419. loc: {
  420. start: functionExpression.loc.end,
  421. end: array.loc.end
  422. }
  423. });
  424. }
  425. function renameProviderDeclarationSite(ctx, literalNode, fragments) {
  426. fragments.push({
  427. start: literalNode.range[0] + 1,
  428. end: literalNode.range[1] - 1,
  429. str: renamedString(ctx, literalNode.value),
  430. loc: {
  431. start: {
  432. line: literalNode.loc.start.line,
  433. column: literalNode.loc.start.column + 1
  434. }, end: {
  435. line: literalNode.loc.end.line,
  436. column: literalNode.loc.end.column - 1
  437. }
  438. }
  439. });
  440. }
  441. function judgeSuspects(ctx) {
  442. const mode = ctx.mode;
  443. const fragments = ctx.fragments;
  444. const quot = ctx.quot;
  445. const blocked = ctx.blocked;
  446. const suspects = makeUnique(ctx.suspects, 1);
  447. for (let n = 0; n < 42; n++) {
  448. // could be while(true), above is just a safety-net
  449. // in practice it will loop just a couple of times
  450. propagateModuleContextAndMethodName(suspects);
  451. if (!setChainedAndMethodNameThroughIifesAndReferences(suspects)) {
  452. break;
  453. }
  454. }
  455. // create final suspects by jumping, following, uniq'ing, blocking
  456. const finalSuspects = makeUnique(suspects.map(function(target) {
  457. const jumped = jumpOverIife(target);
  458. const jumpedAndFollowed = followReference(jumped) || jumped;
  459. if (target.$limitToMethodName && target.$limitToMethodName !== "*never*" && findOuterMethodName(target) !== target.$limitToMethodName) {
  460. return null;
  461. }
  462. if (blocked.indexOf(jumpedAndFollowed) >= 0) {
  463. return null;
  464. }
  465. return jumpedAndFollowed;
  466. }).filter(Boolean), 2);
  467. finalSuspects.forEach(function(target) {
  468. if (target.$chained !== chainedRegular) {
  469. return;
  470. }
  471. if (mode === "rebuild" && isAnnotatedArray(target)) {
  472. replaceArray(ctx, target, fragments, quot);
  473. } else if (mode === "remove" && isAnnotatedArray(target)) {
  474. removeArray(target, fragments);
  475. } else if (is.someof(mode, ["add", "rebuild"]) && isFunctionExpressionWithArgs(target)) {
  476. insertArray(ctx, target, fragments, quot);
  477. } else if (isGenericProviderName(target)) {
  478. renameProviderDeclarationSite(ctx, target, fragments);
  479. } else {
  480. // if it's not array or function-expression, then it's a candidate for foo.$inject = [..]
  481. judgeInjectArraySuspect(target, ctx);
  482. }
  483. });
  484. function propagateModuleContextAndMethodName(suspects) {
  485. suspects.forEach(function(target) {
  486. if (target.$chained !== chainedRegular && isInsideModuleContext(target)) {
  487. target.$chained = chainedRegular;
  488. }
  489. if (!target.$methodName) {
  490. const methodName = findOuterMethodName(target);
  491. if (methodName) {
  492. target.$methodName = methodName;
  493. }
  494. }
  495. });
  496. }
  497. function findOuterMethodName(node) {
  498. for (; node && !node.$methodName; node = node.$parent) {
  499. }
  500. return node ? node.$methodName : null;
  501. }
  502. function setChainedAndMethodNameThroughIifesAndReferences(suspects) {
  503. let modified = false;
  504. suspects.forEach(function(target) {
  505. const jumped = jumpOverIife(target);
  506. if (jumped !== target) { // we did skip an IIFE
  507. if (target.$chained === chainedRegular && jumped.$chained !== chainedRegular) {
  508. modified = true;
  509. jumped.$chained = chainedRegular;
  510. }
  511. if (target.$methodName && !jumped.$methodName) {
  512. modified = true;
  513. jumped.$methodName = target.$methodName;
  514. }
  515. }
  516. const jumpedAndFollowed = followReference(jumped) || jumped;
  517. if (jumpedAndFollowed !== jumped) { // we did follow a reference
  518. if (jumped.$chained === chainedRegular && jumpedAndFollowed.$chained !== chainedRegular) {
  519. modified = true;
  520. jumpedAndFollowed.$chained = chainedRegular;
  521. }
  522. if (jumped.$methodName && !jumpedAndFollowed.$methodName) {
  523. modified = true;
  524. jumpedAndFollowed.$methodName = jumped.$methodName;
  525. }
  526. }
  527. });
  528. return modified;
  529. }
  530. function isInsideModuleContext(node) {
  531. let $parent = node.$parent;
  532. for (; $parent && $parent.$chained !== chainedRegular; $parent = $parent.$parent) {
  533. }
  534. return Boolean($parent);
  535. }
  536. function makeUnique(suspects, val) {
  537. return suspects.filter(function(target) {
  538. if (target.$seen === val) {
  539. return false;
  540. }
  541. target.$seen = val;
  542. return true;
  543. });
  544. }
  545. }
  546. function followReference(node) {
  547. if (!scopeTools.isReference(node)) {
  548. return null;
  549. }
  550. const scope = node.$scope.lookup(node.name);
  551. if (!scope) {
  552. return null;
  553. }
  554. const parent = scope.getNode(node.name).$parent;
  555. const kind = scope.getKind(node.name);
  556. const ptype = parent.type;
  557. if (is.someof(kind, ["const", "let", "var"])) {
  558. assert(ptype === "VariableDeclarator");
  559. // {type: "VariableDeclarator", id: {type: "Identifier", name: "foo"}, init: ..}
  560. return parent;
  561. } else if (kind === "fun") {
  562. assert(ptype === "FunctionDeclaration" || ptype === "FunctionExpression")
  563. // FunctionDeclaration is the common case, i.e.
  564. // function foo(a, b) {}
  565. // FunctionExpression is only applicable for cases similar to
  566. // var f = function asdf(a,b) { mymod.controller("asdf", asdf) };
  567. return parent;
  568. }
  569. // other kinds should not be handled ("param", "caught")
  570. return null;
  571. }
  572. // O(srclength) so should only be used for debugging purposes, else replace with lut
  573. function posToLine(pos, src) {
  574. if (pos >= src.length) {
  575. pos = src.length - 1;
  576. }
  577. if (pos <= -1) {
  578. return -1;
  579. }
  580. let line = 1;
  581. for (let i = 0; i < pos; i++) {
  582. if (src[i] === "\n") {
  583. ++line;
  584. }
  585. }
  586. return line;
  587. }
  588. function judgeInjectArraySuspect(node, ctx) {
  589. if (node.type === "VariableDeclaration") {
  590. // suspect can only be a VariableDeclaration (statement) in case of
  591. // explicitly marked via /*@ngInject*/, not via references because
  592. // references follow to VariableDeclarator (child)
  593. // /*@ngInject*/ var foo = function($scope) {} and
  594. if (node.declarations.length !== 1) {
  595. // more than one declarator => exit
  596. return;
  597. }
  598. // one declarator => jump over declaration into declarator
  599. // rest of code will treat it as any (referenced) declarator
  600. node = node.declarations[0];
  601. }
  602. // onode is a top-level node (inside function block), later verified
  603. // node is inner match, descent in multiple steps
  604. let onode = null;
  605. let declaratorName = null;
  606. if (node.type === "VariableDeclarator") {
  607. onode = node.$parent;
  608. declaratorName = node.id.name;
  609. node = node.init; // var foo = ___;
  610. } else {
  611. onode = node;
  612. }
  613. // suspect must be inside of a block or at the top-level (i.e. inside of node.$parent.body[])
  614. if (!node || !onode.$parent || is.noneof(onode.$parent.type, ["Program", "BlockStatement"])) {
  615. return;
  616. }
  617. const insertPos = {
  618. pos: onode.range[1],
  619. loc: onode.loc.end
  620. };
  621. const isSemicolonTerminated = (ctx.src[insertPos.pos - 1] === ";");
  622. node = jumpOverIife(node);
  623. if (ctx.isFunctionExpressionWithArgs(node)) {
  624. // var x = 1, y = function(a,b) {}, z;
  625. assert(declaratorName);
  626. addRemoveInjectArray(
  627. node.params,
  628. isSemicolonTerminated ? insertPos : {
  629. pos: node.range[1],
  630. loc: node.loc.end
  631. },
  632. declaratorName);
  633. } else if (ctx.isFunctionDeclarationWithArgs(node)) {
  634. // /*@ngInject*/ function foo($scope) {}
  635. addRemoveInjectArray(
  636. node.params,
  637. insertPos,
  638. node.id.name);
  639. } else if (node.type === "ExpressionStatement" && node.expression.type === "AssignmentExpression" &&
  640. ctx.isFunctionExpressionWithArgs(node.expression.right)) {
  641. // /*@ngInject*/ foo.bar[0] = function($scope) {}
  642. const name = ctx.srcForRange(node.expression.left.range);
  643. addRemoveInjectArray(
  644. node.expression.right.params,
  645. isSemicolonTerminated ? insertPos : {
  646. pos: node.expression.right.range[1],
  647. loc: node.expression.right.loc.end
  648. },
  649. name);
  650. } else if (node = followReference(node)) {
  651. // node was a reference and followed node now is either a
  652. // FunctionDeclaration or a VariableDeclarator
  653. // => recurse
  654. judgeInjectArraySuspect(node, ctx);
  655. }
  656. function getIndent(pos) {
  657. const src = ctx.src;
  658. const lineStart = src.lastIndexOf("\n", pos - 1) + 1;
  659. let i = lineStart;
  660. for (; src[i] === " " || src[i] === "\t"; i++) {
  661. }
  662. return src.slice(lineStart, i);
  663. }
  664. function addRemoveInjectArray(params, posAfterFunctionDeclaration, name) {
  665. // if an existing something.$inject = [..] exists then is will always be recycled when rebuilding
  666. const indent = getIndent(posAfterFunctionDeclaration.pos);
  667. let foundSuspectInBody = false;
  668. let existingExpressionStatementWithArray = null;
  669. let troublesomeReturn = false;
  670. onode.$parent.body.forEach(function(bnode) {
  671. if (bnode === onode) {
  672. foundSuspectInBody = true;
  673. }
  674. if (hasInjectArray(bnode)) {
  675. if (existingExpressionStatementWithArray) {
  676. throw fmt("conflicting inject arrays at line {0} and {1}",
  677. posToLine(existingExpressionStatementWithArray.range[0], ctx.src),
  678. posToLine(bnode.range[0], ctx.src));
  679. }
  680. existingExpressionStatementWithArray = bnode;
  681. }
  682. // there's a return statement before our function
  683. if (!foundSuspectInBody && bnode.type === "ReturnStatement") {
  684. troublesomeReturn = bnode;
  685. }
  686. });
  687. assert(foundSuspectInBody);
  688. if (troublesomeReturn && !existingExpressionStatementWithArray) {
  689. posAfterFunctionDeclaration = skipPrevNewline(troublesomeReturn.range[0], troublesomeReturn.loc.start);
  690. }
  691. function hasInjectArray(node) {
  692. let lvalue;
  693. let assignment;
  694. return (node && node.type === "ExpressionStatement" && (assignment = node.expression).type === "AssignmentExpression" &&
  695. assignment.operator === "=" &&
  696. (lvalue = assignment.left).type === "MemberExpression" &&
  697. ((lvalue.computed === false && ctx.srcForRange(lvalue.object.range) === name && lvalue.property.name === "$inject") ||
  698. (lvalue.computed === true && ctx.srcForRange(lvalue.object.range) === name && lvalue.property.type === "Literal" && lvalue.property.value === "$inject")));
  699. }
  700. function skipPrevNewline(pos, loc) {
  701. let prevLF = ctx.src.lastIndexOf("\n", pos);
  702. if (prevLF === -1) {
  703. return { pos: pos, loc: loc };
  704. }
  705. if (prevLF >= 1 && ctx.src[prevLF] === "\r") {
  706. --prevLF;
  707. }
  708. if (/\S/g.test(ctx.src.slice(prevLF, pos - 1))) {
  709. return { pos: pos, loc: loc };
  710. }
  711. return {
  712. pos: prevLF,
  713. loc: {
  714. line: loc.line - 1,
  715. column: prevLF - ctx.src.lastIndexOf("\n", prevLF)
  716. }
  717. };
  718. }
  719. const str = fmt("{0}{1}{2}.$inject = {3};", EOL, indent, name, ctx.stringify(ctx, params, ctx.quot));
  720. if (ctx.mode === "rebuild" && existingExpressionStatementWithArray) {
  721. const strNoWhitespace = fmt("{2}.$inject = {3};", null, null, name, ctx.stringify(ctx, params, ctx.quot));
  722. ctx.fragments.push({
  723. start: existingExpressionStatementWithArray.range[0],
  724. end: existingExpressionStatementWithArray.range[1],
  725. str: strNoWhitespace,
  726. loc: {
  727. start: existingExpressionStatementWithArray.loc.start,
  728. end: existingExpressionStatementWithArray.loc.end
  729. }
  730. });
  731. } else if (ctx.mode === "remove" && existingExpressionStatementWithArray) {
  732. const start = skipPrevNewline(existingExpressionStatementWithArray.range[0], existingExpressionStatementWithArray.loc.start);
  733. ctx.fragments.push({
  734. start: start.pos,
  735. end: existingExpressionStatementWithArray.range[1],
  736. str: "",
  737. loc: {
  738. start: start.loc,
  739. end: existingExpressionStatementWithArray.loc.end
  740. }
  741. });
  742. } else if (is.someof(ctx.mode, ["add", "rebuild"]) && !existingExpressionStatementWithArray) {
  743. ctx.fragments.push({
  744. start: posAfterFunctionDeclaration.pos,
  745. end: posAfterFunctionDeclaration.pos,
  746. str: str,
  747. loc: {
  748. start: posAfterFunctionDeclaration.loc,
  749. end: posAfterFunctionDeclaration.loc
  750. }
  751. });
  752. }
  753. }
  754. }
  755. function jumpOverIife(node) {
  756. let outerfn;
  757. if (!(node.type === "CallExpression" && (outerfn = node.callee).type === "FunctionExpression")) {
  758. return node;
  759. }
  760. const outerbody = outerfn.body.body;
  761. for (let i = 0; i < outerbody.length; i++) {
  762. const statement = outerbody[i];
  763. if (statement.type === "ReturnStatement") {
  764. return statement.argument;
  765. }
  766. }
  767. return node;
  768. }
  769. function addModuleContextDependentSuspect(target, ctx) {
  770. ctx.suspects.push(target);
  771. }
  772. function addModuleContextIndependentSuspect(target, ctx) {
  773. target.$chained = chainedRegular;
  774. ctx.suspects.push(target);
  775. }
  776. function isAnnotatedArray(node) {
  777. if (node.type !== "ArrayExpression") {
  778. return false;
  779. }
  780. const elements = node.elements;
  781. // last should be a function expression
  782. if (elements.length === 0 || last(elements).type !== "FunctionExpression") {
  783. return false;
  784. }
  785. // all but last should be string literals
  786. for (let i = 0; i < elements.length - 1; i++) {
  787. const n = elements[i];
  788. if (n.type !== "Literal" || !is.string(n.value)) {
  789. return false;
  790. }
  791. }
  792. return true;
  793. }
  794. function isFunctionExpressionWithArgs(node) {
  795. return node.type === "FunctionExpression" && node.params.length >= 1;
  796. }
  797. function isFunctionDeclarationWithArgs(node) {
  798. return node.type === "FunctionDeclaration" && node.params.length >= 1;
  799. }
  800. function isGenericProviderName(node) {
  801. return node.type === "Literal" && is.string(node.value);
  802. }
  803. module.exports = function ngAnnotate(src, options) {
  804. const mode = (options.add && options.remove ? "rebuild" :
  805. options.remove ? "remove" :
  806. options.add ? "add" : null);
  807. if (!mode) {
  808. return {src: src};
  809. }
  810. const quot = options.single_quotes ? "'" : '"';
  811. const re = (options.regexp ? new RegExp(options.regexp) : /^[a-zA-Z0-9_\$\.\s]+$/);
  812. const rename = new stringmap();
  813. if (options.rename) {
  814. options.rename.forEach(function(value) {
  815. rename.set(value.from, value.to);
  816. });
  817. }
  818. let ast;
  819. const stats = {};
  820. // [{type: "Block"|"Line", value: str, range: [from,to]}, ..]
  821. let comments = [];
  822. try {
  823. stats.parser_require_t0 = require_acorn_t0;
  824. stats.parser_require_t1 = require_acorn_t1;
  825. stats.parser_parse_t0 = Date.now();
  826. // acorn
  827. ast = parser(src, {
  828. ecmaVersion: 6,
  829. locations: true,
  830. ranges: true,
  831. onComment: comments,
  832. });
  833. stats.parser_parse_t1 = Date.now();
  834. } catch(e) {
  835. return {
  836. errors: ["error: couldn't process source due to parse error", e.message],
  837. };
  838. }
  839. // append a dummy-node to ast so that lut.findNodeFromPos(lastPos) returns something
  840. ast.body.push({
  841. type: "DebuggerStatement",
  842. range: [ast.range[1], ast.range[1]],
  843. loc: {
  844. start: ast.loc.end,
  845. end: ast.loc.end
  846. }
  847. });
  848. // all source modifications are built up as operations in the
  849. // fragments array, later sent to alter in one shot
  850. const fragments = [];
  851. // suspects is built up with suspect nodes by match.
  852. // A suspect node will get annotations added / removed if it
  853. // fulfills the arrayexpression or functionexpression look,
  854. // and if it is in the correct context (inside an angular
  855. // module definition)
  856. const suspects = [];
  857. // blocked is an array of blocked suspects. Any target node
  858. // (final, i.e. IIFE-jumped, reference-followed and such) included
  859. // in blocked will be ignored by judgeSuspects
  860. const blocked = [];
  861. // Position information for all nodes in the AST,
  862. // used for sourcemap generation
  863. const nodePositions = [];
  864. const lut = new Lut(ast, src);
  865. scopeTools.setupScopeAndReferences(ast);
  866. const ctx = {
  867. mode: mode,
  868. quot: quot,
  869. src: src,
  870. srcForRange: function(range) {
  871. return src.slice(range[0], range[1]);
  872. },
  873. re: re,
  874. rename: rename,
  875. comments: comments,
  876. fragments: fragments,
  877. suspects: suspects,
  878. blocked: blocked,
  879. lut: lut,
  880. isFunctionExpressionWithArgs: isFunctionExpressionWithArgs,
  881. isFunctionDeclarationWithArgs: isFunctionDeclarationWithArgs,
  882. isAnnotatedArray: isAnnotatedArray,
  883. addModuleContextDependentSuspect: addModuleContextDependentSuspect,
  884. addModuleContextIndependentSuspect: addModuleContextIndependentSuspect,
  885. stringify: stringify,
  886. nodePositions: nodePositions,
  887. };
  888. const plugins = options.plugin || [];
  889. function matchPlugins(node, isMethodCall) {
  890. for (let i = 0; i < plugins.length; i++) {
  891. const res = plugins[i].match(node, isMethodCall);
  892. if (res) {
  893. return res;
  894. }
  895. }
  896. return false;
  897. }
  898. const matchPluginsOrNull = (plugins.length === 0 ? null : matchPlugins);
  899. ngInject.inspectComments(ctx);
  900. plugins.forEach(function(plugin) {
  901. plugin.init(ctx);
  902. });
  903. traverse(ast, {pre: function(node) {
  904. ngInject.inspectNode(node, ctx);
  905. }, post: function(node) {
  906. ctx.nodePositions.push(node.loc.start);
  907. let targets = match(node, ctx, matchPluginsOrNull);
  908. if (!targets) {
  909. return;
  910. }
  911. if (!is.array(targets)) {
  912. targets = [targets];
  913. }
  914. for (let i = 0; i < targets.length; i++) {
  915. addModuleContextDependentSuspect(targets[i], ctx);
  916. }
  917. }});
  918. try {
  919. judgeSuspects(ctx);
  920. } catch(e) {
  921. return {
  922. errors: ["error: " + e],
  923. };
  924. }
  925. const out = alter(src, fragments);
  926. const result = {
  927. src: out,
  928. _stats: stats,
  929. };
  930. if (options.sourcemap) {
  931. if (typeof(options.sourcemap) !== 'object')
  932. options.sourcemap = {};
  933. stats.sourcemap_t0 = Date.now();
  934. generateSourcemap(result, src, nodePositions, fragments, options.sourcemap);
  935. stats.sourcemap_t1 = Date.now();
  936. }
  937. return result;
  938. }