UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

315 lines 15.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Dataflow = void 0; const graph_1 = require("./graph"); const edge_1 = require("./edge"); const dataflowgraph_builder_1 = require("./dataflowgraph-builder"); const dfg_get_origin_1 = require("../origin/dfg-get-origin"); const graph_helper_1 = require("./graph-helper"); const call_graph_1 = require("./call-graph"); const transitive_side_effects_1 = require("../internal/process/functions/call/built-in/transitive-side-effects"); const node_id_1 = require("../../r-bridge/lang-4.x/ast/model/processing/node-id"); const vertex_1 = require("./vertex"); const identifier_1 = require("../environments/identifier"); const resolve_helper_1 = require("../environments/resolve-helper"); const model_1 = require("../../r-bridge/lang-4.x/ast/model/model"); const r_base_packages_1 = require("../../util/r-base-packages"); /** * This is the root helper object to work with the {@link DataflowGraph}. * * - {@link Dataflow.visualize} - for visualization helpers (e.g., rendering the DFG as a mermaid graph), * - {@link Dataflow.views} - for working with specific views of the dataflow graph (e.g., the call graph), * - {@link Dataflow.edge} - for working with the edges in the dataflow graph, * - {@link Dataflow.qualify} - for the package-qualified `pkg::fn` identifier of a call from its id and graph, * - {@link Dataflow.resolve} - for resolving a name against an environment, * - {@link Dataflow.packagesOf} - for the packages a set of nodes (e.g. a slice) calls into, * - {@link Dataflow.valueIsUsed}/{@link Dataflow.hasComputedArguments} - for what a call does with, and gets as, values, * @example * ```ts * Dataflow.origin(graph, id); // where the use at `id` comes from * Dataflow.edge.includesType(edge, EdgeType.Reads); // the edge helpers * Dataflow.visualize.mermaid.url(graph); // a link to the rendered graph * ``` */ exports.Dataflow = { /** * Maps to flowR's main graph object to store and manipulate the dataflow graph * @see {@link DataflowGraph} */ graph: graph_1.DataflowGraph, ...graph_helper_1.GraphHelper, name: 'Dataflow', /** * Maps to flowR's dataflow edge helper to work with the edges in the dataflow graph */ edge: edge_1.DfEdge, /** * Dispatches to helper objects that relate to (sub-) views of the dataflow graph, e.g. the call graph. */ views: { /** * Maps to flowR's helper object for the call-graph */ callGraph: call_graph_1.CallGraph, }, /** * Dispatches to helper functions to create new dataflow graphs, e.g. from a pipeline or an empty graph. */ create: { /** * Creates an empty dataflow graph with the given id map (or a new one if not provided). * @see {@link emptyGraph} */ empty: dataflowgraph_builder_1.emptyGraph }, /** * Returns the origin of a vertex in the dataflow graph * @see {@link getOriginInDfg} - for the underlying function */ origin: dfg_get_origin_1.getOriginInDfg, /** * Name and value resolution. * @see {@link Resolve} - the helper object itself, which documents which entry point to reach for */ resolve: resolve_helper_1.Resolve, /** * The qualified identifier of the call with the given id, or `undefined` if it does not resolve to a package * export and is not itself already namespaced (with `purrr` loaded, a `map()` call yields * `Identifier.make('map', 'purrr')`; an explicit `pkg::fn()` call yields `pkg::fn` unchanged). * * This is the compact form of {@link Identifier.toQualified}, reconstructing both the * {@link Dataflow.origin|origins} and the call's name from the graph. * @param id - The id of the call to qualify * @param graph - The graph the call is part of * @param qualifyBaseR - Whether to also qualify a bare base-R call from the package exporting it * (`sd` yields `stats::sd`), which needs neither a loaded database nor graph edges. * Set this to `false` to only qualify what the origins resolve to (or what is already namespaced). */ qualify(id, graph, qualifyBaseR = true) { const vertex = graph.getVertex(id); return identifier_1.Identifier.toQualified((0, dfg_get_origin_1.getOriginInDfg)(graph, id), vertex_1.FunctionCallVertex.is(vertex) ? vertex.name : undefined, qualifyBaseR); }, /** * The packages the given nodes call into, as {@link Dataflow.qualify} resolves every call among them. * This is what a selection needs, which is not what the program loads: a `library()` whose exports the * selection never calls does not make the package needed. Base R is left out unless `includeBaseR`. * @param nodes - the ids to consider, e.g. the result of a slice * @param graph - the graph the ids belong to * @param includeBaseR - whether to also report base-R packages */ packagesOf(nodes, graph, includeBaseR = false) { const packages = new Set(); for (const id of nodes) { if (!vertex_1.FunctionCallVertex.is(graph.getVertex(id))) { continue; } const qualified = exports.Dataflow.qualify(id, graph, includeBaseR); const pkg = qualified === undefined ? undefined : identifier_1.Identifier.getNamespace(qualified); if (pkg !== undefined && (includeBaseR || !(0, r_base_packages_1.isBaseRPackage)(pkg))) { packages.add(pkg); } } return packages; }, /** * Whether the call's result is passed on -- assigned, handed to another call, returned -- rather than left * for R to auto-print. A bare `anova(a, b)` is an output the program reports; the `summary(m)` of * `x <- summary(m)` is not. * * Only an edge that carries the value counts. A plain {@link EdgeType.Reads} does not: it also chains the * calls that share a side effect, which would report `plot(x)` as consumed by the `lines(y)` drawn after it. */ valueIsUsed(id, graph) { const consuming = edge_1.EdgeType.Argument | edge_1.EdgeType.Returns | edge_1.EdgeType.DefinedBy; for (const [, edge] of graph.ingoingEdges(id) ?? graph_1.NoEdges) { if (edge_1.DfEdge.includesType(edge, consuming)) { return true; } } return false; }, /** * Whether any argument of the call carries a value the program worked out, rather than only literals the * author typed: `cat("starting\n")` is a log line, `cat("n =", length(m))` is a finding. * A call among the arguments counts as computed, even one over literals such as `paste("a", "b")`. */ hasComputedArguments(id, graph) { for (const [target] of graph.outgoingEdges(id) ?? graph_1.NoEdges) { if (!node_id_1.NodeId.isBuiltIn(target) && !vertex_1.ValueVertex.is(graph.getVertex(target))) { return true; } } return false; }, /** * Interprocedural propagation of escaped side effects (attached packages, `<<-` definitions) to their callers. */ sideEffects: { propagateTransitive: transitive_side_effects_1.propagateTransitiveSideEffects, callGraphSummaries: transitive_side_effects_1.computeCallGraphSummaries, }, /** * Only returns the sub-part of the graph that is determined by the given selection. * In other words, this will return a graph with only vertices that are part of the selected ids, * and edges that are between such selected vertices. * @param graph - the dataflow graph to slice for * @param select - the ids to select in the reduced graph * @param includeMissingTargets - if set to true, this will include edges which target vertices that are not selected! */ reduceGraph(graph, select, includeMissingTargets = false) { const df = new graph_1.DataflowGraph(graph.idMap); const roots = graph.rootIds(); // if the graph has no root ids all selected vertices are non-root in this case we just break the fdef selection and promote all to root! const selectedRoots = roots.intersection(select); const forceRoot = selectedRoots.size === 0; for (const [id, vtx] of graph.vertices(true)) { if (select.has(id)) { df.addVertex(vtx, vtx.environment, forceRoot || roots.has(id)); } } for (const [from, targets] of graph.edges()) { if (!select.has(from)) { continue; } for (const [tar, { types }] of targets.entries()) { if (!includeMissingTargets && !select.has(tar)) { continue; } df.addEdge(from, tar, types); } } for (const u of graph.unknownSideEffects) { const id = graph_1.UnknownSideEffect.id(u); if (select.has(id)) { df.markIdForUnknownSideEffects(id, graph_1.UnknownSideEffect.linkTo(u)); } } return df; }, /** * Equivalent to {@link Dataflow.reduceGraph|`reduceGraph`} followed by {@link Dataflow.invertGraph|`invertGraph`} * but in a single pass over the graph, allocating only one intermediate object instead of two. * Use this when you need the reduced-and-inverted graph for a forward traversal within a restriction set. */ reduceAndInvertGraph(graph, select, cleanEnv) { const df = new graph_1.DataflowGraph(graph.idMap); for (const [id, vtx] of graph.vertices(true)) { if (select.has(id)) { df.addVertex(vtx, cleanEnv); } } for (const [from, targets] of graph.edges()) { if (!select.has(from)) { continue; } for (const [to, { types }] of targets) { if (!select.has(to)) { continue; } df.addEdge(to, from, types); } } for (const u of graph.unknownSideEffects) { const id = graph_1.UnknownSideEffect.id(u); if (select.has(id)) { df.markIdForUnknownSideEffects(id, graph_1.UnknownSideEffect.linkTo(u)); } } return df; }, /** * Whether the node is quoted, i.e., affected by a {@link EdgeType.NonStandardEvaluation} edge that actually * keeps it from being evaluated (as `quote` and `substitute` do). * * Loops mark their body as non-standard-evaluated as well, yet that body really is evaluated (and its symbols * really are read), so such an edge does not quote. Use this instead of testing for the edge type directly * whenever you want to know whether something is evaluated at all. * @param id - The id of the node to check * @param graph - The graph the node is part of * @param withOutgoing - Whether to also consider the outgoing edges of the node (i.e., whether the node itself * quotes something), and not just the ingoing ones (i.e., whether it is quoted) */ isQuoted(id, graph, withOutgoing = false) { /* an nse edge quotes iff it does not originate from a loop marking its body */ const quotes = (source, e) => edge_1.DfEdge.includesType(e, edge_1.EdgeType.NonStandardEvaluation) && !model_1.RLoopConstructs.is(graph.idMap?.get(source)); if (graph.ingoingEdges(id)?.entries().some(([source, e]) => quotes(source, e))) { return true; } return withOutgoing && (graph.outgoingEdges(id)?.values().some(e => quotes(id, e)) ?? false); }, /** * Given the id of a vertex (usually a variable use), * this returns a reachable provenance set by calculating a non-interprocedural and non-context sensitive backward slice, but stopping at the given ids! * You can obtain the corresponding graph using {@link Dataflow.reduceGraph}. * @param id - The id to use as a seed for provenance calculation * @param graph - The graph to perform the provenance calculation on * @param consider - The ids to restrict the calculation too (e.g., the ids contained within a function definition to restrict the analysis to) * @param followEdges - Which edges to consider in the provenance traversal, if you set this to undefined this will automatically track all edges * @see {@link Dataflow.provenanceGraph} - for a convenience wrapper to directly obtain the graph of the provenance. */ provenance(id, graph, consider, followEdges = edge_1.EdgeType.Calls | edge_1.EdgeType.Reads | edge_1.EdgeType.Returns | edge_1.EdgeType.Argument | edge_1.EdgeType.DefinedBy | edge_1.EdgeType.DefinedByOnCall) { const queue = [id]; const visited = new Set(); while (queue.length > 0) { const nodeId = queue.pop(); if (nodeId === undefined || visited.has(nodeId) || (consider && !consider.has(nodeId))) { continue; } visited.add(nodeId); const vtx = graph.get(nodeId); if (vtx === undefined) { continue; } for (const [to, types] of vtx[1]) { if (followEdges === undefined || edge_1.DfEdge.includesType(types, followEdges)) { queue.push(to); } } for (const cd of vtx[0].cds ?? []) { queue.push(cd.id); } } return visited; }, /** * A simple visitor akin to {@link RNode.visitAst} to traverse the dataflow graph starting from the start id and only * respecting edge direction. * @param graph - The dataflow graph to operate on. * @param start - The start id of the visitation. * @param onVertex - The function to execute for each vertex, if this returns `true` the visitation will stop from this vertex. */ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type visitDfg(graph, start, onVertex) { const queue = [start]; const visited = new Set(); while (queue.length > 0) { const nodeId = queue.pop(); if (nodeId === undefined || visited.has(nodeId)) { continue; } visited.add(nodeId); const vtx = graph.get(nodeId); if (vtx === undefined) { continue; } const shouldStop = onVertex(vtx[0]); if (shouldStop) { continue; } for (const [to] of vtx[1]) { queue.push(to); } } }, /** * A convenience wrapper for {@link Dataflow.reduceGraph|reducing} the {@link Dataflow.provenance|provenance} of a graph. * @param id - The id to use as a seed for provenance calculation * @param graph - The graph to perform the provenance calculation on * @param consider - The ids to restrict the calculation too (e.g., the ids contained within a function definition to restrict the analysis to) * @see {@link Dataflow.provenance} */ provenanceGraph(id, graph, consider) { return exports.Dataflow.reduceGraph(graph, exports.Dataflow.provenance(id, graph, consider)); } }; //# sourceMappingURL=df-helper.js.map