@slippy-lint/slippy
Version:
A simple but powerful linter for Solidity
144 lines • 5.94 kB
JavaScript
import { filterByCommentDirectives } from "./comment-directives.js";
import { compilationUnitFromContent } from "./slang/compilation-unit.js";
import { SlippyParsingErrorAfterFixError, SlippyRuleConfigError, SlippyRuleNotRegisteredError, SlippyTooManyFixesError, } from "./errors.js";
import { getAllRules } from "./rules/get-all-rules.js";
import setupDebug from "debug";
import * as z from "zod";
import { getAppliableFixes } from "./internal/autofix.js";
const debug = setupDebug("slippy:linter");
export class Linter {
constructor(configLoader) {
this.configLoader = configLoader;
this.ruleNameToRule = new Map();
this.registerBuiltInRules();
}
/**
* @returns A list of `LintResult`s in the same order of the corresponding file.
*/
async lintFiles(files, options) {
const lintResults = [];
for (const file of files) {
lintResults.push(await this.lintText(file.content, file.filePath, options));
}
return lintResults;
}
async lintText(originalContent, filePath, options) {
const fix = options?.fix ?? false;
const config = this.configLoader.loadConfig(filePath);
let content = originalContent;
for (let fixIteration = 0; fixIteration < 10; fixIteration++) {
const unit = await compilationUnitFromContent({ content, filePath });
const file = unit.file(filePath);
if (file.errors().length > 0) {
if (fixIteration > 0) {
// if this is not the first iteration, it means that the autofix introduced a parsing error
throw new SlippyParsingErrorAfterFixError(filePath);
}
const parsingErrorDiagnostic = {
sourceId: file.id,
rule: null,
line: file.errors()[0].textRange.start.line,
column: file.errors()[0].textRange.start.column,
message: "Parsing error",
severity: "error",
};
return { diagnostics: [parsingErrorDiagnostic] };
}
const unfilteredDiagnostics = this.getDiagnostics(content, unit, file, config);
const diagnostics = filterByCommentDirectives(content, unfilteredDiagnostics, file, unit.languageVersion);
if (!fix) {
debug("autofix is disabled, returning diagnostics only");
const result = { diagnostics: diagnostics };
return result;
}
debug("applying fixes, iteration %s", fixIteration);
const fixedContent = this.applyFixes(content, diagnostics);
if (content === fixedContent) {
if (fixedContent === originalContent) {
return { diagnostics };
}
else {
return { diagnostics, fixedContent };
}
}
content = fixedContent;
}
throw new SlippyTooManyFixesError(filePath);
}
addRule(rule) {
this.ruleNameToRule.set(rule.name, rule);
}
getDiagnostics(content, unit, file, config) {
const diagnostics = [];
for (const [ruleName, ruleConfig] of Object.entries(config.rules)) {
const Rule = this.ruleNameToRule.get(ruleName);
if (Rule === undefined) {
throw new SlippyRuleNotRegisteredError(ruleName);
}
const severity = ruleConfig[0];
if (severity === "off")
continue;
let rule;
if ("parseConfig" in Rule) {
try {
const config = Rule.parseConfig(ruleConfig[1]);
rule = Rule.create(config);
}
catch (error) {
if (error instanceof z.ZodError) {
const problem = z.prettifyError(error);
throw new SlippyRuleConfigError(Rule.name, `\n\n${problem}`);
}
throw error;
}
}
else {
if (ruleConfig.length > 1) {
throw new SlippyRuleConfigError(Rule.name, "Rule requires no configuration, but received an array with more than one element.");
}
rule = Rule.create();
}
const ruleDiagnostics = rule.run({
unit,
file,
content,
});
const ruleDiagnosticsToReport = ruleDiagnostics.map((diagnostic) => {
return {
...diagnostic,
severity,
};
});
diagnostics.push(...ruleDiagnosticsToReport);
}
return diagnostics;
}
applyFixes(content, diagnostics) {
const fixedContentParts = [];
let end = content.length;
const allFixes = diagnostics.flatMap((d) => d.fix === undefined ? [] : [d.fix]);
debug("found %d fixes", allFixes.length);
if (allFixes.length === 0) {
return content;
}
const fixes = getAppliableFixes(allFixes);
debug("applying %d fixes of %d", fixes.length, allFixes.length);
for (const fix of fixes) {
for (const change of fix) {
fixedContentParts.push(content.slice(change.range[1], end));
fixedContentParts.push(change.replacement);
end = change.range[0];
}
}
fixedContentParts.push(content.slice(0, end));
fixedContentParts.reverse();
return fixedContentParts.join("");
}
registerBuiltInRules() {
const rules = getAllRules();
for (const rule of rules) {
this.ruleNameToRule.set(rule.name, rule);
}
}
}
//# sourceMappingURL=linter.js.map