@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
315 lines • 15.7 kB
JavaScript
"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