packages/sqrl-cli/src/cli/generateDot.ts (164 lines of code) (raw):
import { isValidFeatureName, Context, CompiledExecutable } from "sqrl";
import { DefaultDict } from "../jslib/DefaultDict";
interface Options {
includeCost?: boolean;
reverseEdges?: boolean;
clusterRules?: boolean;
clusterFeatures?: boolean;
fillColor?: string;
includeSlots?: boolean;
}
interface FeatureEdges {
[feature: string]: Set<string>;
}
interface FeatureNode {
name: string;
feature: boolean;
rule: boolean;
cost: number;
recursiveCost: number;
}
export function generateDotFile(
ctx: Context,
compiled: CompiledExecutable,
features: string[] = [],
depends: string[] = [],
ignore: string[] = [],
options: Options = {
clusterFeatures: false,
clusterRules: false,
includeCost: false,
reverseEdges: false,
fillColor: "",
includeSlots: false,
}
) {
const {
includeCost,
includeSlots,
reverseEdges,
clusterRules,
clusterFeatures,
} = options;
const ignoreSet = new Set(ignore);
const nodes: {
[name: string]: FeatureNode;
} = {};
const forward: FeatureEdges = new DefaultDict(() => new Set());
const reverse: FeatureEdges = new DefaultDict(() => new Set());
// First add all the slots and dependencies
const slotNames = compiled.getSlotNames();
const slotLoad = compiled.getSlotLoad();
slotNames.forEach((slotName, idx) => {
slotLoad[idx].forEach((loadIdx) => {
const feature = slotNames[loadIdx];
forward[slotName].add(feature);
reverse[feature].add(slotName);
});
nodes[slotName] = {
name: slotName,
feature: false,
rule: false,
cost: 0,
recursiveCost: 0,
};
});
// Add proper nodes for rules/features
Object.entries(compiled.getFeatureDocs()).forEach(([name, doc]) => {
nodes[name] = {
name,
feature: true,
rule: false,
cost: doc.cost,
recursiveCost: doc.recursiveCost,
};
});
Object.entries(compiled.getRuleSpecs()).forEach(([name, spec]) => {
nodes[name].rule = true;
});
function recurse(
name: string,
edges: FeatureEdges,
recurseSeen: Set<string>
) {
if (ignoreSet.has(name)) {
return;
} else if (!nodes.hasOwnProperty(name)) {
// This is caused by everything that isn't a feature (SmyteWhenStatements/etc.)
ctx.warn({}, "Missing node for dot file:: %s", name);
return;
}
recurseSeen.add(name);
edges[name].forEach((feature) => recurse(feature, edges, recurseSeen));
}
const seen: Set<string> = new Set();
const seenDepends: Set<string> = new Set();
for (const feature of features) {
recurse(feature, forward, seen);
}
for (const feature of depends) {
recurse(feature, reverse, seenDepends);
}
const render = new Set([...seen, ...seenDepends]);
let rv = `digraph features {\n`;
const renderNodes = Array.from(render)
.sort()
.filter((name) => !ignoreSet.has(name))
.map((name) => nodes[name]);
/*
const maxCost = Math.max(...renderNodes.map(node => node.recursiveCost));
const minCost = Math.min(...renderNodes.map(node => node.recursiveCost));
*/
function quoteName(name: string) {
return isValidFeatureName(name) ? name : JSON.stringify(name);
}
function nodesToDot(
filteredNodes: FeatureNode[],
color: string,
subgraph: string
) {
let nodesDot = filteredNodes
.map((node) => {
let label = node.name;
if (!includeSlots && !isValidFeatureName(node.name)) {
return `${quoteName(
node.name
)} [label="" style="filled" color="#aaaaaa" fillcolor="#eeeeee"];\n`;
}
if (includeCost) {
label += "\n$" + node.recursiveCost.toFixed(2);
label = label.trim();
}
const fill = "#ffffff";
const style = "";
/* @todo: Requires `color` module
if (fillColor === "cost" && maxCost > 0) {
// Go from red=>green on HSV scale
const v = (node.recursiveCost - minCost) / (maxCost - minCost);
style = "filled";
fill = Color.hsv(120 * (1 - v), 100, 100).hex();
} */
return `${quoteName(node.name)} [label=${JSON.stringify(
label
)} color="${color}" style=${JSON.stringify(
style
)} fillcolor=${JSON.stringify(fill)}];\n`;
})
.join("");
if (subgraph) {
nodesDot = `subgraph ${subgraph} {\n${nodesDot}}\n`;
}
return nodesDot;
}
rv += nodesToDot(
renderNodes.filter((node) => node.rule),
"red",
clusterRules ? "cluster_rules" : null
);
rv += nodesToDot(
renderNodes.filter((node) => !node.rule),
"blue",
clusterFeatures ? "cluster_features" : null
);
renderNodes.forEach((node) => {
Array.from(forward[node.name])
.sort()
.forEach((feature) => {
if (render.has(feature) && !ignoreSet.has(feature)) {
if (reverseEdges) {
rv += `${quoteName(node.name)} -> ${quoteName(feature)};\n`;
} else {
rv += `${quoteName(feature)} -> ${quoteName(node.name)};\n`;
}
}
});
});
rv += `}\n`;
return rv;
}