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