1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.Runner = void 0;
|
4 | const tslib_1 = require("tslib");
|
5 | const linter_1 = require("./linter");
|
6 | const ymir_1 = require("@fimbul/ymir");
|
7 | const path = require("path");
|
8 | const ts = require("typescript");
|
9 | const glob = require("glob");
|
10 | const utils_1 = require("./utils");
|
11 | const minimatch_1 = require("minimatch");
|
12 | const processor_loader_1 = require("./services/processor-loader");
|
13 | const inversify_1 = require("inversify");
|
14 | const cached_file_system_1 = require("./services/cached-file-system");
|
15 | const configuration_manager_1 = require("./services/configuration-manager");
|
16 | const project_host_1 = require("./project-host");
|
17 | const debug = require("debug");
|
18 | const normalize_glob_1 = require("normalize-glob");
|
19 | const program_state_1 = require("./services/program-state");
|
20 | const config_hash_1 = require("./config-hash");
|
21 | const log = debug('wotan:runner');
|
22 | let 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 |
|
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 |
|
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)
|
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 |
|
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 |
|
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 :
|
247 | throw new ymir_1.ConfigurationError(`The specified path does not exist: '${fileOrDirName}'`);
|
248 | case 2 : {
|
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 |
|
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 | };
|
305 | Runner = 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);
|
316 | exports.Runner = Runner;
|
317 | function 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));
|
342 | }
|
343 | function 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 | }
|
348 | function isExcluded(file, exclude) {
|
349 | for (const e of exclude)
|
350 | if (e.match(file))
|
351 | return true;
|
352 | return false;
|
353 | }
|
354 | function 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 |
|
\ | No newline at end of file |