code-path-analyzer.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. /**
  2. * @fileoverview A class of the code path analyzer.
  3. * @author Toru Nagashima
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const assert = require("assert"),
  10. CodePath = require("./code-path"),
  11. CodePathSegment = require("./code-path-segment"),
  12. IdGenerator = require("./id-generator"),
  13. debug = require("./debug-helpers"),
  14. astUtils = require("../ast-utils");
  15. //------------------------------------------------------------------------------
  16. // Helpers
  17. //------------------------------------------------------------------------------
  18. /**
  19. * Checks whether or not a given node is a `case` node (not `default` node).
  20. *
  21. * @param {ASTNode} node - A `SwitchCase` node to check.
  22. * @returns {boolean} `true` if the node is a `case` node (not `default` node).
  23. */
  24. function isCaseNode(node) {
  25. return Boolean(node.test);
  26. }
  27. /**
  28. * Checks whether or not a given logical expression node goes different path
  29. * between the `true` case and the `false` case.
  30. *
  31. * @param {ASTNode} node - A node to check.
  32. * @returns {boolean} `true` if the node is a test of a choice statement.
  33. */
  34. function isForkingByTrueOrFalse(node) {
  35. const parent = node.parent;
  36. switch (parent.type) {
  37. case "ConditionalExpression":
  38. case "IfStatement":
  39. case "WhileStatement":
  40. case "DoWhileStatement":
  41. case "ForStatement":
  42. return parent.test === node;
  43. case "LogicalExpression":
  44. return true;
  45. default:
  46. return false;
  47. }
  48. }
  49. /**
  50. * Gets the boolean value of a given literal node.
  51. *
  52. * This is used to detect infinity loops (e.g. `while (true) {}`).
  53. * Statements preceded by an infinity loop are unreachable if the loop didn't
  54. * have any `break` statement.
  55. *
  56. * @param {ASTNode} node - A node to get.
  57. * @returns {boolean|undefined} a boolean value if the node is a Literal node,
  58. * otherwise `undefined`.
  59. */
  60. function getBooleanValueIfSimpleConstant(node) {
  61. if (node.type === "Literal") {
  62. return Boolean(node.value);
  63. }
  64. return void 0;
  65. }
  66. /**
  67. * Checks that a given identifier node is a reference or not.
  68. *
  69. * This is used to detect the first throwable node in a `try` block.
  70. *
  71. * @param {ASTNode} node - An Identifier node to check.
  72. * @returns {boolean} `true` if the node is a reference.
  73. */
  74. function isIdentifierReference(node) {
  75. const parent = node.parent;
  76. switch (parent.type) {
  77. case "LabeledStatement":
  78. case "BreakStatement":
  79. case "ContinueStatement":
  80. case "ArrayPattern":
  81. case "RestElement":
  82. case "ImportSpecifier":
  83. case "ImportDefaultSpecifier":
  84. case "ImportNamespaceSpecifier":
  85. case "CatchClause":
  86. return false;
  87. case "FunctionDeclaration":
  88. case "FunctionExpression":
  89. case "ArrowFunctionExpression":
  90. case "ClassDeclaration":
  91. case "ClassExpression":
  92. case "VariableDeclarator":
  93. return parent.id !== node;
  94. case "Property":
  95. case "MethodDefinition":
  96. return (
  97. parent.key !== node ||
  98. parent.computed ||
  99. parent.shorthand
  100. );
  101. case "AssignmentPattern":
  102. return parent.key !== node;
  103. default:
  104. return true;
  105. }
  106. }
  107. /**
  108. * Updates the current segment with the head segment.
  109. * This is similar to local branches and tracking branches of git.
  110. *
  111. * To separate the current and the head is in order to not make useless segments.
  112. *
  113. * In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd"
  114. * events are fired.
  115. *
  116. * @param {CodePathAnalyzer} analyzer - The instance.
  117. * @param {ASTNode} node - The current AST node.
  118. * @returns {void}
  119. */
  120. function forwardCurrentToHead(analyzer, node) {
  121. const codePath = analyzer.codePath;
  122. const state = CodePath.getState(codePath);
  123. const currentSegments = state.currentSegments;
  124. const headSegments = state.headSegments;
  125. const end = Math.max(currentSegments.length, headSegments.length);
  126. let i, currentSegment, headSegment;
  127. // Fires leaving events.
  128. for (i = 0; i < end; ++i) {
  129. currentSegment = currentSegments[i];
  130. headSegment = headSegments[i];
  131. if (currentSegment !== headSegment && currentSegment) {
  132. debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`);
  133. if (currentSegment.reachable) {
  134. analyzer.emitter.emit(
  135. "onCodePathSegmentEnd",
  136. currentSegment,
  137. node
  138. );
  139. }
  140. }
  141. }
  142. // Update state.
  143. state.currentSegments = headSegments;
  144. // Fires entering events.
  145. for (i = 0; i < end; ++i) {
  146. currentSegment = currentSegments[i];
  147. headSegment = headSegments[i];
  148. if (currentSegment !== headSegment && headSegment) {
  149. debug.dump(`onCodePathSegmentStart ${headSegment.id}`);
  150. CodePathSegment.markUsed(headSegment);
  151. if (headSegment.reachable) {
  152. analyzer.emitter.emit(
  153. "onCodePathSegmentStart",
  154. headSegment,
  155. node
  156. );
  157. }
  158. }
  159. }
  160. }
  161. /**
  162. * Updates the current segment with empty.
  163. * This is called at the last of functions or the program.
  164. *
  165. * @param {CodePathAnalyzer} analyzer - The instance.
  166. * @param {ASTNode} node - The current AST node.
  167. * @returns {void}
  168. */
  169. function leaveFromCurrentSegment(analyzer, node) {
  170. const state = CodePath.getState(analyzer.codePath);
  171. const currentSegments = state.currentSegments;
  172. for (let i = 0; i < currentSegments.length; ++i) {
  173. const currentSegment = currentSegments[i];
  174. debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`);
  175. if (currentSegment.reachable) {
  176. analyzer.emitter.emit(
  177. "onCodePathSegmentEnd",
  178. currentSegment,
  179. node
  180. );
  181. }
  182. }
  183. state.currentSegments = [];
  184. }
  185. /**
  186. * Updates the code path due to the position of a given node in the parent node
  187. * thereof.
  188. *
  189. * For example, if the node is `parent.consequent`, this creates a fork from the
  190. * current path.
  191. *
  192. * @param {CodePathAnalyzer} analyzer - The instance.
  193. * @param {ASTNode} node - The current AST node.
  194. * @returns {void}
  195. */
  196. function preprocess(analyzer, node) {
  197. const codePath = analyzer.codePath;
  198. const state = CodePath.getState(codePath);
  199. const parent = node.parent;
  200. switch (parent.type) {
  201. case "LogicalExpression":
  202. if (parent.right === node) {
  203. state.makeLogicalRight();
  204. }
  205. break;
  206. case "ConditionalExpression":
  207. case "IfStatement":
  208. /*
  209. * Fork if this node is at `consequent`/`alternate`.
  210. * `popForkContext()` exists at `IfStatement:exit` and
  211. * `ConditionalExpression:exit`.
  212. */
  213. if (parent.consequent === node) {
  214. state.makeIfConsequent();
  215. } else if (parent.alternate === node) {
  216. state.makeIfAlternate();
  217. }
  218. break;
  219. case "SwitchCase":
  220. if (parent.consequent[0] === node) {
  221. state.makeSwitchCaseBody(false, !parent.test);
  222. }
  223. break;
  224. case "TryStatement":
  225. if (parent.handler === node) {
  226. state.makeCatchBlock();
  227. } else if (parent.finalizer === node) {
  228. state.makeFinallyBlock();
  229. }
  230. break;
  231. case "WhileStatement":
  232. if (parent.test === node) {
  233. state.makeWhileTest(getBooleanValueIfSimpleConstant(node));
  234. } else {
  235. assert(parent.body === node);
  236. state.makeWhileBody();
  237. }
  238. break;
  239. case "DoWhileStatement":
  240. if (parent.body === node) {
  241. state.makeDoWhileBody();
  242. } else {
  243. assert(parent.test === node);
  244. state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node));
  245. }
  246. break;
  247. case "ForStatement":
  248. if (parent.test === node) {
  249. state.makeForTest(getBooleanValueIfSimpleConstant(node));
  250. } else if (parent.update === node) {
  251. state.makeForUpdate();
  252. } else if (parent.body === node) {
  253. state.makeForBody();
  254. }
  255. break;
  256. case "ForInStatement":
  257. case "ForOfStatement":
  258. if (parent.left === node) {
  259. state.makeForInOfLeft();
  260. } else if (parent.right === node) {
  261. state.makeForInOfRight();
  262. } else {
  263. assert(parent.body === node);
  264. state.makeForInOfBody();
  265. }
  266. break;
  267. case "AssignmentPattern":
  268. /*
  269. * Fork if this node is at `right`.
  270. * `left` is executed always, so it uses the current path.
  271. * `popForkContext()` exists at `AssignmentPattern:exit`.
  272. */
  273. if (parent.right === node) {
  274. state.pushForkContext();
  275. state.forkBypassPath();
  276. state.forkPath();
  277. }
  278. break;
  279. default:
  280. break;
  281. }
  282. }
  283. /**
  284. * Updates the code path due to the type of a given node in entering.
  285. *
  286. * @param {CodePathAnalyzer} analyzer - The instance.
  287. * @param {ASTNode} node - The current AST node.
  288. * @returns {void}
  289. */
  290. function processCodePathToEnter(analyzer, node) {
  291. let codePath = analyzer.codePath;
  292. let state = codePath && CodePath.getState(codePath);
  293. const parent = node.parent;
  294. switch (node.type) {
  295. case "Program":
  296. case "FunctionDeclaration":
  297. case "FunctionExpression":
  298. case "ArrowFunctionExpression":
  299. if (codePath) {
  300. // Emits onCodePathSegmentStart events if updated.
  301. forwardCurrentToHead(analyzer, node);
  302. debug.dumpState(node, state, false);
  303. }
  304. // Create the code path of this scope.
  305. codePath = analyzer.codePath = new CodePath(
  306. analyzer.idGenerator.next(),
  307. codePath,
  308. analyzer.onLooped
  309. );
  310. state = CodePath.getState(codePath);
  311. // Emits onCodePathStart events.
  312. debug.dump(`onCodePathStart ${codePath.id}`);
  313. analyzer.emitter.emit("onCodePathStart", codePath, node);
  314. break;
  315. case "LogicalExpression":
  316. state.pushChoiceContext(node.operator, isForkingByTrueOrFalse(node));
  317. break;
  318. case "ConditionalExpression":
  319. case "IfStatement":
  320. state.pushChoiceContext("test", false);
  321. break;
  322. case "SwitchStatement":
  323. state.pushSwitchContext(
  324. node.cases.some(isCaseNode),
  325. astUtils.getLabel(node)
  326. );
  327. break;
  328. case "TryStatement":
  329. state.pushTryContext(Boolean(node.finalizer));
  330. break;
  331. case "SwitchCase":
  332. /*
  333. * Fork if this node is after the 2st node in `cases`.
  334. * It's similar to `else` blocks.
  335. * The next `test` node is processed in this path.
  336. */
  337. if (parent.discriminant !== node && parent.cases[0] !== node) {
  338. state.forkPath();
  339. }
  340. break;
  341. case "WhileStatement":
  342. case "DoWhileStatement":
  343. case "ForStatement":
  344. case "ForInStatement":
  345. case "ForOfStatement":
  346. state.pushLoopContext(node.type, astUtils.getLabel(node));
  347. break;
  348. case "LabeledStatement":
  349. if (!astUtils.isBreakableStatement(node.body)) {
  350. state.pushBreakContext(false, node.label.name);
  351. }
  352. break;
  353. default:
  354. break;
  355. }
  356. // Emits onCodePathSegmentStart events if updated.
  357. forwardCurrentToHead(analyzer, node);
  358. debug.dumpState(node, state, false);
  359. }
  360. /**
  361. * Updates the code path due to the type of a given node in leaving.
  362. *
  363. * @param {CodePathAnalyzer} analyzer - The instance.
  364. * @param {ASTNode} node - The current AST node.
  365. * @returns {void}
  366. */
  367. function processCodePathToExit(analyzer, node) {
  368. const codePath = analyzer.codePath;
  369. const state = CodePath.getState(codePath);
  370. let dontForward = false;
  371. switch (node.type) {
  372. case "IfStatement":
  373. case "ConditionalExpression":
  374. case "LogicalExpression":
  375. state.popChoiceContext();
  376. break;
  377. case "SwitchStatement":
  378. state.popSwitchContext();
  379. break;
  380. case "SwitchCase":
  381. /*
  382. * This is the same as the process at the 1st `consequent` node in
  383. * `preprocess` function.
  384. * Must do if this `consequent` is empty.
  385. */
  386. if (node.consequent.length === 0) {
  387. state.makeSwitchCaseBody(true, !node.test);
  388. }
  389. if (state.forkContext.reachable) {
  390. dontForward = true;
  391. }
  392. break;
  393. case "TryStatement":
  394. state.popTryContext();
  395. break;
  396. case "BreakStatement":
  397. forwardCurrentToHead(analyzer, node);
  398. state.makeBreak(node.label && node.label.name);
  399. dontForward = true;
  400. break;
  401. case "ContinueStatement":
  402. forwardCurrentToHead(analyzer, node);
  403. state.makeContinue(node.label && node.label.name);
  404. dontForward = true;
  405. break;
  406. case "ReturnStatement":
  407. forwardCurrentToHead(analyzer, node);
  408. state.makeReturn();
  409. dontForward = true;
  410. break;
  411. case "ThrowStatement":
  412. forwardCurrentToHead(analyzer, node);
  413. state.makeThrow();
  414. dontForward = true;
  415. break;
  416. case "Identifier":
  417. if (isIdentifierReference(node)) {
  418. state.makeFirstThrowablePathInTryBlock();
  419. dontForward = true;
  420. }
  421. break;
  422. case "CallExpression":
  423. case "MemberExpression":
  424. case "NewExpression":
  425. state.makeFirstThrowablePathInTryBlock();
  426. break;
  427. case "WhileStatement":
  428. case "DoWhileStatement":
  429. case "ForStatement":
  430. case "ForInStatement":
  431. case "ForOfStatement":
  432. state.popLoopContext();
  433. break;
  434. case "AssignmentPattern":
  435. state.popForkContext();
  436. break;
  437. case "LabeledStatement":
  438. if (!astUtils.isBreakableStatement(node.body)) {
  439. state.popBreakContext();
  440. }
  441. break;
  442. default:
  443. break;
  444. }
  445. // Emits onCodePathSegmentStart events if updated.
  446. if (!dontForward) {
  447. forwardCurrentToHead(analyzer, node);
  448. }
  449. debug.dumpState(node, state, true);
  450. }
  451. /**
  452. * Updates the code path to finalize the current code path.
  453. *
  454. * @param {CodePathAnalyzer} analyzer - The instance.
  455. * @param {ASTNode} node - The current AST node.
  456. * @returns {void}
  457. */
  458. function postprocess(analyzer, node) {
  459. switch (node.type) {
  460. case "Program":
  461. case "FunctionDeclaration":
  462. case "FunctionExpression":
  463. case "ArrowFunctionExpression": {
  464. let codePath = analyzer.codePath;
  465. // Mark the current path as the final node.
  466. CodePath.getState(codePath).makeFinal();
  467. // Emits onCodePathSegmentEnd event of the current segments.
  468. leaveFromCurrentSegment(analyzer, node);
  469. // Emits onCodePathEnd event of this code path.
  470. debug.dump(`onCodePathEnd ${codePath.id}`);
  471. analyzer.emitter.emit("onCodePathEnd", codePath, node);
  472. debug.dumpDot(codePath);
  473. codePath = analyzer.codePath = analyzer.codePath.upper;
  474. if (codePath) {
  475. debug.dumpState(node, CodePath.getState(codePath), true);
  476. }
  477. break;
  478. }
  479. default:
  480. break;
  481. }
  482. }
  483. //------------------------------------------------------------------------------
  484. // Public Interface
  485. //------------------------------------------------------------------------------
  486. /**
  487. * The class to analyze code paths.
  488. * This class implements the EventGenerator interface.
  489. */
  490. class CodePathAnalyzer {
  491. /**
  492. * @param {EventGenerator} eventGenerator - An event generator to wrap.
  493. */
  494. constructor(eventGenerator) {
  495. this.original = eventGenerator;
  496. this.emitter = eventGenerator.emitter;
  497. this.codePath = null;
  498. this.idGenerator = new IdGenerator("s");
  499. this.currentNode = null;
  500. this.onLooped = this.onLooped.bind(this);
  501. }
  502. /**
  503. * Does the process to enter a given AST node.
  504. * This updates state of analysis and calls `enterNode` of the wrapped.
  505. *
  506. * @param {ASTNode} node - A node which is entering.
  507. * @returns {void}
  508. */
  509. enterNode(node) {
  510. this.currentNode = node;
  511. // Updates the code path due to node's position in its parent node.
  512. if (node.parent) {
  513. preprocess(this, node);
  514. }
  515. /*
  516. * Updates the code path.
  517. * And emits onCodePathStart/onCodePathSegmentStart events.
  518. */
  519. processCodePathToEnter(this, node);
  520. // Emits node events.
  521. this.original.enterNode(node);
  522. this.currentNode = null;
  523. }
  524. /**
  525. * Does the process to leave a given AST node.
  526. * This updates state of analysis and calls `leaveNode` of the wrapped.
  527. *
  528. * @param {ASTNode} node - A node which is leaving.
  529. * @returns {void}
  530. */
  531. leaveNode(node) {
  532. this.currentNode = node;
  533. /*
  534. * Updates the code path.
  535. * And emits onCodePathStart/onCodePathSegmentStart events.
  536. */
  537. processCodePathToExit(this, node);
  538. // Emits node events.
  539. this.original.leaveNode(node);
  540. // Emits the last onCodePathStart/onCodePathSegmentStart events.
  541. postprocess(this, node);
  542. this.currentNode = null;
  543. }
  544. /**
  545. * This is called on a code path looped.
  546. * Then this raises a looped event.
  547. *
  548. * @param {CodePathSegment} fromSegment - A segment of prev.
  549. * @param {CodePathSegment} toSegment - A segment of next.
  550. * @returns {void}
  551. */
  552. onLooped(fromSegment, toSegment) {
  553. if (fromSegment.reachable && toSegment.reachable) {
  554. debug.dump(`onCodePathSegmentLoop ${fromSegment.id} -> ${toSegment.id}`);
  555. this.emitter.emit(
  556. "onCodePathSegmentLoop",
  557. fromSegment,
  558. toSegment,
  559. this.currentNode
  560. );
  561. }
  562. }
  563. }
  564. module.exports = CodePathAnalyzer;