UNPKG

18.2 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.Runner = void 0;
4const tslib_1 = require("tslib");
5const linter_1 = require("./linter");
6const ymir_1 = require("@fimbul/ymir");
7const path = require("path");
8const ts = require("typescript");
9const glob = require("glob");
10const utils_1 = require("./utils");
11const minimatch_1 = require("minimatch");
12const processor_loader_1 = require("./services/processor-loader");
13const inversify_1 = require("inversify");
14const cached_file_system_1 = require("./services/cached-file-system");
15const configuration_manager_1 = require("./services/configuration-manager");
16const project_host_1 = require("./project-host");
17const debug = require("debug");
18const normalize_glob_1 = require("normalize-glob");
19const program_state_1 = require("./services/program-state");
20const config_hash_1 = require("./config-hash");
21const log = debug('wotan:runner');
22let Runner = class Runner {
23 constructor(fs, configManager, linter, processorLoader, directories, logger, filterFactory, programStateFactory) {
24 this.fs = fs;
25 this.configManager = configManager;
26 this.linter = linter;
27 this.processorLoader = processorLoader;
28 this.directories = directories;
29 this.logger = logger;
30 this.filterFactory = filterFactory;
31 this.programStateFactory = programStateFactory;
32 }
33 lintCollection(options) {
34 const config = options.config !== undefined ? this.configManager.loadLocalOrResolved(options.config) : undefined;
35 const cwd = this.directories.getCurrentDirectory();
36 const files = options.files.map((pattern) => ({ hasMagic: glob.hasMagic(pattern), normalized: Array.from(normalize_glob_1.normalizeGlob(pattern, cwd)) }));
37 const exclude = utils_1.flatMap(options.exclude, (pattern) => normalize_glob_1.normalizeGlob(pattern, cwd));
38 const linterOptions = {
39 reportUselessDirectives: options.reportUselessDirectives
40 ? options.reportUselessDirectives === true
41 ? 'error'
42 : options.reportUselessDirectives
43 : undefined,
44 };
45 if (options.project.length === 0 && options.files.length !== 0)
46 return this.lintFiles({ ...options, files, exclude }, config, linterOptions);
47 return this.lintProject({ ...options, files, exclude }, config, linterOptions);
48 }
49 *lintProject(options, config, linterOptions) {
50 const processorHost = new project_host_1.ProjectHost(this.directories.getCurrentDirectory(), config, this.fs, this.configManager, this.processorLoader);
51 for (let { files, program, configFilePath: tsconfigPath } of this.getFilesAndProgram(options.project, options.files, options.exclude, processorHost, options.references)) {
52 const programState = options.cache ? this.programStateFactory.create(program, processorHost, tsconfigPath) : undefined;
53 let invalidatedProgram = false;
54 const factory = {
55 getCompilerOptions() {
56 return program.getCompilerOptions();
57 },
58 getProgram() {
59 if (invalidatedProgram) {
60 log('updating invalidated program');
61 program = processorHost.updateProgram(program);
62 invalidatedProgram = false;
63 }
64 return program;
65 },
66 };
67 for (const file of files) {
68 if (options.config === undefined)
69 config = this.configManager.find(file);
70 const mapped = processorHost.getProcessedFileInfo(file);
71 const originalName = mapped === undefined ? file : mapped.originalName;
72 const effectiveConfig = config && this.configManager.reduce(config, originalName);
73 if (effectiveConfig === undefined)
74 continue;
75 let sourceFile = program.getSourceFile(file);
76 const originalContent = mapped === undefined ? sourceFile.text : mapped.originalContent;
77 let summary;
78 const fix = shouldFix(sourceFile, options, originalName);
79 const configHash = programState === undefined ? undefined : config_hash_1.createConfigHash(effectiveConfig, linterOptions);
80 const resultFromCache = programState === null || programState === void 0 ? void 0 : programState.getUpToDateResult(sourceFile.fileName, configHash);
81 if (fix) {
82 let updatedFile = false;
83 summary = this.linter.lintAndFix(sourceFile, originalContent, effectiveConfig, (content, range) => {
84 invalidatedProgram = true;
85 const oldContent = sourceFile.text;
86 sourceFile = ts.updateSourceFile(sourceFile, content, range);
87 const hasErrors = utils_1.hasParseErrors(sourceFile);
88 if (hasErrors) {
89 log("Autofixing caused syntax errors in '%s', rolling back", sourceFile.fileName);
90 sourceFile = ts.updateSourceFile(sourceFile, oldContent, utils_1.invertChangeRange(range));
91 }
92 else {
93 updatedFile = true;
94 }
95 // either way we need to store the new SourceFile as the old one is now corrupted
96 processorHost.updateSourceFile(sourceFile);
97 return hasErrors ? undefined : sourceFile;
98 }, fix === true ? undefined : fix, factory, mapped === null || mapped === void 0 ? void 0 : mapped.processor, linterOptions,
99 // pass cached results so we can apply fixes from cache
100 resultFromCache);
101 if (updatedFile)
102 programState === null || programState === void 0 ? void 0 : programState.update(factory.getProgram(), sourceFile.fileName);
103 }
104 else {
105 summary = {
106 findings: resultFromCache !== null && resultFromCache !== void 0 ? resultFromCache : this.linter.getFindings(sourceFile, effectiveConfig, factory, mapped === null || mapped === void 0 ? void 0 : mapped.processor, linterOptions),
107 fixes: 0,
108 content: originalContent,
109 };
110 }
111 if (programState !== undefined && resultFromCache !== summary.findings)
112 programState.setFileResult(file, configHash, summary.findings);
113 yield [originalName, summary];
114 }
115 programState === null || programState === void 0 ? void 0 : programState.save();
116 }
117 }
118 *lintFiles(options, config, linterOptions) {
119 let processor;
120 for (const file of getFiles(options.files, options.exclude, this.directories.getCurrentDirectory())) {
121 if (options.config === undefined)
122 config = this.configManager.find(file);
123 const effectiveConfig = config && this.configManager.reduce(config, file);
124 if (effectiveConfig === undefined)
125 continue;
126 let originalContent;
127 let name;
128 let content;
129 if (effectiveConfig.processor) {
130 const ctor = this.processorLoader.loadProcessor(effectiveConfig.processor);
131 if (utils_1.hasSupportedExtension(file, options.extensions)) {
132 name = file;
133 }
134 else {
135 name = file + ctor.getSuffixForFile({
136 fileName: file,
137 getSettings: () => effectiveConfig.settings,
138 readFile: () => originalContent = this.fs.readFile(file),
139 });
140 if (!utils_1.hasSupportedExtension(name, options.extensions))
141 continue;
142 }
143 if (originalContent === undefined) // might be initialized by the processor requesting the file content
144 originalContent = this.fs.readFile(file);
145 processor = new ctor({
146 source: originalContent,
147 sourceFileName: file,
148 targetFileName: name,
149 settings: effectiveConfig.settings,
150 });
151 content = processor.preprocess();
152 }
153 else if (utils_1.hasSupportedExtension(file, options.extensions)) {
154 processor = undefined;
155 name = file;
156 content = originalContent = this.fs.readFile(file);
157 }
158 else {
159 continue;
160 }
161 let sourceFile = ts.createSourceFile(name, content, ts.ScriptTarget.ESNext, true);
162 const fix = shouldFix(sourceFile, options, file);
163 let summary;
164 if (fix) {
165 summary = this.linter.lintAndFix(sourceFile, originalContent, effectiveConfig, (newContent, range) => {
166 sourceFile = ts.updateSourceFile(sourceFile, newContent, range);
167 if (utils_1.hasParseErrors(sourceFile)) {
168 log("Autofixing caused syntax errors in '%s', rolling back", sourceFile.fileName);
169 // Note: 'sourceFile' shouldn't be used after this as it contains invalid code
170 return;
171 }
172 return sourceFile;
173 }, fix === true ? undefined : fix, undefined, processor, linterOptions);
174 }
175 else {
176 summary = {
177 findings: this.linter.getFindings(sourceFile, effectiveConfig, undefined, processor, linterOptions),
178 fixes: 0,
179 content: originalContent,
180 };
181 }
182 yield [file, summary];
183 }
184 }
185 *getFilesAndProgram(projects, patterns, exclude, host, references) {
186 const cwd = utils_1.unixifyPath(this.directories.getCurrentDirectory());
187 if (projects.length !== 0) {
188 projects = projects.map((configFile) => this.checkConfigDirectory(utils_1.unixifyPath(path.resolve(cwd, configFile))));
189 }
190 else if (references) {
191 projects = [this.checkConfigDirectory(cwd)];
192 }
193 else {
194 const project = ts.findConfigFile(cwd, (f) => this.fs.isFile(f));
195 if (project === undefined)
196 throw new ymir_1.ConfigurationError(`Cannot find tsconfig.json for directory '${cwd}'.`);
197 projects = [project];
198 }
199 const allMatchedFiles = [];
200 const include = [];
201 const nonMagicGlobs = [];
202 for (const pattern of patterns) {
203 if (!pattern.hasMagic) {
204 const mm = new minimatch_1.Minimatch(pattern.normalized[0]);
205 nonMagicGlobs.push({ raw: pattern.normalized[0], match: mm });
206 include.push(mm);
207 }
208 else {
209 include.push(...pattern.normalized.map((p) => new minimatch_1.Minimatch(p)));
210 }
211 }
212 const ex = exclude.map((p) => new minimatch_1.Minimatch(p, { dot: true }));
213 const projectsSeen = [];
214 let filesOfPreviousProject;
215 for (const { program, configFilePath } of this.createPrograms(projects, host, projectsSeen, references, isFileIncluded)) {
216 const ownFiles = [];
217 const files = [];
218 const fileFilter = this.filterFactory.create({ program, host });
219 for (const sourceFile of program.getSourceFiles()) {
220 if (!fileFilter.filter(sourceFile))
221 continue;
222 const { fileName } = sourceFile;
223 ownFiles.push(fileName);
224 const originalName = host.getFileSystemFile(fileName);
225 if (!isFileIncluded(originalName))
226 continue;
227 files.push(fileName);
228 allMatchedFiles.push(originalName);
229 }
230 // uncache all files of the previous project if they are no longer needed
231 if (filesOfPreviousProject !== undefined)
232 for (const oldFile of filesOfPreviousProject)
233 if (!ownFiles.includes(oldFile))
234 host.uncacheFile(oldFile);
235 filesOfPreviousProject = ownFiles;
236 if (files.length !== 0)
237 yield { files, program, configFilePath };
238 }
239 ensurePatternsMatch(nonMagicGlobs, ex, allMatchedFiles, projectsSeen);
240 function isFileIncluded(fileName) {
241 return (include.length === 0 || include.some((p) => p.match(fileName))) && !ex.some((p) => p.match(fileName));
242 }
243 }
244 checkConfigDirectory(fileOrDirName) {
245 switch (this.fs.getKind(fileOrDirName)) {
246 case 0 /* NonExistent */:
247 throw new ymir_1.ConfigurationError(`The specified path does not exist: '${fileOrDirName}'`);
248 case 2 /* Directory */: {
249 const file = utils_1.unixifyPath(path.join(fileOrDirName, 'tsconfig.json'));
250 if (!this.fs.isFile(file))
251 throw new ymir_1.ConfigurationError(`Cannot find a tsconfig.json file at the specified directory: '${fileOrDirName}'`);
252 return file;
253 }
254 default:
255 return fileOrDirName;
256 }
257 }
258 *createPrograms(projects, host, seen, references, isFileIncluded) {
259 for (const configFile of projects) {
260 if (configFile === undefined)
261 continue;
262 const configFilePath = typeof configFile === 'string' ? configFile : configFile.sourceFile.fileName;
263 if (!utils_1.addUnique(seen, configFilePath))
264 continue;
265 let commandLine;
266 if (typeof configFile !== 'string') {
267 ({ commandLine } = configFile);
268 }
269 else {
270 commandLine = host.getParsedCommandLine(configFile);
271 if (commandLine === undefined)
272 continue;
273 }
274 if (commandLine.errors.length !== 0)
275 this.logger.warn(ts.formatDiagnostics(commandLine.errors, host));
276 if (commandLine.fileNames.length !== 0) {
277 if (!commandLine.options.composite || commandLine.fileNames.some((file) => isFileIncluded(host.getFileSystemFile(file)))) {
278 log("Using project '%s'", configFilePath);
279 let resolvedReferences;
280 {
281 // this is in a nested block to allow garbage collection while recursing
282 const program = host.createProgram(commandLine.fileNames, commandLine.options, undefined, commandLine.projectReferences);
283 yield { program, configFilePath };
284 if (references)
285 resolvedReferences = program.getResolvedProjectReferences();
286 }
287 if (resolvedReferences !== undefined)
288 yield* this.createPrograms(resolvedReferences, host, seen, true, isFileIncluded);
289 continue;
290 }
291 log("Project '%s' contains no file to lint", configFilePath);
292 }
293 if (references) {
294 if (typeof configFile !== 'string') {
295 if (configFile.references !== undefined)
296 yield* this.createPrograms(configFile.references, host, seen, true, isFileIncluded);
297 }
298 else if (commandLine.projectReferences !== undefined) {
299 yield* this.createPrograms(commandLine.projectReferences.map((ref) => this.checkConfigDirectory(ref.path)), host, seen, true, isFileIncluded);
300 }
301 }
302 }
303 }
304};
305Runner = tslib_1.__decorate([
306 inversify_1.injectable(),
307 tslib_1.__metadata("design:paramtypes", [cached_file_system_1.CachedFileSystem,
308 configuration_manager_1.ConfigurationManager,
309 linter_1.Linter,
310 processor_loader_1.ProcessorLoader,
311 ymir_1.DirectoryService,
312 ymir_1.MessageHandler,
313 ymir_1.FileFilterFactory,
314 program_state_1.ProgramStateFactory])
315], Runner);
316exports.Runner = Runner;
317function getFiles(patterns, exclude, cwd) {
318 const result = [];
319 const globOptions = {
320 cwd,
321 nobrace: true,
322 cache: {},
323 ignore: exclude,
324 nodir: true,
325 realpathCache: {},
326 statCache: {},
327 symlinks: {},
328 };
329 for (const pattern of patterns) {
330 let matched = pattern.hasMagic;
331 for (const normalized of pattern.normalized) {
332 const match = glob.sync(normalized, globOptions);
333 if (match.length !== 0) {
334 matched = true;
335 result.push(...match);
336 }
337 }
338 if (!matched && !isExcluded(pattern.normalized[0], exclude.map((p) => new minimatch_1.Minimatch(p, { dot: true }))))
339 throw new ymir_1.ConfigurationError(`'${pattern.normalized[0]}' does not exist.`);
340 }
341 return new Set(result.map(utils_1.unixifyPath)); // deduplicate files
342}
343function ensurePatternsMatch(include, exclude, files, projects) {
344 for (const pattern of include)
345 if (!isExcluded(pattern.raw, exclude) && !files.some((f) => pattern.match.match(f)))
346 throw new ymir_1.ConfigurationError(`'${pattern.raw}' is not included in any of the projects: '${projects.join("', '")}'.`);
347}
348function isExcluded(file, exclude) {
349 for (const e of exclude)
350 if (e.match(file))
351 return true;
352 return false;
353}
354function shouldFix(sourceFile, options, originalName) {
355 if (options.fix && utils_1.hasParseErrors(sourceFile)) {
356 log("Not fixing '%s' because of parse errors.", originalName);
357 return false;
358 }
359 return options.fix;
360}
361//# sourceMappingURL=runner.js.map
\No newline at end of file