@eagleoutice/flowr
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
294 lines (258 loc) • 14.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const log_1 = require("../../test/functionality/_helper/log");
const doc_auto_gen_1 = require("./doc-util/doc-auto-gen");
const doc_files_1 = require("./doc-util/doc-files");
const linter_rules_1 = require("../linter/linter-rules");
const doc_code_1 = require("./doc-util/doc-code");
const shell_1 = require("../r-bridge/shell");
const doc_query_1 = require("./doc-util/doc-query");
const doc_types_1 = require("./doc-util/doc-types");
const path_1 = __importDefault(require("path"));
const doc_repl_1 = require("./doc-util/doc-repl");
const doc_structure_1 = require("./doc-util/doc-structure");
const linter_tags_1 = require("../linter/linter-tags");
const html_hover_over_1 = require("../util/html-hover-over");
const strings_1 = require("../util/text/strings");
const assert_1 = require("../util/assert");
const doc_print_1 = require("./doc-util/doc-print");
const doc_functions_1 = require("./doc-util/doc-functions");
const linter_format_1 = require("../linter/linter-format");
const SpecialTagColors = {
[linter_tags_1.LintingRuleTag.Bug]: 'red',
[linter_tags_1.LintingRuleTag.Security]: 'orange',
[linter_tags_1.LintingRuleTag.Smell]: 'yellow',
[linter_tags_1.LintingRuleTag.QuickFix]: 'lightgray'
};
function makeTagBadge(name, info) {
const doc = (0, doc_types_1.getDocumentationForType)('LintingRuleTag::' + name, info, '', true).replaceAll('\n', ' ');
return (0, html_hover_over_1.textWithTooltip)(`<a href='#${name}'> + `-${SpecialTagColors[name] ?? 'teal'}) </a>`, doc);
}
function prettyPrintExpectedOutput(expected) {
if (expected.trim() === '[]') {
return '* no lints';
}
let lines = expected.trim().split('\n');
if (lines.length <= 1) {
return expected;
}
//
lines = expected.trim().replace(/^\s*\[+\s*{*/m, '').replace(/\s*}*\s*]+\s*$/, '').split('\n').filter(l => l.trim() !== '');
/* take the indentation of the last line and remove it from all but the first: */
const indentation = lines[lines.length - 1].match(/^\s*/)?.[0] ?? '';
return lines.map((line, i) => {
if (i === 0) {
return line;
}
return line.replace(new RegExp('^' + indentation, 'g'), '');
}).join('\n');
}
function buildSamplesFromLinterTestCases(shell, testFile) {
const reports = (0, doc_functions_1.getFunctionsFromFolder)({ files: [path_1.default.resolve('test/functionality/linter/' + testFile)], fname: /assertLinter/ });
if (reports.info.length === 0) {
return '';
}
let result = `#### Additional Examples
These examples are synthesized from the test cases in: ${(0, doc_files_1.linkFlowRSourceFile)('test/functionality/linter/' + testFile)}\n\n`;
for (const report of reports.info) {
const args = report.arguments;
if (args.length < 5) {
console.error('Test case for linter rule ' + report.name + ' does not have enough arguments! Expected at least 5, got ' + args.length);
continue;
}
const testName = args[0].getText(report.source);
if (report.comments?.some(c => c.includes('@ignore-in-wiki'))) {
console.warn(`Skipping test case for linter rule ${testName} (${testFile}) as it is marked with @ignore-in-wiki`);
continue;
}
// drop any quotes around the test name
const testNameClean = testName.replace(/^['"]|['"]$/g, '');
result += `\n${(0, doc_structure_1.section)('Test Case: ' + testNameClean, 4)}
${report.comments ? report.comments.map(c => `> ${c}`).join('\n') + '\n' : ''}
Given the following input:
${(0, doc_code_1.codeBlock)('r', args[2].getText(report.source).replace(/^['"]|['"]$/g, '').replace(/\\n/g, '\n'))}
${args.length >= 7 ? `\nAnd using the following [configuration](#configuration): ${(0, doc_code_1.codeBlock)('ts', prettyPrintExpectedOutput(args[6].getText(report.source)))}` : ''}
We expect the linter to report the following:
${(0, doc_code_1.codeBlock)('ts', prettyPrintExpectedOutput(args[4].getText(report.source)))}
See [here](${(0, doc_types_1.getTypePathLink)({ filePath: report.source.fileName, lineNumber: report.lineNumber })}) for the test-case implementation.
`;
}
return result;
}
function registerRules(rVersion, shell, tagTypes, format = 'short') {
const ruleExplanations = new Map();
rule(shell, 'deprecated-functions', 'DeprecatedFunctionsConfig', 'DEPRECATED_FUNCTIONS', 'lint-deprecated-functions', `
first <- data.frame(x = c(1, 2, 3), y = c(1, 2, 3))
second <- data.frame(x = c(1, 3, 2), y = c(1, 3, 2))
dplyr::all_equal(first, second)
`, tagTypes);
rule(shell, 'file-path-validity', 'FilePathValidityConfig', 'FILE_PATH_VALIDITY', 'lint-file-path-validity', `
my_data <- read.csv("C:/Users/me/Documents/My R Scripts/Reproducible.csv")
`, tagTypes);
rule(shell, 'absolute-file-paths', 'AbsoluteFilePathConfig', 'ABSOLUTE_PATH', 'lint-absolute-path', `
read.csv("C:/Users/me/Documents/My R Scripts/Reproducible.csv")
`, tagTypes);
rule(shell, 'unused-definitions', 'UnusedDefinitionConfig', 'UNUSED_DEFINITION', 'lint-unused-definition', `
x <- 42
y <- 3
print(x)
`, tagTypes);
rule(shell, 'seeded-randomness', 'SeededRandomnessConfig', 'SEEDED_RANDOMNESS', 'lint-seeded-randomness', 'runif(1)', tagTypes);
rule(shell, 'naming-convention', 'NamingConventionConfig', 'NAMING_CONVENTION', 'lint-naming-convention', `
myVar <- 42
print(myVar)
`, tagTypes);
rule(shell, 'dataframe-access-validation', 'DataFrameAccessValidationConfig', 'DATA_FRAME_ACCESS_VALIDATION', 'lint-dataframe-access-validation', `
df <- data.frame(id = 1:5, name = 6:10)
df[6, "value"]
`, tagTypes);
rule(shell, 'dead-code', 'DeadCodeConfig', 'DEAD_CODE', 'lint-dead-code', 'if(TRUE) 1 else 2', tagTypes);
rule(shell, 'useless-loop', 'UselessLoopConfig', 'USELESS_LOOP', 'lint-useless-loop', 'for(i in c(1)) { print(i) }', tagTypes);
function rule(shell, name, configType, ruleType, testfile, example, types) {
const rule = linter_rules_1.LintingRules[name];
const tags = rule.info.tags.toSorted((a, b) => {
// sort but specials first
if (a === b) {
return 0;
}
if (SpecialTagColors[a] && SpecialTagColors[b]) {
return SpecialTagColors[b].localeCompare(SpecialTagColors[a]);
}
else if (SpecialTagColors[a]) {
return -1;
}
else if (SpecialTagColors[b]) {
return 1;
}
return a.localeCompare(b);
}).map(t => makeTagBadge(t, types)).join(' ');
const certaintyDoc = (0, doc_types_1.getDocumentationForType)(`LintingRuleCertainty::${rule.info.certainty}`, types, '', true).replaceAll('\n', ' ');
const certaintyText = `\`${(0, html_hover_over_1.textWithTooltip)(rule.info.certainty, certaintyDoc)}\``;
if (format === 'short') {
ruleExplanations.set(name, () => Promise.resolve(`
**[${rule.info.name}](${doc_files_1.FlowrWikiBaseRef}/lint-${name}):** ${rule.info.description} [see ${(0, doc_types_1.shortLinkFile)(ruleType, types)}]\\
${tags}
`.trim()));
}
else {
ruleExplanations.set(name, async () => `
${(0, doc_auto_gen_1.autoGenHeader)({ filename: module.filename, purpose: 'linter', rVersion })}
${(0, doc_structure_1.section)(rule.info.name + ` <sup>[<a href="${doc_files_1.FlowrWikiBaseRef}/Linter">overview</a>]</sup>`, 2, name)}
${tags}
This rule is a ${certaintyText} rule.
${rule.info.description}\\
_This linting rule is implemented in ${(0, doc_types_1.shortLinkFile)(ruleType, types)}._
### Configuration
Linting rules can be configured by passing a configuration object to the linter query as shown in the example below.
The \`${name}\` rule accepts the following configuration options:
${Object.getOwnPropertyNames(linter_rules_1.LintingRules[name].info.defaultConfig).sort().map(key => `- ${(0, doc_types_1.shortLink)(`${configType}:::${key}`, types)}\\\n${(0, doc_types_1.getDocumentationForType)(`${configType}::${key}`, types)}`).join('\n')}
### Examples
${(0, doc_code_1.codeBlock)('r', example)}
The linting query can be used to run this rule on the above example:
${await (0, doc_query_1.showQuery)(shell, example, [{ type: 'linter', rules: [{ name, config: {} }] }], { collapseQuery: true })}
${buildSamplesFromLinterTestCases(shell, `${testfile}.test.ts`)}
`.trim());
}
}
return ruleExplanations;
}
function getAllLintingRulesWithTag(tag) {
return Object.entries(linter_rules_1.LintingRules).filter(([_, rule]) => rule.info.tags.includes(tag)).map(([name]) => name);
}
function getAllLintingRulesWitCertainty(certainty) {
return Object.entries(linter_rules_1.LintingRules).filter(([_, rule]) => rule.info.certainty === certainty).map(([name]) => name);
}
function linkToRule(name) {
return `[${name}](${doc_files_1.FlowrWikiBaseRef}/lint-${name})`;
}
async function getTextMainPage(shell, tagTypes, rVersion) {
const rules = registerRules(rVersion, shell, tagTypes.info);
return `${(0, doc_auto_gen_1.autoGenHeader)({ filename: module.filename, purpose: 'linter', rVersion })}
This page describes the flowR linter, which is a tool that utilizes flowR's dataflow analysis to find common issues in R scripts. The linter can currently be used through the linter [query](${doc_files_1.FlowrWikiBaseRef}/Query%20API).
For example:
${await (async () => {
const code = 'read.csv("/root/x.txt")';
const res = await (0, doc_query_1.showQuery)(shell, code, [{ type: 'linter' }], { showCode: false, collapseQuery: true, collapseResult: false });
return await (0, doc_repl_1.documentReplSession)(shell, [{
command: `:query @linter ${JSON.stringify(code)}`,
description: `
The linter will analyze the code and return any issues found.
Formatted more nicely, this returns:
${res}
`
}]);
})()}
${(0, doc_structure_1.section)('Linting Rules', 2, 'linting-rules')}
The following linting rules are available:
${await (async () => {
let result = '';
for (const k of Object.keys(linter_rules_1.LintingRules).sort()) {
const rule = rules.get(k);
(0, assert_1.guard)(rule !== undefined, `Linting rule ${k} is not documented!`);
result += '\n\n' + await rule();
}
return result;
})()}
${(0, doc_structure_1.section)('Tags', 2, 'tags')}
We use tags to categorize linting rules for users. The following tags are available:
| Tag/Badge   | Description |
| --- | :-- |
${Object.entries(linter_tags_1.LintingRuleTag).map(([name, tag]) => {
return `| <a id="${tag}"></a> ${(makeTagBadge(tag, tagTypes.info))} | ${(0, doc_types_1.getDocumentationForType)('LintingRuleTag::' + name, tagTypes.info).replaceAll(/\n/g, ' ')} (rule${getAllLintingRulesWithTag(tag).length === 1 ? '' : 's'}: ${(0, strings_1.joinWithLast)(getAllLintingRulesWithTag(tag).map(l => linkToRule(l))) || '_none_'}) | `;
}).join('\n')}
${(0, doc_structure_1.section)('Certainty', 2, 'certainty')}
Both linting rules and their individual results are additionally categorized by how certain the linter is that the results it is returning are valid.
${(0, doc_structure_1.section)('Rule Certainty', 3, 'rule-certainty')}
| Rule Certainty | Description |
| -------------- | :---------- |
${Object.entries(linter_format_1.LintingRuleCertainty).map(([name, certainty]) => {
return `| <a id="${certainty}"></a> \`${certainty}\` | ${(0, doc_types_1.getDocumentationForType)('LintingRuleCertainty::' + name, tagTypes.info).replaceAll(/\n/g, ' ')} (rule${getAllLintingRulesWitCertainty(certainty).length === 1 ? '' : 's'}: ${(0, strings_1.joinWithLast)(getAllLintingRulesWitCertainty(certainty).map(l => linkToRule(l))) || '_none_'}) |`;
}).join('\n')}
${(0, doc_structure_1.section)('Result Certainty', 3, 'result-certainty')}
| Result Certainty | Description |
| ---------------- | :---------- |
${Object.entries(linter_format_1.LintingResultCertainty).map(([name, certainty]) => `| <a id="${certainty}"></a> \`${certainty}\` | ${(0, doc_types_1.getDocumentationForType)('LintingResultCertainty::' + name, tagTypes.info).replaceAll(/\n/g, ' ')} |`).join('\n')}
`.trim();
}
async function getRulesPages(shell, tagTypes, rVersion) {
const rules = registerRules(rVersion, shell, tagTypes.info, 'long');
const result = {};
for (const [name, rule] of rules) {
const filepath = path_1.default.resolve('./wiki', `lint-${name}.md`);
result[filepath] = await rule();
}
return result;
}
/** Maps file-names to their content, the 'main' file is named 'main' */
async function getTexts(shell) {
const rVersion = (await shell.usedRVersion())?.format() ?? 'unknown';
const tagTypes = (0, doc_types_1.getTypesFromFolder)({
rootFolder: path_1.default.resolve('./src/linter/'),
inlineTypes: doc_types_1.mermaidHide
});
return {
'main': await getTextMainPage(shell, tagTypes, rVersion),
...await getRulesPages(shell, tagTypes, rVersion)
};
}
/* As an intermediary solution to changing the wiki system, we make this script generate separate files for each linter rule using fixed paths */
if (require.main === module) {
(0, log_1.setMinLevelOfAllLogs)(6 /* LogLevel.Fatal */);
const shell = new shell_1.RShell();
void (getTexts(shell).then(data => {
console.log(data['main']);
for (const [file, content] of Object.entries(data)) {
if (file === 'main') {
continue; // main is printed above
}
const filepath = path_1.default.resolve('./wiki', file);
(0, doc_print_1.writeWikiTo)(content, filepath);
}
}).finally(() => {
shell.close();
}));
}
//# sourceMappingURL=print-linter-wiki.js.map