@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
213 lines • 12.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LinterQueryDefinition = void 0;
const joi_1 = __importDefault(require("joi"));
const linter_query_executor_1 = require("./linter-query-executor");
const linter_rules_1 = require("../../../linter/linter-rules");
const linter_format_1 = require("../../../linter/linter-format");
const ansi_1 = require("../../../util/text/ansi");
const time_1 = require("../../../util/text/time");
const retriever_1 = require("../../../r-bridge/retriever");
const assert_1 = require("../../../util/assert");
const fs_1 = __importDefault(require("fs"));
const linter_output_1 = require("../../../linter/linter-output");
function rulesFromInput(output, rulesPart) {
return rulesPart
.reduce((acc, ruleName) => {
ruleName = ruleName.trim();
if (ruleName in linter_rules_1.LintingRules) {
acc.valid.push(ruleName);
}
else {
acc.invalid.push(ruleName);
}
return acc;
}, { valid: [], invalid: [] });
}
const rulesPrefix = 'rules:';
const formatPrefix = 'format:';
/** the {@link LinterOutputFormat} of a `format:` argument, warning about an unknown one instead of ignoring it */
function formatFromInput(output, argument) {
const wanted = argument.slice(formatPrefix.length).trim();
const format = Object.values(linter_output_1.LinterOutputFormat).find(f => f === wanted);
if (format === undefined) {
output.stderr(`Invalid linting format ${(0, ansi_1.bold)(wanted, output.formatter)}, expected one of `
+ Object.values(linter_output_1.LinterOutputFormat).map(f => (0, ansi_1.bold)(f, output.formatter)).join(', '));
}
return format;
}
function linterQueryLineParser(output, line, _config) {
let rules = undefined;
let format = undefined;
const rest = [...line];
while (rest.length > 0 && (rest[0].startsWith(rulesPrefix) || rest[0].startsWith(formatPrefix))) {
const argument = rest.shift();
if (argument.startsWith(formatPrefix)) {
format = formatFromInput(output, argument);
continue;
}
const parseResult = rulesFromInput(output, argument.slice(rulesPrefix.length).split(','));
if (parseResult.invalid.length > 0) {
output.stderr(`Invalid linting rule name(s): ${parseResult.invalid.map(r => (0, ansi_1.bold)(r, output.formatter)).join(', ')}`
+ `\nValid rule names are: ${Object.keys(linter_rules_1.LintingRules).map(r => (0, ansi_1.bold)(r, output.formatter)).join(', ')}`);
}
rules = parseResult.valid;
}
/* an absent format must not show up as a key, a query is compared by its fingerprint */
return { query: [{ type: 'linter', rules, ...(format ? { format } : {}) }], rCode: rest.join(' ').trim() || undefined };
}
function linterQueryCompleter(line, startingNewArg, _config) {
const current = startingNewArg ? '' : line[line.length - 1] ?? '';
if (current.startsWith(formatPrefix)) {
const wanted = current.slice(formatPrefix.length);
return { completions: Object.values(linter_output_1.LinterOutputFormat).filter(f => f !== wanted), argumentPart: wanted };
}
else if (current.startsWith(rulesPrefix)) {
const usedRules = current.slice(rulesPrefix.length).split(',').map(r => r.trim());
const allRules = Object.keys(linter_rules_1.LintingRules);
const unusedRules = allRules.filter(r => !usedRules.includes(r));
const lastRule = usedRules[usedRules.length - 1];
const lastRuleIsUnfinished = !allRules.includes(lastRule);
if (lastRuleIsUnfinished) {
// Return all rules that have not been added yet
return { completions: unusedRules, argumentPart: lastRule };
}
else if (unusedRules.length > 0) {
// Add a comma, if the current last rule is complete
return { completions: [','], argumentPart: '' };
}
else {
// All rules are used, complete with a space
return { completions: [' '], argumentPart: '' };
}
}
/* both are optional and may come in any order, so offer whatever is not given yet */
const given = startingNewArg ? line : line.slice(0, -1);
return { completions: [
...given.some(a => a.startsWith(rulesPrefix)) ? [] : [rulesPrefix],
...given.some(a => a.startsWith(formatPrefix)) ? [] : [formatPrefix],
retriever_1.fileProtocol
] };
}
exports.LinterQueryDefinition = {
title: 'Linter Query',
executor: linter_query_executor_1.executeLinterQuery,
asciiSummarizer: (formatter, analyzer, queryResults, result) => {
const out = queryResults;
/* a machine-readable format is the whole output: a consumer must not have to strip a summary around it */
if (out.formatted !== undefined) {
result.push(out.formatted);
return true;
}
result.push(`Query: ${(0, ansi_1.bold)('linter', formatter)} (${(0, time_1.printAsMs)(out['.meta'].timing, 0)})`);
const allDidFail = Object.values(out.results).every(linter_format_1.LintingResults.isError);
if (allDidFail) {
result.push('All linting rules failed to execute.');
const files = analyzer.inspectContext().files;
if (files.loadingOrder.getUnorderedRequests().length === 0) {
const missing = files.getRequestedRoots().filter(p => !fs_1.default.existsSync(p));
if (missing.length > 0) {
result.push(formatter.format(`Path does not exist: ${missing.map(p => `'${p}'`).join(', ')}`, { color: 1 /* Colors.Red */, effect: ansi_1.ColorEffect.Foreground, style: 1 /* FontStyles.Bold */ }));
return true;
}
result.push(formatter.format('No requests to lint for were found in the analysis.', { color: 1 /* Colors.Red */, effect: ansi_1.ColorEffect.Foreground, style: 1 /* FontStyles.Bold */ }));
result.push('If you consider this an error, please report a bug: ' + (0, assert_1.getGuardIssueUrl)('analyzer found no requests to lint for'));
}
else if (Object.values(out.results).length === 1) {
const fst = Object.values(out.results)[0];
result.push('Error: ' + linter_format_1.LintingResults.stringifyError(fst));
if (fst.error instanceof Error) {
// print stack
result.push('Stack Trace:\n' + fst.error.stack);
}
}
result.push('If you consider this an error that should be fixed, please report a bug: ' + (0, assert_1.getGuardIssueUrl)('linting rule threw an error'));
return true;
}
for (const [ruleName, results] of Object.entries(out.results)) {
addLintingRuleResult(ruleName, results, result, formatter);
}
return true;
},
completer: linterQueryCompleter,
fromLine: linterQueryLineParser,
syntax: '@linter [rules:<r1>,<r2>,...] [format:<fmt>] <code | file://path>',
schema: joi_1.default.object({
type: joi_1.default.string().valid('linter').required().description('The type of the query.'),
format: joi_1.default.string().valid(...Object.values(linter_output_1.LinterOutputFormat)).optional().description('Print the findings in a machine-readable format instead of the human-readable summary.'),
rules: joi_1.default.array().items(joi_1.default.string().valid(...Object.keys(linter_rules_1.LintingRules)), joi_1.default.object({
name: joi_1.default.string().valid(...Object.keys(linter_rules_1.LintingRules)).required(),
config: joi_1.default.object()
})).description('The rules to lint for. If unset, all rules will be included.'),
}).description('The linter query lints for the given set of rules and returns the result.'),
flattenInvolvedNodes: (queryResults, _queries, certainty) => {
const out = queryResults;
return Object.values(out.results).flatMap(v => {
if (linter_format_1.LintingResults.isError(v)) {
return [];
}
const rows = certainty !== undefined ? v.results.filter(r => r.certainty === certainty) : v.results;
return rows.flatMap(r => r.involvedId);
}).filter(assert_1.isNotUndefined);
}
};
/** cap on findings shown per certainty before collapsing to a `+N more` line; the full set is in `:query*` JSON */
const MaxFindingsShown = 10;
function addLintingRuleResult(ruleName, results, result, formatter) {
const rule = linter_rules_1.LintingRules[ruleName];
const header = `${(0, ansi_1.bold)(rule.info.name, formatter)} (${ruleName})`;
if (linter_format_1.LintingResults.isError(results)) {
const error = linter_format_1.LintingResults.stringifyError(results).includes('At least one request must be set') ? 'No requests to lint for were found in the analysis.' : 'Error during execution of rule: ' + linter_format_1.LintingResults.stringifyError(results);
result.push(` ╰ ${header}:`);
result.push(` ╰ ${error}`);
return;
}
// a rule with no findings collapses to a single line (no per-certainty block, no metadata)
if (results.results.length === 0) {
result.push(` ╰ ${header}: ${(0, ansi_1.italic)('no findings', formatter)}`);
return;
}
result.push(` ╰ ${header}:`);
for (const certainty of [linter_format_1.LintingResultCertainty.Certain, linter_format_1.LintingResultCertainty.Uncertain]) {
const certaintyResults = results.results.filter(r => r.certainty === certainty);
if (certaintyResults.length) {
result.push(` ╰ ${certainty}:`);
for (const res of certaintyResults.slice(0, MaxFindingsShown)) {
const pretty = rule.prettyPrint[linter_format_1.LintingPrettyPrintContext.Query](res, results['.meta']);
result.push(` ╰ ${hyperlinkLocations(pretty, formatter)}${res.quickFix ? ` (${res.quickFix.length} quick fix(es) available)` : ''}`);
}
if (certaintyResults.length > MaxFindingsShown) {
result.push(` ╰ ${(0, ansi_1.italic)(`… +${certaintyResults.length - MaxFindingsShown} more (:query* for the full JSON)`, formatter)}`);
}
}
}
result.push(` ╰ ${(0, ansi_1.italic)('Metadata', formatter)}: ${renderMetaData(results['.meta'])}`);
}
/**
* Matches an absolute file path (POSIX `/...` or Windows `X:\...`) with an extension, followed by `:` and a flowR
* position `<line>(.<col>)?(-<endline>.<endcol>)?`. Used to turn linting finding locations into clickable links.
*/
const locationPattern = /((?:[A-Za-z]:\\|\/)[^\s:]*\.[A-Za-z]+):(\d+(?:\.\d+)?(?:-\d+(?:\.\d+)?)?)/g;
/** Wrap every `path:loc` location in the given pretty string in a `file://path:line:col` hyperlink via the formatter. */
function hyperlinkLocations(pretty, formatter) {
return pretty.replace(locationPattern, (match, path, position) => {
const [line, col] = position.split('-')[0].split('.');
const url = `file://${path}:${line}${col ? `:${col}` : ''}`;
return formatter.hyperlink(match, url, true);
});
}
function renderMetaData(metadata) {
return Object.entries(metadata).map(([k, v]) => `${k}: ${renderMetaValue(v)}`).join(', ');
}
/** Render a metadata value; a nested object (e.g. suppression counts) becomes `(key=value, ...)`, omitting zero counts (`0` if all zero). */
function renderMetaValue(value) {
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
const nonZero = Object.entries(value).filter(([, v]) => v !== 0);
return nonZero.length === 0 ? '0' : `(${nonZero.map(([k, v]) => `${k}=${renderMetaValue(v)}`).join(', ')})`;
}
return JSON.stringify(value);
}
//# sourceMappingURL=linter-query-format.js.map