]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/cli-engine/cli-engine.js
72d1fa4d5dcd5df09899c6fe30adbe5d0bde0fc2
[pve-eslint.git] / eslint / lib / cli-engine / cli-engine.js
1 /**
2 * @fileoverview Main CLI object.
3 * @author Nicholas C. Zakas
4 */
5
6 "use strict";
7
8 /*
9 * The CLI object should *not* call process.exit() directly. It should only return
10 * exit codes. This allows other programs to use the CLI object and still control
11 * when the program exits.
12 */
13
14 //------------------------------------------------------------------------------
15 // Requirements
16 //------------------------------------------------------------------------------
17
18 const fs = require("fs");
19 const path = require("path");
20 const defaultOptions = require("../../conf/default-cli-options");
21 const pkg = require("../../package.json");
22 const ConfigOps = require("../shared/config-ops");
23 const naming = require("../shared/naming");
24 const ModuleResolver = require("../shared/relative-module-resolver");
25 const { Linter } = require("../linter");
26 const builtInRules = require("../rules");
27 const { CascadingConfigArrayFactory } = require("./cascading-config-array-factory");
28 const { IgnorePattern, getUsedExtractedConfigs } = require("./config-array");
29 const { FileEnumerator } = require("./file-enumerator");
30 const hash = require("./hash");
31 const LintResultCache = require("./lint-result-cache");
32
33 const debug = require("debug")("eslint:cli-engine");
34 const validFixTypes = new Set(["problem", "suggestion", "layout"]);
35
36 //------------------------------------------------------------------------------
37 // Typedefs
38 //------------------------------------------------------------------------------
39
40 // For VSCode IntelliSense
41 /** @typedef {import("../shared/types").ConfigData} ConfigData */
42 /** @typedef {import("../shared/types").LintMessage} LintMessage */
43 /** @typedef {import("../shared/types").ParserOptions} ParserOptions */
44 /** @typedef {import("../shared/types").Plugin} Plugin */
45 /** @typedef {import("../shared/types").RuleConf} RuleConf */
46 /** @typedef {import("../shared/types").Rule} Rule */
47 /** @typedef {ReturnType<CascadingConfigArrayFactory["getConfigArrayForFile"]>} ConfigArray */
48 /** @typedef {ReturnType<ConfigArray["extractConfig"]>} ExtractedConfig */
49
50 /**
51 * The options to configure a CLI engine with.
52 * @typedef {Object} CLIEngineOptions
53 * @property {boolean} allowInlineConfig Enable or disable inline configuration comments.
54 * @property {ConfigData} baseConfig Base config object, extended by all configs used with this CLIEngine instance
55 * @property {boolean} cache Enable result caching.
56 * @property {string} cacheLocation The cache file to use instead of .eslintcache.
57 * @property {string} configFile The configuration file to use.
58 * @property {string} cwd The value to use for the current working directory.
59 * @property {string[]} envs An array of environments to load.
60 * @property {string[]|null} extensions An array of file extensions to check.
61 * @property {boolean|Function} fix Execute in autofix mode. If a function, should return a boolean.
62 * @property {string[]} fixTypes Array of rule types to apply fixes for.
63 * @property {string[]} globals An array of global variables to declare.
64 * @property {boolean} ignore False disables use of .eslintignore.
65 * @property {string} ignorePath The ignore file to use instead of .eslintignore.
66 * @property {string|string[]} ignorePattern One or more glob patterns to ignore.
67 * @property {boolean} useEslintrc False disables looking for .eslintrc
68 * @property {string} parser The name of the parser to use.
69 * @property {ParserOptions} parserOptions An object of parserOption settings to use.
70 * @property {string[]} plugins An array of plugins to load.
71 * @property {Record<string,RuleConf>} rules An object of rules to use.
72 * @property {string[]} rulePaths An array of directories to load custom rules from.
73 * @property {boolean} reportUnusedDisableDirectives `true` adds reports for unused eslint-disable directives
74 * @property {boolean} globInputPaths Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file.
75 * @property {string} resolvePluginsRelativeTo The folder where plugins should be resolved from, defaulting to the CWD
76 */
77
78 /**
79 * A linting result.
80 * @typedef {Object} LintResult
81 * @property {string} filePath The path to the file that was linted.
82 * @property {LintMessage[]} messages All of the messages for the result.
83 * @property {number} errorCount Number of errors for the result.
84 * @property {number} warningCount Number of warnings for the result.
85 * @property {number} fixableErrorCount Number of fixable errors for the result.
86 * @property {number} fixableWarningCount Number of fixable warnings for the result.
87 * @property {string} [source] The source code of the file that was linted.
88 * @property {string} [output] The source code of the file that was linted, with as many fixes applied as possible.
89 */
90
91 /**
92 * Information of deprecated rules.
93 * @typedef {Object} DeprecatedRuleInfo
94 * @property {string} ruleId The rule ID.
95 * @property {string[]} replacedBy The rule IDs that replace this deprecated rule.
96 */
97
98 /**
99 * Linting results.
100 * @typedef {Object} LintReport
101 * @property {LintResult[]} results All of the result.
102 * @property {number} errorCount Number of errors for the result.
103 * @property {number} warningCount Number of warnings for the result.
104 * @property {number} fixableErrorCount Number of fixable errors for the result.
105 * @property {number} fixableWarningCount Number of fixable warnings for the result.
106 * @property {DeprecatedRuleInfo[]} usedDeprecatedRules The list of used deprecated rules.
107 */
108
109 /**
110 * Private data for CLIEngine.
111 * @typedef {Object} CLIEngineInternalSlots
112 * @property {Map<string, Plugin>} additionalPluginPool The map for additional plugins.
113 * @property {string} cacheFilePath The path to the cache of lint results.
114 * @property {CascadingConfigArrayFactory} configArrayFactory The factory of configs.
115 * @property {(filePath: string) => boolean} defaultIgnores The default predicate function to check if a file ignored or not.
116 * @property {FileEnumerator} fileEnumerator The file enumerator.
117 * @property {ConfigArray[]} lastConfigArrays The list of config arrays that the last `executeOnFiles` or `executeOnText` used.
118 * @property {LintResultCache|null} lintResultCache The cache of lint results.
119 * @property {Linter} linter The linter instance which has loaded rules.
120 * @property {CLIEngineOptions} options The normalized options of this instance.
121 */
122
123 //------------------------------------------------------------------------------
124 // Helpers
125 //------------------------------------------------------------------------------
126
127 /** @type {WeakMap<CLIEngine, CLIEngineInternalSlots>} */
128 const internalSlotsMap = new WeakMap();
129
130 /**
131 * Determines if each fix type in an array is supported by ESLint and throws
132 * an error if not.
133 * @param {string[]} fixTypes An array of fix types to check.
134 * @returns {void}
135 * @throws {Error} If an invalid fix type is found.
136 */
137 function validateFixTypes(fixTypes) {
138 for (const fixType of fixTypes) {
139 if (!validFixTypes.has(fixType)) {
140 throw new Error(`Invalid fix type "${fixType}" found.`);
141 }
142 }
143 }
144
145 /**
146 * It will calculate the error and warning count for collection of messages per file
147 * @param {LintMessage[]} messages Collection of messages
148 * @returns {Object} Contains the stats
149 * @private
150 */
151 function calculateStatsPerFile(messages) {
152 return messages.reduce((stat, message) => {
153 if (message.fatal || message.severity === 2) {
154 stat.errorCount++;
155 if (message.fix) {
156 stat.fixableErrorCount++;
157 }
158 } else {
159 stat.warningCount++;
160 if (message.fix) {
161 stat.fixableWarningCount++;
162 }
163 }
164 return stat;
165 }, {
166 errorCount: 0,
167 warningCount: 0,
168 fixableErrorCount: 0,
169 fixableWarningCount: 0
170 });
171 }
172
173 /**
174 * It will calculate the error and warning count for collection of results from all files
175 * @param {LintResult[]} results Collection of messages from all the files
176 * @returns {Object} Contains the stats
177 * @private
178 */
179 function calculateStatsPerRun(results) {
180 return results.reduce((stat, result) => {
181 stat.errorCount += result.errorCount;
182 stat.warningCount += result.warningCount;
183 stat.fixableErrorCount += result.fixableErrorCount;
184 stat.fixableWarningCount += result.fixableWarningCount;
185 return stat;
186 }, {
187 errorCount: 0,
188 warningCount: 0,
189 fixableErrorCount: 0,
190 fixableWarningCount: 0
191 });
192 }
193
194 /**
195 * Processes an source code using ESLint.
196 * @param {Object} config The config object.
197 * @param {string} config.text The source code to verify.
198 * @param {string} config.cwd The path to the current working directory.
199 * @param {string|undefined} config.filePath The path to the file of `text`. If this is undefined, it uses `<text>`.
200 * @param {ConfigArray} config.config The config.
201 * @param {boolean} config.fix If `true` then it does fix.
202 * @param {boolean} config.allowInlineConfig If `true` then it uses directive comments.
203 * @param {boolean} config.reportUnusedDisableDirectives If `true` then it reports unused `eslint-disable` comments.
204 * @param {FileEnumerator} config.fileEnumerator The file enumerator to check if a path is a target or not.
205 * @param {Linter} config.linter The linter instance to verify.
206 * @returns {LintResult} The result of linting.
207 * @private
208 */
209 function verifyText({
210 text,
211 cwd,
212 filePath: providedFilePath,
213 config,
214 fix,
215 allowInlineConfig,
216 reportUnusedDisableDirectives,
217 fileEnumerator,
218 linter
219 }) {
220 const filePath = providedFilePath || "<text>";
221
222 debug(`Lint ${filePath}`);
223
224 /*
225 * Verify.
226 * `config.extractConfig(filePath)` requires an absolute path, but `linter`
227 * doesn't know CWD, so it gives `linter` an absolute path always.
228 */
229 const filePathToVerify = filePath === "<text>" ? path.join(cwd, filePath) : filePath;
230 const { fixed, messages, output } = linter.verifyAndFix(
231 text,
232 config,
233 {
234 allowInlineConfig,
235 filename: filePathToVerify,
236 fix,
237 reportUnusedDisableDirectives,
238
239 /**
240 * Check if the linter should adopt a given code block or not.
241 * @param {string} blockFilename The virtual filename of a code block.
242 * @returns {boolean} `true` if the linter should adopt the code block.
243 */
244 filterCodeBlock(blockFilename) {
245 return fileEnumerator.isTargetPath(blockFilename);
246 }
247 }
248 );
249
250 // Tweak and return.
251 const result = {
252 filePath,
253 messages,
254 ...calculateStatsPerFile(messages)
255 };
256
257 if (fixed) {
258 result.output = output;
259 }
260 if (
261 result.errorCount + result.warningCount > 0 &&
262 typeof result.output === "undefined"
263 ) {
264 result.source = text;
265 }
266
267 return result;
268 }
269
270 /**
271 * Returns result with warning by ignore settings
272 * @param {string} filePath File path of checked code
273 * @param {string} baseDir Absolute path of base directory
274 * @returns {LintResult} Result with single warning
275 * @private
276 */
277 function createIgnoreResult(filePath, baseDir) {
278 let message;
279 const isHidden = filePath.split(path.sep)
280 .find(segment => /^\./u.test(segment));
281 const isInNodeModules = baseDir && path.relative(baseDir, filePath).startsWith("node_modules");
282
283 if (isHidden) {
284 message = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!<relative/path/to/filename>'\") to override.";
285 } else if (isInNodeModules) {
286 message = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override.";
287 } else {
288 message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.";
289 }
290
291 return {
292 filePath: path.resolve(filePath),
293 messages: [
294 {
295 fatal: false,
296 severity: 1,
297 message
298 }
299 ],
300 errorCount: 0,
301 warningCount: 1,
302 fixableErrorCount: 0,
303 fixableWarningCount: 0
304 };
305 }
306
307 /**
308 * Get a rule.
309 * @param {string} ruleId The rule ID to get.
310 * @param {ConfigArray[]} configArrays The config arrays that have plugin rules.
311 * @returns {Rule|null} The rule or null.
312 */
313 function getRule(ruleId, configArrays) {
314 for (const configArray of configArrays) {
315 const rule = configArray.pluginRules.get(ruleId);
316
317 if (rule) {
318 return rule;
319 }
320 }
321 return builtInRules.get(ruleId) || null;
322 }
323
324 /**
325 * Collect used deprecated rules.
326 * @param {ConfigArray[]} usedConfigArrays The config arrays which were used.
327 * @returns {IterableIterator<DeprecatedRuleInfo>} Used deprecated rules.
328 */
329 function *iterateRuleDeprecationWarnings(usedConfigArrays) {
330 const processedRuleIds = new Set();
331
332 // Flatten used configs.
333 /** @type {ExtractedConfig[]} */
334 const configs = [].concat(
335 ...usedConfigArrays.map(getUsedExtractedConfigs)
336 );
337
338 // Traverse rule configs.
339 for (const config of configs) {
340 for (const [ruleId, ruleConfig] of Object.entries(config.rules)) {
341
342 // Skip if it was processed.
343 if (processedRuleIds.has(ruleId)) {
344 continue;
345 }
346 processedRuleIds.add(ruleId);
347
348 // Skip if it's not used.
349 if (!ConfigOps.getRuleSeverity(ruleConfig)) {
350 continue;
351 }
352 const rule = getRule(ruleId, usedConfigArrays);
353
354 // Skip if it's not deprecated.
355 if (!(rule && rule.meta && rule.meta.deprecated)) {
356 continue;
357 }
358
359 // This rule was used and deprecated.
360 yield {
361 ruleId,
362 replacedBy: rule.meta.replacedBy || []
363 };
364 }
365 }
366 }
367
368 /**
369 * Checks if the given message is an error message.
370 * @param {LintMessage} message The message to check.
371 * @returns {boolean} Whether or not the message is an error message.
372 * @private
373 */
374 function isErrorMessage(message) {
375 return message.severity === 2;
376 }
377
378
379 /**
380 * return the cacheFile to be used by eslint, based on whether the provided parameter is
381 * a directory or looks like a directory (ends in `path.sep`), in which case the file
382 * name will be the `cacheFile/.cache_hashOfCWD`
383 *
384 * if cacheFile points to a file or looks like a file then in will just use that file
385 * @param {string} cacheFile The name of file to be used to store the cache
386 * @param {string} cwd Current working directory
387 * @returns {string} the resolved path to the cache file
388 */
389 function getCacheFile(cacheFile, cwd) {
390
391 /*
392 * make sure the path separators are normalized for the environment/os
393 * keeping the trailing path separator if present
394 */
395 const normalizedCacheFile = path.normalize(cacheFile);
396
397 const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile);
398 const looksLikeADirectory = normalizedCacheFile.slice(-1) === path.sep;
399
400 /**
401 * return the name for the cache file in case the provided parameter is a directory
402 * @returns {string} the resolved path to the cacheFile
403 */
404 function getCacheFileForDirectory() {
405 return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`);
406 }
407
408 let fileStats;
409
410 try {
411 fileStats = fs.lstatSync(resolvedCacheFile);
412 } catch (ex) {
413 fileStats = null;
414 }
415
416
417 /*
418 * in case the file exists we need to verify if the provided path
419 * is a directory or a file. If it is a directory we want to create a file
420 * inside that directory
421 */
422 if (fileStats) {
423
424 /*
425 * is a directory or is a file, but the original file the user provided
426 * looks like a directory but `path.resolve` removed the `last path.sep`
427 * so we need to still treat this like a directory
428 */
429 if (fileStats.isDirectory() || looksLikeADirectory) {
430 return getCacheFileForDirectory();
431 }
432
433 // is file so just use that file
434 return resolvedCacheFile;
435 }
436
437 /*
438 * here we known the file or directory doesn't exist,
439 * so we will try to infer if its a directory if it looks like a directory
440 * for the current operating system.
441 */
442
443 // if the last character passed is a path separator we assume is a directory
444 if (looksLikeADirectory) {
445 return getCacheFileForDirectory();
446 }
447
448 return resolvedCacheFile;
449 }
450
451 /**
452 * Convert a string array to a boolean map.
453 * @param {string[]|null} keys The keys to assign true.
454 * @param {boolean} defaultValue The default value for each property.
455 * @param {string} displayName The property name which is used in error message.
456 * @returns {Record<string,boolean>} The boolean map.
457 */
458 function toBooleanMap(keys, defaultValue, displayName) {
459 if (keys && !Array.isArray(keys)) {
460 throw new Error(`${displayName} must be an array.`);
461 }
462 if (keys && keys.length > 0) {
463 return keys.reduce((map, def) => {
464 const [key, value] = def.split(":");
465
466 if (key !== "__proto__") {
467 map[key] = value === void 0
468 ? defaultValue
469 : value === "true";
470 }
471
472 return map;
473 }, {});
474 }
475 return void 0;
476 }
477
478 /**
479 * Create a config data from CLI options.
480 * @param {CLIEngineOptions} options The options
481 * @returns {ConfigData|null} The created config data.
482 */
483 function createConfigDataFromOptions(options) {
484 const {
485 ignorePattern,
486 parser,
487 parserOptions,
488 plugins,
489 rules
490 } = options;
491 const env = toBooleanMap(options.envs, true, "envs");
492 const globals = toBooleanMap(options.globals, false, "globals");
493
494 if (
495 env === void 0 &&
496 globals === void 0 &&
497 (ignorePattern === void 0 || ignorePattern.length === 0) &&
498 parser === void 0 &&
499 parserOptions === void 0 &&
500 plugins === void 0 &&
501 rules === void 0
502 ) {
503 return null;
504 }
505 return {
506 env,
507 globals,
508 ignorePatterns: ignorePattern,
509 parser,
510 parserOptions,
511 plugins,
512 rules
513 };
514 }
515
516 /**
517 * Checks whether a directory exists at the given location
518 * @param {string} resolvedPath A path from the CWD
519 * @returns {boolean} `true` if a directory exists
520 */
521 function directoryExists(resolvedPath) {
522 try {
523 return fs.statSync(resolvedPath).isDirectory();
524 } catch (error) {
525 if (error && error.code === "ENOENT") {
526 return false;
527 }
528 throw error;
529 }
530 }
531
532 //------------------------------------------------------------------------------
533 // Public Interface
534 //------------------------------------------------------------------------------
535
536 class CLIEngine {
537
538 /**
539 * Creates a new instance of the core CLI engine.
540 * @param {CLIEngineOptions} providedOptions The options for this instance.
541 */
542 constructor(providedOptions) {
543 const options = Object.assign(
544 Object.create(null),
545 defaultOptions,
546 { cwd: process.cwd() },
547 providedOptions
548 );
549
550 if (options.fix === void 0) {
551 options.fix = false;
552 }
553
554 const additionalPluginPool = new Map();
555 const cacheFilePath = getCacheFile(
556 options.cacheLocation || options.cacheFile,
557 options.cwd
558 );
559 const configArrayFactory = new CascadingConfigArrayFactory({
560 additionalPluginPool,
561 baseConfig: options.baseConfig || null,
562 cliConfig: createConfigDataFromOptions(options),
563 cwd: options.cwd,
564 ignorePath: options.ignorePath,
565 resolvePluginsRelativeTo: options.resolvePluginsRelativeTo,
566 rulePaths: options.rulePaths,
567 specificConfigPath: options.configFile,
568 useEslintrc: options.useEslintrc
569 });
570 const fileEnumerator = new FileEnumerator({
571 configArrayFactory,
572 cwd: options.cwd,
573 extensions: options.extensions,
574 globInputPaths: options.globInputPaths,
575 errorOnUnmatchedPattern: options.errorOnUnmatchedPattern,
576 ignore: options.ignore
577 });
578 const lintResultCache =
579 options.cache ? new LintResultCache(cacheFilePath) : null;
580 const linter = new Linter({ cwd: options.cwd });
581
582 /** @type {ConfigArray[]} */
583 const lastConfigArrays = [configArrayFactory.getConfigArrayForFile()];
584
585 // Store private data.
586 internalSlotsMap.set(this, {
587 additionalPluginPool,
588 cacheFilePath,
589 configArrayFactory,
590 defaultIgnores: IgnorePattern.createDefaultIgnore(options.cwd),
591 fileEnumerator,
592 lastConfigArrays,
593 lintResultCache,
594 linter,
595 options
596 });
597
598 // setup special filter for fixes
599 if (options.fix && options.fixTypes && options.fixTypes.length > 0) {
600 debug(`Using fix types ${options.fixTypes}`);
601
602 // throw an error if any invalid fix types are found
603 validateFixTypes(options.fixTypes);
604
605 // convert to Set for faster lookup
606 const fixTypes = new Set(options.fixTypes);
607
608 // save original value of options.fix in case it's a function
609 const originalFix = (typeof options.fix === "function")
610 ? options.fix : () => true;
611
612 options.fix = message => {
613 const rule = message.ruleId && getRule(message.ruleId, lastConfigArrays);
614 const matches = rule && rule.meta && fixTypes.has(rule.meta.type);
615
616 return matches && originalFix(message);
617 };
618 }
619 }
620
621 getRules() {
622 const { lastConfigArrays } = internalSlotsMap.get(this);
623
624 return new Map(function *() {
625 yield* builtInRules;
626
627 for (const configArray of lastConfigArrays) {
628 yield* configArray.pluginRules;
629 }
630 }());
631 }
632
633 /**
634 * Returns results that only contains errors.
635 * @param {LintResult[]} results The results to filter.
636 * @returns {LintResult[]} The filtered results.
637 */
638 static getErrorResults(results) {
639 const filtered = [];
640
641 results.forEach(result => {
642 const filteredMessages = result.messages.filter(isErrorMessage);
643
644 if (filteredMessages.length > 0) {
645 filtered.push({
646 ...result,
647 messages: filteredMessages,
648 errorCount: filteredMessages.length,
649 warningCount: 0,
650 fixableErrorCount: result.fixableErrorCount,
651 fixableWarningCount: 0
652 });
653 }
654 });
655
656 return filtered;
657 }
658
659 /**
660 * Outputs fixes from the given results to files.
661 * @param {LintReport} report The report object created by CLIEngine.
662 * @returns {void}
663 */
664 static outputFixes(report) {
665 report.results.filter(result => Object.prototype.hasOwnProperty.call(result, "output")).forEach(result => {
666 fs.writeFileSync(result.filePath, result.output);
667 });
668 }
669
670
671 /**
672 * Add a plugin by passing its configuration
673 * @param {string} name Name of the plugin.
674 * @param {Plugin} pluginObject Plugin configuration object.
675 * @returns {void}
676 */
677 addPlugin(name, pluginObject) {
678 const {
679 additionalPluginPool,
680 configArrayFactory,
681 lastConfigArrays
682 } = internalSlotsMap.get(this);
683
684 additionalPluginPool.set(name, pluginObject);
685 configArrayFactory.clearCache();
686 lastConfigArrays.length = 1;
687 lastConfigArrays[0] = configArrayFactory.getConfigArrayForFile();
688 }
689
690 /**
691 * Resolves the patterns passed into executeOnFiles() into glob-based patterns
692 * for easier handling.
693 * @param {string[]} patterns The file patterns passed on the command line.
694 * @returns {string[]} The equivalent glob patterns.
695 */
696 resolveFileGlobPatterns(patterns) {
697 const { options } = internalSlotsMap.get(this);
698
699 if (options.globInputPaths === false) {
700 return patterns.filter(Boolean);
701 }
702
703 const extensions = (options.extensions || [".js"]).map(ext => ext.replace(/^\./u, ""));
704 const dirSuffix = `/**/*.{${extensions.join(",")}}`;
705
706 return patterns.filter(Boolean).map(pathname => {
707 const resolvedPath = path.resolve(options.cwd, pathname);
708 const newPath = directoryExists(resolvedPath)
709 ? pathname.replace(/[/\\]$/u, "") + dirSuffix
710 : pathname;
711
712 return path.normalize(newPath).replace(/\\/gu, "/");
713 });
714 }
715
716 /**
717 * Executes the current configuration on an array of file and directory names.
718 * @param {string[]} patterns An array of file and directory names.
719 * @returns {LintReport} The results for all files that were linted.
720 */
721 executeOnFiles(patterns) {
722 const {
723 cacheFilePath,
724 fileEnumerator,
725 lastConfigArrays,
726 lintResultCache,
727 linter,
728 options: {
729 allowInlineConfig,
730 cache,
731 cwd,
732 fix,
733 reportUnusedDisableDirectives
734 }
735 } = internalSlotsMap.get(this);
736 const results = [];
737 const startTime = Date.now();
738
739 // Clear the last used config arrays.
740 lastConfigArrays.length = 0;
741
742 // Delete cache file; should this do here?
743 if (!cache) {
744 try {
745 fs.unlinkSync(cacheFilePath);
746 } catch (error) {
747 const errorCode = error && error.code;
748
749 // Ignore errors when no such file exists or file system is read only (and cache file does not exist)
750 if (errorCode !== "ENOENT" && !(errorCode === "EROFS" && !fs.existsSync(cacheFilePath))) {
751 throw error;
752 }
753 }
754 }
755
756 // Iterate source code files.
757 for (const { config, filePath, ignored } of fileEnumerator.iterateFiles(patterns)) {
758 if (ignored) {
759 results.push(createIgnoreResult(filePath, cwd));
760 continue;
761 }
762
763 /*
764 * Store used configs for:
765 * - this method uses to collect used deprecated rules.
766 * - `getRules()` method uses to collect all loaded rules.
767 * - `--fix-type` option uses to get the loaded rule's meta data.
768 */
769 if (!lastConfigArrays.includes(config)) {
770 lastConfigArrays.push(config);
771 }
772
773 // Skip if there is cached result.
774 if (lintResultCache) {
775 const cachedResult =
776 lintResultCache.getCachedLintResults(filePath, config);
777
778 if (cachedResult) {
779 const hadMessages =
780 cachedResult.messages &&
781 cachedResult.messages.length > 0;
782
783 if (hadMessages && fix) {
784 debug(`Reprocessing cached file to allow autofix: ${filePath}`);
785 } else {
786 debug(`Skipping file since it hasn't changed: ${filePath}`);
787 results.push(cachedResult);
788 continue;
789 }
790 }
791 }
792
793 // Do lint.
794 const result = verifyText({
795 text: fs.readFileSync(filePath, "utf8"),
796 filePath,
797 config,
798 cwd,
799 fix,
800 allowInlineConfig,
801 reportUnusedDisableDirectives,
802 fileEnumerator,
803 linter
804 });
805
806 results.push(result);
807
808 /*
809 * Store the lint result in the LintResultCache.
810 * NOTE: The LintResultCache will remove the file source and any
811 * other properties that are difficult to serialize, and will
812 * hydrate those properties back in on future lint runs.
813 */
814 if (lintResultCache) {
815 lintResultCache.setCachedLintResults(filePath, config, result);
816 }
817 }
818
819 // Persist the cache to disk.
820 if (lintResultCache) {
821 lintResultCache.reconcile();
822 }
823
824 // Collect used deprecated rules.
825 const usedDeprecatedRules = Array.from(
826 iterateRuleDeprecationWarnings(lastConfigArrays)
827 );
828
829 debug(`Linting complete in: ${Date.now() - startTime}ms`);
830 return {
831 results,
832 ...calculateStatsPerRun(results),
833 usedDeprecatedRules
834 };
835 }
836
837 /**
838 * Executes the current configuration on text.
839 * @param {string} text A string of JavaScript code to lint.
840 * @param {string} [filename] An optional string representing the texts filename.
841 * @param {boolean} [warnIgnored] Always warn when a file is ignored
842 * @returns {LintReport} The results for the linting.
843 */
844 executeOnText(text, filename, warnIgnored) {
845 const {
846 configArrayFactory,
847 fileEnumerator,
848 lastConfigArrays,
849 linter,
850 options: {
851 allowInlineConfig,
852 cwd,
853 fix,
854 reportUnusedDisableDirectives
855 }
856 } = internalSlotsMap.get(this);
857 const results = [];
858 const startTime = Date.now();
859 const resolvedFilename = filename && path.resolve(cwd, filename);
860
861 // Clear the last used config arrays.
862 lastConfigArrays.length = 0;
863
864 if (resolvedFilename && this.isPathIgnored(resolvedFilename)) {
865 if (warnIgnored) {
866 results.push(createIgnoreResult(resolvedFilename, cwd));
867 }
868 } else {
869 const config = configArrayFactory.getConfigArrayForFile(
870 resolvedFilename || "__placeholder__.js"
871 );
872
873 /*
874 * Store used configs for:
875 * - this method uses to collect used deprecated rules.
876 * - `getRules()` method uses to collect all loaded rules.
877 * - `--fix-type` option uses to get the loaded rule's meta data.
878 */
879 lastConfigArrays.push(config);
880
881 // Do lint.
882 results.push(verifyText({
883 text,
884 filePath: resolvedFilename,
885 config,
886 cwd,
887 fix,
888 allowInlineConfig,
889 reportUnusedDisableDirectives,
890 fileEnumerator,
891 linter
892 }));
893 }
894
895 // Collect used deprecated rules.
896 const usedDeprecatedRules = Array.from(
897 iterateRuleDeprecationWarnings(lastConfigArrays)
898 );
899
900 debug(`Linting complete in: ${Date.now() - startTime}ms`);
901 return {
902 results,
903 ...calculateStatsPerRun(results),
904 usedDeprecatedRules
905 };
906 }
907
908 /**
909 * Returns a configuration object for the given file based on the CLI options.
910 * This is the same logic used by the ESLint CLI executable to determine
911 * configuration for each file it processes.
912 * @param {string} filePath The path of the file to retrieve a config object for.
913 * @returns {ConfigData} A configuration object for the file.
914 */
915 getConfigForFile(filePath) {
916 const { configArrayFactory, options } = internalSlotsMap.get(this);
917 const absolutePath = path.resolve(options.cwd, filePath);
918
919 if (directoryExists(absolutePath)) {
920 throw Object.assign(
921 new Error("'filePath' should not be a directory path."),
922 { messageTemplate: "print-config-with-directory-path" }
923 );
924 }
925
926 return configArrayFactory
927 .getConfigArrayForFile(absolutePath)
928 .extractConfig(absolutePath)
929 .toCompatibleObjectAsConfigFileContent();
930 }
931
932 /**
933 * Checks if a given path is ignored by ESLint.
934 * @param {string} filePath The path of the file to check.
935 * @returns {boolean} Whether or not the given path is ignored.
936 */
937 isPathIgnored(filePath) {
938 const {
939 configArrayFactory,
940 defaultIgnores,
941 options: { cwd, ignore }
942 } = internalSlotsMap.get(this);
943 const absolutePath = path.resolve(cwd, filePath);
944
945 if (ignore) {
946 const config = configArrayFactory
947 .getConfigArrayForFile(absolutePath)
948 .extractConfig(absolutePath);
949 const ignores = config.ignores || defaultIgnores;
950
951 return ignores(absolutePath);
952 }
953
954 return defaultIgnores(absolutePath);
955 }
956
957 /**
958 * Returns the formatter representing the given format or null if no formatter
959 * with the given name can be found.
960 * @param {string} [format] The name of the format to load or the path to a
961 * custom formatter.
962 * @returns {Function} The formatter function or null if not found.
963 */
964 getFormatter(format) {
965
966 // default is stylish
967 const resolvedFormatName = format || "stylish";
968
969 // only strings are valid formatters
970 if (typeof resolvedFormatName === "string") {
971
972 // replace \ with / for Windows compatibility
973 const normalizedFormatName = resolvedFormatName.replace(/\\/gu, "/");
974
975 const slots = internalSlotsMap.get(this);
976 const cwd = slots ? slots.options.cwd : process.cwd();
977 const namespace = naming.getNamespaceFromTerm(normalizedFormatName);
978
979 let formatterPath;
980
981 // if there's a slash, then it's a file (TODO: this check seems dubious for scoped npm packages)
982 if (!namespace && normalizedFormatName.indexOf("/") > -1) {
983 formatterPath = path.resolve(cwd, normalizedFormatName);
984 } else {
985 try {
986 const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter");
987
988 formatterPath = ModuleResolver.resolve(npmFormat, path.join(cwd, "__placeholder__.js"));
989 } catch (e) {
990 formatterPath = path.resolve(__dirname, "formatters", normalizedFormatName);
991 }
992 }
993
994 try {
995 return require(formatterPath);
996 } catch (ex) {
997 ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;
998 throw ex;
999 }
1000
1001 } else {
1002 return null;
1003 }
1004 }
1005 }
1006
1007 CLIEngine.version = pkg.version;
1008 CLIEngine.getFormatter = CLIEngine.prototype.getFormatter;
1009
1010 module.exports = {
1011 CLIEngine,
1012
1013 /**
1014 * Get the internal slots of a given CLIEngine instance for tests.
1015 * @param {CLIEngine} instance The CLIEngine instance to get.
1016 * @returns {CLIEngineInternalSlots} The internal slots.
1017 */
1018 getCLIEngineInternalSlots(instance) {
1019 return internalSlotsMap.get(instance);
1020 }
1021 };