This commit is contained in:
Erich Gamma 2018-08-24 17:28:49 +02:00
Родитель b9b05c0eeb
Коммит 12785c2679
3 изменённых файлов: 605 добавлений и 424 удалений

Просмотреть файл

@ -0,0 +1,33 @@
export class MruCache<T> {
private readonly _map = new Map<string, T>();
private readonly _entries = new Set<string>();
public constructor(
private readonly _maxSize: number
) {}
public set(filePath: string, entry: T): void {
this._map.set(filePath, entry);
this._entries.add(filePath);
for (const key of this._entries.keys()) {
if (this._entries.size <= this._maxSize) {
break;
}
this._map.delete(key);
this._entries.delete(key);
}
}
public has(filePath: string): boolean {
return this._map.has(filePath);
}
public get(filePath: string): (T) | undefined {
if (this._entries.has(filePath)) {
this._entries.delete(filePath);
this._entries.add(filePath);
}
return this._map.get(filePath);
}
}

506
tslint-server/src/runner.ts Normal file
Просмотреть файл

@ -0,0 +1,506 @@
import * as cp from 'child_process';
import * as fs from 'fs';
import * as minimatch from 'minimatch';
import * as path from 'path';
import * as tslint from 'tslint'; // this is a dev dependency only
import * as typescript from 'typescript'; // this is a dev dependency only
import * as util from 'util';
import * as server from 'vscode-languageserver';
import { MruCache } from './mruCache';
export interface RunConfiguration {
readonly jsEnable?: boolean;
readonly rulesDirectory?: string | string[];
readonly configFile?: string;
readonly ignoreDefinitionFiles?: boolean;
readonly exclude?: string | string[];
readonly validateWithDefaultConfig?: boolean;
readonly nodePath?: string;
readonly packageManager?: 'npm' | 'yarn';
readonly traceLevel?: 'verbose' | 'normal';
readonly workspaceFolderPath?: string;
}
interface Configuration {
readonly linterConfiguration: tslint.Configuration.IConfigurationFile | undefined;
isDefaultLinterConfig: boolean;
readonly path?: string;
}
class ConfigCache {
public configuration: Configuration | undefined;
private filePath: string | undefined;
constructor() {
this.filePath = undefined;
this.configuration = undefined;
}
public set(filePath: string, configuration: Configuration) {
this.filePath = filePath;
this.configuration = configuration;
}
public get(forPath: string): Configuration | undefined {
return forPath === this.filePath ? this.configuration : undefined;
}
public isDefaultLinterConfig(): boolean {
return !!(this.configuration && this.configuration.isDefaultLinterConfig);
}
public flush() {
this.filePath = undefined;
this.configuration = undefined;
}
}
export interface RunResult {
readonly lintResult: tslint.LintResult;
readonly warnings: string[];
readonly workspaceFolderPath?: string;
readonly configFilePath?: string;
}
const emptyLintResult: tslint.LintResult = {
errorCount: 0,
warningCount: 0,
failures: [],
fixes: [],
format: '',
output: '',
};
const emptyResult: RunResult = {
lintResult: emptyLintResult,
warnings: [],
};
export class TsLintRunner {
private readonly tslintPath2Library = new Map<string, typeof tslint | undefined>();
private readonly document2LibraryCache = new MruCache<() => typeof tslint | undefined>(100);
// map stores undefined values to represent failed resolutions
private readonly globalPackageManagerPath = new Map<string, string>();
private readonly configCache = new ConfigCache();
constructor(
private trace: (data: string) => void,
) { }
public runTsLint(
filePath: string,
contents: string | typescript.Program,
configuration: RunConfiguration,
): RunResult {
this.trace('start validateTextDocument');
this.trace('validateTextDocument: about to load tslint library');
const warnings: string[] = [];
if (!this.document2LibraryCache.has(filePath)) {
this.loadLibrary(filePath, configuration, warnings);
}
this.trace('validateTextDocument: loaded tslint library');
if (!this.document2LibraryCache.has(filePath)) {
return emptyResult;
}
const library = this.document2LibraryCache.get(filePath)!();
if (!library) {
return {
lintResult: emptyLintResult,
warnings: [
getInstallFailureMessage(
filePath,
configuration.workspaceFolderPath,
configuration.packageManager || 'npm'),
],
};
}
this.trace('About to validate ' + filePath);
return this.doRun(filePath, contents, library, configuration, warnings);
}
/**
* Filter failures for the given document
*/
public filterProblemsForFile(
filePath: string,
failures: tslint.RuleFailure[],
): tslint.RuleFailure[] {
const normalizedPath = path.normalize(filePath);
// we only show diagnostics targetting this open document, some tslint rule return diagnostics for other documents/files
const normalizedFiles = new Map<string, string>();
return failures.filter((each) => {
const fileName = each.getFileName();
if (!normalizedFiles.has(fileName)) {
normalizedFiles.set(fileName, path.normalize(fileName));
}
return normalizedFiles.get(fileName) === normalizedPath;
});
}
public onConfigFileChange(_tsLintFilePath: string) {
this.configCache.flush();
}
public getNonOverlappingReplacements(failures: tslint.RuleFailure[]): tslint.Replacement[] {
function overlaps(a: tslint.Replacement, b: tslint.Replacement): boolean {
return a.end >= b.start;
}
let sortedFailures = this.sortFailures(failures);
let nonOverlapping: tslint.Replacement[] = [];
for (let i = 0; i < sortedFailures.length; i++) {
let replacements = this.getReplacements(sortedFailures[i].getFix());
if (i === 0 || !overlaps(nonOverlapping[nonOverlapping.length - 1], replacements[0])) {
nonOverlapping.push(...replacements);
}
}
return nonOverlapping;
}
private getReplacements(fix: tslint.Fix | undefined): tslint.Replacement[] {
let replacements: tslint.Replacement[] | null = null;
// in tslint4 a Fix has a replacement property with the Replacements
if ((<any>fix).replacements) {
// tslint4
replacements = (<any>fix).replacements;
} else {
// in tslint 5 a Fix is a Replacement | Replacement[]
if (!Array.isArray(fix)) {
replacements = [<any>fix];
} else {
replacements = fix;
}
}
return replacements || [];
}
private getReplacement(failure: tslint.RuleFailure, at: number): tslint.Replacement {
return this.getReplacements(failure.getFix())[at];
}
private sortFailures(failures: tslint.RuleFailure[]): tslint.RuleFailure[] {
// The failures.replacements are sorted by position, we sort on the position of the first replacement
return failures.sort((a, b) => {
return this.getReplacement(a, 0).start - this.getReplacement(b, 0).start;
});
}
private loadLibrary(filePath: string, configuration: RunConfiguration, warningsOutput: string[]): void {
this.trace(`loadLibrary for ${filePath}`);
const getGlobalPath = () => this.getGlobalPackageManagerPath(configuration.packageManager);
const directory = path.dirname(filePath);
let np: string | undefined = undefined;
if (configuration && configuration.nodePath) {
const exists = fs.existsSync(configuration.nodePath);
if (exists) {
np = configuration.nodePath;
} else {
warningsOutput.push(`The setting 'tslint.nodePath' refers to '${configuration.nodePath}', but this path does not exist. The setting will be ignored.`);
}
}
let tsLintPath: string;
if (np) {
try {
tsLintPath = this.resolveTsLint(np, np!);
} catch {
tsLintPath = this.resolveTsLint(getGlobalPath(), directory);
}
} else {
try {
tsLintPath = this.resolveTsLint(undefined, directory);
} catch {
tsLintPath = this.resolveTsLint(getGlobalPath(), directory);
}
}
this.document2LibraryCache.set(filePath, () => {
let library;
if (!this.tslintPath2Library.has(tsLintPath)) {
try {
library = require(tsLintPath);
} catch (e) {
this.tslintPath2Library.set(tsLintPath, undefined);
return;
}
this.tslintPath2Library.set(tsLintPath, library);
}
return this.tslintPath2Library.get(tsLintPath);
});
}
private getGlobalPackageManagerPath(packageManager: string | undefined): string | undefined {
this.trace(`Begin - Resolve Global Package Manager Path for: ${packageManager}`);
if (!packageManager) {
packageManager = 'npm';
}
if (!this.globalPackageManagerPath.has(packageManager)) {
let path: string | undefined;
if (packageManager === 'npm') {
path = server.Files.resolveGlobalNodePath(this.trace);
} else if (packageManager === 'yarn') {
path = server.Files.resolveGlobalYarnPath(this.trace);
}
this.globalPackageManagerPath.set(packageManager, path!);
}
this.trace(`Done - Resolve Global Package Manager Path for: ${packageManager}`);
return this.globalPackageManagerPath.get(packageManager);
}
private doRun(
filePath: string,
contents: string | typescript.Program,
library: typeof import('tslint'),
configuration: RunConfiguration,
warnings: string[],
): RunResult {
this.trace('start doValidate ' + filePath);
const uri = filePath;
if (this.fileIsExcluded(configuration, filePath)) {
this.trace(`No linting: file ${filePath} is excluded`);
return emptyResult;
}
if (configuration.workspaceFolderPath) {
this.trace(`Changed directory to ${configuration.workspaceFolderPath}`);
process.chdir(configuration.workspaceFolderPath);
}
const configFile = configuration.configFile || null;
let linterConfiguration: Configuration | undefined;
this.trace('validateTextDocument: about to getConfiguration');
try {
linterConfiguration = this.getConfiguration(uri, filePath, library, configFile);
} catch (err) {
this.trace(`No linting: exception when getting tslint configuration for ${filePath}, configFile= ${configFile}`);
warnings.push(getConfigurationFailureMessage(err));
return {
lintResult: emptyLintResult,
warnings,
};
}
if (!linterConfiguration) {
this.trace(`No linting: no tslint configuration`);
return emptyResult;
}
this.trace('validateTextDocument: configuration fetched');
if (isJsDocument(filePath) && !configuration.jsEnable) {
this.trace(`No linting: a JS document, but js linting is disabled`);
return emptyResult;
}
if (configuration.validateWithDefaultConfig === false && this.configCache.configuration!.isDefaultLinterConfig) {
this.trace(`No linting: linting with default tslint configuration is disabled`);
return emptyResult;
}
if (isExcludedFromLinterOptions(linterConfiguration.linterConfiguration, filePath)) {
this.trace(`No linting: file is excluded using linterOptions.exclude`);
return emptyResult;
}
let result: tslint.LintResult;
const options: tslint.ILinterOptions = {
formatter: "json",
fix: false,
rulesDirectory: configuration.rulesDirectory || undefined,
formattersDirectory: undefined,
};
if (configuration.traceLevel && configuration.traceLevel === 'verbose') {
this.traceConfigurationFile(linterConfiguration.linterConfiguration);
}
// tslint writes warnings using console.warn, capture these warnings and send them to the client
const originalConsoleWarn = console.warn;
const captureWarnings = (message?: any) => {
warnings.push(message);
originalConsoleWarn(message);
};
console.warn = captureWarnings;
try { // clean up if tslint crashes
const tslint = new library.Linter(options, typeof contents === 'string' ? undefined : contents);
this.trace(`Linting: start linting`);
tslint.lint(filePath, typeof contents === 'string' ? contents : '', linterConfiguration.linterConfiguration);
result = tslint.getResult();
this.trace(`Linting: ended linting`);
} finally {
console.warn = originalConsoleWarn;
}
return {
lintResult: result,
warnings,
workspaceFolderPath: configuration.workspaceFolderPath,
configFilePath: linterConfiguration.path,
};
}
private getConfiguration(uri: string, filePath: string, library: typeof tslint, configFileName: string | null): Configuration | undefined {
this.trace('getConfiguration for' + uri);
const config = this.configCache.get(filePath);
if (config) {
return config;
}
let isDefaultConfig = false;
let linterConfiguration: tslint.Configuration.IConfigurationFile | undefined = undefined;
const linter = library.Linter;
if (linter.findConfigurationPath) {
isDefaultConfig = linter.findConfigurationPath(configFileName, filePath) === undefined;
}
const configurationResult = linter.findConfiguration(configFileName, filePath);
// between tslint 4.0.1 and tslint 4.0.2 the attribute 'error' has been removed from IConfigurationLoadResult
// in 4.0.2 findConfiguration throws an exception as in version ^3.0.0
if ((configurationResult as any).error) {
throw (configurationResult as any).error;
}
linterConfiguration = configurationResult.results;
// In tslint version 5 the 'no-unused-variable' rules breaks the TypeScript language service plugin.
// See https://github.com/Microsoft/TypeScript/issues/15344
// Therefore we remove the rule from the configuration.
//
// In tslint 5 the rules are stored in a Map, in earlier versions they were stored in an Object
if (linterConfiguration) {
if (linterConfiguration.rules && linterConfiguration.rules instanceof Map) {
linterConfiguration.rules.delete('no-unused-variable');
}
if (linterConfiguration.jsRules && linterConfiguration.jsRules instanceof Map) {
linterConfiguration.jsRules.delete('no-unused-variable');
}
}
const configuration: Configuration = {
isDefaultLinterConfig: isDefaultConfig,
linterConfiguration,
path: configurationResult.path
};
this.configCache.set(filePath, configuration);
return this.configCache.configuration;
}
private fileIsExcluded(settings: RunConfiguration, filePath: string): boolean {
if (settings.ignoreDefinitionFiles) {
if (filePath.endsWith('.d.ts')) {
return true;
}
}
if (settings.exclude) {
if (Array.isArray(settings.exclude)) {
for (const pattern of settings.exclude) {
if (testForExclusionPattern(filePath, pattern)) {
return true;
}
}
} else if (testForExclusionPattern(filePath, settings.exclude)) {
return true;
}
}
return false;
}
private traceConfigurationFile(configuration: tslint.Configuration.IConfigurationFile | undefined) {
if (!configuration) {
this.trace("no tslint configuration");
return;
}
this.trace("tslint configuration:" + util.inspect(configuration, undefined, 4));
}
private resolveTsLint(nodePath: string | undefined, cwd: string): string {
const nodePathKey = 'NODE_PATH';
const app = [
"console.log(require.resolve('tslint'));",
].join('');
const env = process.env;
const newEnv = Object.create(null);
Object.keys(env).forEach(key => newEnv[key] = env[key]);
if (nodePath) {
if (newEnv[nodePathKey]) {
newEnv[nodePathKey] = nodePath + path.delimiter + newEnv[nodePathKey];
} else {
newEnv[nodePathKey] = nodePath;
}
this.trace(`NODE_PATH value is: ${newEnv[nodePathKey]}`);
}
newEnv.ELECTRON_RUN_AS_NODE = '1';
const spanwResults = cp.spawnSync(process.argv0, ['-e', app], { cwd, env: newEnv });
return spanwResults.stdout.toString().trim();
}
}
function testForExclusionPattern(filePath: string, pattern: string): boolean {
return minimatch(filePath, pattern, { dot: true });
}
function getInstallFailureMessage(filePath: string, workspaceFolder: string | undefined, packageManager: 'npm' | 'yarn'): string {
const localCommands = {
npm: 'npm install tslint',
yarn: 'yarn add tslint',
};
const globalCommands = {
npm: 'npm install -g tslint',
yarn: 'yarn global add tslint',
};
if (workspaceFolder) { // workspace opened on a folder
return [
'',
`Failed to load the TSLint library for the document ${filePath}`,
'',
`To use TSLint in this workspace please install tslint using \'${localCommands[packageManager]}\' or globally using \'${globalCommands[packageManager]}\'.`,
'TSLint has a peer dependency on `typescript`, make sure that `typescript` is installed as well.',
'You need to reopen the workspace after installing tslint.',
].join('\n');
} else {
return [
`Failed to load the TSLint library for the document ${filePath}`,
`To use TSLint for single file install tslint globally using \'${globalCommands[packageManager]}\'.`,
'TSLint has a peer dependency on `typescript`, make sure that `typescript` is installed as well.',
'You need to reopen VS Code after installing tslint.',
].join('\n');
}
}
function isJsDocument(filePath: string) {
return filePath.match(/\.jsx?$/i);
}
function isExcludedFromLinterOptions(
config: tslint.Configuration.IConfigurationFile | undefined,
fileName: string,
): boolean {
if (config === undefined || config.linterOptions === undefined || config.linterOptions.exclude === undefined) {
return false;
}
return config.linterOptions.exclude.some((pattern) => testForExclusionPattern(fileName, pattern));
}
function getConfigurationFailureMessage(err: any): string {
let errorMessage = `unknown error`;
if (typeof err.message === 'string' || err.message instanceof String) {
errorMessage = err.message;
}
return `vscode-tslint: Cannot read tslint configuration - '${errorMessage}'`;
}

