@eagleoutice/flowr
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
248 lines • 9.51 kB
JavaScript
"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");
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) ?? []) {
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 (targetVtx?.tag !== vertex_1.VertexType.FunctionDefinition) {
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.substring('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 && oriVtx.tag === vertex_1.VertexType.Use) {
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) ?? []) {
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);
}
}
}
/**
* Helper object for call-graphs, you can compute new call graphs based on {@link CallGraph.compute}.
* @see {@link Dataflow}
* @see {@link CallGraph}
*/
exports.CallGraph = {
name: 'CallGraph',
...graph_helper_1.GraphHelper,
/**
* 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) ?? []) {
if (edge_1.DfEdge.includesType(e, edge_1.EdgeType.Calls)) {
result.addEdge(currentId, tar, edge_1.EdgeType.Calls);
toVisit.push(tar);
}
}
}
return result;
},
/**
* 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 (vert.tag === vertex_1.VertexType.FunctionCall) {
processCall(vert, undefined, graph, result, state);
}
else if (vert.tag === vertex_1.VertexType.FunctionDefinition) {
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 (v.tag === vertex_1.VertexType.FunctionDefinition) {
processFunctionDefinition(v, from, graph, result, state);
}
}
}
else {
result.addEdge(from, to, edge_1.EdgeType.Calls);
}
}
}
return result;
}
};
//# sourceMappingURL=call-graph.js.map