Leverage tslintRunner
This commit is contained in:
Родитель
b9b05c0eeb
Коммит
12785c2679
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
Загрузка…
Ссылка в новой задаче