UNPKG

playground-code-show-flow

Version:

Visualize TypeScript's code inference flow nodes

473 lines (463 loc) 20.5 kB
define(function () { 'use strict'; /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } // These come from https://github.com/dsherret/ts-ast-viewer var TreeMode; (function (TreeMode) { TreeMode[TreeMode["forEachChild"] = 0] = "forEachChild"; TreeMode[TreeMode["getChildren"] = 1] = "getChildren"; })(TreeMode || (TreeMode = {})); function getNodeAtPosition(ts, sourceFile, position) { let current = sourceFile; const getContainingChild = (child) => { if (child.pos <= position && (position < child.end || (position === child.end && (child.kind === ts.SyntaxKind.EndOfFileToken)))) { return child; } }; while (true) { const child = ts.forEachChild(current, getContainingChild); if (!child) { return current; } current = child; } } let FlowFlags; let getSourceFileOfNode; let getSourceTextOfNodeFromSourceFile; let isDefaultClause; function graphInit(ts) { FlowFlags = ts.FlowFlags; getSourceFileOfNode = ts.getSourceFileOfNode; getSourceTextOfNodeFromSourceFile = ts.getSourceTextOfNodeFromSourceFile; isDefaultClause = ts.isDefaultClause; } let nextDebugFlowId = -1; function getDebugFlowNodeId(f) { if (!f.id) { f.id = nextDebugFlowId; nextDebugFlowId--; } return f.id; } function formatControlFlowGraph(flowNode) { const hasAntecedentFlags = FlowFlags.Assignment | FlowFlags.Condition | FlowFlags.SwitchClause | FlowFlags.ArrayMutation | FlowFlags.Call | FlowFlags.ReduceLabel; const hasNodeFlags = FlowFlags.Start | FlowFlags.Assignment | FlowFlags.Call | FlowFlags.Condition | FlowFlags.ArrayMutation; const links = Object.create(/*o*/ null); // eslint-disable-line no-null/no-null const nodes = []; const root = buildGraphNode(flowNode, new Set()); for (const node of nodes) { node.text = renderFlowNode(node.flowNode, node.circular); computeLevel(node); } const height = computeHeight(root); const columnWidths = computeColumnWidths(height); computeLanes(root, 0); return { text: renderGraph(), root, getChildren, hasNode, getNodeText }; function isFlowSwitchClause(f) { return !!(f.flags & FlowFlags.SwitchClause); } function hasAntecedents(f) { return !!(f.flags & FlowFlags.Label) && !!f.antecedents; } function hasAntecedent(f) { return !!(f.flags & hasAntecedentFlags); } function hasNode(f) { return !!(f.flags & hasNodeFlags); } function getChildren(node) { const children = []; for (const edge of node.edges) { if (edge.source === node) { children.push(edge.target); } } return children; } function getParents(node) { const parents = []; for (const edge of node.edges) { if (edge.target === node) { parents.push(edge.source); } } return parents; } function buildGraphNode(flowNode, seen) { const id = getDebugFlowNodeId(flowNode); let graphNode = links[id]; if (graphNode && seen.has(flowNode)) { graphNode.circular = true; graphNode = { id: -1, flowNode, edges: [], text: "", lane: -1, endLane: -1, level: -1, circular: "circularity" }; nodes.push(graphNode); return graphNode; } seen.add(flowNode); if (!graphNode) { links[id] = graphNode = { id, flowNode, edges: [], text: "", lane: -1, endLane: -1, level: -1, circular: false }; nodes.push(graphNode); if (hasAntecedents(flowNode)) { for (const antecedent of flowNode.antecedents) { buildGraphEdge(graphNode, antecedent, seen); } } else if (hasAntecedent(flowNode)) { buildGraphEdge(graphNode, flowNode.antecedent, seen); } } seen.delete(flowNode); return graphNode; } function buildGraphEdge(source, antecedent, seen) { const target = buildGraphNode(antecedent, seen); const edge = { source, target }; source.edges.push(edge); target.edges.push(edge); } function computeLevel(node) { if (node.level !== -1) { return node.level; } let level = 0; for (const parent of getParents(node)) { level = Math.max(level, computeLevel(parent) + 1); } return node.level = level; } function computeHeight(node) { let height = 0; for (const child of getChildren(node)) { height = Math.max(height, computeHeight(child)); } return height + 1; } function computeColumnWidths(height) { const columns = fill(Array(height), 0); for (const node of nodes) { columns[node.level] = Math.max(columns[node.level], node.text.length); } return columns; } function computeLanes(node, lane) { if (node.lane === -1) { node.lane = lane; node.endLane = lane; const children = getChildren(node); for (let i = 0; i < children.length; i++) { if (i > 0) lane++; const child = children[i]; computeLanes(child, lane); if (child.endLane > node.endLane) { lane = child.endLane; } } node.endLane = lane; } } function getHeader(flags) { if (flags & FlowFlags.Start) return "Start"; if (flags & FlowFlags.BranchLabel) return "Branch"; if (flags & FlowFlags.LoopLabel) return "Loop"; if (flags & FlowFlags.Assignment) return "Assignment"; if (flags & FlowFlags.TrueCondition) return "True"; if (flags & FlowFlags.FalseCondition) return "False"; if (flags & FlowFlags.SwitchClause) return "SwitchClause"; if (flags & FlowFlags.ArrayMutation) return "ArrayMutation"; if (flags & FlowFlags.Call) return "Call"; if (flags & FlowFlags.ReduceLabel) return "ReduceLabel"; if (flags & FlowFlags.Unreachable) return "Unreachable"; throw new Error(); } function getNodeText(node) { const sourceFile = getSourceFileOfNode(node); return getSourceTextOfNodeFromSourceFile(sourceFile, node, /*includeTrivia*/ false); } function renderFlowNode(flowNode, circular) { let text = getHeader(flowNode.flags); if (circular) { text = `${text}#${getDebugFlowNodeId(flowNode)}`; } if (hasNode(flowNode)) { if (flowNode.node) { text += ` (${getNodeText(flowNode.node)})`; } } else if (isFlowSwitchClause(flowNode)) { const clauses = []; for (let i = flowNode.clauseStart; i < flowNode.clauseEnd; i++) { const clause = flowNode.switchStatement.caseBlock.clauses[i]; if (isDefaultClause(clause)) { clauses.push("default"); } else { clauses.push(getNodeText(clause.expression)); } } text += ` (${clauses.join(", ")})`; } return circular === "circularity" ? `Circular(${text})` : text; } function renderGraph() { const columnCount = columnWidths.length; const laneCount = nodes.reduce((x, n) => Math.max(x, n.lane), 0) + 1; const lanes = fill(Array(laneCount), ""); const grid = columnWidths.map(() => Array(laneCount)); const connectors = columnWidths.map(() => fill(Array(laneCount), 0)); // build connectors for (const node of nodes) { grid[node.level][node.lane] = node; const children = getChildren(node); for (let i = 0; i < children.length; i++) { const child = children[i]; let connector = 8 /* Right */; if (child.lane === node.lane) connector |= 4 /* Left */; if (i > 0) connector |= 1 /* Up */; if (i < children.length - 1) connector |= 2 /* Down */; connectors[node.level][child.lane] |= connector; } if (children.length === 0) { connectors[node.level][node.lane] |= 16 /* NoChildren */; } const parents = getParents(node); for (let i = 0; i < parents.length; i++) { const parent = parents[i]; let connector = 4 /* Left */; if (i > 0) connector |= 1 /* Up */; if (i < parents.length - 1) connector |= 2 /* Down */; connectors[node.level - 1][parent.lane] |= connector; } } // fill in missing connectors for (let column = 0; column < columnCount; column++) { for (let lane = 0; lane < laneCount; lane++) { const left = column > 0 ? connectors[column - 1][lane] : 0; const above = lane > 0 ? connectors[column][lane - 1] : 0; let connector = connectors[column][lane]; if (!connector) { if (left & 8 /* Right */) connector |= 12 /* LeftRight */; if (above & 2 /* Down */) connector |= 3 /* UpDown */; connectors[column][lane] = connector; } } } for (let column = 0; column < columnCount; column++) { for (let lane = 0; lane < lanes.length; lane++) { const connector = connectors[column][lane]; const fill = connector & 4 /* Left */ ? "\u2500" /* lr */ : " "; const node = grid[column][lane]; if (!node) { if (column < columnCount - 1) { writeLane(lane, repeat(fill, columnWidths[column] + 1)); } } else { writeLane(lane, node.text); if (column < columnCount - 1) { writeLane(lane, " "); writeLane(lane, repeat(fill, columnWidths[column] - node.text.length)); } } writeLane(lane, getBoxCharacter(connector)); writeLane(lane, connector & 8 /* Right */ && column < columnCount - 1 && !grid[column + 1][lane] ? "\u2500" /* lr */ : " "); } } return `\n${lanes.join("\n")}\n`; function writeLane(lane, text) { lanes[lane] += text; } } function getBoxCharacter(connector) { switch (connector) { case 3 /* UpDown */: return "\u2502" /* ud */; case 12 /* LeftRight */: return "\u2500" /* lr */; case 5 /* UpLeft */: return "\u256F" /* ul */; case 9 /* UpRight */: return "\u2570" /* ur */; case 6 /* DownLeft */: return "\u256E" /* dl */; case 10 /* DownRight */: return "\u256D" /* dr */; case 7 /* UpDownLeft */: return "\u2524" /* udl */; case 11 /* UpDownRight */: return "\u251C" /* udr */; case 13 /* UpLeftRight */: return "\u2534" /* ulr */; case 14 /* DownLeftRight */: return "\u252C" /* dlr */; case 15 /* UpDownLeftRight */: return "\u256B" /* udlr */; } return " "; } function fill(array, value) { if (array.fill) { array.fill(value); } else { for (let i = 0; i < array.length; i++) { array[i] = value; } } return array; } function repeat(ch, length) { if (ch.repeat) { return length > 0 ? ch.repeat(length) : ""; } let s = ""; while (s.length < length) { s += ch; } return s; } } const makePlugin = (utils) => { let sourceFile; let decorations = []; const customPlugin = { id: "flow", displayName: "Flow", didMount: (sandbox, container) => { const ds = utils.createDesignSystem(container); ds.subtitle("Code Flow Analysis"); ds.p("This plugin will look backwards from where your cursor is placed, to find the flow nodes which affected its types."); const graph = document.createElement("div"); container.appendChild(graph); const infoDS = utils.createDesignSystem(graph); graphInit(sandbox.ts); sandbox.editor.onDidChangeCursorPosition(event => { const ts = sandbox.ts; const pos = event.position; infoDS.clear(); // ts.get const tspos = ts.getPositionOfLineAndCharacter(sourceFile, pos.lineNumber - 1, pos.column); const highlightedASTNode = getNodeAtPosition(sandbox.ts, sourceFile, tspos - 1); const model = sandbox.getModel(); if ("flowNode" in highlightedASTNode) { const flowNode = highlightedASTNode["flowNode"]; const node = highlightedASTNode; const flowGraph = formatControlFlowGraph(flowNode); const s = node.getStart(sourceFile, false); const e = node.getEnd(); const { hasNode, getChildren, getNodeText } = flowGraph; if (s && e) { const start = model.getPositionAt(s); const end = model.getPositionAt(e); const main = { range: new sandbox.monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), options: { inlineClassName: "neutral-highlight" }, }; decorations = sandbox.editor.deltaDecorations(decorations, [main]); } const renderNode = (graphNode) => { const name = graphNode.text.split(" ")[0]; if (name === "Start") return; infoDS.p(name); if (hasNode(graphNode.flowNode)) { if (graphNode.flowNode.node) { const astNode = graphNode.flowNode.node; const diagForAST = { category: 0, start: astNode.getStart(sourceFile), code: 1, file: sourceFile, length: astNode.getFullWidth(), messageText: getNodeText(astNode), }; infoDS.listDiags(model, [diagForAST]); } } const children = getChildren(graphNode); children.forEach((element) => { renderNode(element); }); }; infoDS.title("Flow Graph"); infoDS.subtitle("Starting backwards from:"); infoDS.createASTTree(node, { closedByDefault: true }); renderNode(flowGraph.root); graph.appendChild(document.createElement("hr")); infoDS.subtitle("Text representation"); infoDS.code(flowGraph.text); } else { if (highlightedASTNode) { infoDS.p(`No flow node found in '${highlightedASTNode.getText()}'`); } else { infoDS.p(`No TS AST node found`); } } }); }, // This is called occasionally as text changes in monaco, // it does not directly map 1 keyup to once run of the function // because it is intentionally called at most once every 0.3 seconds // and then will always run at the end. modelChangedDebounce: (sandbox, _model) => __awaiter(void 0, void 0, void 0, function* () { sourceFile = yield sandbox.getAST(); }), // Gives you a chance to remove anything set up, // the container itself if wiped of children after this. }; return customPlugin; }; return makePlugin; });