@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
207 lines • 12.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.UNDEFINED_SYMBOL = void 0;
const vertex_1 = require("../../dataflow/graph/vertex");
const graph_1 = require("../../dataflow/graph/graph");
const identifier_1 = require("../../dataflow/environments/identifier");
const flowr_search_builder_1 = require("../../search/flowr-search-builder");
const flowr_search_filters_1 = require("../../search/flowr-search-filters");
const assert_1 = require("../../util/assert");
const range_1 = require("../../util/range");
const flowr_file_1 = require("../../project/context/flowr-file");
const type_1 = require("../../r-bridge/lang-4.x/ast/model/type");
const r_base_packages_1 = require("../../util/r-base-packages");
const linter_format_1 = require("../linter-format");
const linter_tags_1 = require("../linter-tags");
const undefined_symbol_util_1 = require("./undefined-symbol-util");
const df_helper_1 = require("../../dataflow/graph/df-helper");
/** calls that load a package; an unresolved one means we cannot enumerate all exports in scope */
const LibraryLoadFunctions = new Set(['library', 'require', 'requireNamespace', 'loadNamespace', 'attachNamespace', 'load_all', 'use', 'p_load']);
// standard packages attached by default; exports are in scope without library()
/** test frameworks whose namespace a test file runs under implicitly (e.g. `tests/testthat/test-*.R` sees testthat's exports without a `library()`) */
const ImplicitTestFrameworks = new Set(['testthat', 'tinytest', 'RUnit']);
/** whether name is an export of a default-attached base package (needs no library()) */
function isAttachedBaseName(name) {
const owner = (0, r_base_packages_1.baseRExportOwner)(name);
return owner !== undefined && r_base_packages_1.AttachedBasePackages.includes(owner);
}
/** upper bound on the number of packages named in a "did you forget to load it" hint */
const MaxHintPackages = 5;
/**
* Flags function calls (`sd()`) and, opt-in, variable reads (`x`) that are neither defined locally, a
* builtin, nor exported by a package in scope - the DESCRIPTION/`library()` packages plus the default-attached
* base packages, all resolved from the `flowr-sigdb` database. To stay precise it consults flowR's dataflow:
* non-standard evaluation (quoting) is not reported, and an unloaded package that exports the name is offered
* as a hint. Over-approximative: NSE beyond what flowR models can still cause false positives.
*/
exports.UNDEFINED_SYMBOL = {
createSearch: (_config) => flowr_search_builder_1.Q.all().filter(flowr_search_filters_1.F.or(vertex_1.VertexType.FunctionCall, vertex_1.VertexType.Use)),
processSearchResult: async (elements, config, data) => {
const graph = (await data.dataflow()).graph;
const ctx = data.inspectContext();
const deps = ctx.deps;
const meta = {
totalFunctionCalls: 0,
totalVariableUses: 0,
suppressed: { installed: 0, loadedPackage: 0, enclosingScope: 0, nonStandardEval: 0, subscript: 0 }
};
// `inst/` files are installed verbatim (resources, not namespace source); skip them. Key off the
// FileRole.Install role, with a path fallback for requests that bypass the file-role plugins.
const installedFiles = new Set(ctx.files.getFilesByRole(flowr_file_1.FileRole.Install).map(f => f.path()));
const isInstalledFile = (file) => file !== undefined && (installedFiles.has(file) || (0, undefined_symbol_util_1.isInstalledResourceFile)(file));
// test files run under their framework's attached namespace, so a bare name a test framework exports is defined there
const testFiles = new Set(ctx.files.getFilesByRole(flowr_file_1.FileRole.Test).map(f => f.path()));
const isImplicitTestExport = (name, file) => file !== undefined && testFiles.has(file) && deps.packagesExporting(name).some(p => ImplicitTestFrameworks.has(p));
// a library() we could not resolve could export any of these symbols; we still report but flag the
// findings as low-confidence (`mayBeProvidedByUnresolvedLibrary`) so the severity can be lowered
const unknownIds = new Set();
for (const e of graph.unknownSideEffects) {
unknownIds.add(graph_1.UnknownSideEffect.id(e));
}
let unresolvedLibraryInScope = false;
if (unknownIds.size > 0) {
for (const [id, v] of graph.verticesOfType(vertex_1.VertexType.FunctionCall)) {
if (LibraryLoadFunctions.has(identifier_1.Identifier.getName(v.name)) && unknownIds.has(id)) {
unresolvedLibraryInScope = true;
break;
}
}
}
// scope-defined names, a fallback for closure variables flowR's dataflow did not statically link
const scopeDefined = (0, undefined_symbol_util_1.collectScopeDefinedNames)(graph);
// packages whose exports are in scope: DESCRIPTION/library() deps plus the default-attached base
// packages, each resolved (exports enriched) from the package database via getDependency
const loadedPackages = [
...deps.getDependencies().map(p => deps.getDependency(p.name) ?? p),
...r_base_packages_1.AttachedBasePackages.map(n => deps.getDependency(n)).filter(assert_1.isNotUndefined)
];
const exportedByLoaded = new Map();
const isExportedByLoadedPackage = (name) => {
let known = exportedByLoaded.get(name);
if (known === undefined) {
known = loadedPackages.some(p => p.has(name));
exportedByLoaded.set(name, known);
}
return known;
};
// hint an unloaded package that exports `name` (e.g. `ggplot` -> `ggplot2`), from the package database
const attached = new Set(r_base_packages_1.AttachedBasePackages);
const hintPackagesFor = (name) => deps.packagesExporting(name).filter(p => !attached.has(p)).slice(0, MaxHintPackages);
const results = elements.getElements().map(element => {
const id = element.node.info.id;
const vtx = graph.getVertex(id);
if (!vtx) {
return undefined;
}
const inInstalledFile = isInstalledFile(element.node.info.file);
if (vertex_1.FunctionCallVertex.is(vtx)) {
if (vtx.origin === 'unnamed' || !config.checkFunctions) {
return undefined;
}
meta.totalFunctionCalls++;
if (inInstalledFile) {
meta.suppressed.installed++;
return undefined;
}
return check(element, id, identifier_1.Identifier.getName(vtx.name), identifier_1.Identifier.getNamespace(vtx.name), 'function');
}
// variable use: only plain symbols (not argument names, `...`, or empty)
if (vertex_1.UseVertex.is(vtx) && config.checkVariables) {
const node = element.node;
if (node.type !== type_1.RType.Symbol || node.lexeme === '...' || node.lexeme === undefined) {
return undefined;
}
meta.totalVariableUses++;
if (inInstalledFile) {
meta.suppressed.installed++;
return undefined;
}
return check(element, id, node.lexeme, undefined, 'variable');
}
return undefined;
}).filter(assert_1.isNotUndefined);
/** shared resolution logic for a name used either as a function call or a variable */
function check(element, id, name, namespace, kind) {
// resolved by flowR itself (local/param/builtin) - never a candidate, so not counted as suppressed
if (kind === 'variable' ? (0, undefined_symbol_util_1.useResolvesToDefinitionOrBuiltin)(graph, id) : (df_helper_1.Dataflow.origin(graph, id)?.length ?? 0) > 0) {
return undefined;
}
// a bare call to a registered flowR builtin whose dataflow origin was rewritten away from its
// builtin marker (e.g. a fully-resolved `UseMethod` dispatch is re-tagged as a plain function
// call) is still defined, so recognise it directly from the built-in environment
if (kind === 'function' && namespace === undefined && ctx.env.builtInEnvironment.memory.has(name)) {
return undefined;
}
// a bare name from a default-attached base package (or a primitive) is defined without a library() call
if (namespace === undefined && isAttachedBaseName(name)) {
return undefined;
}
// a bare name a test framework exports, used in a test file where that framework's namespace is attached
if (namespace === undefined && isImplicitTestExport(name, element.node.info.file)) {
return suppress('loadedPackage');
}
// exported by a package in scope (`pkg::fn`, a loaded package, or a default-attached base package)
if (namespace !== undefined ? deps.getDependency(namespace)?.has(name) === true : isExportedByLoadedPackage(name)) {
return suppress('loadedPackage');
}
// forward-referenced closure binding flowR's dataflow did not link (unconditional bindings only)
if ((0, undefined_symbol_util_1.isDefinedInEnclosingScope)(graph, scopeDefined, id, name)) {
return suppress('enclosingScope');
}
// consumed by non-standard evaluation (quoting), hence not an ordinary read
if ((0, undefined_symbol_util_1.isNonStandardEvaluated)(graph, id)) {
return suppress('nonStandardEval');
}
// `[`/`[[` subscript: muted by default (indistinguishable from `data.table` column masking)
if (kind === 'variable' && !config.checkSubscripts && (0, undefined_symbol_util_1.isInSubscript)(graph, id)) {
return suppress('subscript');
}
const loc = range_1.SourceLocation.fromNode(element.node);
if (loc === undefined) {
return undefined;
}
const availableInPackages = namespace === undefined ? hintPackagesFor(name) : [];
return {
certainty: linter_format_1.LintingResultCertainty.Uncertain,
name,
kind,
involvedId: id,
loc,
...(availableInPackages.length > 0 ? { availableInPackages } : {}),
...(unresolvedLibraryInScope ? { mayBeProvidedByUnresolvedLibrary: true } : {})
};
}
function suppress(reason) {
meta.suppressed[reason]++;
return undefined;
}
return { results, '.meta': meta };
},
prettyPrint: {
[linter_format_1.LintingPrettyPrintContext.Query]: result => `${result.mayBeProvidedByUnresolvedLibrary ? 'Possibly undefined' : 'Undefined'} ${result.kind} \`${result.name}\` at ${range_1.SourceLocation.format(result.loc)}`,
[linter_format_1.LintingPrettyPrintContext.Full]: result => {
const where = range_1.SourceLocation.format(result.loc);
const caveat = result.mayBeProvidedByUnresolvedLibrary ? ' (an unresolved library is loaded that might provide it)' : '';
if (result.availableInPackages && result.availableInPackages.length > 0) {
const pkgs = result.availableInPackages.map(p => `\`${p}\``).join(', ');
const hint = result.availableInPackages.length === 1 ? `\`library(${result.availableInPackages[0]})\`` : `one of ${pkgs}`;
return `\`${result.name}\` at ${where} is used but not defined; it is exported by ${pkgs} - did you forget to load it (e.g. ${hint})?${caveat}`;
}
return result.kind === 'function'
? `\`${result.name}\` at ${where} is called but is neither defined locally, a base R builtin, nor exported by a loaded package${caveat}`
: `\`${result.name}\` at ${where} is used as a variable but is never defined in scope${caveat}`;
}
},
info: {
name: 'Undefined Symbol',
certainty: linter_format_1.LintingRuleCertainty.OverApproximative,
description: 'Flags functions and variables that are neither defined locally, a base R builtin, nor exported by a loaded package.',
tags: [linter_tags_1.LintingRuleTag.Bug, linter_tags_1.LintingRuleTag.Experimental],
defaultConfig: {
checkFunctions: true,
checkVariables: true,
checkSubscripts: false
}
}
};
//# sourceMappingURL=undefined-symbol.js.map