typescript-tslint-plugin
Version:
TypeScript tslint language service plugin
426 lines • 18.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TsLintRunner = exports.WorkspaceLibraryExecution = exports.toPackageManager = void 0;
const cp = require("child_process");
const minimatch = require("minimatch");
const path_1 = require("path");
const util = require("util");
const server = require("vscode-languageserver");
const mruCache_1 = require("./mruCache");
function toPackageManager(manager) {
switch (manager && manager.toLowerCase()) {
case 'npm': return 'npm';
case 'pnpm': return 'pnpm';
case 'yarn': return 'yarn';
default: return undefined;
}
}
exports.toPackageManager = toPackageManager;
/**
* Controls where TSlint and other scripts can be loaded from.
*/
var WorkspaceLibraryExecution;
(function (WorkspaceLibraryExecution) {
/**
* Block executing TSLint, linter rules, and other scripts from the current workspace.
*/
WorkspaceLibraryExecution[WorkspaceLibraryExecution["Disallow"] = 1] = "Disallow";
/**
* Enable loading TSLint and rules from the workspace.
*/
WorkspaceLibraryExecution[WorkspaceLibraryExecution["Allow"] = 2] = "Allow";
/**
* The workspace library execution has not yet been configured or cannot be determined.
*/
WorkspaceLibraryExecution[WorkspaceLibraryExecution["Unknown"] = 3] = "Unknown";
})(WorkspaceLibraryExecution = exports.WorkspaceLibraryExecution || (exports.WorkspaceLibraryExecution = {}));
class ConfigCache {
constructor() {
this.filePath = undefined;
this.configuration = undefined;
}
set(filePath, configuration) {
this.filePath = filePath;
this.configuration = configuration;
}
get(forPath) {
return forPath === this.filePath ? this.configuration : undefined;
}
isDefaultLinterConfig() {
return !!(this.configuration && this.configuration.isDefaultLinterConfig);
}
flush() {
this.filePath = undefined;
this.configuration = undefined;
}
}
const emptyLintResult = {
errorCount: 0,
warningCount: 0,
failures: [],
fixes: [],
format: '',
output: '',
};
const emptyResult = {
lintResult: emptyLintResult,
warnings: [],
};
class TsLintRunner {
constructor(trace) {
this.trace = trace;
this.tslintPath2Library = new Map();
this.document2LibraryCache = new mruCache_1.MruCache(100);
// map stores undefined values to represent failed resolutions
this.globalPackageManagerPath = new Map();
this.configCache = new ConfigCache();
}
runTsLint(filePath, contents, configuration) {
this.traceMethod('runTsLint', 'start');
const warnings = [];
if (!this.document2LibraryCache.has(filePath)) {
this.loadLibrary(filePath, configuration);
}
this.traceMethod('runTsLint', 'Loaded tslint library');
if (!this.document2LibraryCache.has(filePath)) {
return emptyResult;
}
const cacheEntry = this.document2LibraryCache.get(filePath);
let library;
switch (configuration.workspaceLibraryExecution) {
case WorkspaceLibraryExecution.Disallow:
library = cacheEntry.getTSLint(false);
break;
case WorkspaceLibraryExecution.Allow:
library = cacheEntry.getTSLint(true);
break;
default:
if (cacheEntry.workspaceTslintPath) {
if (this.isWorkspaceImplicitlyTrusted(cacheEntry.workspaceTslintPath)) {
configuration = Object.assign(Object.assign({}, configuration), { workspaceLibraryExecution: WorkspaceLibraryExecution.Allow });
library = cacheEntry.getTSLint(true);
break;
}
// If the user has not explicitly trusted/not trusted the workspace AND we have a workspace TS version
// show a special error that lets the user trust/untrust the workspace
return {
lintResult: emptyLintResult,
warnings: [
getWorkspaceNotTrustedMessage(filePath),
],
};
}
else if (cacheEntry.globalTsLintPath) {
library = cacheEntry.getTSLint(false);
}
break;
}
if (!library) {
return {
lintResult: emptyLintResult,
warnings: [
getInstallFailureMessage(filePath, configuration.packageManager || 'npm'),
],
};
}
this.traceMethod('runTsLint', 'About to validate ' + filePath);
return this.doRun(filePath, contents, library, configuration, warnings);
}
onConfigFileChange(_tsLintFilePath) {
this.configCache.flush();
}
traceMethod(method, message) {
this.trace(`(${method}) ${message}`);
}
loadLibrary(filePath, configuration) {
this.traceMethod('loadLibrary', `trying to load ${filePath}`);
const directory = path_1.dirname(filePath);
const tsLintPaths = this.getTsLintPaths(directory, configuration.packageManager);
this.traceMethod('loadLibrary', `Resolved tslint to workspace: '${tsLintPaths.workspaceTsLintPath}' global: '${tsLintPaths.globalTsLintPath}'`);
this.document2LibraryCache.set(filePath, {
workspaceTslintPath: tsLintPaths.workspaceTsLintPath || undefined,
globalTsLintPath: tsLintPaths.globalTsLintPath || undefined,
getTSLint: (allowWorkspaceLibraryExecution) => {
const tsLintPath = allowWorkspaceLibraryExecution
? tsLintPaths.workspaceTsLintPath || tsLintPaths.globalTsLintPath
: tsLintPaths.globalTsLintPath;
if (!tsLintPath) {
return;
}
let library;
if (!this.tslintPath2Library.has(tsLintPath)) {
try {
library = require(tsLintPath);
}
catch (e) {
this.tslintPath2Library.set(tsLintPath, undefined);
return;
}
this.tslintPath2Library.set(tsLintPath, { tslint: library, path: tsLintPath });
}
return this.tslintPath2Library.get(tsLintPath);
}
});
}
getTsLintPaths(directory, packageManager) {
const globalPath = this.getGlobalPackageManagerPath(packageManager);
let workspaceTsLintPath;
try {
workspaceTsLintPath = this.resolveTsLint({ nodePath: undefined, cwd: directory }) || undefined;
}
catch (_a) {
// noop
}
let globalTSLintPath;
try {
globalTSLintPath = this.resolveTsLint({ nodePath: undefined, cwd: globalPath })
|| this.resolveTsLint({ nodePath: globalPath, cwd: globalPath });
}
catch (_b) {
// noop
}
return { workspaceTsLintPath, globalTsLintPath: globalTSLintPath };
}
getGlobalPackageManagerPath(packageManager = 'npm') {
this.traceMethod('getGlobalPackageManagerPath', `Begin - Resolve Global Package Manager Path for: ${packageManager}`);
if (!this.globalPackageManagerPath.has(packageManager)) {
let path;
if (packageManager === 'npm') {
path = server.Files.resolveGlobalNodePath(this.trace);
}
else if (packageManager === 'yarn') {
path = server.Files.resolveGlobalYarnPath(this.trace);
}
else if (packageManager === 'pnpm') {
path = cp.execSync('pnpm root -g').toString().trim();
}
this.globalPackageManagerPath.set(packageManager, path);
}
this.traceMethod('getGlobalPackageManagerPath', `Done - Resolve Global Package Manager Path for: ${packageManager}`);
return this.globalPackageManagerPath.get(packageManager);
}
doRun(filePath, contents, library, configuration, warnings) {
this.traceMethod('doRun', `starting validation for ${filePath}`);
let cwd = configuration.workspaceFolderPath;
if (!cwd && typeof contents === "object") {
cwd = contents.getCurrentDirectory();
}
if (this.fileIsExcluded(configuration, filePath, cwd)) {
this.traceMethod('doRun', `No linting: file ${filePath} is excluded`);
return emptyResult;
}
let cwdToRestore;
if (cwd && configuration.workspaceLibraryExecution === WorkspaceLibraryExecution.Allow) {
this.traceMethod('doRun', `Changed directory to ${cwd}`);
cwdToRestore = process.cwd();
process.chdir(cwd);
}
try {
const configFile = configuration.configFile || null;
let linterConfiguration;
this.traceMethod('doRun', 'About to getConfiguration');
try {
linterConfiguration = this.getConfiguration(filePath, filePath, library.tslint, configFile);
}
catch (err) {
this.traceMethod('doRun', `No linting: exception when getting tslint configuration for ${filePath}, configFile= ${configFile}`);
warnings.push(getConfigurationFailureMessage(err));
return {
lintResult: emptyLintResult,
warnings,
};
}
if (!linterConfiguration) {
this.traceMethod('doRun', `No linting: no tslint configuration`);
return emptyResult;
}
this.traceMethod('doRun', 'Configuration fetched');
if (isJsDocument(filePath) && !configuration.jsEnable) {
this.traceMethod('doRun', `No linting: a JS document, but js linting is disabled`);
return emptyResult;
}
if (configuration.validateWithDefaultConfig === false && this.configCache.configuration.isDefaultLinterConfig) {
this.traceMethod('doRun', `No linting: linting with default tslint configuration is disabled`);
return emptyResult;
}
if (isExcludedFromLinterOptions(linterConfiguration.linterConfiguration, filePath)) {
this.traceMethod('doRun', `No linting: file is excluded using linterOptions.exclude`);
return emptyResult;
}
let result;
const isTrustedWorkspace = configuration.workspaceLibraryExecution === WorkspaceLibraryExecution.Allow;
// Only allow using a custom rules directory if the workspace has been trusted by the user
const rulesDirectory = isTrustedWorkspace ? configuration.rulesDirectory : [];
const options = {
formatter: "json",
fix: false,
rulesDirectory,
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) => {
warnings.push(message);
originalConsoleWarn(message);
};
console.warn = captureWarnings;
const sanitizedLintConfiguration = Object.assign({}, linterConfiguration.linterConfiguration);
// Only allow using a custom rules directory if the workspace has been trusted by the user
if (!isTrustedWorkspace) {
sanitizedLintConfiguration.rulesDirectory = [];
}
try { // clean up if tslint crashes
const linter = new library.tslint.Linter(options, typeof contents === 'string' ? undefined : contents);
this.traceMethod('doRun', `Linting: start linting`);
linter.lint(filePath, typeof contents === 'string' ? contents : '', sanitizedLintConfiguration);
result = linter.getResult();
this.traceMethod('doRun', `Linting: ended linting`);
}
finally {
console.warn = originalConsoleWarn;
}
return {
lintResult: result,
warnings,
workspaceFolderPath: configuration.workspaceFolderPath,
configFilePath: linterConfiguration.path,
};
}
finally {
if (typeof cwdToRestore === 'string') {
process.chdir(cwdToRestore);
}
}
}
/**
* Check if `tslintPath` is next to the running TS version. This indicates that the user has
* implicitly trusted the workspace since they are already running TS from it.
*/
isWorkspaceImplicitlyTrusted(tslintPath) {
const tsPath = process.argv[1];
const nodeModulesPath = path_1.join(tsPath, '..', '..', '..');
const rel = path_1.relative(nodeModulesPath, path_1.normalize(tslintPath));
if (rel === `tslint${path_1.sep}lib${path_1.sep}index.js`) {
return true;
}
return false;
}
getConfiguration(uri, filePath, library, configFileName) {
this.traceMethod('getConfiguration', `Starting for ${uri}`);
const config = this.configCache.get(filePath);
if (config) {
return config;
}
let isDefaultConfig = false;
let linterConfiguration;
const linter = library.Linter;
if (linter.findConfigurationPath) {
isDefaultConfig = linter.findConfigurationPath(configFileName, filePath) === undefined;
}
const configurationResult = linter.findConfiguration(configFileName, filePath);
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.
if (linterConfiguration) {
if (linterConfiguration.rules) {
linterConfiguration.rules.delete('no-unused-variable');
}
if (linterConfiguration.jsRules) {
linterConfiguration.jsRules.delete('no-unused-variable');
}
}
const configuration = {
isDefaultLinterConfig: isDefaultConfig,
linterConfiguration,
path: configurationResult.path,
};
this.configCache.set(filePath, configuration);
return this.configCache.configuration;
}
fileIsExcluded(settings, filePath, cwd) {
if (settings.ignoreDefinitionFiles && filePath.endsWith('.d.ts')) {
return true;
}
return settings.exclude.some(pattern => testForExclusionPattern(filePath, pattern, cwd));
}
traceConfigurationFile(configuration) {
if (!configuration) {
this.trace("no tslint configuration");
return;
}
this.trace("tslint configuration:" + util.inspect(configuration, undefined, 4));
}
resolveTsLint(options) {
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 (options.nodePath) {
newEnv[nodePathKey] = options.nodePath;
}
newEnv.ELECTRON_RUN_AS_NODE = '1';
const spawnResults = cp.spawnSync(process.argv0, ['-e', app], { cwd: options.cwd, env: newEnv });
return spawnResults.stdout.toString().trim() || undefined;
}
}
exports.TsLintRunner = TsLintRunner;
function testForExclusionPattern(filePath, pattern, cwd) {
if (cwd) {
// try first as relative
const relPath = path_1.relative(cwd, filePath);
if (minimatch(relPath, pattern, { dot: true })) {
return true;
}
if (relPath === filePath) {
return false;
}
}
return minimatch(filePath, pattern, { dot: true });
}
function getInstallFailureMessage(filePath, packageManager) {
const localCommands = {
npm: 'npm install tslint',
pnpm: 'pnpm install tslint',
yarn: 'yarn add tslint',
};
const globalCommands = {
npm: 'npm install -g tslint',
pnpm: 'pnpm install -g tslint',
yarn: 'yarn global add tslint',
};
return [
`Failed to load the TSLint library for '${filePath}'`,
`To use TSLint, please install tslint using \'${localCommands[packageManager]}\' or globally using \'${globalCommands[packageManager]}\'.`,
'Be sure to restart your editor after installing tslint.',
].join('\n');
}
function getWorkspaceNotTrustedMessage(filePath) {
return [
`Not using the local TSLint version found for '${filePath}'`,
'To enable code execution from the current workspace you must enable workspace library execution.',
].join('\n');
}
function isJsDocument(filePath) {
return /\.(jsx?|mjs)$/i.test(filePath);
}
function isExcludedFromLinterOptions(config, fileName) {
if (config === undefined || config.linterOptions === undefined || config.linterOptions.exclude === undefined) {
return false;
}
return config.linterOptions.exclude.some(pattern => testForExclusionPattern(fileName, pattern, undefined));
}
function getConfigurationFailureMessage(err) {
let errorMessage = `unknown error`;
if (typeof err.message === 'string' || err.message instanceof String) {
errorMessage = err.message;
}
return `Cannot read tslint configuration - '${errorMessage}'`;
}
//# sourceMappingURL=index.js.map