UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

195 lines 9.44 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Nse = exports.Unquote = void 0; const model_1 = require("../../../../../r-bridge/lang-4.x/ast/model/model"); const r_unary_op_1 = require("../../../../../r-bridge/lang-4.x/ast/model/nodes/r-unary-op"); const r_function_call_1 = require("../../../../../r-bridge/lang-4.x/ast/model/nodes/r-function-call"); const identifier_1 = require("../../../../environments/identifier"); const data_masking_functions_1 = require("../../../../environments/data-masking-functions"); const graph_1 = require("../../../../graph/graph"); const vertex_1 = require("../../../../graph/vertex"); const edge_1 = require("../../../../graph/edge"); /** The escape a quoting function offers, a property of the function: `quote(!!x)` negates, `expr(!!x)` splices. */ var Unquote; (function (Unquote) { /** no escape at all, everything within the argument stays unevaluated */ Unquote["None"] = "none"; /** rlang's `!!` and `!!!` splice in the value of their operand */ Unquote["Rlang"] = "rlang"; /** `bquote`'s `.(x)` evaluates its operand */ Unquote["Bquote"] = "bquote"; })(Unquote || (exports.Unquote = Unquote = {})); /** `!!x` parses as `!(!x)`, so peel the run of `!` to reach the operand. */ function unquotedOperandOf(node, style) { if (style === Unquote.Bquote) { if (!r_function_call_1.RFunctionCall.isNamed(node) || identifier_1.Identifier.getName(node.functionName.content) !== '.' || node.arguments.length !== 1) { return undefined; } const arg = node.arguments[0]; return arg === r_function_call_1.EmptyArgument ? undefined : arg.value; } if (!isNegation(node) || !isNegation(node.operand)) { return undefined; } let operand = node.operand; while (isNegation(operand)) { operand = operand.operand; } return operand; } function isNegation(node) { return r_unary_op_1.RUnaryOp.is(node) && node.operator === '!'; } /** * Whether the definition is a bare function: the vertex itself, or a variable bound straight to a function * definition. A binding that anything was done to on the way (`structure(f, class = "cls")`, `class(f) <- `) * is not one, and deliberately so: such a closure carries a class, `>` may dispatch on it, and flowR cannot * tell that it does not. Only where nothing at all can be attached is a function ruled out as an operand. */ function definesAFunction(graph, id) { const vertex = graph.getVertex(id); if (vertex_1.FunctionDefinitionVertex.is(vertex)) { return true; } else if (!vertex_1.VariableDefinitionVertex.is(vertex)) { return false; } for (const [target, edge] of graph.outgoingEdges(id) ?? graph_1.NoEdges) { if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.DefinedBy) && vertex_1.FunctionDefinitionVertex.is(graph.getVertex(target))) { return true; } } return false; } /** * Whether `id` reads something and everything it reads is a bare function. In a data mask such a name is a * column after all: R asks the data first, and a plain closure is not what `id > 2` compares. A closure that * was given a class is left alone, see {@link definesAFunction}. */ function readsOnlyFunctions(graph, id) { let reads = false; for (const [target, edge] of graph.outgoingEdges(id) ?? graph_1.NoEdges) { if (!edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Reads)) { continue; } else if (!definesAFunction(graph, target)) { return false; } reads = true; } return reads; } /** The parts of a call R does not evaluate the standard way. */ exports.Nse = { name: 'Nse', /** The ids an {@link Unquote} escape hands back to standard evaluation, the markers themselves excluded. */ unquoted(root, style) { if (root === undefined || style === Unquote.None) { return undefined; } const evaluated = new Set(); model_1.RNode.visitAst(root, node => { const operand = unquotedOperandOf(node, style); if (operand === undefined) { return false; } model_1.RNode.visitAst(operand, inner => { evaluated.add(inner.info.id); return false; }); return true; }); return evaluated.size > 0 ? evaluated : undefined; }, /** Whether everything below `node` is unquoted. */ isUnquote(node, style) { return style !== Unquote.None && unquotedOperandOf(node, style) !== undefined; }, /** * Drops the mask mark from names the caller binds to a value: while `filter(d, k)` is processed `k` has no * read yet. A name bound to a function keeps its mark, as `filter(d, id > 2)` next to `id <- function(...)` * still means the column. Hands back the masking call and the names it just unmarked, `undefined` when * `name` does not mask at all, so {@link linkMasksToData} need not ask a second time. */ dropResolvedMask(graph, id, name) { if (!data_masking_functions_1.DataMaskingFunctionNames.has(identifier_1.Identifier.getName(name))) { return undefined; } return { id, bound: exports.Nse.unmark(graph, id, target => !exports.Nse.suppliedByMask(graph, target) && !readsOnlyFunctions(graph, target)) }; }, /** Drops the non-standard-evaluation mark from the outgoing edges of `id` that `which` accepts, and reports them. */ unmark(graph, id, which = () => true) { const dropped = []; for (const [target, edge] of graph.outgoingEdges(id) ?? graph_1.NoEdges) { if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.NonStandardEvaluation) && which(target)) { graph.removeEdgeType(id, target, edge_1.EdgeType.NonStandardEvaluation); dropped.push(target); } } return dropped; }, /** Whether the vertex is a name a data mask may supply, which is every name appearing in the mask. */ maskCandidate(vertex) { return vertex_1.UseVertex.is(vertex); }, /** Whether `id` is a name the data mask supplies, i.e. a use the caller does not bind itself. */ suppliedByMask(graph, id, vertex = graph.getVertex(id)) { return vertex_1.UseVertex.is(vertex) && !graph.outgoingEdges(id)?.values().some(e => edge_1.DfEdge.includesType(e, edge_1.EdgeType.Reads)); }, /** * Whether `id` is a name a data mask supplied, which the mark the masking call carries is what records. * Unlike {@link suppliedByMask} this survives {@link linkMasksToData}, so ask this one after the dataflow * is complete and that one while a call is still being processed. */ maskedName(graph, id) { for (const [source, edge] of graph.ingoingEdges(id) ?? graph_1.NoEdges) { const call = edge_1.DfEdge.includesType(edge, edge_1.EdgeType.NonStandardEvaluation) ? graph.getVertex(source) : undefined; if (vertex_1.FunctionCallVertex.is(call) && call.name !== undefined && data_masking_functions_1.DataMaskingFunctionNames.has(identifier_1.Identifier.getName(call.name))) { return true; } } return false; }, /** * Links every name the data masks of `calls` supply to the data it comes from: `id` in `filter(df, id > 2)` * is a column of `df`, so it reads `df`. Run once every mark has settled, as adding the read makes * {@link suppliedByMask} false for the name; a call whose first argument is masked too has no data to link * against. */ linkMasksToData(graph, calls) { const links = []; for (const { id, bound } of calls) { const call = graph.getVertex(id); const data = vertex_1.FunctionCallVertex.is(call) ? call.args.find(a => a !== r_function_call_1.EmptyArgument) : undefined; if (data === undefined) { continue; } /* what is still marked are the names the data supplies, the unmarked ones mean the binding too */ const masked = new Set(bound); for (const [target, edge] of graph.outgoingEdges(id) ?? graph_1.NoEdges) { if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.NonStandardEvaluation)) { masked.add(target); } } /* a call masking its first argument too, like `aes(x, y)`, has no data to link against */ if (masked.has(data.nodeId)) { continue; } for (const target of masked) { /* a function of that name is not what the mask means, so the column takes its place */ if (readsOnlyFunctions(graph, target)) { for (const [definition, edge] of graph.outgoingEdges(target) ?? graph_1.NoEdges) { if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Reads)) { graph.removeEdgeType(target, definition, edge_1.EdgeType.Reads); } } } links.push([target, data.nodeId]); } } for (const [from, to] of links) { graph.addEdge(from, to, edge_1.EdgeType.Reads); } } }; //# sourceMappingURL=nse.js.map