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