Просмотреть файл

@ -3,18 +3,14 @@
*--------------------------------------------------------*/
'use strict';
import * as minimatch from 'minimatch';
import * as server from 'vscode-languageserver';
import * as path from 'path';
import * as semver from 'semver';
import Uri from 'vscode-uri';
import * as util from 'util';
import * as fs from 'fs';
import * as tslint from 'tslint'; // this is a dev dependency only
import { Delayer } from './delayer';
import { createVscFixForRuleFailure, TSLintAutofixEdit } from './fixer';
import { TsLintRunner, RunConfiguration} from './runner';
// Settings as defined in VS Code
interface Settings {
@ -35,45 +31,6 @@ interface Settings {
workspaceFolderPath: string | undefined;
}
interface Configuration {
linterConfiguration: tslint.Configuration.IConfigurationFile | undefined;
isDefaultLinterConfig: boolean;
}
class ConfigCache {
filePath: string | undefined;
configuration: Configuration | undefined;
constructor() {
this.filePath = undefined;
this.configuration = undefined;
}
set(path: string, configuration: Configuration) {
this.filePath = path;
this.configuration = configuration;
}
get(forPath: string): Configuration | undefined {
if (forPath === this.filePath) {
return this.configuration;
}
return undefined;
}
isDefaultLinterConfig(): boolean {
if (this.configuration) {
return this.configuration.isDefaultLinterConfig;
}
return false;
}
flush() {
this.filePath = undefined;
this.configuration = undefined;
}
}
class SettingsCache {
uri: string | undefined;
promise: Promise<Settings> | undefined;
@ -107,11 +64,9 @@ class SettingsCache {
}
}
let configCache = new ConfigCache();
let settingsCache = new SettingsCache();
let globalSettings: Settings = <Settings>{};
let scopedSettingsSupport = false;
let globalPackageManagerPath: Map<string, string> = new Map(); // map stores undefined values to represent failed resolutions
process.on('unhandledRejection', (reason, p) => {
connection.console.info(`Unhandled Rejection at: Promise ${p} reason:, ${reason}`);
@ -143,25 +98,8 @@ namespace StatusNotification {
export const type = new server.NotificationType<StatusParams, void>('tslint/status');
}
interface NoTSLintLibraryParams {
source: server.TextDocumentIdentifier;
}
interface NoTSLintLibraryResult {
}
namespace NoTSLintLibraryRequest {
export const type = new server.RequestType<NoTSLintLibraryParams, NoTSLintLibraryResult, void, void>('tslint/noLibrary');
}
// if tslint < tslint4 then the linter is the module therefore the type `any`
let path2Library: Map<string, typeof tslint.Linter | any> = new Map();
let document2Library: Map<string, Thenable<typeof tslint.Linter | any>> = new Map();
let validationDelayer = new Map<string, Delayer<void>>(); // key is the URI of the document
//let configFileWatchers: Map<string, fs.FSWatcher> = new Map();
function makeDiagnostic(settings: Settings | undefined, problem: tslint.RuleFailure): server.Diagnostic {
let message = (problem.getRuleName())
? `${problem.getFailure()} (${problem.getRuleName()})`
@ -241,47 +179,6 @@ function convertReplacementToAutoFix(document: server.TextDocument, repl: tslint
};
}
async function getConfiguration(uri: string, filePath: string, library: any, configFileName: string | null): Promise<Configuration | undefined> {
trace('getConfiguration for' + uri);
let config = configCache.get(filePath);
if (config) {
return config;
}
let isDefaultConfig = false;
let linterConfiguration: tslint.Configuration.IConfigurationFile | undefined;
let linter = getLinterFromLibrary(library);
if (isTsLintVersion4(library)) {
if (linter.findConfigurationPath) {
isDefaultConfig = linter.findConfigurationPath(configFileName, filePath) === undefined;
}
let configurationResult = linter.findConfiguration(configFileName, filePath);
// between tslint 4.0.1 and tslint 4.0.2 the attribute 'error' has been removed from IConfigurationLoadResult
// in 4.0.2 findConfiguration throws an exception as in version ^3.0.0
if ((<any>configurationResult).error) {
throw (<any>configurationResult).error;
}
linterConfiguration = configurationResult.results;
} else {
// prior to tslint 4.0 the findconfiguration functions where attached to the linter function
if (linter.findConfigurationPath) {
isDefaultConfig = linter.findConfigurationPath(configFileName, filePath) === undefined;
}
linterConfiguration = <tslint.Configuration.IConfigurationFile>linter.findConfiguration(configFileName, filePath);
}
let configuration: Configuration = {
isDefaultLinterConfig: isDefaultConfig,
linterConfiguration: linterConfiguration,
};
configCache.set(filePath, configuration);
return configCache.configuration;
}
function getErrorMessage(err: any, document: server.TextDocument): string {
let errorMessage = `unknown error`;
if (typeof err.message === 'string' || err.message instanceof String) {
@ -301,11 +198,6 @@ function getConfigurationFailureMessage(err: any): string {
}
function showConfigurationFailure(conn: server.IConnection, err: any) {
conn.console.info(getConfigurationFailureMessage(err));
conn.sendNotification(StatusNotification.type, { state: Status.error });
}
function validateAllTextDocuments(conn: server.IConnection, documents: server.TextDocument[]): void {
trace('validateAllTextDocuments');
let tracker = new server.ErrorMessageTracker();
@ -318,26 +210,13 @@ function validateAllTextDocuments(conn: server.IConnection, documents: server.Te
});
}
function getLinterFromLibrary(library): typeof tslint.Linter {
let isTsLint4 = isTsLintVersion4(library);
let linter;
if (!isTsLint4) {
linter = library;
} else {
linter = library.Linter;
}
return linter;
}
let tslintRunner: TsLintRunner | undefined = undefined;
async function validateTextDocument(connection: server.IConnection, document: server.TextDocument) {
trace('start validateTextDocument');
let uri = document.uri;
// tslint can only validate files on disk
if (Uri.parse(uri).scheme !== 'file') {
return;
}
let settings = await settingsCache.get(uri);
trace('validateTextDocument: settings fetched');
if (settings && !settings.enable) {
@ -346,29 +225,68 @@ async function validateTextDocument(connection: server.IConnection, document: se
return;
}
trace('validateTextDocument: about to load tslint library');
if (!document2Library.has(document.uri)) {
await loadLibrary(document.uri);
}
trace('validateTextDocument: loaded tslint library');
let diagnostics: server.Diagnostic[] = [];
delete codeFixActions[uri];
delete codeDisableRuleActions[uri];
if (!document2Library.has(document.uri)) {
return;
// tslint can only validate files on disk
if (Uri.parse(uri).scheme !== 'file') {
trace(`No linting: file is not saved on disk`);
return diagnostics;
}
document2Library.get(document.uri)!.then(async (library) => {
if (!library) {
return;
}
try {
trace('validateTextDocument: about to validate ' + document.uri);
connection.sendNotification(StatusNotification.type, { state: Status.ok });
let diagnostics = await doValidate(connection, library, document);
connection.sendDiagnostics({ uri, diagnostics });
} catch (err) {
connection.window.showErrorMessage(getErrorMessage(err, document));
}
let fsPath = server.Files.uriToFilePath(uri);
if (!settings) {
trace('No linting: settings could not be loaded');
return diagnostics;
}
if (!settings.enable) {
trace('No linting: tslint is disabled');
return diagnostics;
}
if (!tslintRunner) {
tslintRunner = new TsLintRunner(trace);
}
let traceLevel: 'normal' | 'verbose' = 'normal';
if (settings.trace && settings.trace.server && settings.trace.server === 'verbose') {
traceLevel = 'verbose';
}
let runConfiguration: RunConfiguration = {
workspaceFolderPath: settings.workspaceFolderPath,
configFile: settings.configFile,
jsEnable: settings.jsEnable,
exclude: settings.exclude,
ignoreDefinitionFiles: settings.ignoreDefinitionFiles,
nodePath: settings.nodePath,
packageManager: settings.packageManager,
rulesDirectory: settings.rulesDirectory,
validateWithDefaultConfig: settings.validateWithDefaultConfig,
traceLevel: traceLevel
};
let result = tslintRunner.runTsLint(fsPath!, document.getText(), runConfiguration);
if (result.warnings.length > 0) {
connection.sendNotification(StatusNotification.type, { state: Status.warn });
result.warnings.forEach(each => {
connection.console.warn(each);
});
}
let filterdFailures = tslintRunner.filterProblemsForFile(fsPath!, result.lintResult.failures);
diagnostics = filterdFailures.map(each => makeDiagnostic(settings, each));
filterdFailures.forEach(each => {
let diagnostic = makeDiagnostic(settings, each);
diagnostics.push(diagnostic);
recordCodeAction(document, diagnostic, each);
});
connection.sendDiagnostics({ uri, diagnostics });
}
let connection: server.IConnection = server.createConnection(new server.IPCMessageReader(process), new server.IPCMessageWriter(process));
@ -398,261 +316,6 @@ connection.onInitialize((params) => {
};
});
function isTsLintVersion4(library) {
let version = '1.0.0';
try {
version = library.Linter.VERSION;
} catch (e) {
}
return !(semver.satisfies(version, "<= 3.x.x"));
}
function isExcludedFromLinterOptions(config: tslint.Configuration.IConfigurationFile | undefined, fileName: string): boolean {
if (config === undefined || config.linterOptions === undefined || config.linterOptions.exclude === undefined) {
return false;
}
return config.linterOptions.exclude.some((pattern) => testForExclusionPattern(fileName, pattern));
}
async function nodePathExists(file: string): Promise<boolean> {
return new Promise<boolean>((resolve, _reject) => {
fs.exists(file, (value) => {
resolve(value);
});
});
}
async function loadLibrary(docUri: string) {
trace('loadLibrary for ' + docUri);
let uri = Uri.parse(docUri);
let promise: Thenable<string>;
let settings = await settingsCache.get(docUri);
let getGlobalPath = () => getGlobalPackageManagerPath(settings.packageManager);
if (uri.scheme === 'file') {
let file = uri.fsPath;
let directory = path.dirname(file);
let np: string | undefined = undefined;
if (settings && settings.nodePath) {
let exists = await nodePathExists(settings.nodePath);
if (exists) {
np = settings.nodePath;
} else {
connection.window.showErrorMessage(`The setting 'tslint.nodePath' refers to '${settings.nodePath}', but this path does not exist. The setting will be ignored.`);
}
}
if (np) {
promise = server.Files.resolve('tslint', np, np!, trace).then<string, string>(undefined, () => {
return server.Files.resolve('tslint', getGlobalPath(), directory, trace);
});
} else {
promise = server.Files.resolve('tslint', undefined, directory, trace).then<string, string>(undefined, () => {
return promise = server.Files.resolve('tslint', getGlobalPath(), directory, trace);
});
}
} else {
promise = server.Files.resolve('tslint', getGlobalPath(), undefined!, trace); // cwd argument can be undefined
}
document2Library.set(docUri, promise.then((path) => {
let library;
if (!path2Library.has(path)) {
try {
library = require(path);
} catch (e) {
connection.console.info(`TSLint failed to load tslint`);
connection.sendRequest(NoTSLintLibraryRequest.type, { source: { uri: docUri } });
return undefined;
}
connection.console.info(`TSLint library loaded from: ${path}`);
path2Library.set(path, library);
}
return path2Library.get(path);
}, () => {
connection.sendRequest(NoTSLintLibraryRequest.type, { source: { uri: docUri } });
return undefined;
}));
}
async function doValidate(conn: server.IConnection, library: any, document: server.TextDocument): Promise<server.Diagnostic[]> {
trace('start doValidate ' + document.uri);
let uri = document.uri;
let diagnostics: server.Diagnostic[] = [];
delete codeFixActions[uri];
delete codeDisableRuleActions[uri];
let fsPath = server.Files.uriToFilePath(uri);
if (!fsPath) {
// tslint can only lint files on disk
trace(`No linting: file is not saved on disk`);
return diagnostics;
}
let settings = await settingsCache.get(uri);
if (!settings) {
trace('No linting: settings could not be loaded');
return diagnostics;
}
if (!settings.enable) {
trace('No linting: tslint is disabled');
return diagnostics;
}
if (fileIsExcluded(settings, fsPath)) {
trace(`No linting: file ${fsPath} is excluded`);
return diagnostics;
}
if (settings.workspaceFolderPath) {
trace(`Changed directory to ${settings.workspaceFolderPath}`);
process.chdir(settings.workspaceFolderPath);
}
let contents = document.getText();
let configFile = settings.configFile || null;
let configuration: Configuration | undefined;
trace('validateTextDocument: about to getConfiguration');
try {
configuration = await getConfiguration(uri, fsPath, library, configFile);
} catch (err) {
// this should not happen since we guard against incorrect configurations
showConfigurationFailure(conn, err);
trace(`No linting: exception when getting tslint configuration for ${fsPath}, configFile= ${configFile}`);
return diagnostics;
}
if (!configuration) {
trace(`No linting: no tslint configuration`);
return diagnostics;
}
trace('validateTextDocument: configuration fetched');
if (isJsDocument(document) && !settings.jsEnable) {
trace(`No linting: a JS document, but js linting is disabled`);
return diagnostics;
}
if (settings.validateWithDefaultConfig === false && configCache.configuration!.isDefaultLinterConfig) {
trace(`No linting: linting with default tslint configuration is disabled`);
return diagnostics;
}
if (isExcludedFromLinterOptions(configuration.linterConfiguration, fsPath)) {
trace(`No linting: file is excluded using linterOptions.exclude`);
return diagnostics;
}
let result: tslint.LintResult;
let options: tslint.ILinterOptions = {
formatter: "json",
fix: false,
rulesDirectory: settings.rulesDirectory || undefined,
formattersDirectory: undefined
};
if (settings.trace && settings.trace.server === 'verbose') {
traceConfigurationFile(configuration.linterConfiguration);
}
// tslint writes warnings using console.warn, capture these warnings and send them to the client
let originalConsoleWarn = console.warn;
let captureWarnings = (message?: any) => {
conn.sendNotification(StatusNotification.type, { state: Status.warn });
originalConsoleWarn(message);
};
console.warn = captureWarnings;
try { // protect against tslint crashes
let linter = getLinterFromLibrary(library);
if (isTsLintVersion4(library)) {
let tslint = new linter(options);
trace(`Linting: start linting with tslint > version 4`);
tslint.lint(fsPath, contents, configuration.linterConfiguration);
result = tslint.getResult();
trace(`Linting: ended linting`);
}
// support for linting js files is only available in tslint > 4.0
else if (!isJsDocument(document)) {
(<any>options).configuration = configuration.linterConfiguration;
trace(`Linting: with tslint < version 4`);
let tslint = new (<any>linter)(fsPath, contents, options);
result = tslint.lint();
trace(`Linting: ended linting`);
} else {
trace(`No linting: JS linting not supported in tslint < version 4`);
return diagnostics;
}
} catch (err) {
conn.console.info(getErrorMessage(err, document));
connection.sendNotification(StatusNotification.type, { state: Status.error });
trace(`No linting: tslint exception while linting`);
return diagnostics;
}
finally {
console.warn = originalConsoleWarn;
}
if (result.failures.length > 0) {
filterProblemsForDocument(fsPath, result.failures).forEach(problem => {
let diagnostic = makeDiagnostic(settings, problem);
diagnostics.push(diagnostic);
recordCodeAction(document, diagnostic, problem);
});
}
trace('doValidate: sending diagnostics: ' + result.failures.length);
return diagnostics;
}
/**
* Filter failures for the given document
*/
function filterProblemsForDocument(documentPath: string, failures: tslint.RuleFailure[]): tslint.RuleFailure[] {
let normalizedPath = path.normalize(documentPath);
// we only show diagnostics targetting this open document, some tslint rule return diagnostics for other documents/files
let normalizedFiles = {};
return failures.filter(each => {
let fileName = each.getFileName();
if (!normalizedFiles[fileName]) {
normalizedFiles[fileName] = path.normalize(fileName);
}
return normalizedFiles[fileName] === normalizedPath;
});
}
function isJsDocument(document: server.TextDocument) {
return (document.languageId === "javascript" || document.languageId === "javascriptreact");
}
function testForExclusionPattern(path: string, pattern: string): boolean {
return minimatch(path, pattern, { dot: true });
}
function fileIsExcluded(settings: Settings, path: string): boolean {
if (settings.ignoreDefinitionFiles) {
if (path.endsWith('.d.ts')) {
return true;
}
}
if (settings.exclude) {
if (Array.isArray(settings.exclude)) {
for (let pattern of settings.exclude) {
if (testForExclusionPattern(path, pattern)) {
return true;
}
}
} else if (testForExclusionPattern(path, <string>settings.exclude)) {
return true;
}
}
return false;
}
documents.onDidOpen(async (event) => {
trace('onDidOpen');
triggerValidateDocument(event.document);
@ -683,7 +346,6 @@ documents.onDidClose((event) => {
// A text document was closed we clear the diagnostics
trace('onDidClose' + event.document.uri);
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
document2Library.delete(event.document.uri);
});
function triggerValidateDocument(document: server.TextDocument) {
@ -727,7 +389,9 @@ connection.onDidChangeConfiguration((params) => {
trace('onDidChangeConfiguraton');
globalSettings = params.settings;
configCache.flush();
if (tslintRunner) {
tslintRunner.onConfigFileChange('');
}
settingsCache.flush();
validateAllTextDocuments(connection, documents.all());
});
@ -747,7 +411,9 @@ connection.onDidChangeWatchedFiles((params) => {
}
});
configCache.flush();
if (tslintRunner) {
tslintRunner.onConfigFileChange('');
}
if (tslintConfigurationValid()) {
validateAllTextDocuments(connection, documents.all());
}
@ -1108,28 +774,4 @@ function concatenateEdits(fixes: AutoFix[]): server.TextEdit[] {
return textEdits;
}
function traceConfigurationFile(configuration: tslint.Configuration.IConfigurationFile | undefined) {
if (!configuration) {
trace("no tslint configuration");
return;
}
trace("tslint configuration:", util.inspect(configuration, undefined, 4));
}
function getGlobalPackageManagerPath(packageManager: string): string | undefined {
trace(`Begin - Resolve Global Package Manager Path for: ${packageManager}`);
if (!globalPackageManagerPath.has(packageManager)) {
let path: string | undefined;
if (packageManager === 'npm') {
path = server.Files.resolveGlobalNodePath(trace);
} else if (packageManager === 'yarn') {
path = server.Files.resolveGlobalYarnPath(trace);
}
globalPackageManagerPath.set(packageManager, path!);
}
trace(`Done - Resolve Global Package Manager Path for: ${packageManager}`);
return globalPackageManagerPath.get(packageManager);
}
connection.listen();