@eagleoutice/flowr
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
307 lines (272 loc) • 14.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WikiLinter = void 0;
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 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_functions_1 = require("./doc-util/doc-functions");
const linter_format_1 = require("../linter/linter-format");
const doc_maker_1 = require("./wiki-mk/doc-maker");
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, '', { fuzzy: true }).replaceAll('\n', ' ');
return (0, html_hover_over_1.textWithTooltip)(`<a href='#${name}'> + `-${SpecialTagColors[name] ?? 'teal'}) </a>`, doc);
}
function getPageNameForLintingRule(name) {
// convert file-path-validity to 'Linting Rule: File Path Validity'
const words = name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1));
return '[Linting Rule] ' + words.join(' ');
}
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.at(-1)?.match(/^\s*/)?.[0] ?? '';
return lines.map((line, i) => {
if (i === 0) {
return line;
}
return line.replaceAll(new RegExp('^' + indentation, 'g'), '');
}).join('\n');
}
function buildSamplesFromLinterTestCases(_parser, 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'))) {
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(knownParser, tagTypes, format = 'short') {
const ruleExplanations = new Map();
rule(knownParser, 'deprecated-functions', 'FunctionsToDetectConfig', '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(knownParser, 'network-functions', 'NetworkFunctionsConfig', 'NETWORK_FUNCTIONS', 'lint-network-functions', `
read.csv("https://example.com/data.csv")
download.file("https://foo.bar")
`, tagTypes);
rule(knownParser, '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(knownParser, 'absolute-file-paths', 'AbsoluteFilePathConfig', 'ABSOLUTE_PATH', 'lint-absolute-path', `
read.csv("C:/Users/me/Documents/My R Scripts/Reproducible.csv")
`, tagTypes);
rule(knownParser, 'unused-definitions', 'UnusedDefinitionConfig', 'UNUSED_DEFINITION', 'lint-unused-definition', `
x <- 42
y <- 3
print(x)
`, tagTypes);
rule(knownParser, 'seeded-randomness', 'SeededRandomnessConfig', 'SEEDED_RANDOMNESS', 'lint-seeded-randomness', 'runif(1)', tagTypes);
rule(knownParser, 'naming-convention', 'NamingConventionConfig', 'NAMING_CONVENTION', 'lint-naming-convention', `
myVar <- 42
print(myVar)
`, tagTypes);
rule(knownParser, '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(knownParser, 'dead-code', 'DeadCodeConfig', 'DEAD_CODE', 'lint-dead-code', 'if(TRUE) 1 else 2', tagTypes);
rule(knownParser, 'useless-loop', 'UselessLoopConfig', 'USELESS_LOOP', 'lint-useless-loop', 'for(i in c(1)) { print(i) }', tagTypes);
rule(knownParser, 'stop-call', 'StopWithCallConfig', 'STOP_WITH_CALL_ARG', 'lint-stop-call', 'stop(42)', tagTypes);
rule(knownParser, 'problematic-eval', 'ProblematicEvalConfig', 'PROBLEMATIC_EVAL', 'lint-problematic-eval', `
function(x) {
eval(x)
}
`, tagTypes);
function rule(parser, 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, '', { fuzzy: 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}/${encodeURIComponent(getPageNameForLintingRule(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' })}
${(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)(parser, example, [{ type: 'linter', rules: [{ name, config: {} }] }], { collapseQuery: true })}
${buildSamplesFromLinterTestCases(parser, `${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}/${encodeURIComponent(getPageNameForLintingRule(name).replaceAll(' ', '-'))})`;
}
async function getTextMainPage(knownParser, tagTypes) {
const rules = registerRules(knownParser, tagTypes.info);
return `
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)(knownParser, code, [{ type: 'linter' }], { showCode: false, collapseQuery: true, collapseResult: false });
return await (0, doc_repl_1.documentReplSession)(knownParser, [{
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', ' ')} (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', ' ')} (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', ' ')} |`).join('\n')}
`.trim();
}
async function getRulesPages(knownParser, tagTypes) {
const rules = registerRules(knownParser, tagTypes.info, 'long');
const result = {};
for (const [name, rule] of rules) {
const filepath = path_1.default.join('wiki', `${getPageNameForLintingRule(name)}.md`);
result[filepath] = await rule();
}
return result;
}
/** Maps file-names to their content, the 'main' file is named 'main' */
async function getTexts(parser) {
const tagTypes = (0, doc_types_1.getTypesFromFolder)({
rootFolder: path_1.default.resolve('./src/linter/'),
inlineTypes: doc_types_1.mermaidHide
});
return {
'main': await getTextMainPage(parser, tagTypes),
...await getRulesPages(parser, tagTypes)
};
}
/**
* https://github.com/flowr-analysis/flowr/wiki/Linter
*/
class WikiLinter extends doc_maker_1.DocMaker {
constructor() {
super('wiki/Linter.md', module.filename, 'linter');
}
async text({ treeSitter }) {
const texts = await getTexts(treeSitter);
for (const [file, content] of Object.entries(texts)) {
if (file === 'main') {
continue; // main is printed below
}
this.writeSubFile(file, content);
}
return texts['main'];
}
}
exports.WikiLinter = WikiLinter;
//# sourceMappingURL=wiki-linter.js.map