code-path.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. /**
  2. * @fileoverview A class of the code path.
  3. * @author Toru Nagashima
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const CodePathState = require("./code-path-state");
  10. const IdGenerator = require("./id-generator");
  11. //------------------------------------------------------------------------------
  12. // Public Interface
  13. //------------------------------------------------------------------------------
  14. /**
  15. * A code path.
  16. */
  17. class CodePath {
  18. /**
  19. * Creates a new instance.
  20. * @param {Object} options Options for the function (see below).
  21. * @param {string} options.id An identifier.
  22. * @param {string} options.origin The type of code path origin.
  23. * @param {CodePath|null} options.upper The code path of the upper function scope.
  24. * @param {Function} options.onLooped A callback function to notify looping.
  25. */
  26. constructor({ id, origin, upper, onLooped }) {
  27. /**
  28. * The identifier of this code path.
  29. * Rules use it to store additional information of each rule.
  30. * @type {string}
  31. */
  32. this.id = id;
  33. /**
  34. * The reason that this code path was started. May be "program",
  35. * "function", "class-field-initializer", or "class-static-block".
  36. * @type {string}
  37. */
  38. this.origin = origin;
  39. /**
  40. * The code path of the upper function scope.
  41. * @type {CodePath|null}
  42. */
  43. this.upper = upper;
  44. /**
  45. * The code paths of nested function scopes.
  46. * @type {CodePath[]}
  47. */
  48. this.childCodePaths = [];
  49. // Initializes internal state.
  50. Object.defineProperty(
  51. this,
  52. "internal",
  53. { value: new CodePathState(new IdGenerator(`${id}_`), onLooped) }
  54. );
  55. // Adds this into `childCodePaths` of `upper`.
  56. if (upper) {
  57. upper.childCodePaths.push(this);
  58. }
  59. }
  60. /**
  61. * Gets the state of a given code path.
  62. * @param {CodePath} codePath A code path to get.
  63. * @returns {CodePathState} The state of the code path.
  64. */
  65. static getState(codePath) {
  66. return codePath.internal;
  67. }
  68. /**
  69. * The initial code path segment.
  70. * @type {CodePathSegment}
  71. */
  72. get initialSegment() {
  73. return this.internal.initialSegment;
  74. }
  75. /**
  76. * Final code path segments.
  77. * This array is a mix of `returnedSegments` and `thrownSegments`.
  78. * @type {CodePathSegment[]}
  79. */
  80. get finalSegments() {
  81. return this.internal.finalSegments;
  82. }
  83. /**
  84. * Final code path segments which is with `return` statements.
  85. * This array contains the last path segment if it's reachable.
  86. * Since the reachable last path returns `undefined`.
  87. * @type {CodePathSegment[]}
  88. */
  89. get returnedSegments() {
  90. return this.internal.returnedForkContext;
  91. }
  92. /**
  93. * Final code path segments which is with `throw` statements.
  94. * @type {CodePathSegment[]}
  95. */
  96. get thrownSegments() {
  97. return this.internal.thrownForkContext;
  98. }
  99. /**
  100. * Current code path segments.
  101. * @type {CodePathSegment[]}
  102. */
  103. get currentSegments() {
  104. return this.internal.currentSegments;
  105. }
  106. /**
  107. * Traverses all segments in this code path.
  108. *
  109. * codePath.traverseSegments(function(segment, controller) {
  110. * // do something.
  111. * });
  112. *
  113. * This method enumerates segments in order from the head.
  114. *
  115. * The `controller` object has two methods.
  116. *
  117. * - `controller.skip()` - Skip the following segments in this branch.
  118. * - `controller.break()` - Skip all following segments.
  119. * @param {Object} [options] Omittable.
  120. * @param {CodePathSegment} [options.first] The first segment to traverse.
  121. * @param {CodePathSegment} [options.last] The last segment to traverse.
  122. * @param {Function} callback A callback function.
  123. * @returns {void}
  124. */
  125. traverseSegments(options, callback) {
  126. let resolvedOptions;
  127. let resolvedCallback;
  128. if (typeof options === "function") {
  129. resolvedCallback = options;
  130. resolvedOptions = {};
  131. } else {
  132. resolvedOptions = options || {};
  133. resolvedCallback = callback;
  134. }
  135. const startSegment = resolvedOptions.first || this.internal.initialSegment;
  136. const lastSegment = resolvedOptions.last;
  137. let item = null;
  138. let index = 0;
  139. let end = 0;
  140. let segment = null;
  141. const visited = Object.create(null);
  142. const stack = [[startSegment, 0]];
  143. let skippedSegment = null;
  144. let broken = false;
  145. const controller = {
  146. skip() {
  147. if (stack.length <= 1) {
  148. broken = true;
  149. } else {
  150. skippedSegment = stack[stack.length - 2][0];
  151. }
  152. },
  153. break() {
  154. broken = true;
  155. }
  156. };
  157. /**
  158. * Checks a given previous segment has been visited.
  159. * @param {CodePathSegment} prevSegment A previous segment to check.
  160. * @returns {boolean} `true` if the segment has been visited.
  161. */
  162. function isVisited(prevSegment) {
  163. return (
  164. visited[prevSegment.id] ||
  165. segment.isLoopedPrevSegment(prevSegment)
  166. );
  167. }
  168. while (stack.length > 0) {
  169. item = stack[stack.length - 1];
  170. segment = item[0];
  171. index = item[1];
  172. if (index === 0) {
  173. // Skip if this segment has been visited already.
  174. if (visited[segment.id]) {
  175. stack.pop();
  176. continue;
  177. }
  178. // Skip if all previous segments have not been visited.
  179. if (segment !== startSegment &&
  180. segment.prevSegments.length > 0 &&
  181. !segment.prevSegments.every(isVisited)
  182. ) {
  183. stack.pop();
  184. continue;
  185. }
  186. // Reset the flag of skipping if all branches have been skipped.
  187. if (skippedSegment && segment.prevSegments.includes(skippedSegment)) {
  188. skippedSegment = null;
  189. }
  190. visited[segment.id] = true;
  191. // Call the callback when the first time.
  192. if (!skippedSegment) {
  193. resolvedCallback.call(this, segment, controller);
  194. if (segment === lastSegment) {
  195. controller.skip();
  196. }
  197. if (broken) {
  198. break;
  199. }
  200. }
  201. }
  202. // Update the stack.
  203. end = segment.nextSegments.length - 1;
  204. if (index < end) {
  205. item[1] += 1;
  206. stack.push([segment.nextSegments[index], 0]);
  207. } else if (index === end) {
  208. item[0] = segment.nextSegments[index];
  209. item[1] = 0;
  210. } else {
  211. stack.pop();
  212. }
  213. }
  214. }
  215. }
  216. module.exports = CodePath;