UNPKG

@fimbul/wotan

Version:

Pluggable TypeScript and JavaScript linter

244 lines 10.6 kB
"use strict"; 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