UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

192 lines 9.71 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FILE_PATH_VALIDITY = void 0; const linter_format_1 = require("../linter-format"); const strings_1 = require("../../util/text/strings"); const flowr_search_builder_1 = require("../../search/flowr-search-builder"); const range_1 = require("../../util/range"); const dependencies_query_format_1 = require("../../queries/catalog/dependencies-query/dependencies-query-format"); const built_in_source_1 = require("../../dataflow/internal/process/functions/call/built-in/built-in-source"); const logic_1 = require("../../util/logic"); const happens_before_1 = require("../../control-flow/happens-before"); const linter_tags_1 = require("../linter-tags"); const search_enrichers_1 = require("../../search/search-executor/search-enrichers"); const resolve_working_directory_1 = require("../../dataflow/eval/resolve/resolve-working-directory"); const config_1 = require("../../config"); const type_1 = require("../../r-bridge/lang-4.x/ast/model/type"); const r_function_call_1 = require("../../r-bridge/lang-4.x/ast/model/nodes/r-function-call"); const r_string_1 = require("../../r-bridge/lang-4.x/ast/model/nodes/r-string"); async function urlExists(url) { try { const response = await fetch(url, { method: 'HEAD' }); return response.ok; } catch { return false; } } exports.FILE_PATH_VALIDITY = { createSearch: (config) => flowr_search_builder_1.Q.fromQuery({ type: 'dependencies', enabledCategories: ['read', 'write'], readFunctions: config.additionalReadFunctions, writeFunctions: config.additionalWriteFunctions }).with(search_enrichers_1.Enrichment.CfgInformation), processSearchResult: async (elements, config, data) => { const cfg = elements.enrichmentContent(search_enrichers_1.Enrichment.CfgInformation).cfg.graph; const metadata = { totalReads: 0, totalUnknown: 0, totalWritesBeforeAlways: 0, totalValid: 0 }; const results = elements.enrichmentContent(search_enrichers_1.Enrichment.QueryData).queries['dependencies']; const ctx = data.inspectContext(); const dfg = (await data.dataflow()).graph; const resolveSource = data.flowrConfig.solver.resolveSource; const wdRootsFor = resolve_working_directory_1.WorkingDirectory.rootsResolver(dfg, cfg, ctx); const findings = await Promise.all(elements.getElements().map(async (element) => { const matchingRead = results.read.find(r => r.nodeId === element.node.info.id); if (!matchingRead) { return []; } metadata.totalReads++; const loc = range_1.SourceLocation.fromNode(element.node); if (!loc) { return []; } // check if we can't parse the file path statically if (matchingRead.value === dependencies_query_format_1.Unknown) { metadata.totalUnknown++; if (config.includeUnknown) { return [{ involvedId: matchingRead.nodeId, loc, filePath: dependencies_query_format_1.Unknown, certainty: linter_format_1.LintingResultCertainty.Uncertain }]; } else { return []; } } // file:// URIs are local paths; resolve and check existence directly const localFromFileUrl = (0, strings_1.fileUrlToPath)(matchingRead.value); if (localFromFileUrl !== undefined) { const paths = (0, built_in_source_1.findSource)(data.flowrConfig.solver.resolveSource, localFromFileUrl, { referenceChain: element.node.info.file ? [element.node.info.file] : [], ctx: data.inspectContext() }); if (paths && paths.length) { metadata.totalValid++; return []; } return [{ involvedId: matchingRead.nodeId, loc, filePath: localFromFileUrl, certainty: linter_format_1.LintingResultCertainty.Certain }]; } // handle remote URLs separately from file paths if ((0, strings_1.isUrl)(matchingRead.value)) { if (!config.checkUrls) { return []; } const exists = await urlExists(matchingRead.value); if (exists) { metadata.totalValid++; return []; } return [{ involvedId: matchingRead.nodeId, loc, filePath: matchingRead.value, certainty: linter_format_1.LintingResultCertainty.Uncertain }]; } // check if any write to the same file happens before the read, and exclude this case if so const writesToFile = results.write.filter(r => samePath(r.value, matchingRead.value, data.flowrConfig.solver.resolveSource?.ignoreCapitalization)); const writesBefore = writesToFile.map(w => (0, happens_before_1.happensBefore)(cfg, w.nodeId, element.node.info.id)); if (writesBefore.includes(logic_1.Ternary.Always)) { metadata.totalWritesBeforeAlways++; return []; } // check if the file exists, resolving relative paths against the effective working directory const value = matchingRead.value; const referenceChain = element.node.info.file ? [element.node.info.file] : []; const wdRoots = wdRootsFor(element.node.info.id, element.node.info.file); const withWd = { ...resolveSource, searchPath: [...(resolveSource?.searchPath ?? []), ...wdRoots] }; const paths = (0, built_in_source_1.findSource)(withWd, value, { referenceChain, ctx }); if (paths && paths.length) { metadata.totalValid++; return []; } // a lax retry (drop leading dirs, ignore case) may surface a near match to offer as a quick fix const near = (0, built_in_source_1.findSource)({ ...withWd, ignoreCapitalization: true, dropPaths: config_1.DropPathsOption.All }, value, { referenceChain, ctx }); const quickFix = dfg.idMap && near && near.length ? buildDidYouMeanFix(dfg.idMap, matchingRead.nodeId, value, near[0]) : undefined; return [{ involvedId: matchingRead.nodeId, loc, filePath: value, certainty: writesBefore && writesBefore.length && writesBefore.every(w => w === logic_1.Ternary.Maybe) ? linter_format_1.LintingResultCertainty.Uncertain : linter_format_1.LintingResultCertainty.Certain, ...(quickFix ? { quickFix } : {}) }]; })); return { results: findings.flat(), '.meta': metadata }; }, info: { name: 'File Path Validity', description: 'Checks whether file paths used in read and write operations are valid and point to existing files.', // checks all found paths for whether they're valid to ensure correctness, but doesn't handle non-constant paths so not all will be returned certainty: linter_format_1.LintingRuleCertainty.BestEffort, tags: [linter_tags_1.LintingRuleTag.Robustness, linter_tags_1.LintingRuleTag.Reproducibility, linter_tags_1.LintingRuleTag.Bug, linter_tags_1.LintingRuleTag.QuickFix], defaultConfig: { additionalReadFunctions: [], additionalWriteFunctions: [], includeUnknown: false, checkUrls: false } }, prettyPrint: { [linter_format_1.LintingPrettyPrintContext.Query]: result => `Path \`${result.filePath}\` at ${range_1.SourceLocation.format(result.loc)}`, [linter_format_1.LintingPrettyPrintContext.Full]: result => `Path \`${result.filePath}\` at ${range_1.SourceLocation.format(result.loc)} does not point to a valid file` } }; function samePath(a, b, ignoreCapitalization) { if (ignoreCapitalization === true) { a = a.toLowerCase(); b = b.toLowerCase(); } return a === b; } /** the string-literal path argument of a call whose resolved value is `value`, for anchoring a quick fix */ function pathArgStringNode(idMap, callId, value) { const call = idMap.get(callId); if (call?.type !== type_1.RType.FunctionCall) { return undefined; } for (const arg of call.arguments) { if (arg !== r_function_call_1.EmptyArgument && arg.value && r_string_1.RString.is(arg.value) && arg.value.content.str === value) { return arg.value; } } return undefined; } /** rewrite the read path to `found`, an existing file surfaced by the lax retry */ function buildDidYouMeanFix(idMap, callId, value, found) { const str = pathArgStringNode(idMap, callId, value); if (!str) { return undefined; } return [{ type: 'replace', loc: range_1.SourceLocation.fromNode(str) ?? range_1.SourceLocation.invalid(), description: `Replace with existing path \`${found}\``, replacement: str.content.quotes + found + str.content.quotes }]; } //# sourceMappingURL=file-path-validity.js.map