playground-code-show-flow
Version:
Visualize TypeScript's code inference flow nodes
473 lines (463 loc) • 20.5 kB
JavaScript
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;
});