You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

722 lines
21 KiB

4 years ago
  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 "||" or "??"
  31. */
  32. function isHandledLogicalOperator(operator) {
  33. return operator === "&&" || 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. // The `arguments.length == 0` case is in `postprocess` function.
  215. case "CallExpression":
  216. if (parent.optional === true && parent.arguments.length >= 1 && parent.arguments[0] === node) {
  217. state.makeOptionalRight();
  218. }
  219. break;
  220. case "MemberExpression":
  221. if (parent.optional === true && parent.property === node) {
  222. state.makeOptionalRight();
  223. }
  224. break;
  225. case "LogicalExpression":
  226. if (
  227. parent.right === node &&
  228. isHandledLogicalOperator(parent.operator)
  229. ) {
  230. state.makeLogicalRight();
  231. }
  232. break;
  233. case "ConditionalExpression":
  234. case "IfStatement":
  235. /*
  236. * Fork if this node is at `consequent`/`alternate`.
  237. * `popForkContext()` exists at `IfStatement:exit` and
  238. * `ConditionalExpression:exit`.
  239. */
  240. if (parent.consequent === node) {
  241. state.makeIfConsequent();
  242. } else if (parent.alternate === node) {
  243. state.makeIfAlternate();
  244. }
  245. break;
  246. case "SwitchCase":
  247. if (parent.consequent[0] === node) {
  248. state.makeSwitchCaseBody(false, !parent.test);
  249. }
  250. break;
  251. case "TryStatement":
  252. if (parent.handler === node) {
  253. state.makeCatchBlock();
  254. } else if (parent.finalizer === node) {
  255. state.makeFinallyBlock();
  256. }
  257. break;
  258. case "WhileStatement":
  259. if (parent.test === node) {
  260. state.makeWhileTest(getBooleanValueIfSimpleConstant(node));
  261. } else {
  262. assert(parent.body === node);
  263. state.makeWhileBody();
  264. }
  265. break;
  266. case "DoWhileStatement":
  267. if (parent.body === node) {
  268. state.makeDoWhileBody();
  269. } else {
  270. assert(parent.test === node);
  271. state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node));
  272. }
  273. break;
  274. case "ForStatement":
  275. if (parent.test === node) {
  276. state.makeForTest(getBooleanValueIfSimpleConstant(node));
  277. } else if (parent.update === node) {
  278. state.makeForUpdate();
  279. } else if (parent.body === node) {
  280. state.makeForBody();
  281. }
  282. break;
  283. case "ForInStatement":
  284. case "ForOfStatement":
  285. if (parent.left === node) {
  286. state.makeForInOfLeft();
  287. } else if (parent.right === node) {
  288. state.makeForInOfRight();
  289. } else {
  290. assert(parent.body === node);
  291. state.makeForInOfBody();
  292. }
  293. break;
  294. case "AssignmentPattern":
  295. /*
  296. * Fork if this node is at `right`.
  297. * `left` is executed always, so it uses the current path.
  298. * `popForkContext()` exists at `AssignmentPattern:exit`.
  299. */
  300. if (parent.right === node) {
  301. state.pushForkContext();
  302. state.forkBypassPath();
  303. state.forkPath();
  304. }
  305. break;
  306. default:
  307. break;
  308. }
  309. }
  310. /**
  311. * Updates the code path due to the type of a given node in entering.
  312. * @param {CodePathAnalyzer} analyzer The instance.
  313. * @param {ASTNode} node The current AST node.
  314. * @returns {void}
  315. */
  316. function processCodePathToEnter(analyzer, node) {
  317. let codePath = analyzer.codePath;
  318. let state = codePath && CodePath.getState(codePath);
  319. const parent = node.parent;
  320. switch (node.type) {
  321. case "Program":
  322. case "FunctionDeclaration":
  323. case "FunctionExpression":
  324. case "ArrowFunctionExpression":
  325. if (codePath) {
  326. // Emits onCodePathSegmentStart events if updated.
  327. forwardCurrentToHead(analyzer, node);
  328. debug.dumpState(node, state, false);
  329. }
  330. // Create the code path of this scope.
  331. codePath = analyzer.codePath = new CodePath(
  332. analyzer.idGenerator.next(),
  333. codePath,
  334. analyzer.onLooped
  335. );
  336. state = CodePath.getState(codePath);
  337. // Emits onCodePathStart events.
  338. debug.dump(`onCodePathStart ${codePath.id}`);
  339. analyzer.emitter.emit("onCodePathStart", codePath, node);
  340. break;
  341. case "ChainExpression":
  342. state.pushChainContext();
  343. break;
  344. case "CallExpression":
  345. if (node.optional === true) {
  346. state.makeOptionalNode();
  347. }
  348. break;
  349. case "MemberExpression":
  350. if (node.optional === true) {
  351. state.makeOptionalNode();
  352. }
  353. break;
  354. case "LogicalExpression":
  355. if (isHandledLogicalOperator(node.operator)) {
  356. state.pushChoiceContext(
  357. node.operator,
  358. isForkingByTrueOrFalse(node)
  359. );
  360. }
  361. break;
  362. case "ConditionalExpression":
  363. case "IfStatement":
  364. state.pushChoiceContext("test", false);
  365. break;
  366. case "SwitchStatement":
  367. state.pushSwitchContext(
  368. node.cases.some(isCaseNode),
  369. getLabel(node)
  370. );
  371. break;
  372. case "TryStatement":
  373. state.pushTryContext(Boolean(node.finalizer));
  374. break;
  375. case "SwitchCase":
  376. /*
  377. * Fork if this node is after the 2st node in `cases`.
  378. * It's similar to `else` blocks.
  379. * The next `test` node is processed in this path.
  380. */
  381. if (parent.discriminant !== node && parent.cases[0] !== node) {
  382. state.forkPath();
  383. }
  384. break;
  385. case "WhileStatement":
  386. case "DoWhileStatement":
  387. case "ForStatement":
  388. case "ForInStatement":
  389. case "ForOfStatement":
  390. state.pushLoopContext(node.type, getLabel(node));
  391. break;
  392. case "LabeledStatement":
  393. if (!breakableTypePattern.test(node.body.type)) {
  394. state.pushBreakContext(false, node.label.name);
  395. }
  396. break;
  397. default:
  398. break;
  399. }
  400. // Emits onCodePathSegmentStart events if updated.
  401. forwardCurrentToHead(analyzer, node);
  402. debug.dumpState(node, state, false);
  403. }
  404. /**
  405. * Updates the code path due to the type of a given node in leaving.
  406. * @param {CodePathAnalyzer} analyzer The instance.
  407. * @param {ASTNode} node The current AST node.
  408. * @returns {void}
  409. */
  410. function processCodePathToExit(analyzer, node) {
  411. const codePath = analyzer.codePath;
  412. const state = CodePath.getState(codePath);
  413. let dontForward = false;
  414. switch (node.type) {
  415. case "ChainExpression":
  416. state.popChainContext();
  417. break;
  418. case "IfStatement":
  419. case "ConditionalExpression":
  420. state.popChoiceContext();
  421. break;
  422. case "LogicalExpression":
  423. if (isHandledLogicalOperator(node.operator)) {
  424. state.popChoiceContext();
  425. }
  426. break;
  427. case "SwitchStatement":
  428. state.popSwitchContext();
  429. break;
  430. case "SwitchCase":
  431. /*
  432. * This is the same as the process at the 1st `consequent` node in
  433. * `preprocess` function.
  434. * Must do if this `consequent` is empty.
  435. */
  436. if (node.consequent.length === 0) {
  437. state.makeSwitchCaseBody(true, !node.test);
  438. }
  439. if (state.forkContext.reachable) {
  440. dontForward = true;
  441. }
  442. break;
  443. case "TryStatement":
  444. state.popTryContext();
  445. break;
  446. case "BreakStatement":
  447. forwardCurrentToHead(analyzer, node);
  448. state.makeBreak(node.label && node.label.name);
  449. dontForward = true;
  450. break;
  451. case "ContinueStatement":
  452. forwardCurrentToHead(analyzer, node);
  453. state.makeContinue(node.label && node.label.name);
  454. dontForward = true;
  455. break;
  456. case "ReturnStatement":
  457. forwardCurrentToHead(analyzer, node);
  458. state.makeReturn();
  459. dontForward = true;
  460. break;
  461. case "ThrowStatement":
  462. forwardCurrentToHead(analyzer, node);
  463. state.makeThrow();
  464. dontForward = true;
  465. break;
  466. case "Identifier":
  467. if (isIdentifierReference(node)) {
  468. state.makeFirstThrowablePathInTryBlock();
  469. dontForward = true;
  470. }
  471. break;
  472. case "CallExpression":
  473. case "ImportExpression":
  474. case "MemberExpression":
  475. case "NewExpression":
  476. case "YieldExpression":
  477. state.makeFirstThrowablePathInTryBlock();
  478. break;
  479. case "WhileStatement":
  480. case "DoWhileStatement":
  481. case "ForStatement":
  482. case "ForInStatement":
  483. case "ForOfStatement":
  484. state.popLoopContext();
  485. break;
  486. case "AssignmentPattern":
  487. state.popForkContext();
  488. break;
  489. case "LabeledStatement":
  490. if (!breakableTypePattern.test(node.body.type)) {
  491. state.popBreakContext();
  492. }
  493. break;
  494. default:
  495. break;
  496. }
  497. // Emits onCodePathSegmentStart events if updated.
  498. if (!dontForward) {
  499. forwardCurrentToHead(analyzer, node);
  500. }
  501. debug.dumpState(node, state, true);
  502. }
  503. /**
  504. * Updates the code path to finalize the current code path.
  505. * @param {CodePathAnalyzer} analyzer The instance.
  506. * @param {ASTNode} node The current AST node.
  507. * @returns {void}
  508. */
  509. function postprocess(analyzer, node) {
  510. switch (node.type) {
  511. case "Program":
  512. case "FunctionDeclaration":
  513. case "FunctionExpression":
  514. case "ArrowFunctionExpression": {
  515. let codePath = analyzer.codePath;
  516. // Mark the current path as the final node.
  517. CodePath.getState(codePath).makeFinal();
  518. // Emits onCodePathSegmentEnd event of the current segments.
  519. leaveFromCurrentSegment(analyzer, node);
  520. // Emits onCodePathEnd event of this code path.
  521. debug.dump(`onCodePathEnd ${codePath.id}`);
  522. analyzer.emitter.emit("onCodePathEnd", codePath, node);
  523. debug.dumpDot(codePath);
  524. codePath = analyzer.codePath = analyzer.codePath.upper;
  525. if (codePath) {
  526. debug.dumpState(node, CodePath.getState(codePath), true);
  527. }
  528. break;
  529. }
  530. // The `arguments.length >= 1` case is in `preprocess` function.
  531. case "CallExpression":
  532. if (node.optional === true && node.arguments.length === 0) {
  533. CodePath.getState(analyzer.codePath).makeOptionalRight();
  534. }
  535. break;
  536. default:
  537. break;
  538. }
  539. }
  540. //------------------------------------------------------------------------------
  541. // Public Interface
  542. //------------------------------------------------------------------------------
  543. /**
  544. * The class to analyze code paths.
  545. * This class implements the EventGenerator interface.
  546. */
  547. class CodePathAnalyzer {
  548. // eslint-disable-next-line jsdoc/require-description
  549. /**
  550. * @param {EventGenerator} eventGenerator An event generator to wrap.
  551. */
  552. constructor(eventGenerator) {
  553. this.original = eventGenerator;
  554. this.emitter = eventGenerator.emitter;
  555. this.codePath = null;
  556. this.idGenerator = new IdGenerator("s");
  557. this.currentNode = null;
  558. this.onLooped = this.onLooped.bind(this);
  559. }
  560. /**
  561. * Does the process to enter a given AST node.
  562. * This updates state of analysis and calls `enterNode` of the wrapped.
  563. * @param {ASTNode} node A node which is entering.
  564. * @returns {void}
  565. */
  566. enterNode(node) {
  567. this.currentNode = node;
  568. // Updates the code path due to node's position in its parent node.
  569. if (node.parent) {
  570. preprocess(this, node);
  571. }
  572. /*
  573. * Updates the code path.
  574. * And emits onCodePathStart/onCodePathSegmentStart events.
  575. */
  576. processCodePathToEnter(this, node);
  577. // Emits node events.
  578. this.original.enterNode(node);
  579. this.currentNode = null;
  580. }
  581. /**
  582. * Does the process to leave a given AST node.
  583. * This updates state of analysis and calls `leaveNode` of the wrapped.
  584. * @param {ASTNode} node A node which is leaving.
  585. * @returns {void}
  586. */
  587. leaveNode(node) {
  588. this.currentNode = node;
  589. /*
  590. * Updates the code path.
  591. * And emits onCodePathStart/onCodePathSegmentStart events.
  592. */
  593. processCodePathToExit(this, node);
  594. // Emits node events.
  595. this.original.leaveNode(node);
  596. // Emits the last onCodePathStart/onCodePathSegmentStart events.
  597. postprocess(this, node);
  598. this.currentNode = null;
  599. }
  600. /**
  601. * This is called on a code path looped.
  602. * Then this raises a looped event.
  603. * @param {CodePathSegment} fromSegment A segment of prev.
  604. * @param {CodePathSegment} toSegment A segment of next.
  605. * @returns {void}
  606. */
  607. onLooped(fromSegment, toSegment) {
  608. if (fromSegment.reachable && toSegment.reachable) {
  609. debug.dump(`onCodePathSegmentLoop ${fromSegment.id} -> ${toSegment.id}`);
  610. this.emitter.emit(
  611. "onCodePathSegmentLoop",
  612. fromSegment,
  613. toSegment,
  614. this.currentNode
  615. );
  616. }
  617. }
  618. }
  619. module.exports = CodePathAnalyzer;