@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
192 lines • 9.71 kB
JavaScript
"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