UNPKG

@fimbul/wotan

Version:

Pluggable TypeScript and JavaScript linter

361 lines 18.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Runner = void 0; const tslib_1 = require("tslib"); const linter_1 = require("./linter"); const ymir_1 = require("@fimbul/ymir"); const path = require("path"); const ts = require("typescript"); const glob = require("glob"); const utils_1 = require("./utils"); const minimatch_1 = require("minimatch"); const processor_loader_1 = require("./services/processor-loader"); const inversify_1 = require("inversify"); const cached_file_system_1 = require("./services/cached-file-system"); const configuration_manager_1 = require("./services/configuration-manager"); const project_host_1 = require("./project-host"); const debug = require("debug"); const normalize_glob_1 = require("normalize-glob"); const program_state_1 = require("./services/program-state"); const config_hash_1 = require("./config-hash"); const log = debug('wotan:runner'); let Runner = class Runner { constructor(fs, configManager, linter, processorLoader, directories, logger, filterFactory, programStateFactory) { this.fs = fs; this.configManager = configManager; this.linter = linter; this.processorLoader = processorLoader; this.directories = directories; this.logger = logger; this.filterFactory = filterFactory; this.programStateFactory = programStateFactory; } lintCollection(options) { const config = options.config !== undefined ? this.configManager.loadLocalOrResolved(options.config) : undefined; const cwd = this.directories.getCurrentDirectory(); const files = options.files.map((pattern) => ({ hasMagic: glob.hasMagic(pattern), normalized: Array.from(normalize_glob_1.normalizeGlob(pattern, cwd)) })); const exclude = utils_1.flatMap(options.exclude, (pattern) => normalize_glob_1.normalizeGlob(pattern, cwd)); const linterOptions = { reportUselessDirectives: options.reportUselessDirectives ? options.reportUselessDirectives === true ? 'error' : options.reportUselessDirectives : undefined, }; if (options.project.length === 0 && options.files.length !== 0) return this.lintFiles({ ...options, files, exclude }, config, linterOptions); return this.lintProject({ ...options, files, exclude }, config, linterOptions); } *lintProject(options, config, linterOptions) { const processorHost = new project_host_1.ProjectHost(this.directories.getCurrentDirectory(), config, this.fs, this.configManager, this.processorLoader); for (let { files, program, configFilePath: tsconfigPath } of this.getFilesAndProgram(options.project, options.files, options.exclude, processorHost, options.references)) { const programState = options.cache ? this.programStateFactory.create(program, processorHost, tsconfigPath) : undefined; let invalidatedProgram = false; const factory = { getCompilerOptions() { return program.getCompilerOptions(); }, getProgram() { if (invalidatedProgram) { log('updating invalidated program'); program = processorHost.updateProgram(program); invalidatedProgram = false; } return program; }, }; for (const file of files) { if (options.config === undefined) config = this.configManager.find(file); const mapped = processorHost.getProcessedFileInfo(file); const originalName = mapped === undefined ? file : mapped.originalName; const effectiveConfig = config && this.configManager.reduce(config, originalName); if (effectiveConfig === undefined) continue; let sourceFile = program.getSourceFile(file); const originalContent = mapped === undefined ? sourceFile.text : mapped.originalContent; let summary; const fix = shouldFix(sourceFile, options, originalName); const configHash = programState === undefined ? undefined : config_hash_1.createConfigHash(effectiveConfig, linterOptions); const resultFromCache = programState === null || programState === void 0 ? void 0 : programState.getUpToDateResult(sourceFile.fileName, configHash); if (fix) { let updatedFile = false; summary = this.linter.lintAndFix(sourceFile, originalContent, effectiveConfig, (content, range) => { invalidatedProgram = true; const oldContent = sourceFile.text; sourceFile = ts.updateSourceFile(sourceFile, content, range); const hasErrors = utils_1.hasParseErrors(sourceFile); if (hasErrors) { log("Autofixing caused syntax errors in '%s', rolling back", sourceFile.fileName); sourceFile = ts.updateSourceFile(sourceFile, oldContent, utils_1.invertChangeRange(range)); } else { updatedFile = true; } // either way we need to store the new SourceFile as the old one is now corrupted processorHost.updateSourceFile(sourceFile); return hasErrors ? undefined : sourceFile; }, fix === true ? undefined : fix, factory, mapped === null || mapped === void 0 ? void 0 : mapped.processor, linterOptions, // pass cached results so we can apply fixes from cache resultFromCache); if (updatedFile) programState === null || programState === void 0 ? void 0 : programState.update(factory.getProgram(), sourceFile.fileName); } else { summary = { findings: resultFromCache !== null && resultFromCache !== void 0 ? resultFromCache : this.linter.getFindings(sourceFile, effectiveConfig, factory, mapped === null || mapped === void 0 ? void 0 : mapped.processor, linterOptions), fixes: 0, content: originalContent, }; } if (programState !== undefined && resultFromCache !== summary.findings) programState.setFileResult(file, configHash, summary.findings); yield [originalName, summary]; } programState === null || programState === void 0 ? void 0 : programState.save(); } } *lintFiles(options, config, linterOptions) { let processor; for (const file of getFiles(options.files, options.exclude, this.directories.getCurrentDirectory())) { if (options.config === undefined) config = this.configManager.find(file); const effectiveConfig = config && this.configManager.reduce(config, file); if (effectiveConfig === undefined) continue; let originalContent; let name; let content; if (effectiveConfig.processor) { const ctor = this.processorLoader.loadProcessor(effectiveConfig.processor); if (utils_1.hasSupportedExtension(file, options.extensions)) { name = file; } else { name = file + ctor.getSuffixForFile({ fileName: file, getSettings: () => effectiveConfig.settings, readFile: () => originalContent = this.fs.readFile(file), }); if (!utils_1.hasSupportedExtension(name, options.extensions)) continue; } if (originalContent === undefined) // might be initialized by the processor requesting the file content originalContent = this.fs.readFile(file); processor = new ctor({ source: originalContent, sourceFileName: file, targetFileName: name, settings: effectiveConfig.settings, }); content = processor.preprocess(); } else if (utils_1.hasSupportedExtension(file, options.extensions)) { processor = undefined; name = file; content = originalContent = this.fs.readFile(file); } else { continue; } let sourceFile = ts.createSourceFile(name, content, ts.ScriptTarget.ESNext, true); const fix = shouldFix(sourceFile, options, file); let summary; if (fix) { summary = this.linter.lintAndFix(sourceFile, originalContent, effectiveConfig, (newContent, range) => { sourceFile = ts.updateSourceFile(sourceFile, newContent, range); if (utils_1.hasParseErrors(sourceFile)) { log("Autofixing caused syntax errors in '%s', rolling back", sourceFile.fileName); // Note: 'sourceFile' shouldn't be used after this as it contains invalid code return; } return sourceFile; }, fix === true ? undefined : fix, undefined, processor, linterOptions); } else { summary = { findings: this.linter.getFindings(sourceFile, effectiveConfig, undefined, processor, linterOptions), fixes: 0, content: originalContent, }; } yield [file, summary]; } } *getFilesAndProgram(projects, patterns, exclude, host, references) { const cwd = utils_1.unixifyPath(this.directories.getCurrentDirectory()); if (projects.length !== 0) { projects = projects.map((configFile) => this.checkConfigDirectory(utils_1.unixifyPath(path.resolve(cwd, configFile)))); } else if (references) { projects = [this.checkConfigDirectory(cwd)]; } else { const project = ts.findConfigFile(cwd, (f) => this.fs.isFile(f)); if (project === undefined) throw new ymir_1.ConfigurationError(`Cannot find tsconfig.json for directory '${cwd}'.`); projects = [project]; } const allMatchedFiles = []; const include = []; const nonMagicGlobs = []; for (const pattern of patterns) { if (!pattern.hasMagic) { const mm = new minimatch_1.Minimatch(pattern.normalized[0]); nonMagicGlobs.push({ raw: pattern.normalized[0], match: mm }); include.push(mm); } else { include.push(...pattern.normalized.map((p) => new minimatch_1.Minimatch(p))); } } const ex = exclude.map((p) => new minimatch_1.Minimatch(p, { dot: true })); const projectsSeen = []; let filesOfPreviousProject; for (const { program, configFilePath } of this.createPrograms(projects, host, projectsSeen, references, isFileIncluded)) { const ownFiles = []; const files = []; const fileFilter = this.filterFactory.create({ program, host }); for (const sourceFile of program.getSourceFiles()) { if (!fileFilter.filter(sourceFile)) continue; const { fileName } = sourceFile; ownFiles.push(fileName); const originalName = host.getFileSystemFile(fileName); if (!isFileIncluded(originalName)) continue; files.push(fileName); allMatchedFiles.push(originalName); } // uncache all files of the previous project if they are no longer needed if (filesOfPreviousProject !== undefined) for (const oldFile of filesOfPreviousProject) if (!ownFiles.includes(oldFile)) host.uncacheFile(oldFile); filesOfPreviousProject = ownFiles; if (files.length !== 0) yield { files, program, configFilePath }; } ensurePatternsMatch(nonMagicGlobs, ex, allMatchedFiles, projectsSeen); function isFileIncluded(fileName) { return (include.length === 0 || include.some((p) => p.match(fileName))) && !ex.some((p) => p.match(fileName)); } } checkConfigDirectory(fileOrDirName) { switch (this.fs.getKind(fileOrDirName)) { case 0 /* NonExistent */: throw new ymir_1.ConfigurationError(`The specified path does not exist: '${fileOrDirName}'`); case 2 /* Directory */: { const file = utils_1.unixifyPath(path.join(fileOrDirName, 'tsconfig.json')); if (!this.fs.isFile(file)) throw new ymir_1.ConfigurationError(`Cannot find a tsconfig.json file at the specified directory: '${fileOrDirName}'`); return file; } default: return fileOrDirName; } } *createPrograms(projects, host, seen, references, isFileIncluded) { for (const configFile of projects) { if (configFile === undefined) continue; const configFilePath = typeof configFile === 'string' ? configFile : configFile.sourceFile.fileName; if (!utils_1.addUnique(seen, configFilePath)) continue; let commandLine; if (typeof configFile !== 'string') { ({ commandLine } = configFile); } else { commandLine = host.getParsedCommandLine(configFile); if (commandLine === undefined) continue; } if (commandLine.errors.length !== 0) this.logger.warn(ts.formatDiagnostics(commandLine.errors, host)); if (commandLine.fileNames.length !== 0) { if (!commandLine.options.composite || commandLine.fileNames.some((file) => isFileIncluded(host.getFileSystemFile(file)))) { log("Using project '%s'", configFilePath); let resolvedReferences; { // this is in a nested block to allow garbage collection while recursing const program = host.createProgram(commandLine.fileNames, commandLine.options, undefined, commandLine.projectReferences); yield { program, configFilePath }; if (references) resolvedReferences = program.getResolvedProjectReferences(); } if (resolvedReferences !== undefined) yield* this.createPrograms(resolvedReferences, host, seen, true, isFileIncluded); continue; } log("Project '%s' contains no file to lint", configFilePath); } if (references) { if (typeof configFile !== 'string') { if (configFile.references !== undefined) yield* this.createPrograms(configFile.references, host, seen, true, isFileIncluded); } else if (commandLine.projectReferences !== undefined) { yield* this.createPrograms(commandLine.projectReferences.map((ref) => this.checkConfigDirectory(ref.path)), host, seen, true, isFileIncluded); } } } } }; Runner = tslib_1.__decorate([ inversify_1.injectable(), tslib_1.__metadata("design:paramtypes", [cached_file_system_1.CachedFileSystem, configuration_manager_1.ConfigurationManager, linter_1.Linter, processor_loader_1.ProcessorLoader, ymir_1.DirectoryService, ymir_1.MessageHandler, ymir_1.FileFilterFactory, program_state_1.ProgramStateFactory]) ], Runner); exports.Runner = Runner; function getFiles(patterns, exclude, cwd) { const result = []; const globOptions = { cwd, nobrace: true, cache: {}, ignore: exclude, nodir: true, realpathCache: {}, statCache: {}, symlinks: {}, }; for (const pattern of patterns) { let matched = pattern.hasMagic; for (const normalized of pattern.normalized) { const match = glob.sync(normalized, globOptions); if (match.length !== 0) { matched = true; result.push(...match); } } if (!matched && !isExcluded(pattern.normalized[0], exclude.map((p) => new minimatch_1.Minimatch(p, { dot: true })))) throw new ymir_1.ConfigurationError(`'${pattern.normalized[0]}' does not exist.`); } return new Set(result.map(utils_1.unixifyPath)); // deduplicate files } function ensurePatternsMatch(include, exclude, files, projects) { for (const pattern of include) if (!isExcluded(pattern.raw, exclude) && !files.some((f) => pattern.match.match(f))) throw new ymir_1.ConfigurationError(`'${pattern.raw}' is not included in any of the projects: '${projects.join("', '")}'.`); } function isExcluded(file, exclude) { for (const e of exclude) if (e.match(file)) return true; return false; } function shouldFix(sourceFile, options, originalName) { if (options.fix && utils_1.hasParseErrors(sourceFile)) { log("Not fixing '%s' because of parse errors.", originalName); return false; } return options.fix; } //# sourceMappingURL=runner.js.map