UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

200 lines 10.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UNCLOSED_CONNECTION = void 0; const node_id_1 = require("../../r-bridge/lang-4.x/ast/model/processing/node-id"); const type_1 = require("../../r-bridge/lang-4.x/ast/model/type"); const graph_1 = require("../../dataflow/graph/graph"); const edge_1 = require("../../dataflow/graph/edge"); const vertex_1 = require("../../dataflow/graph/vertex"); const info_1 = require("../../dataflow/info"); const built_in_props_1 = require("../../dataflow/environments/built-in-props"); const query_fn_props_1 = require("../../dataflow/environments/query-fn-props"); const identifier_1 = require("../../dataflow/environments/identifier"); const flowr_search_builder_1 = require("../../search/flowr-search-builder"); const range_1 = require("../../util/range"); const assert_1 = require("../../util/assert"); const linter_format_1 = require("../linter-format"); const linter_tags_1 = require("../linter-tags"); /** the edges the connection flows along, from the call opening it to the argument of the call closing it */ const ConnectionFlow = edge_1.EdgeType.Reads | edge_1.EdgeType.DefinedBy | edge_1.EdgeType.DefinedByOnCall | edge_1.EdgeType.Returns | edge_1.EdgeType.Argument; /** The arguments holding the handle the call acts on, all of them if it does not state which. */ function handleArguments(vertex, sig) { const stated = sig && built_in_props_1.FnSig.posWith(built_in_props_1.FnSig.layout(sig), vertex.args.length, built_in_props_1.ArgProp.Handle); return stated?.length ? stated.map(i => vertex.args[i]) : vertex.args; } /** * Adds every opening call whose connection may reach `start`, an argument of a closing call, to `out`. * Unlike {@link Dataflow.provenance} this stops at an opening call and follows no control dependency, * so a close in a branch does not claim what that branch opens beside it. */ function openCallsReaching(graph, start, opens, out) { const visited = new Set([start]); const pending = [start]; while (pending.length > 0) { const current = pending.pop(); if (opens.has(current)) { out.add(current); continue; } for (const [target, edge] of graph.outgoingEdges(current) ?? graph_1.NoEdges) { if (edge_1.DfEdge.includesType(edge, ConnectionFlow) && !visited.has(target)) { visited.add(target); pending.push(target); } } } } /** The variable definition the opened connection is bound to, if it is bound to one. */ function bindingOf(graph, open) { for (const [source, edge] of graph.ingoingEdges(open) ?? graph_1.NoEdges) { if (edge_1.DfEdge.includesType(edge, edge_1.EdgeType.DefinedBy) && vertex_1.VariableDefinitionVertex.is(graph.getVertex(source))) { return source; } } return undefined; } /** The statement `id` belongs to, i.e. the ancestor that is an element of an expression list. */ function enclosingStatement(idMap, id) { let node = idMap.get(id); while (node !== undefined) { const parent = node.info.parent === undefined ? undefined : idMap.get(node.info.parent); if (parent === undefined || parent.type === type_1.RType.ExpressionList) { return node; } node = parent; } return undefined; } /** A fix closing the connection after the last statement using it, if it is bound to a name to close. */ function closeFix(graph, open) { const idMap = graph.idMap; const definition = bindingOf(graph, open); const name = idMap && definition !== undefined ? (0, node_id_1.recoverName)(definition, idMap) : undefined; if (idMap === undefined || definition === undefined || name === undefined) { return undefined; } const reads = (graph.ingoingEdges(definition) ?? graph_1.NoEdges).entries() .filter(([, edge]) => edge_1.DfEdge.includesType(edge, edge_1.EdgeType.Reads)).map(([source]) => source); const statements = [definition, ...reads] .map(id => range_1.SourceLocation.fromNode(enclosingStatement(idMap, id))) .filter(assert_1.isNotUndefined); const last = statements.reduce((a, b) => a === undefined || range_1.SourceRange.compare(range_1.SourceLocation.getRange(a), range_1.SourceLocation.getRange(b)) < 0 ? b : a, undefined); if (last === undefined) { return undefined; } const [startLine, startColumn, endLine, endColumn] = last; return [{ type: 'replace', loc: range_1.SourceLocation.from([endLine, endColumn + 1, endLine, endColumn], range_1.SourceLocation.getFile(last)), description: `Close the connection with \`close(${name})\``, replacement: `\n${' '.repeat(startLine === endLine ? startColumn - 1 : 0)}close(${name})` }]; } /** * How certain we are that the connection opened at `open` is left open, * or `undefined` if the given closing calls close it in every run that opens it. */ function unclosedCertainty(graph, open, closes) { if (closes === undefined) { return linter_format_1.LintingResultCertainty.Certain; } const openCds = graph.getVertex(open)?.cds ?? []; const uncovered = new Set(); for (const close of closes) { const closeCds = graph.getVertex(close)?.cds ?? []; /* opening within a loop and closing outside of it closes the connection of the last iteration only */ if (info_1.ControlDependency.minus(openCds, closeCds).some(cd => info_1.ControlDependency.isIterated(cd, graph.idMap))) { continue; } const only = info_1.ControlDependency.minus(closeCds, openCds); if (only.length === 0) { return undefined; } for (const cd of only) { uncovered.add(cd); } } /* the closes may still cover every branch between them, as in `if(p) close(c) else close(c)` */ return uncovered.size > 0 && info_1.ControlDependency.happensInEveryBranchSet(uncovered) ? undefined : linter_format_1.LintingResultCertainty.Uncertain; } /** The calls opening and the calls closing a connection, as the props state and the configuration adds. */ function connectionCalls(elements, dataflow, config) { const opensByName = config.openFns.length > 0 ? identifier_1.Identifier.regex(...config.openFns) : undefined; const closesByName = config.closeFns.length > 0 ? identifier_1.Identifier.regex(...config.closeFns) : undefined; const opens = new Map(); const closes = []; for (const { node } of elements) { const stated = (0, query_fn_props_1.callFnProps)(node.info.id, dataflow); if (stated === undefined) { continue; } const props = stated.props ?? 0; const name = identifier_1.Identifier.toString(stated.name); const loc = range_1.SourceLocation.fromNode(node); if (loc !== undefined && ((props & built_in_props_1.CallProp.Opens) !== 0 || opensByName?.test(name))) { opens.set(node.info.id, loc); } else if ((props & built_in_props_1.CallProp.Closes) !== 0 || closesByName?.test(name)) { const vertex = dataflow.graph.getVertex(node.info.id); if (vertex_1.FunctionCallVertex.is(vertex)) { closes.push([vertex, stated.sig]); } } } return { opens, closes }; } exports.UNCLOSED_CONNECTION = { createSearch: () => flowr_search_builder_1.Q.all().filter(vertex_1.VertexType.FunctionCall), processSearchResult: async (elements, config, data) => { const dataflow = await data.dataflow(); const graph = dataflow.graph; const { opens, closes } = connectionCalls(elements.getElements(), dataflow, config); const openIds = new Set(opens.keys()); const closedBy = new Map(); for (const [close, sig] of opens.size > 0 ? closes : []) { const reached = new Set(); for (const arg of handleArguments(close, sig)) { const ref = graph_1.FunctionArgument.getReference(arg); if (ref !== undefined) { openCallsReaching(graph, ref, openIds, reached); } } for (const open of reached) { const known = closedBy.get(open); if (known) { known.push(close.id); } else { closedBy.set(open, [close.id]); } } } const results = []; for (const [open, loc] of opens) { const certainty = unclosedCertainty(graph, open, closedBy.get(open)); if (certainty === undefined) { continue; } /* closing what another path closes as well errors in R, so only a connection nothing closes gets a fix */ const quickFix = certainty === linter_format_1.LintingResultCertainty.Certain ? closeFix(graph, open) : undefined; results.push({ certainty, involvedId: open, loc, ...(quickFix ? { quickFix } : {}) }); } return { results, '.meta': { totalOpened: opens.size, totalClosed: closes.length } }; }, prettyPrint: { [linter_format_1.LintingPrettyPrintContext.Query]: result => `Unclosed connection at ${range_1.SourceLocation.format(result.loc)}`, [linter_format_1.LintingPrettyPrintContext.Full]: result => `The connection opened at ${range_1.SourceLocation.format(result.loc)} is not closed on every path that opens it` }, info: { name: 'Unclosed Connection', tags: [linter_tags_1.LintingRuleTag.Robustness, linter_tags_1.LintingRuleTag.Smell], /* a connection handed to code flowR cannot resolve, as in `lapply(cons, close)`, is reported although it is closed */ certainty: linter_format_1.LintingRuleCertainty.BestEffort, description: 'Flags connections that are opened but not closed on every path opening them.', defaultConfig: { openFns: [], closeFns: [] } } }; //# sourceMappingURL=unclosed-connection.js.map