UNPKG

typescript-tslint-plugin

Version:
426 lines 18.3 kB
"use strict"; 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