• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/// <reference lib="es2019" />
2
3/* @internal */
4namespace Debug {
5    interface Node {
6        kind: number;
7    }
8
9    type FunctionExpression = Node;
10    type ArrowFunction = Node;
11    type MethodDeclaration = Node;
12    type Expression = Node;
13    type SourceFile = Node;
14    type VariableDeclaration = Node;
15    type BindingElement = Node;
16    type CallExpression = Node;
17    type BinaryExpression = Node;
18
19    interface SwitchStatement extends Node {
20        caseBlock: CaseBlock;
21    }
22
23    interface CaseBlock extends Node {
24        clauses: (CaseClause | DefaultClause)[];
25    }
26
27    interface CaseClause extends Node {
28        _caseclauseBrand: any;
29        expression: Expression;
30    }
31
32    interface DefaultClause extends Node {
33        _defaultClauseBrand: any;
34    }
35
36    interface TypeScriptModule {
37        readonly SyntaxKind: {
38            readonly CaseClause: number;
39            readonly DefaultClause: number;
40        };
41
42        readonly FlowFlags: {
43            readonly Unreachable: number,
44            readonly Start: number,
45            readonly BranchLabel: number,
46            readonly LoopLabel: number,
47            readonly Assignment: number,
48            readonly TrueCondition: number,
49            readonly FalseCondition: number,
50            readonly SwitchClause: number,
51            readonly ArrayMutation: number,
52            readonly Call: number,
53            readonly ReduceLabel: number,
54            readonly Referenced: number,
55            readonly Shared: number,
56            readonly Label: number,
57            readonly Condition: number,
58        };
59
60        getSourceFileOfNode(node: Node): SourceFile;
61        getSourceTextOfNodeFromSourceFile(sourceFile: SourceFile, node: Node, includeTrivia?: boolean): string;
62        isDefaultClause(node: Node): node is DefaultClause;
63    }
64
65    type FlowNode =
66        | FlowStart
67        | FlowLabel
68        | FlowAssignment
69        | FlowCall
70        | FlowCondition
71        | FlowSwitchClause
72        | FlowArrayMutation
73        | FlowReduceLabel
74        ;
75
76    interface FlowNodeBase {
77        flags: FlowFlags;
78        id?: number;
79    }
80
81    interface FlowStart extends FlowNodeBase {
82        node?: FunctionExpression | ArrowFunction | MethodDeclaration;
83    }
84
85    interface FlowLabel extends FlowNodeBase {
86        antecedents: FlowNode[] | undefined;
87    }
88
89    interface FlowAssignment extends FlowNodeBase {
90        node: Expression | VariableDeclaration | BindingElement;
91        antecedent: FlowNode;
92    }
93
94    interface FlowCall extends FlowNodeBase {
95        node: CallExpression;
96        antecedent: FlowNode;
97    }
98
99    interface FlowCondition extends FlowNodeBase {
100        node: Expression;
101        antecedent: FlowNode;
102    }
103
104    interface FlowSwitchClause extends FlowNodeBase {
105        switchStatement: SwitchStatement;
106        clauseStart: number;
107        clauseEnd: number;
108        antecedent: FlowNode;
109    }
110
111    interface FlowArrayMutation extends FlowNodeBase {
112        node: CallExpression | BinaryExpression;
113        antecedent: FlowNode;
114    }
115
116    export interface FlowReduceLabel extends FlowNodeBase {
117        target: FlowLabel;
118        antecedents: FlowNode[];
119        antecedent: FlowNode;
120    }
121
122    type FlowFlags = number;
123    let FlowFlags: TypeScriptModule["FlowFlags"];
124    let getSourceFileOfNode: TypeScriptModule["getSourceFileOfNode"];
125    let getSourceTextOfNodeFromSourceFile: TypeScriptModule["getSourceTextOfNodeFromSourceFile"];
126    let isDefaultClause: TypeScriptModule["isDefaultClause"];
127
128    export function init(ts: TypeScriptModule) {
129        FlowFlags = ts.FlowFlags;
130        getSourceFileOfNode = ts.getSourceFileOfNode;
131        getSourceTextOfNodeFromSourceFile = ts.getSourceTextOfNodeFromSourceFile;
132        isDefaultClause = ts.isDefaultClause;
133    }
134
135    let nextDebugFlowId = -1;
136
137    function getDebugFlowNodeId(f: FlowNode) {
138        if (!f.id) {
139            f.id = nextDebugFlowId;
140            nextDebugFlowId--;
141        }
142        return f.id;
143    }
144
145    export function formatControlFlowGraph(flowNode: FlowNode) {
146        const enum BoxCharacter {
147            lr = "─",
148            ud = "│",
149            dr = "╭",
150            dl = "╮",
151            ul = "╯",
152            ur = "╰",
153            udr = "├",
154            udl = "┤",
155            dlr = "┬",
156            ulr = "┴",
157            udlr = "╫",
158        }
159
160        const enum Connection {
161            Up = 1 << 0,
162            Down = 1 << 1,
163            Left = 1 << 2,
164            Right = 1 << 3,
165
166            UpDown = Up | Down,
167            LeftRight = Left | Right,
168            UpLeft = Up | Left,
169            UpRight = Up | Right,
170            DownLeft = Down | Left,
171            DownRight = Down | Right,
172            UpDownLeft = UpDown | Left,
173            UpDownRight = UpDown | Right,
174            UpLeftRight = Up | LeftRight,
175            DownLeftRight = Down | LeftRight,
176            UpDownLeftRight = UpDown | LeftRight,
177
178            NoChildren = 1 << 4,
179        }
180
181        interface FlowGraphNode {
182            id: number;
183            flowNode: FlowNode;
184            edges: FlowGraphEdge[];
185            text: string;
186            lane: number;
187            endLane: number;
188            level: number;
189            circular: boolean | "circularity";
190        }
191
192        interface FlowGraphEdge {
193            source: FlowGraphNode;
194            target: FlowGraphNode;
195        }
196
197        const hasAntecedentFlags =
198            FlowFlags.Assignment |
199            FlowFlags.Condition |
200            FlowFlags.SwitchClause |
201            FlowFlags.ArrayMutation |
202            FlowFlags.Call |
203            FlowFlags.ReduceLabel;
204
205        const hasNodeFlags =
206            FlowFlags.Start |
207            FlowFlags.Assignment |
208            FlowFlags.Call |
209            FlowFlags.Condition |
210            FlowFlags.ArrayMutation;
211
212        const links: Record<number, FlowGraphNode> = Object.create(/*o*/ null); // eslint-disable-line no-null/no-null
213        const nodes: FlowGraphNode[] = [];
214        const edges: FlowGraphEdge[] = [];
215        const root = buildGraphNode(flowNode, new Set());
216        for (const node of nodes) {
217            node.text = renderFlowNode(node.flowNode, node.circular);
218            computeLevel(node);
219        }
220
221        const height = computeHeight(root);
222        const columnWidths = computeColumnWidths(height);
223        computeLanes(root, 0);
224        return renderGraph();
225
226        function isFlowSwitchClause(f: FlowNode): f is FlowSwitchClause {
227            return !!(f.flags & FlowFlags.SwitchClause);
228        }
229
230        function hasAntecedents(f: FlowNode): f is FlowLabel & { antecedents: FlowNode[] } {
231            return !!(f.flags & FlowFlags.Label) && !!(f as FlowLabel).antecedents;
232        }
233
234        function hasAntecedent(f: FlowNode): f is Extract<FlowNode, { antecedent: FlowNode }> {
235            return !!(f.flags & hasAntecedentFlags);
236        }
237
238        function hasNode(f: FlowNode): f is Extract<FlowNode, { node?: Node }> {
239            return !!(f.flags & hasNodeFlags);
240        }
241
242        function getChildren(node: FlowGraphNode) {
243            const children: FlowGraphNode[] = [];
244            for (const edge of node.edges) {
245                if (edge.source === node) {
246                    children.push(edge.target);
247                }
248            }
249            return children;
250        }
251
252        function getParents(node: FlowGraphNode) {
253            const parents: FlowGraphNode[] = [];
254            for (const edge of node.edges) {
255                if (edge.target === node) {
256                    parents.push(edge.source);
257                }
258            }
259            return parents;
260        }
261
262        function buildGraphNode(flowNode: FlowNode, seen: Set<FlowNode>): FlowGraphNode {
263            const id = getDebugFlowNodeId(flowNode);
264            let graphNode = links[id];
265            if (graphNode && seen.has(flowNode)) {
266                graphNode.circular = true;
267                graphNode = {
268                    id: -1,
269                    flowNode,
270                    edges: [],
271                    text: "",
272                    lane: -1,
273                    endLane: -1,
274                    level: -1,
275                    circular: "circularity"
276                };
277                nodes.push(graphNode);
278                return graphNode;
279            }
280            seen.add(flowNode);
281            if (!graphNode) {
282                links[id] = graphNode = { id, flowNode, edges: [], text: "", lane: -1, endLane: -1, level: -1, circular: false };
283                nodes.push(graphNode);
284                if (hasAntecedents(flowNode)) {
285                    for (const antecedent of flowNode.antecedents) {
286                        buildGraphEdge(graphNode, antecedent, seen);
287                    }
288                }
289                else if (hasAntecedent(flowNode)) {
290                    buildGraphEdge(graphNode, flowNode.antecedent, seen);
291                }
292            }
293            seen.delete(flowNode);
294            return graphNode;
295        }
296
297        function buildGraphEdge(source: FlowGraphNode, antecedent: FlowNode, seen: Set<FlowNode>) {
298            const target = buildGraphNode(antecedent, seen);
299            const edge: FlowGraphEdge = { source, target };
300            edges.push(edge);
301            source.edges.push(edge);
302            target.edges.push(edge);
303        }
304
305        function computeLevel(node: FlowGraphNode): number {
306            if (node.level !== -1) {
307                return node.level;
308            }
309            let level = 0;
310            for (const parent of getParents(node)) {
311                level = Math.max(level, computeLevel(parent) + 1);
312            }
313            return node.level = level;
314        }
315
316        function computeHeight(node: FlowGraphNode): number {
317            let height = 0;
318            for (const child of getChildren(node)) {
319                height = Math.max(height, computeHeight(child));
320            }
321            return height + 1;
322        }
323
324        function computeColumnWidths(height: number) {
325            const columns: number[] = fill(Array(height), 0);
326            for (const node of nodes) {
327                columns[node.level] = Math.max(columns[node.level], node.text.length);
328            }
329            return columns;
330        }
331
332        function computeLanes(node: FlowGraphNode, lane: number) {
333            if (node.lane === -1) {
334                node.lane = lane;
335                node.endLane = lane;
336                const children = getChildren(node);
337                for (let i = 0; i < children.length; i++) {
338                    if (i > 0) lane++;
339                    const child = children[i];
340                    computeLanes(child, lane);
341                    if (child.endLane > node.endLane) {
342                        lane = child.endLane;
343                    }
344                }
345                node.endLane = lane;
346            }
347        }
348
349        function getHeader(flags: FlowFlags) {
350            if (flags & FlowFlags.Start) return "Start";
351            if (flags & FlowFlags.BranchLabel) return "Branch";
352            if (flags & FlowFlags.LoopLabel) return "Loop";
353            if (flags & FlowFlags.Assignment) return "Assignment";
354            if (flags & FlowFlags.TrueCondition) return "True";
355            if (flags & FlowFlags.FalseCondition) return "False";
356            if (flags & FlowFlags.SwitchClause) return "SwitchClause";
357            if (flags & FlowFlags.ArrayMutation) return "ArrayMutation";
358            if (flags & FlowFlags.Call) return "Call";
359            if (flags & FlowFlags.ReduceLabel) return "ReduceLabel";
360            if (flags & FlowFlags.Unreachable) return "Unreachable";
361            throw new Error();
362        }
363
364        function getNodeText(node: Node) {
365            const sourceFile = getSourceFileOfNode(node);
366            return getSourceTextOfNodeFromSourceFile(sourceFile, node, /*includeTrivia*/ false);
367        }
368
369        function renderFlowNode(flowNode: FlowNode, circular: boolean | "circularity") {
370            let text = getHeader(flowNode.flags);
371            if (circular) {
372                text = `${text}#${getDebugFlowNodeId(flowNode)}`;
373            }
374            if (hasNode(flowNode)) {
375                if (flowNode.node) {
376                    text += ` (${getNodeText(flowNode.node)})`;
377                }
378            }
379            else if (isFlowSwitchClause(flowNode)) {
380                const clauses: string[] = [];
381                for (let i = flowNode.clauseStart; i < flowNode.clauseEnd; i++) {
382                    const clause = flowNode.switchStatement.caseBlock.clauses[i];
383                    if (isDefaultClause(clause)) {
384                        clauses.push("default");
385                    }
386                    else {
387                        clauses.push(getNodeText(clause.expression));
388                    }
389                }
390                text += ` (${clauses.join(", ")})`;
391            }
392            return circular === "circularity" ? `Circular(${text})` : text;
393        }
394
395        function renderGraph() {
396            const columnCount = columnWidths.length;
397            const laneCount = nodes.reduce((x, n) => Math.max(x, n.lane), 0) + 1;
398            const lanes: string[] = fill(Array(laneCount), "");
399            const grid: (FlowGraphNode | undefined)[][] = columnWidths.map(() => Array(laneCount));
400            const connectors: Connection[][] = columnWidths.map(() => fill(Array(laneCount), 0));
401
402            // build connectors
403            for (const node of nodes) {
404                grid[node.level][node.lane] = node;
405                const children = getChildren(node);
406                for (let i = 0; i < children.length; i++) {
407                    const child = children[i];
408                    let connector: Connection = Connection.Right;
409                    if (child.lane === node.lane) connector |= Connection.Left;
410                    if (i > 0) connector |= Connection.Up;
411                    if (i < children.length - 1) connector |= Connection.Down;
412                    connectors[node.level][child.lane] |= connector;
413                }
414                if (children.length === 0) {
415                    connectors[node.level][node.lane] |= Connection.NoChildren;
416                }
417                const parents = getParents(node);
418                for (let i = 0; i < parents.length; i++) {
419                    const parent = parents[i];
420                    let connector: Connection = Connection.Left;
421                    if (i > 0) connector |= Connection.Up;
422                    if (i < parents.length - 1) connector |= Connection.Down;
423                    connectors[node.level - 1][parent.lane] |= connector;
424                }
425            }
426
427            // fill in missing connectors
428            for (let column = 0; column < columnCount; column++) {
429                for (let lane = 0; lane < laneCount; lane++) {
430                    const left = column > 0 ? connectors[column - 1][lane] : 0;
431                    const above = lane > 0 ? connectors[column][lane - 1] : 0;
432                    let connector = connectors[column][lane];
433                    if (!connector) {
434                        if (left & Connection.Right) connector |= Connection.LeftRight;
435                        if (above & Connection.Down) connector |= Connection.UpDown;
436                        connectors[column][lane] = connector;
437                    }
438                }
439            }
440
441            for (let column = 0; column < columnCount; column++) {
442                for (let lane = 0; lane < lanes.length; lane++) {
443                    const connector = connectors[column][lane];
444                    const fill = connector & Connection.Left ? BoxCharacter.lr : " ";
445                    const node = grid[column][lane];
446                    if (!node) {
447                        if (column < columnCount - 1) {
448                            writeLane(lane, repeat(fill, columnWidths[column] + 1));
449                        }
450                    }
451                    else {
452                        writeLane(lane, node.text);
453                        if (column < columnCount - 1) {
454                            writeLane(lane, " ");
455                            writeLane(lane, repeat(fill, columnWidths[column] - node.text.length));
456                        }
457                    }
458                    writeLane(lane, getBoxCharacter(connector));
459                    writeLane(lane, connector & Connection.Right && column < columnCount - 1 && !grid[column + 1][lane] ? BoxCharacter.lr : " ");
460                }
461            }
462
463            return `\n${lanes.join("\n")}\n`;
464
465            function writeLane(lane: number, text: string) {
466                lanes[lane] += text;
467            }
468        }
469
470        function getBoxCharacter(connector: Connection) {
471            switch (connector) {
472                case Connection.UpDown: return BoxCharacter.ud;
473                case Connection.LeftRight: return BoxCharacter.lr;
474                case Connection.UpLeft: return BoxCharacter.ul;
475                case Connection.UpRight: return BoxCharacter.ur;
476                case Connection.DownLeft: return BoxCharacter.dl;
477                case Connection.DownRight: return BoxCharacter.dr;
478                case Connection.UpDownLeft: return BoxCharacter.udl;
479                case Connection.UpDownRight: return BoxCharacter.udr;
480                case Connection.UpLeftRight: return BoxCharacter.ulr;
481                case Connection.DownLeftRight: return BoxCharacter.dlr;
482                case Connection.UpDownLeftRight: return BoxCharacter.udlr;
483            }
484            return " ";
485        }
486
487        function fill<T>(array: T[], value: T) {
488            if (array.fill) {
489                array.fill(value);
490            }
491            else {
492                for (let i = 0; i < array.length; i++) {
493                    array[i] = value;
494                }
495            }
496            return array;
497        }
498
499        function repeat(ch: string, length: number) {
500            if (ch.repeat) {
501                return length > 0 ? ch.repeat(length) : "";
502            }
503            let s = "";
504            while (s.length < length) {
505                s += ch;
506            }
507            return s;
508        }
509    }
510
511    // Export as a module. NOTE: Can't use module exports as this is built using --outFile
512    declare const module: { exports: {} };
513    if (typeof module !== "undefined" && module.exports) {
514        module.exports = Debug;
515    }
516}