@fimbul/wotan
Version:
Pluggable TypeScript and JavaScript linter
361 lines • 18.2 kB
JavaScript
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
;