UNPKG

@eagleoutice/flowr

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

307 lines (272 loc) 14.5 kB
"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}'>![` + name + '](https://img.shields.io/badge/' + name.toLowerCase() + `-${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 + `&emsp;<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&emsp;&emsp; | 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