UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

277 lines 14.7 kB
"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