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