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}