typescript-tslint-plugin
Version:
TypeScript tslint language service plugin
358 lines • 15.5 kB
JavaScript
"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