@fimbul/wotan
Version:
Pluggable TypeScript and JavaScript linter
244 lines • 10.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.Linter = void 0;
const tslib_1 = require("tslib");
const ts = require("typescript");
const ymir_1 = require("@fimbul/ymir");
const fix_1 = require("./fix");
const debug = require("debug");
const inversify_1 = require("inversify");
const rule_loader_1 = require("./services/rule-loader");
const utils_1 = require("./utils");
const tsutils_1 = require("tsutils");
const log = debug('wotan:linter');
class StaticProgramFactory {
constructor(program) {
this.program = program;
}
getCompilerOptions() {
return this.program.getCompilerOptions();
}
getProgram() {
return this.program;
}
}
class CachedProgramFactory {
constructor(factory) {
this.factory = factory;
this.program = undefined;
this.options = undefined;
}
getCompilerOptions() {
var _a;
return (_a = this.options) !== null && _a !== void 0 ? _a : (this.options = this.factory.getCompilerOptions());
}
getProgram() {
if (this.program === undefined) {
this.program = this.factory.getProgram();
this.options = this.program.getCompilerOptions();
}
return this.program;
}
}
let Linter = class Linter {
constructor(ruleLoader, logger, deprecationHandler, filterFactory) {
this.ruleLoader = ruleLoader;
this.logger = logger;
this.deprecationHandler = deprecationHandler;
this.filterFactory = filterFactory;
}
lintFile(file, config, programOrFactory, options = {}) {
return this.getFindings(file, config, programOrFactory !== undefined && 'getTypeChecker' in programOrFactory
? new StaticProgramFactory(programOrFactory)
: programOrFactory, undefined, options);
}
lintAndFix(file, content, config, updateFile, iterations = 10, programFactory, processor, options = {},
/** Initial set of findings from a cache. If provided, the initial linting is skipped and these findings are used for fixing. */
findings = this.getFindings(file, config, programFactory, processor, options)) {
let totalFixes = 0;
for (let i = 0; i < iterations; ++i) {
if (findings.length === 0)
break;
const fixes = utils_1.mapDefined(findings, (f) => f.fix);
if (fixes.length === 0) {
log('No fixes');
break;
}
log('Trying to apply %d fixes in %d. iteration', fixes.length, i + 1);
const fixed = fix_1.applyFixes(content, fixes);
log('Applied %d fixes', fixed.fixed);
let newSource;
let fixedRange;
if (processor !== undefined) {
const { transformed, changeRange } = processor.updateSource(fixed.result, fixed.range);
fixedRange = changeRange !== null && changeRange !== void 0 ? changeRange : utils_1.calculateChangeRange(file.text, transformed);
newSource = transformed;
}
else {
newSource = fixed.result;
fixedRange = fixed.range;
}
const updateResult = updateFile(newSource, fixedRange);
if (updateResult === undefined) {
log('Rolling back latest fixes and abort linting');
processor === null || processor === void 0 ? void 0 : processor.updateSource(content, utils_1.invertChangeRange(fixed.range)); // reset processor state
break;
}
file = updateResult;
content = fixed.result;
totalFixes += fixed.fixed;
findings = this.getFindings(file, config, programFactory, processor, options);
}
return {
content,
findings,
fixes: totalFixes,
};
}
// @internal
getFindings(sourceFile, config, programFactory, processor, options) {
// make sure that all rules get the same Program and CompilerOptions for this run
programFactory && (programFactory = new CachedProgramFactory(programFactory));
let suppressMissingTypeInfoWarning = false;
log('Linting file %s', sourceFile.fileName);
if (programFactory !== undefined) {
const directive = tsutils_1.getTsCheckDirective(sourceFile.text);
if (directive !== undefined
? !directive.enabled
: /\.jsx?/.test(sourceFile.fileName) && !tsutils_1.isCompilerOptionEnabled(programFactory.getCompilerOptions(), 'checkJs')) {
log('Not using type information for this unchecked file');
programFactory = undefined;
suppressMissingTypeInfoWarning = true;
}
}
const rules = this.prepareRules(config, sourceFile, programFactory, suppressMissingTypeInfoWarning);
let findings;
if (rules.length === 0) {
log('No active rules');
if (options.reportUselessDirectives !== undefined) {
findings = this.filterFactory
.create({ sourceFile, getWrappedAst() { return tsutils_1.convertAst(sourceFile).wrapped; }, ruleNames: utils_1.emptyArray })
.reportUseless(options.reportUselessDirectives);
log('Found %d useless directives', findings.length);
}
else {
findings = utils_1.emptyArray;
}
}
else {
findings = this.applyRules(sourceFile, programFactory, rules, config.settings, options);
}
return processor === undefined ? findings : processor.postprocess(findings);
}
prepareRules(config, sourceFile, programFactory, noWarn) {
const rules = [];
for (const [ruleName, { options, severity, rulesDirectories, rule }] of config.rules) {
if (severity === 'off')
continue;
const ctor = this.ruleLoader.loadRule(rule, rulesDirectories);
if (ctor === undefined)
continue;
if (ctor.deprecated)
this.deprecationHandler.handle("rule" /* Rule */, ruleName, typeof ctor.deprecated === 'string' ? ctor.deprecated : undefined);
if (programFactory === undefined && ctor.requiresTypeInformation) {
if (noWarn) {
log('Rule %s requires type information', ruleName);
}
else {
this.logger.warn(`Rule '${ruleName}' requires type information.`);
}
continue;
}
if (ctor.supports !== undefined) {
const supports = ctor.supports(sourceFile, {
get program() { return programFactory && programFactory.getProgram(); },
get compilerOptions() { return programFactory && programFactory.getCompilerOptions(); },
options,
settings: config.settings,
});
if (supports !== true) {
if (!supports) {
log(`Rule %s does not support this file`, ruleName);
}
else {
log(`Rule %s does not support this file: %s`, ruleName, supports);
}
continue;
}
}
rules.push({ ruleName, options, severity, ctor });
}
return rules;
}
applyRules(sourceFile, programFactory, rules, settings, options) {
const result = [];
let findingFilter;
let ruleName;
let severity;
let ctor;
let convertedAst;
const getFindingFilter = () => {
return findingFilter !== null && findingFilter !== void 0 ? findingFilter : (findingFilter = this.filterFactory.create({ sourceFile, getWrappedAst, ruleNames: rules.map((r) => r.ruleName) }));
};
const addFinding = (pos, end, message, fix) => {
const finding = {
ruleName,
severity,
message,
start: {
position: pos,
...ts.getLineAndCharacterOfPosition(sourceFile, pos),
},
end: {
position: end,
...ts.getLineAndCharacterOfPosition(sourceFile, end),
},
fix: fix === undefined
? undefined
: !Array.isArray(fix)
? { replacements: [fix] }
: fix.length === 0
? undefined
: { replacements: fix },
};
if (getFindingFilter().filter(finding))
result.push(finding);
};
const context = {
addFinding,
getFlatAst,
getWrappedAst,
get program() { return programFactory === null || programFactory === void 0 ? void 0 : programFactory.getProgram(); },
get compilerOptions() { return programFactory === null || programFactory === void 0 ? void 0 : programFactory.getCompilerOptions(); },
sourceFile,
settings,
options: undefined,
};
for ({ ruleName, severity, ctor, options: context.options } of rules) {
log('Executing rule %s', ruleName);
new ctor(context).apply();
}
log('Found %d findings', result.length);
if (options.reportUselessDirectives !== undefined) {
const useless = getFindingFilter().reportUseless(options.reportUselessDirectives);
log('Found %d useless directives', useless.length);
result.push(...useless);
}
return result;
function getFlatAst() {
return (convertedAst !== null && convertedAst !== void 0 ? convertedAst : (convertedAst = tsutils_1.convertAst(sourceFile))).flat;
}
function getWrappedAst() {
return (convertedAst !== null && convertedAst !== void 0 ? convertedAst : (convertedAst = tsutils_1.convertAst(sourceFile))).wrapped;
}
}
};
Linter = tslib_1.__decorate([
inversify_1.injectable(),
tslib_1.__metadata("design:paramtypes", [rule_loader_1.RuleLoader,
ymir_1.MessageHandler,
ymir_1.DeprecationHandler,
ymir_1.FindingFilterFactory])
], Linter);
exports.Linter = Linter;
//# sourceMappingURL=linter.js.map
;