UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

309 lines 16.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DataflowMermaid = void 0; exports.mermaidNodeBrackets = mermaidNodeBrackets; exports.printIdentifier = printIdentifier; exports.diffGraphsToMermaid = diffGraphsToMermaid; exports.diffGraphsToMermaidUrl = diffGraphsToMermaidUrl; const mermaid_1 = require("./mermaid"); const graph_1 = require("../../dataflow/graph/graph"); const node_id_1 = require("../../r-bridge/lang-4.x/ast/model/processing/node-id"); const identifier_1 = require("../../dataflow/environments/identifier"); const r_function_call_1 = require("../../r-bridge/lang-4.x/ast/model/nodes/r-function-call"); const edge_1 = require("../../dataflow/graph/edge"); const vertex_1 = require("../../dataflow/graph/vertex"); const type_1 = require("../../r-bridge/lang-4.x/ast/model/type"); const info_1 = require("./info"); const range_1 = require("../range"); const model_1 = require("../../r-bridge/lang-4.x/ast/model/model"); const info_2 = require("../../dataflow/info"); const df_helper_1 = require("../../dataflow/graph/df-helper"); function subflowToMermaid(nodeId, subflow, mermaid, idPrefix = '') { if (subflow === undefined) { return; } const id = mermaid_1.Mermaid.escapeId(nodeId); const subflowId = mermaid_1.Mermaid.escapeId(`${idPrefix}flow-${nodeId}`); if (mermaid.simplified) { // get parent const idMap = mermaid.rootGraph.idMap; const node = idMap?.get(nodeId); const nodeLexeme = model_1.RNode.lexeme(node) ?? 'function'; const location = node?.location?.[0] ? ` (L. ${node?.location?.[0]})` : ''; mermaid.nodeLines.push(`\nsubgraph "${subflowId}" ["${mermaid_1.Mermaid.escape(nodeLexeme)}${location}"]`); } else { mermaid.nodeLines.push(`\nsubgraph "${subflowId}" [function ${id}]`); } const subgraph = graphToMermaidGraph(subflow.graph, { graph: mermaid.rootGraph, rootGraph: mermaid.rootGraph, idPrefix, includeEnvironments: mermaid.includeEnvironments, mark: mermaid.mark, prefix: null, simplified: mermaid.simplified, qualifyBaseR: mermaid.qualifyBaseR }); mermaid.nodeLines.push(...subgraph.nodeLines); mermaid.edgeLines.push(...subgraph.edgeLines); for (const present of subgraph.presentEdges) { mermaid.presentEdges.add(present); } for (const [color, pool] of [['purple', subflow.in], ['green', subflow.out], ['orange', subflow.unknownReferences]]) { for (const out of pool) { if (!mermaid.mark?.has(out.nodeId)) { // in/out/active for unmarked mermaid.nodeLines.push(` style ${idPrefix}${mermaid_1.Mermaid.escapeId(out.nodeId)} stroke:${color},stroke-width:4px; `); } } } mermaid.nodeLines.push('end'); mermaid.edgeLines.push(`${idPrefix}${id} -.-|function| ${subflowId}\n`); /* mark edge as present */ const edgeId = encodeEdge(idPrefix + id, subflowId, new Set(['function'])); mermaid.presentEdges.add(edgeId); } function printArg(arg) { if (arg === undefined) { return '??'; } else if (arg === r_function_call_1.EmptyArgument) { return '[empty]'; } else if (graph_1.FunctionArgument.isNamed(arg)) { const deps = arg.cds ? ', ' + arg.cds.map(c => c.id + (c.when ? '+' : '-')).join(', ') : ''; return `${arg.name} (${arg.nodeId}${deps})`; } else if (graph_1.FunctionArgument.isPositional(arg)) { const deps = arg.cds ? ' (' + arg.cds.map(c => c.id + (c.when ? '+' : '-')).join(', ') + ')' : ''; return `${arg.nodeId}${deps}`; } else { return '??'; } } function displayFunctionArgMapping(argMapping) { const result = []; for (const arg of argMapping) { result.push(mermaid_1.Mermaid.escape(printArg(arg))); } return result.length === 0 ? '' : `\n arg: (${result.join(', ')})`; } function encodeEdge(from, to, types) { return `${from}->${to}["${Array.from(types).join(':')}"]`; } /** * Renders the (mermaid-escaped) node name with only the *lexeme* -- what the source actually wrote -- in bold. * When the displayed name was extended by package qualification (e.g. the code wrote `acf` but we show * `stats::acf`), the added `stats::` prefix stays non-bold so it is visually distinct from the written token. A * namespace written in the source (`stats::acf` verbatim) is part of the lexeme and is therefore bold as a whole. */ function boldLexeme(lexeme, display) { if (display !== lexeme && display.endsWith(lexeme)) { const addedPrefix = display.slice(0, display.length - lexeme.length); // the qualification we added, e.g. `stats::` return `${mermaid_1.Mermaid.escape(addedPrefix)}**${mermaid_1.Mermaid.escape(lexeme)}**`; } return `**${mermaid_1.Mermaid.escape(display)}**`; } /** * Translates a vertex tag to the corresponding mermaid node brackets. */ function mermaidNodeBrackets(tag) { let open; let close; if (tag === vertex_1.VertexType.FunctionDefinition || tag === vertex_1.VertexType.VariableDefinition) { open = '['; close = ']'; } else if (tag === vertex_1.VertexType.FunctionCall) { open = '[['; close = ']]'; } else if (tag === 'value') { open = '{{'; close = '}}'; } else { open = '(['; close = '])'; } return { open, close }; } /** * Prints an identifier definition in a human-readable format. */ function printIdentifier(id) { return `**${id.name ? identifier_1.Identifier.toString(id.name) : 'undefined'}** (id: ${id.nodeId}, type: ${identifier_1.ReferenceTypeReverseMapping.get(id.type)},${id.cds ? ' cds: {' + id.cds.map(c => c.id + (c.when ? '+' : '-')).join(',') + '},' : ''} def. @${id.definedAt})`; } function environmentLevel(env) { return env === undefined || env.builtInEnv ? 0 : environmentLevel(env.parent) + 1; } function printEnvironmentToLines(env) { if (env === undefined) { return ['??']; } else if (env.builtInEnv) { return ['Built-in']; } const lines = [...printEnvironmentToLines(env.parent), `${environmentLevel(env)}${'-'.repeat(40)}`]; const longestName = Math.max(...[...env.memory.keys()].map(x => x.length)); for (const [name, defs] of env.memory.entries()) { const printName = `${name}:`; lines.push(` ${printName.padEnd(longestName + 1, ' ')} {${defs.map(printIdentifier).join(', ')}}`); } return lines; } /** label a built-in node: a package export shows as `pkg::fn` (`built-in:stats:acf` becomes `stats::acf`), everything else as its bare name */ function builtInDisplayName(builtInId) { const pkgFn = node_id_1.NodeId.toPkgFn(builtInId); return pkgFn ? `${pkgFn[0]}::${pkgFn[1]}` : String(builtInId).replace('built-in:', ''); } function vertexToMermaid(info, mermaid, id, idPrefix, mark, includeOnlyIds) { const fCall = vertex_1.FunctionCallVertex.is(info); const { open, close } = mermaidNodeBrackets(info.tag); const origId = id; id = mermaid_1.Mermaid.escapeId(id); // a vertex with a built-in id (e.g. a base-R export attached via `linkBaseR`) has no AST node, so it would // otherwise render as a bogus `?? *??-??* (id: built-in:...)` box; draw it as the gray Built-In placeholder // instead -- the same node an edge to it produces, which also dedups the two renderings if (node_id_1.NodeId.isBuiltIn(origId)) { if (!mermaid.presentVertices.has(id)) { mermaid.nodeLines.push(` ${idPrefix}${id}["\`Built-In:\n${mermaid_1.Mermaid.escape(builtInDisplayName(origId))}\`"]`); mermaid.nodeLines.push(` style ${idPrefix}${id} stroke:gray,fill:gray,stroke-width:2px,opacity:.8;`); mermaid.presentVertices.add(id); } return; } if (info.environment && mermaid.includeEnvironments) { if (info.environment.level > 0 || info.environment.current.memory.size !== 0) { mermaid.nodeLines.push(` %% Environment of ${id} [level: ${info.environment.level}]:`, printEnvironmentToLines(info.environment.current).map(x => ` %% ${x}`).join('\n')); } } const node = mermaid.rootGraph.idMap?.get(info.id); const lexeme = node?.lexeme ?? (node?.type === type_1.RType.ExpressionList ? node?.grouping?.[0]?.lexeme : '') ?? '??'; let display = lexeme; if (fCall && vertex_1.FunctionCallVertex.is(info)) { const q = identifier_1.Identifier.toQualified(df_helper_1.Dataflow.origin(mermaid.rootGraph, origId), info.name, mermaid.qualifyBaseR !== false); const qs = q !== undefined ? identifier_1.Identifier.toString(q) : undefined; if (qs !== undefined && qs !== lexeme) { display = qs; } } if (mermaid.simplified) { const location = node?.location?.[0] ? ` (L. ${node?.location?.[0]})` : ''; const escapedName = (node ? boldLexeme(lexeme, display) : '**??**') + location + (node ? `\n*${node.type}*` : ''); mermaid.nodeLines.push(` ${idPrefix}${id}${open}"\`${escapedName}\`"${close}`); } else { const escapedName = node ? `*${mermaid_1.Mermaid.escape(`[${node.type}]`)}* ${boldLexeme(lexeme, display)}` : '??'; const deps = info.cds ? ', ' + info.cds.map(c => mermaid_1.Mermaid.escapeId(c.id) + (c.when ? '+' : '-')).join(', ') : ''; const lnks = info.link?.origin ? ', links: ' + info.link.origin.map(o => mermaid_1.Mermaid.escapeId(o)).join(', ') : ''; const source = vertex_1.VariableDefinitionVertex.is(info) ? info.source : undefined; const sources = source ? ', v: ' + source.map(s => mermaid_1.Mermaid.escapeId(s)).join(', ') : ''; const n = node?.info.fullRange ?? node?.location ?? (node?.type === type_1.RType.ExpressionList ? node?.grouping?.[0].location : undefined); mermaid.nodeLines.push(` ${idPrefix}${id}${open}"\`${escapedName}\n *${range_1.SourceRange.format(n)}* (**id: ${id}**${deps}${lnks}${sources})${fCall ? displayFunctionArgMapping(info.args) : '' + (vertex_1.FunctionDefinitionVertex.is(info) && info.mode && info.mode.length > 0 ? mermaid_1.Mermaid.escape(JSON.stringify(info.mode)) : '')}\`"${close}`); } if (mark?.has(id)) { mermaid.nodeLines.push(` style ${idPrefix}${id} ${mermaid.markStyle.vertex} `); } if (mermaid.rootGraph.unknownSideEffects.values().some(l => node_id_1.NodeId.normalize(l) === node_id_1.NodeId.normalize(origId))) { mermaid.nodeLines.push(` style ${idPrefix}${id} stroke:red,stroke-width:5px; `); } if (vertex_1.FunctionDefinitionVertex.is(info)) { subflowToMermaid(origId, info.subflow, mermaid, idPrefix); } const edges = mermaid.rootGraph.outgoingEdges(node_id_1.NodeId.normalize(origId)); if (edges === undefined) { mermaid.nodeLines.push(' %% No edges found for ' + id); return; } const artificialCdEdges = (info.cds ?? []).map(x => [x.id, { types: new Set([x.when ? 'CD-True' : 'CD-False']), file: x.file }]); // eslint-disable-next-line prefer-const for (let [target, edge] of [...edges, ...artificialCdEdges]) { if (includeOnlyIds && !includeOnlyIds.has(target)) { continue; } const originalTarget = target; target = mermaid_1.Mermaid.escapeId(target); const edgeTypes = typeof edge.types == 'number' ? new Set(edge_1.DfEdge.splitTypes(edge)) : edge.types; const edgeId = encodeEdge(idPrefix + id, idPrefix + target, edgeTypes); if (!mermaid.presentEdges.has(edgeId)) { mermaid.presentEdges.add(edgeId); const style = node_id_1.NodeId.isBuiltIn(target) ? '-.->' : '-->'; mermaid.edgeLines.push(` ${idPrefix}${id} ${style}|"${[...edgeTypes].map(e => typeof e === 'number' ? edge_1.DfEdge.typeToName(e) : e).join(', ')}${'file' in edge && edge.file ? `, from: ${mermaid_1.Mermaid.escape(String(edge.file))}` : ''}"| ${idPrefix}${target}`); if (mermaid.mark?.has(id + '->' + target)) { // who invented this syntax?! mermaid.edgeLines.push(` linkStyle ${mermaid.presentEdges.size - 1} ${mermaid.markStyle.edge}`); } if (edgeTypes.has('CD-True') || edgeTypes.has('CD-False')) { mermaid.edgeLines.push(` linkStyle ${mermaid.presentEdges.size - 1} stroke:gray,color:gray;`); } if (node_id_1.NodeId.isBuiltIn(target)) { mermaid.edgeLines.push(` linkStyle ${mermaid.presentEdges.size - 1} stroke:gray;`); if (!mermaid.presentVertices.has(target)) { mermaid.nodeLines.push(` ${idPrefix}${target}["\`Built-In:\n${mermaid_1.Mermaid.escape(builtInDisplayName(originalTarget))}\`"]`); mermaid.nodeLines.push(` style ${idPrefix}${target} stroke:gray,fill:gray,stroke-width:2px,opacity:.8;`); mermaid.presentVertices.add(target); } } } } } // make the passing of root ids more performant again function graphToMermaidGraph(rootIds, { simplified, graph, prefix = 'flowchart BT', idPrefix = '', includeEnvironments = !simplified, mark, rootGraph, presentEdges = new Set(), markStyle = info_1.MermaidDefaultMarkStyle, includeOnlyIds, qualifyBaseR = true }) { const mermaid = { nodeLines: prefix === null ? [] : [prefix], edgeLines: [], presentEdges, presentVertices: new Set(), mark, rootGraph: rootGraph ?? graph, includeEnvironments, markStyle, simplified, qualifyBaseR }; for (const [id, info] of graph.vertices(true)) { if (rootIds.has(id)) { vertexToMermaid(info, mermaid, id, idPrefix, mark, includeOnlyIds); } } return mermaid; } /** uses same id map but ensures, it is different from the rhs so that mermaid can work with that */ function diffGraphsToMermaid(left, right, prefix) { // we add the prefix ourselves const { string: leftGraph, mermaid } = exports.DataflowMermaid.convert({ graph: left.graph, prefix: '', idPrefix: `l-${left.label}`, includeEnvironments: true, mark: left.mark }); const { string: rightGraph } = exports.DataflowMermaid.convert({ graph: right.graph, prefix: '', idPrefix: `r-${right.label}`, includeEnvironments: true, mark: right.mark, presentEdges: mermaid.presentEdges }); return `${prefix}flowchart BT\nsubgraph "${left.label}"\n${leftGraph}\nend\nsubgraph "${right.label}"\n${rightGraph}\nend`; } /** * Converts two dataflow graphs to a mermaid url that visualizes their differences. */ function diffGraphsToMermaidUrl(left, right, prefix) { return mermaid_1.Mermaid.codeToUrl(diffGraphsToMermaid(left, right, prefix)); } /** * The helper object for all things regarding the mermaid based visualization of dataflow graphs! */ exports.DataflowMermaid = { name: 'DataflowMermaid', /** * Converts a dataflow graph to mermaid graph code that visualizes the graph. * @see {@link DataflowMermaid.url} - render the given graph to a url to mermaid.live */ convert(config) { const mermaid = graphToMermaidGraph(config.includeOnlyIds ?? config.graph.rootIds(), config); return { string: `${mermaid.nodeLines.join('\n')}\n${mermaid.edgeLines.join('\n')}`, mermaid }; }, /** * This is a simplified version of {@link DataflowMermaid.convert} */ raw(graph, includeEnvironments, mark, simplified = false, qualifyBaseR = true) { graph = info_2.DataflowInformation.is(graph) ? graph.graph : graph; return exports.DataflowMermaid.convert({ graph, includeEnvironments, mark, simplified, qualifyBaseR }).string; }, /** * Converts a dataflow graph to a mermaid url that visualizes the graph. * This is basically a combination of {@link DataflowMermaid.raw} and {@link Mermaid.codeToUrl}. * @param graph - the dataflow graph to render * @param includeEnvironments - whether to include the environment content in the output * @param mark - which vertices to highlight in the visualization * @param simplified - whether to show a simplified use of the graph with fewer details on the vertices and edges * @param qualifyBaseR - show the edge-free base-R qualification (`stats::acf`); `false` when the signature database is disabled */ url(graph, includeEnvironments, mark, simplified = false, qualifyBaseR = true) { return mermaid_1.Mermaid.codeToUrl(exports.DataflowMermaid.raw(graph, includeEnvironments, mark, simplified, qualifyBaseR)); } }; //# sourceMappingURL=dfg.js.map