UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

213 lines 12.2 kB
"use strict"; 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