UNPKG

typescript-tslint-plugin

Version:
358 lines 15.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TSLintPlugin = void 0; const path = require("path"); const config_1 = require("./config"); const configFileWatcher_1 = require("./configFileWatcher"); const runner_1 = require("./runner"); const failures_1 = require("./runner/failures"); const isTsLintLanguageServiceMarker = Symbol('__isTsLintLanguageServiceMarker__'); class TsLintFixId { static fromFailure(failure) { return `tslint:${failure.getRuleName()}`; } static toRuleName(fixId) { if (typeof fixId !== 'string' || !fixId.startsWith('tslint:')) { return undefined; } return fixId.replace(/^tslint:/, ''); } } class ProblemMap { constructor() { this._map = new Map(); } get(start, end) { return this._map.get(this.key(start, end)); } set(start, end, problem) { this._map.set(this.key(start, end), problem); } values() { return this._map.values(); } // key to identify a rule failure key(start, end) { return `[${start},${end}]`; } } class TSLintPlugin { constructor(ts, languageServiceHost, logger, project, configurationManager) { this.ts = ts; this.languageServiceHost = languageServiceHost; this.logger = logger; this.project = project; this.configurationManager = configurationManager; this.codeFixActions = new Map(); this.workspaceTrust = runner_1.WorkspaceLibraryExecution.Unknown; this.logger.info('loaded'); this.runner = new runner_1.TsLintRunner(message => { this.logger.info(message); }); this.configFileWatcher = new configFileWatcher_1.ConfigFileWatcher(ts, filePath => { this.logger.info('TSlint file changed'); this.runner.onConfigFileChange(filePath); this.project.refreshDiagnostics(); }); this.configurationManager.onUpdatedConfig(() => { this.logger.info('TSConfig configuration changed'); project.refreshDiagnostics(); }); } decorate(languageService) { if (languageService[isTsLintLanguageServiceMarker]) { // Already decorated return; } const oldGetSupportedCodeFixes = this.ts.getSupportedCodeFixes.bind(this.ts); this.ts.getSupportedCodeFixes = () => { return [ ...oldGetSupportedCodeFixes(), '' + config_1.TSLINT_ERROR_CODE, ]; }; const intercept = Object.create(null); const oldGetSemanticDiagnostics = languageService.getSemanticDiagnostics.bind(languageService); intercept.getSemanticDiagnostics = (...args) => { return this.getSemanticDiagnostics(oldGetSemanticDiagnostics, ...args); }; const oldGetCodeFixesAtPosition = languageService.getCodeFixesAtPosition.bind(languageService); intercept.getCodeFixesAtPosition = (...args) => { return this.getCodeFixesAtPosition(oldGetCodeFixesAtPosition, ...args); }; const oldGetCombinedCodeFix = languageService.getCombinedCodeFix.bind(languageService); intercept.getCombinedCodeFix = (...args) => { return this.getCombinedCodeFix(oldGetCombinedCodeFix, ...args); }; return new Proxy(languageService, { get: (target, property) => { if (property === isTsLintLanguageServiceMarker) { return true; } return intercept[property] || target[property]; }, }); } updateWorkspaceTrust(workspaceTrust) { this.workspaceTrust = workspaceTrust; // Reset the runner this.runner = new runner_1.TsLintRunner(message => { this.logger.info(message); }); } getSemanticDiagnostics(delegate, fileName) { const diagnostics = delegate(fileName); if (isInMemoryFile(fileName)) { // In-memory file. TS-lint crashes on these so ignore them return diagnostics; } const config = this.configurationManager.config; if (diagnostics.length > 0 && config.suppressWhileTypeErrorsPresent) { return diagnostics; } try { this.logger.info(`Computing tslint semantic diagnostics for '${fileName}'`); if (this.codeFixActions.has(fileName)) { this.codeFixActions.delete(fileName); } if (config.ignoreDefinitionFiles && fileName.endsWith('.d.ts')) { return diagnostics; } let result; try { // protect against tslint crashes result = this.runner.runTsLint(fileName, this.getProgram(), { configFile: config.configFile, ignoreDefinitionFiles: config.ignoreDefinitionFiles, jsEnable: config.jsEnable, exclude: config.exclude ? Array.isArray(config.exclude) ? config.exclude : [config.exclude] : [], packageManager: runner_1.toPackageManager(config.packageManager), workspaceLibraryExecution: this.workspaceTrust, }); if (result.configFilePath) { this.configFileWatcher.ensureWatching(result.configFilePath); } } catch (err) { let errorMessage = `unknown error`; if (typeof err.message === 'string' || err.message instanceof String) { errorMessage = err.message; } this.logger.info('tslint error ' + errorMessage); return diagnostics; } const program = this.getProgram(); const file = program.getSourceFile(fileName); if (result.warnings) { const defaultTsconfigJsonPath = path.join(program.getCurrentDirectory(), 'tslint.json'); if ((result.configFilePath && this.ts.sys.fileExists(result.configFilePath)) || this.ts.sys.fileExists(defaultTsconfigJsonPath)) { // If we have a config file, the user likely wanted to lint. The fact that linting has a // warning should be reported to them. for (const warning of result.warnings) { diagnostics.unshift({ file, start: 0, length: 1, category: this.ts.DiagnosticCategory.Warning, source: config_1.TSLINT_ERROR_SOURCE, code: config_1.TSLINT_ERROR_CODE, messageText: warning, }); } } else { // If we have not found a config file, then we don't want to annoy users by generating warnings // about tslint not being installed or misconfigured. In many cases, the user is opening a // file/project that was not intended to be linted. for (const warning of result.warnings) { this.logger.info(`[tslint] ${warning}`); } } } const tslintProblems = failures_1.filterProblemsForFile(fileName, result.lintResult.failures); for (const problem of tslintProblems) { diagnostics.push(this.makeDiagnostic(problem, file)); this.recordCodeAction(problem, file); } } catch (e) { this.logger.info(`tslint-language service error: ${e.toString()}`); this.logger.info(`Stack trace: ${e.stack}`); } return diagnostics; } getCodeFixesAtPosition(delegate, fileName, start, end, errorCodes, formatOptions, userPreferences) { const fixes = Array.from(delegate(fileName, start, end, errorCodes, formatOptions, userPreferences)); if (isInMemoryFile(fileName)) { return fixes; // We don't have any tslint errors for these files } if (this.configurationManager.config.suppressWhileTypeErrorsPresent && fixes.length > 0) { return fixes; } this.logger.info(`getCodeFixes ${errorCodes[0]}`); this.logger.info(JSON.stringify(fixes)); const documentFixes = this.codeFixActions.get(fileName); if (documentFixes) { const problem = documentFixes.get(start, end); if (problem) { if (problem.fixable) { const fix = problem.failure.getFix(); if (fix) { const codeFixAction = this.getRuleFailureQuickFix(problem.failure, fileName); fixes.push(codeFixAction); const fixAll = this.getRuleFailureFixAllQuickFix(problem.failure.getRuleName(), documentFixes, fileName); if (fixAll) { codeFixAction.fixId = TsLintFixId.fromFailure(problem.failure); codeFixAction.fixAllDescription = `Fix all '${problem.failure.getRuleName()}'`; } fixes.push(this.getFixAllAutoFixableQuickFix(documentFixes, fileName)); } } fixes.push(this.getDisableRuleQuickFix(problem.failure, fileName, this.getProgram().getSourceFile(fileName))); } } return fixes; } getCombinedCodeFix(delegate, scope, fixId, formatOptions, preferences) { const ruleName = TsLintFixId.toRuleName(fixId); if (!ruleName) { return delegate(scope, fixId, formatOptions, preferences); } const documentFixes = this.codeFixActions.get(scope.fileName); if (documentFixes) { const fixAll = this.getRuleFailureFixAllQuickFix(ruleName, documentFixes, scope.fileName); if (fixAll) { return { changes: fixAll.changes, commands: fixAll.commands, }; } } return { changes: [] }; } recordCodeAction(failure, file) { // tslint can return a fix with an empty replacements array, these fixes are ignored const fixable = !!(failure.getFix && failure.getFix() && !replacementsAreEmpty(failure.getFix())); let documentAutoFixes = this.codeFixActions.get(file.fileName); if (!documentAutoFixes) { documentAutoFixes = new ProblemMap(); this.codeFixActions.set(file.fileName, documentAutoFixes); } documentAutoFixes.set(failure.getStartPosition().getPosition(), failure.getEndPosition().getPosition(), { failure, fixable }); } getRuleFailureQuickFix(failure, fileName) { return { description: `Fix: ${failure.getFailure()}`, fixName: `tslint:${failure.getRuleName()}`, changes: [failureToFileTextChange(failure, fileName)], }; } /** * Generate a code action that fixes all instances of ruleName. */ getRuleFailureFixAllQuickFix(ruleName, problems, fileName) { const changes = []; for (const problem of problems.values()) { if (problem.fixable) { if (problem.failure.getRuleName() === ruleName) { changes.push(failureToFileTextChange(problem.failure, fileName)); } } } // No need for this action if there's only one instance. if (changes.length < 2) { return undefined; } return { description: `Fix all '${ruleName}'`, fixName: `tslint:fix-all:${ruleName}`, changes, }; } getDisableRuleQuickFix(failure, fileName, file) { const line = failure.getStartPosition().getLineAndCharacter().line; const lineStarts = file.getLineStarts(); const lineStart = lineStarts[line]; let prefix = ''; const snapshot = this.languageServiceHost.getScriptSnapshot(fileName); if (snapshot) { const lineEnd = line < lineStarts.length - 1 ? lineStarts[line + 1] : file.end; const lineText = snapshot.getText(lineStart, lineEnd); const leadingSpace = lineText.match(/^([ \t]+)/); if (leadingSpace) { prefix = leadingSpace[0]; } } return { description: `Disable rule '${failure.getRuleName()}'`, fixName: `tslint:disable:${failure.getRuleName()}`, changes: [{ fileName, textChanges: [{ newText: `${prefix}// tslint:disable-next-line: ${failure.getRuleName()}\n`, span: { start: lineStart, length: 0 }, }], }], }; } getFixAllAutoFixableQuickFix(documentFixes, fileName) { const allReplacements = failures_1.getNonOverlappingReplacements(Array.from(documentFixes.values()).filter(x => x.fixable).map(x => x.failure)); return { description: `Fix all auto-fixable tslint failures`, fixName: `tslint:fix-all`, changes: [{ fileName, textChanges: allReplacements.map(convertReplacementToTextChange), }], }; } getProgram() { return this.project.getLanguageService().getProgram(); } makeDiagnostic(failure, file) { const message = (failure.getRuleName() !== null) ? `${failure.getFailure()} (${failure.getRuleName()})` : `${failure.getFailure()}`; const category = this.getDiagnosticCategory(failure); return { file, start: failure.getStartPosition().getPosition(), length: failure.getEndPosition().getPosition() - failure.getStartPosition().getPosition(), messageText: message, category, source: config_1.TSLINT_ERROR_SOURCE, code: config_1.TSLINT_ERROR_CODE, }; } getDiagnosticCategory(failure) { if (this.configurationManager.config.alwaysShowRuleFailuresAsWarnings || typeof this.configurationManager.config.alwaysShowRuleFailuresAsWarnings === 'undefined') { return this.ts.DiagnosticCategory.Warning; } if (failure.getRuleSeverity && failure.getRuleSeverity() === 'error') { return this.ts.DiagnosticCategory.Error; } return this.ts.DiagnosticCategory.Warning; } } exports.TSLintPlugin = TSLintPlugin; function isInMemoryFile(fileName) { return fileName.startsWith('^'); } function convertReplacementToTextChange(repl) { return { newText: repl.text, span: { start: repl.start, length: repl.length }, }; } function failureToFileTextChange(failure, fileName) { const fix = failure.getFix(); const replacements = failures_1.getReplacements(fix); return { fileName, textChanges: replacements.map(convertReplacementToTextChange), }; } function replacementsAreEmpty(fix) { if (Array.isArray(fix)) { return fix.length === 0; } return false; } //# sourceMappingURL=plugin.js.map