@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
277 lines • 14.7 kB
JavaScript
"use strict";
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 flowr_file_1 = require("../../project/context/flowr-file");
const flowr_namespace_file_1 = require("../../project/plugins/file-plugins/files/flowr-namespace-file");
const identifier_1 = require("../../dataflow/environments/identifier");
const type_1 = require("../../r-bridge/lang-4.x/ast/model/type");
const retriever_1 = require("../../r-bridge/retriever");
const query_fn_props_1 = require("../../dataflow/environments/query-fn-props");
const built_in_props_1 = require("../../dataflow/environments/built-in-props");
const default_builtin_config_1 = require("../../dataflow/environments/default-builtin-config");
const r_function_call_1 = require("../../r-bridge/lang-4.x/ast/model/nodes/r-function-call");
const r_function_definition_1 = require("../../r-bridge/lang-4.x/ast/model/nodes/r-function-definition");
const graph_1 = require("../../dataflow/graph/graph");
/**
* The dots parameter is a special parameter and must never be reported as an unused definition.
*/
const DotsParameter = '...';
/** the standard-library S3 generics flowR models no built-in for, so the store cannot state them */
const OtherKnownS3Generics = new Set([
'summary', 'coef', 'vcov', 'residuals', 'fitted', 'predict', 'as.vector', 'str', 'toString', 'all.equal',
'aggregate', 'update', 'anova', 'confint', 'logLik', 'AIC', 'BIC', 'deviance', 'df.residual',
'model.matrix', 'terms', 'weights', 'merge', 'split', 'window'
]);
let knownS3Generics;
/**
* Whether a definition named `name.class` may be an S3 method: `name` is a built-in flowR labels
* {@link CallProp.Generic} or one of {@link OtherKnownS3Generics}. Such a method is dispatched indirectly
* (`print(x)` on an object of that class), so it is used without a textual call.
*/
function isKnownS3Generic(name) {
knownS3Generics ??= new Set([...OtherKnownS3Generics, ...Object.keys(default_builtin_config_1.RGroupGenerics),
...query_fn_props_1.BuiltInIndex.default().with(built_in_props_1.CallProp.Generic).map(g => identifier_1.Identifier.getName(g))]);
return knownS3Generics.has(name);
}
/** Whether `generic`, or any member of it when it is a group generic (`Ops.cls` dispatches on `+`), is called. */
function isDispatched(generic, called) {
const group = default_builtin_config_1.RGroupGenerics[generic];
return called.has(generic) || (group?.some(member => called.has(member)) ?? false);
}
/**
* R package lifecycle hooks called automatically by R's package machinery.
* These functions are invoked by the package system, so they are used even without textual callers.
*/
const PackageHookFunctions = new Set([
'.onLoad', '.onAttach', '.onUnload', '.onDetach', '.Last.lib', '.First.lib'
]);
/** Gathers the analyzed project's own `NAMESPACE` exports and declared S3 generics (empty when it is not a package). */
function collectPackageInfo(data) {
const exported = new Set();
const s3Generics = new Set();
for (const ns of data.inspectContext().files.getFilesByRole(flowr_file_1.FileRole.Namespace)) {
const info = ns.content().current;
for (const name of (0, flowr_namespace_file_1.getExportedNames)(info)) {
exported.add(name);
}
for (const generic of info.exportS3Generics.keys()) {
s3Generics.add(generic);
}
}
return { exported, s3Generics };
}
/** Collects the names of every function call in the graph, so we can tell whether an S3 generic is dispatched anywhere. */
function collectCalledNames(dfg) {
const names = new Set();
for (const [, vertex] of dfg.verticesOfType(vertex_1.VertexType.FunctionCall)) {
names.add(identifier_1.Identifier.getName(vertex.name));
}
return names;
}
/** S3 (`UseMethod`) and S4/S7 (`standardGeneric`) generic dispatchers, invoked indirectly by R's dispatch. */
const GenericDispatchers = new Set(['UseMethod', 'standardGeneric']);
/** Whether a function body is a single call to a generic dispatcher. */
function isGenericDispatcherOnlyBody(node) {
if (r_function_call_1.RFunctionCall.isNamed(node) && GenericDispatchers.has(identifier_1.Identifier.getName(node.functionName.content))) {
return true;
}
const nodeWithChildren = node;
if (Array.isArray(nodeWithChildren.children) && nodeWithChildren.children.length === 1) {
const child = nodeWithChildren.children[0];
if (child && r_function_call_1.RFunctionCall.isNamed(child) && GenericDispatchers.has(identifier_1.Identifier.getName(child.functionName.content))) {
return true;
}
}
return false;
}
/** Collects the parameter IDs of generic dispatcher functions. */
function collectS3GenericParameterIds(ast) {
const paramIds = new Set();
for (const [, node] of ast.idMap) {
if (!r_function_definition_1.RFunctionDefinition.is(node)) {
continue;
}
if (isGenericDispatcherOnlyBody(node.body)) {
for (const param of node.parameters) {
paramIds.add(param.name.info.id);
}
}
}
return paramIds;
}
/**
* A definition is treated as used (and hence not reported) if it is the dots parameter, a package lifecycle hook,
* an S3 method for a dispatched generic, or - when {@link UnusedDefinitionConfig#excludeExportedDefinitions} is set -
* a package export.
*/
function isConsideredUsed(lexeme, config, pkg, called) {
if (lexeme === undefined) {
return false;
}
// non-syntactic definition names (e.g. S3 methods like `"[.irts"`) carry their R quotes or backticks in the lexeme
const unquoted = (0, retriever_1.removeRQuotes)(lexeme);
const name = unquoted.length > 1 && unquoted.startsWith('`') && unquoted.endsWith('`') ? unquoted.slice(1, -1) : unquoted;
// the dots are a special parameter and must never be reported
if (name === DotsParameter) {
return true;
}
// package lifecycle hooks are called by R's package machinery
if (PackageHookFunctions.has(name)) {
return true;
}
// every dot may be the one splitting method from class, as the generic may carry dots itself (`as.character.foo`)
for (let dot = name.indexOf('.'); dot > 0; dot = name.indexOf('.', dot + 1)) {
const generic = name.slice(0, dot);
if (isKnownS3Generic(generic) || pkg.s3Generics.has(generic) || isDispatched(generic, called)) {
return true;
}
}
if (config.excludeExportedDefinitions && pkg.exported.has(name)) {
return true;
}
return false;
}
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) ?? graph_1.NoEdges))
.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
});
}
/** Whether the node sits inside a promise, i.e. an argument default value or a `delayedAssign` body, which may never run. */
function isWithinPromise(node, idMap) {
let child = node;
let parentId = node.info.parent;
while (parentId !== undefined) {
const parent = idMap.get(parentId);
if (parent === undefined) {
return false;
}
if (parent.type === type_1.RType.Parameter && parent.defaultValue?.info.id === child.info.id) {
return true;
}
if (parent.type === type_1.RType.FunctionCall && parent.named && identifier_1.Identifier.getName(parent.functionName.content) === 'delayedAssign') {
return true;
}
child = parent;
parentId = parent.info.parent;
}
return false;
}
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.F.or(vertex_1.VertexType.VariableDefinition, vertex_1.VertexType.FunctionDefinition) : vertex_1.VertexType.VariableDefinition),
processSearchResult: async (elements, config, data) => {
const normalize = await data.normalize();
const dataflow = await data.dataflow();
const packageInfo = collectPackageInfo(data);
const calledNames = collectCalledNames(dataflow.graph);
const s3GenericParams = collectS3GenericParameterIds(normalize);
const metadata = {
totalConsidered: 0
};
return {
results: onlyKeepSupersetOfUnused(elements.getElements().flatMap(element => {
metadata.totalConsidered++;
if (isWithinPromise(element.node, normalize.idMap)) {
return [];
}
const dfgVertex = dataflow.graph.getVertex(element.node.info.id);
if (!dfgVertex || (!vertex_1.VariableDefinitionVertex.is(dfgVertex)
&& vertex_1.FunctionDefinitionVertex.is(dfgVertex) && !config.includeFunctionDefinitions)) {
return undefined;
}
if (s3GenericParams.has(element.node.info.id)) {
return undefined;
}
// an anonymous dispatcher passed to setGeneric()/new_generic() runs on every dispatch, so it is used
if (vertex_1.FunctionDefinitionVertex.is(dfgVertex) && r_function_definition_1.RFunctionDefinition.is(element.node) && isGenericDispatcherOnlyBody(element.node.body)) {
return undefined;
}
if (isConsideredUsed(element.node.lexeme, config, packageInfo, calledNames)) {
return undefined;
}
const ingoingEdges = dataflow.graph.ingoingEdges(dfgVertex.id);
const interestedIn = vertex_1.VariableDefinitionVertex.is(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, dataflow.graph, 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,
excludeExportedDefinitions: true
}
}
};
//# sourceMappingURL=unused-definition.js.map