UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

207 lines 12.9 kB
"use strict"; 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