code-path-analyzer.js 20 KB

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