@eagleoutice/flowr
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
108 lines • 6.36 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.UNUSED_DEFINITION = void 0;
const linter_format_1 = require("../linter-format");
const flowr_search_builder_1 = require("../../search/flowr-search-builder");
const range_1 = require("../../util/range");
const linter_tags_1 = require("../linter-tags");
const assert_1 = require("../../util/assert");
const vertex_1 = require("../../dataflow/graph/vertex");
const edge_1 = require("../../dataflow/graph/edge");
const flowr_search_filters_1 = require("../../search/flowr-search-filters");
const InterestingEdgesVariable = edge_1.EdgeType.Reads | edge_1.EdgeType.Calls | edge_1.EdgeType.DefinesOnCall;
const InterestingEdgesFunction = edge_1.EdgeType.Reads | edge_1.EdgeType.Calls; // include read as this could print the function definition
const InterestingEdgesTargets = edge_1.EdgeType.SideEffectOnCall;
function getDefinitionArguments(def, dfg) {
return dfg.outgoingEdges(def)?.entries().filter(([, e]) => edge_1.DfEdge.includesType(e, edge_1.EdgeType.DefinedBy))
.map(([target]) => target).toArray() ?? [];
}
function buildQuickFix(variable, dfg, ast) {
// first we check whether any of the 'Defined by' targets have any obligations - if so, we can not remove the definition
// otherwise we can automatically remove the full definition!
if (variable.info.role === "acc" /* RoleInParent.Accessed */ || variable.info.role === "for-var" /* RoleInParent.ForVariable */) {
// this is an access or a for variable, we can not remove it currently
return undefined;
}
const definedBys = getDefinitionArguments(variable.info.id, dfg);
const hasImportantArgs = definedBys.some(d => dfg.unknownSideEffects.has(d))
|| definedBys.flatMap(e => Array.from(dfg.outgoingEdges(e) ?? []))
.some(([target, e]) => {
return edge_1.DfEdge.includesType(e, InterestingEdgesTargets) || dfg.unknownSideEffects.has(target);
});
if (hasImportantArgs) {
return undefined; // we can not remove this definition, it has important arguments
}
const totalRangeToRemove = range_1.SourceLocation.merge([...definedBys.map(d => {
const vertex = ast.idMap.get(d);
return vertex ? range_1.SourceLocation.fromNode(vertex) : undefined;
}),
variable.info.fullRange ?? variable.location]);
return [{
type: 'remove',
loc: totalRangeToRemove ?? range_1.SourceLocation.invalid(),
description: `Remove unused definition of \`${variable.lexeme}\``
}];
}
/**
* consider `x <- function() ...` if we say `x` is unused and propose to remove everything, there should be no separate quick fix for the function definition
*/
function onlyKeepSupersetOfUnused(elements) {
const locs = elements.flatMap(e => e.quickFix?.map(q => q.loc) ?? [e.loc]);
if (locs.length <= 1) {
return elements; // nothing to filter, only one element
}
return elements.filter(e => {
const otherLoc = range_1.SourceLocation.merge((e.quickFix?.map(q => q.loc) ?? [e.loc])) ?? range_1.SourceLocation.invalid();
return !locs.some(r => range_1.SourceLocation.compare(r, otherLoc) !== 0 && range_1.SourceLocation.isSubsetOf(otherLoc, r)); // there is no smaller remove
});
}
exports.UNUSED_DEFINITION = {
/* this can be done better once we have types */
createSearch: config => flowr_search_builder_1.Q.all().filter(config.includeFunctionDefinitions ? flowr_search_filters_1.FlowrFilterCombinator.is(vertex_1.VertexType.VariableDefinition).or(vertex_1.VertexType.FunctionDefinition) : vertex_1.VertexType.VariableDefinition),
processSearchResult: (elements, config, data) => {
const metadata = {
totalConsidered: 0
};
return {
results: onlyKeepSupersetOfUnused(elements.getElements().flatMap(element => {
metadata.totalConsidered++;
const dfgVertex = data.dataflow.graph.getVertex(element.node.info.id);
if (!dfgVertex || (!(0, vertex_1.isVariableDefinitionVertex)(dfgVertex)
&& (0, vertex_1.isFunctionDefinitionVertex)(dfgVertex) && !config.includeFunctionDefinitions)) {
return undefined;
}
const ingoingEdges = data.dataflow.graph.ingoingEdges(dfgVertex.id);
const interestedIn = (0, vertex_1.isVariableDefinitionVertex)(dfgVertex) ? InterestingEdgesVariable : InterestingEdgesFunction;
const ingoingInteresting = ingoingEdges?.values().some(e => edge_1.DfEdge.includesType(e, interestedIn));
if (ingoingInteresting) {
return undefined;
}
// found an unused definition
const variableName = element.node.lexeme;
return [{
certainty: linter_format_1.LintingResultCertainty.Uncertain,
variableName,
involvedId: element.node.info.id,
loc: range_1.SourceLocation.fromNode(element.node) ?? range_1.SourceLocation.invalid(),
quickFix: buildQuickFix(element.node, data.dataflow.graph, data.normalize)
}];
}).filter(assert_1.isNotUndefined)),
'.meta': metadata
};
},
prettyPrint: {
[linter_format_1.LintingPrettyPrintContext.Query]: result => `Definition of \`${result.variableName}\` at ${range_1.SourceLocation.format(result.loc)}`,
[linter_format_1.LintingPrettyPrintContext.Full]: result => `Definition of \`${result.variableName}\` at ${range_1.SourceLocation.format(result.loc)} is unused`
},
info: {
name: 'Unused Definitions',
description: 'Checks for unused definitions.',
tags: [linter_tags_1.LintingRuleTag.Readability, linter_tags_1.LintingRuleTag.Smell, linter_tags_1.LintingRuleTag.QuickFix],
// our limited analysis causes unused definitions involving complex reflection etc. not to be included in our result, but unused definitions are correctly validated
certainty: linter_format_1.LintingRuleCertainty.BestEffort,
defaultConfig: {
includeFunctionDefinitions: true
}
}
};
//# sourceMappingURL=unused-definition.js.map