UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

351 lines 13.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CallGraph = void 0; const graph_1 = require("./graph"); const vertex_1 = require("./vertex"); const node_id_1 = require("../../r-bridge/lang-4.x/ast/model/processing/node-id"); const linker_1 = require("../internal/linker"); const edge_1 = require("./edge"); const built_in_proc_name_1 = require("../environments/built-in-proc-name"); const defaultmap_1 = require("../../util/collections/defaultmap"); const graph_helper_1 = require("./graph-helper"); const r_function_definition_1 = require("../../r-bridge/lang-4.x/ast/model/nodes/r-function-definition"); const r_binary_op_1 = require("../../r-bridge/lang-4.x/ast/model/nodes/r-binary-op"); const model_1 = require("../../r-bridge/lang-4.x/ast/model/model"); const type_1 = require("../../r-bridge/lang-4.x/ast/model/type"); const identifier_1 = require("../environments/identifier"); function processCds(vtx, graph, result, state) { for (const tar of vtx.cds ?? []) { const targetVtx = graph.getVertex(tar.id); if (targetVtx) { processUnknown(targetVtx, undefined, graph, result, state); } } } const UntargetedCallFollow = edge_1.EdgeType.Reads | edge_1.EdgeType.DefinedByOnCall | edge_1.EdgeType.DefinedBy | edge_1.EdgeType.Returns; const UntargetedCallAvoid = edge_1.EdgeType.NonStandardEvaluation | edge_1.EdgeType.Argument; /** * This tracks the known symbol origins for a function call for which we know that flowr found no targets! */ function fallbackUntargetedCall(vtx, graph) { // we track all aliases to their roots here, we know there is no known call target const collected = new Set(); const visited = new Set(); const toVisit = [vtx.id]; while (toVisit.length > 0) { const currentId = toVisit.pop(); if (visited.has(currentId)) { continue; } visited.add(currentId); const currentVtx = graph.getVertex(currentId); if (!currentVtx) { continue; } let addedNew = false; for (const [tar, e] of graph.outgoingEdges(currentId) ?? graph_1.NoEdges) { if (edge_1.DfEdge.includesType(e, UntargetedCallFollow) && edge_1.DfEdge.doesNotIncludeType(e, UntargetedCallAvoid)) { addedNew = true; toVisit.push(tar); } } // we have reached our end(s) if (!addedNew && currentId !== vtx.id) { collected.add(currentId); } } return collected; } function processCall(vtx, from, graph, result, state) { const vid = vtx.id; if (from) { result.addEdge(from, vid, edge_1.EdgeType.Calls); } if (state.visited.has(vid)) { return; } state.visited.add(vid); result.addVertex(vtx, undefined, true); processCds(vtx, graph, result, state); // for each call, resolve the targets const tars = (0, linker_1.getAllFunctionCallTargets)(vid, graph, vtx.environment); let addedTarget = false; let addedBiTarget = false; for (const tar of tars) { if (node_id_1.NodeId.isBuiltIn(tar)) { result.addEdge(vid, tar, edge_1.EdgeType.Calls); addedTarget = true; addedBiTarget = true; continue; } const targetVtx = graph.getVertex(tar); if (!vertex_1.FunctionDefinitionVertex.is(targetVtx)) { continue; } addedTarget = true; processFunctionDefinition(targetVtx, vid, graph, result, state); } if (!addedBiTarget && vtx.origin !== 'unnamed') { for (const origs of vtx.origin) { if (origs.startsWith('builtin:')) { addedTarget = true; result.addEdge(vid, node_id_1.NodeId.toBuiltIn(origs.slice('builtin:'.length)), edge_1.EdgeType.Calls); } } } if (!addedTarget) { const origs = fallbackUntargetedCall(vtx, graph); for (const ori of origs) { const oriVtx = graph.getVertex(ori); if (!oriVtx) { continue; } result.addEdge(vid, ori, edge_1.EdgeType.Calls); const name = graph.idMap?.get(ori); if (name?.lexeme && vertex_1.UseVertex.is(oriVtx)) { result.addVertex({ ...oriVtx, tag: vertex_1.VertexType.FunctionCall, name: name.lexeme, onlyBuiltin: false, origin: [built_in_proc_name_1.BuiltInProcName.Function], args: [] }, oriVtx.environment); } } } // handle arguments, traversing the 'reads' and the 'returns' edges for (const [tar, e] of graph.outgoingEdges(vtx.id) ?? graph_1.NoEdges) { if (edge_1.DfEdge.doesNotIncludeType(e, edge_1.EdgeType.Reads | edge_1.EdgeType.Returns | edge_1.EdgeType.Argument)) { continue; } const tVtx = graph.getVertex(tar); if (!tVtx) { continue; } processUnknown(tVtx, vtx.id, graph, result, state); } } function processUnknown(vtx, from, graph, result, state) { switch (vtx.tag) { case vertex_1.VertexType.FunctionCall: processCall(vtx, from, graph, result, state); return; case vertex_1.VertexType.FunctionDefinition: if (from) { result.addEdge(from, node_id_1.NodeId.toBuiltIn('function'), edge_1.EdgeType.Calls); } return; default: return; } } function processFunctionDefinition(vtx, from, graph, result, state) { if (from) { result.addEdge(from, vtx.id, edge_1.EdgeType.Calls); } if (state.visited.has(vtx.id)) { return; } state.visited.add(vtx.id); result.addVertex(vtx, undefined, true); processCds(vtx, graph, result, state); const exits = new Set(vtx.exitPoints); state.potentials.push([vtx.id, vtx.subflow.graph.difference(exits)]); for (const { nodeId } of exits) { const v = graph.getVertex(nodeId); if (v) { processUnknown(v, vtx.id, graph, result, state); } } } /** Follows the `Calls` edges from `seeds`, collecting everything they reach in `reached`. */ function followCalls(graph, seeds, reached) { const toVisit = Array.from(seeds); while (toVisit.length > 0) { const current = toVisit.pop(); if (reached.has(current)) { continue; } reached.add(current); for (const [target, edge] of graph.outgoingEdges(current) ?? graph_1.NoEdges) { if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Calls)) { toVisit.push(target); } } } } /** The names the reached calls go by, which are the generics a method of the program may be dispatched to for. */ function reachedCallNames(graph, reached) { const names = new Set(); for (const id of reached) { const vertex = graph.getVertex(id); if (vertex_1.FunctionCallVertex.is(vertex)) { names.add(identifier_1.Identifier.getName(vertex.name)); } } return names; } /** Whether `name` is a `<generic>.<class>` of a generic that is called, as `print.foo` is when `print` is. */ function mayBeDispatchedTo(name, generics) { for (let dot = name.indexOf('.'); dot > 0; dot = name.indexOf('.', dot + 1)) { if (generics.has(name.slice(0, dot))) { return true; } } return false; } /** * The unreached function definitions that may still run: one the code hands to another call (a callback, an * S4 method, a shiny handler), and one bound to a `<generic>.<class>` name of a generic the program calls. */ function mayRunAnyway(graph, reached) { const idMap = graph.idMap; if (idMap === undefined) { return []; } const seeds = []; let generics; for (const [id, vertex] of graph.vertices(true)) { if (!vertex_1.FunctionDefinitionVertex.is(vertex) || reached.has(id)) { continue; } const node = idMap.get(id); if (node === undefined) { continue; } const parent = model_1.RNode.directParent(node, idMap); if (parent?.type === type_1.RType.Argument) { seeds.push(id); continue; } const bound = r_binary_op_1.RBinaryOp.is(parent) ? model_1.RNode.lexeme(parent.lhs) : undefined; generics ??= reachedCallNames(graph, reached); if (bound !== undefined && mayBeDispatchedTo(bound, generics)) { seeds.push(id); } } return seeds; } /** * Helper object for call-graphs, you can compute new call graphs based on {@link CallGraph.compute}. * @see {@link Dataflow} * @see {@link CallGraph} */ exports.CallGraph = { ...graph_helper_1.GraphHelper, name: 'CallGraph', /** * Extracts the sub call graph from the given call graph, starting from the given entry points. */ computeSubCallGraph(graph, entryPoints) { const result = new graph_1.DataflowGraph(graph.idMap); const toVisit = Array.from(entryPoints); const visited = new Set(); while (toVisit.length > 0) { const currentId = toVisit.pop(); if (visited.has(currentId)) { continue; } visited.add(currentId); const currentVtx = graph.getVertex(currentId); if (!currentVtx) { continue; } result.addVertex(currentVtx, undefined, true); for (const [tar, e] of graph.outgoingEdges(currentId) ?? graph_1.NoEdges) { if (edge_1.DfEdge.includesType(e, edge_1.EdgeType.Calls)) { result.addEdge(currentId, tar, edge_1.EdgeType.Calls); toVisit.push(tar); } } } return result; }, /** The calls the program makes on its own: those outside of any function definition. */ entryPoints(graph) { const entries = new Set(); const idMap = graph.idMap; if (idMap === undefined) { return entries; } for (const [id, vertex] of graph.vertices(true)) { const node = vertex_1.FunctionCallVertex.is(vertex) ? idMap.get(id) : undefined; if (node !== undefined && r_function_definition_1.RFunctionDefinition.wrappingFunctionDefinition(node, idMap) === undefined) { entries.add(id); } } return entries; }, /** * The calls no execution starting at the top level reaches: those in a function nothing (transitively) calls. * What {@link mayRunAnyway} may still run is left out, so a call reported here really does not run. */ unreachableCalls(graph) { const reached = new Set(); followCalls(graph, exports.CallGraph.entryPoints(graph), reached); for (let seeds = mayRunAnyway(graph, reached); seeds.length > 0; seeds = mayRunAnyway(graph, reached)) { followCalls(graph, seeds, reached); } return graph.vertices(true) .filter(([id, vertex]) => vertex_1.FunctionCallVertex.is(vertex) && !reached.has(id)) .map(([id]) => id) .toArray(); }, /** * Reduces the call graph by dropping all transitive edges. */ dropTransitiveEdges(graph) { const newCg = new graph_1.DataflowGraph(graph.idMap); newCg.mergeVertices(graph); const knownReachability = new defaultmap_1.DefaultMap(() => new Set()); // heuristically sort by dif in ids const es = Array.from(graph.edges(), ([e, ts]) => ts.entries().map(([t, { types }]) => [e, t, types]).toArray()).flat() .sort((a, b) => String(a[0]).localeCompare(String(a[1])) - String(b[0]).localeCompare(String(b[1]))); for (const [from, to, types] of es) { if (!exports.CallGraph.reaches(from, to, newCg, knownReachability)) { newCg.addEdge(from, to, types); } } return newCg; }, /** * Computes the call graph from the given dataflow graph. * @see {@link CallGraph} - for details * @see {@link CallGraph.computeSubCallGraph} - to extract sub call graphs * @see {@link CallGraph.dropTransitiveEdges} - to reduce the call graph by dropping transitive edges */ compute(graph) { const result = new graph_1.DataflowGraph(graph.idMap); const state = { visited: new Set(), potentials: [] }; for (const [, vert] of graph.vertices(false)) { if (vertex_1.FunctionCallVertex.is(vert)) { processCall(vert, undefined, graph, result, state); } else if (vertex_1.FunctionDefinitionVertex.is(vert)) { processFunctionDefinition(vert, undefined, graph, result, state); } } for (const [from, tos] of state.potentials) { for (const to of tos) { if (!result.hasVertex(to)) { const v = graph.getVertex(to); if (v) { processUnknown(v, from, graph, result, state); if (vertex_1.FunctionDefinitionVertex.is(v)) { processFunctionDefinition(v, from, graph, result, state); } } } else { result.addEdge(from, to, edge_1.EdgeType.Calls); } } } return result; } }; //# sourceMappingURL=call-graph.js.map