1 | /**
|
2 | * @fileoverview Main CLI object.
|
3 | * @author Nicholas C. Zakas
|
4 | */
|
5 |
|
6 | ;
|
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 | path = require("path"),
|
20 | defaultOptions = require("../conf/default-cli-options"),
|
21 | Linter = require("./linter"),
|
22 | lodash = require("lodash"),
|
23 | IgnoredPaths = require("./util/ignored-paths"),
|
24 | Config = require("./config"),
|
25 | ConfigOps = require("./config/config-ops"),
|
26 | LintResultCache = require("./util/lint-result-cache"),
|
27 | globUtils = require("./util/glob-utils"),
|
28 | validator = require("./config/config-validator"),
|
29 | hash = require("./util/hash"),
|
30 | ModuleResolver = require("./util/module-resolver"),
|
31 | naming = require("./util/naming"),
|
32 | pkg = require("../package.json"),
|
33 | loadRules = require("./load-rules");
|
34 |
|
35 | const debug = require("debug")("eslint:cli-engine");
|
36 | const resolver = new ModuleResolver();
|
37 | const validFixTypes = new Set(["problem", "suggestion", "layout"]);
|
38 |
|
39 | //------------------------------------------------------------------------------
|
40 | // Typedefs
|
41 | //------------------------------------------------------------------------------
|
42 |
|
43 | /**
|
44 | * The options to configure a CLI engine with.
|
45 | * @typedef {Object} CLIEngineOptions
|
46 | * @property {boolean} allowInlineConfig Enable or disable inline configuration comments.
|
47 | * @property {Object} baseConfig Base config object, extended by all configs used with this CLIEngine instance
|
48 | * @property {boolean} cache Enable result caching.
|
49 | * @property {string} cacheLocation The cache file to use instead of .eslintcache.
|
50 | * @property {string} configFile The configuration file to use.
|
51 | * @property {string} cwd The value to use for the current working directory.
|
52 | * @property {string[]} envs An array of environments to load.
|
53 | * @property {string[]} extensions An array of file extensions to check.
|
54 | * @property {boolean|Function} fix Execute in autofix mode. If a function, should return a boolean.
|
55 | * @property {string[]} fixTypes Array of rule types to apply fixes for.
|
56 | * @property {string[]} globals An array of global variables to declare.
|
57 | * @property {boolean} ignore False disables use of .eslintignore.
|
58 | * @property {string} ignorePath The ignore file to use instead of .eslintignore.
|
59 | * @property {string} ignorePattern A glob pattern of files to ignore.
|
60 | * @property {boolean} useEslintrc False disables looking for .eslintrc
|
61 | * @property {string} parser The name of the parser to use.
|
62 | * @property {Object} parserOptions An object of parserOption settings to use.
|
63 | * @property {string[]} plugins An array of plugins to load.
|
64 | * @property {Object<string,*>} rules An object of rules to use.
|
65 | * @property {string[]} rulePaths An array of directories to load custom rules from.
|
66 | * @property {boolean} reportUnusedDisableDirectives `true` adds reports for unused eslint-disable directives
|
67 | */
|
68 |
|
69 | /**
|
70 | * A linting warning or error.
|
71 | * @typedef {Object} LintMessage
|
72 | * @property {string} message The message to display to the user.
|
73 | */
|
74 |
|
75 | /**
|
76 | * A linting result.
|
77 | * @typedef {Object} LintResult
|
78 | * @property {string} filePath The path to the file that was linted.
|
79 | * @property {LintMessage[]} messages All of the messages for the result.
|
80 | * @property {number} errorCount Number of errors for the result.
|
81 | * @property {number} warningCount Number of warnings for the result.
|
82 | * @property {number} fixableErrorCount Number of fixable errors for the result.
|
83 | * @property {number} fixableWarningCount Number of fixable warnings for the result.
|
84 | * @property {string=} [source] The source code of the file that was linted.
|
85 | * @property {string=} [output] The source code of the file that was linted, with as many fixes applied as possible.
|
86 | */
|
87 |
|
88 | //------------------------------------------------------------------------------
|
89 | // Helpers
|
90 | //------------------------------------------------------------------------------
|
91 |
|
92 | /**
|
93 | * Determines if each fix type in an array is supported by ESLint and throws
|
94 | * an error if not.
|
95 | * @param {string[]} fixTypes An array of fix types to check.
|
96 | * @returns {void}
|
97 | * @throws {Error} If an invalid fix type is found.
|
98 | */
|
99 | function validateFixTypes(fixTypes) {
|
100 | for (const fixType of fixTypes) {
|
101 | if (!validFixTypes.has(fixType)) {
|
102 | throw new Error(`Invalid fix type "${fixType}" found.`);
|
103 | }
|
104 | }
|
105 | }
|
106 |
|
107 | /**
|
108 | * It will calculate the error and warning count for collection of messages per file
|
109 | * @param {Object[]} messages - Collection of messages
|
110 | * @returns {Object} Contains the stats
|
111 | * @private
|
112 | */
|
113 | function calculateStatsPerFile(messages) {
|
114 | return messages.reduce((stat, message) => {
|
115 | if (message.fatal || message.severity === 2) {
|
116 | stat.errorCount++;
|
117 | if (message.fix) {
|
118 | stat.fixableErrorCount++;
|
119 | }
|
120 | } else {
|
121 | stat.warningCount++;
|
122 | if (message.fix) {
|
123 | stat.fixableWarningCount++;
|
124 | }
|
125 | }
|
126 | return stat;
|
127 | }, {
|
128 | errorCount: 0,
|
129 | warningCount: 0,
|
130 | fixableErrorCount: 0,
|
131 | fixableWarningCount: 0
|
132 | });
|
133 | }
|
134 |
|
135 | /**
|
136 | * It will calculate the error and warning count for collection of results from all files
|
137 | * @param {Object[]} results - Collection of messages from all the files
|
138 | * @returns {Object} Contains the stats
|
139 | * @private
|
140 | */
|
141 | function calculateStatsPerRun(results) {
|
142 | return results.reduce((stat, result) => {
|
143 | stat.errorCount += result.errorCount;
|
144 | stat.warningCount += result.warningCount;
|
145 | stat.fixableErrorCount += result.fixableErrorCount;
|
146 | stat.fixableWarningCount += result.fixableWarningCount;
|
147 | return stat;
|
148 | }, {
|
149 | errorCount: 0,
|
150 | warningCount: 0,
|
151 | fixableErrorCount: 0,
|
152 | fixableWarningCount: 0
|
153 | });
|
154 | }
|
155 |
|
156 | /**
|
157 | * Processes an source code using ESLint.
|
158 | * @param {string} text The source code to check.
|
159 | * @param {Object} configHelper The configuration options for ESLint.
|
160 | * @param {string} filename An optional string representing the texts filename.
|
161 | * @param {boolean|Function} fix Indicates if fixes should be processed.
|
162 | * @param {boolean} allowInlineConfig Allow/ignore comments that change config.
|
163 | * @param {boolean} reportUnusedDisableDirectives Allow/ignore comments that change config.
|
164 | * @param {Linter} linter Linter context
|
165 | * @returns {{rules: LintResult, config: Object}} The results for linting on this text and the fully-resolved config for it.
|
166 | * @private
|
167 | */
|
168 | function processText(text, configHelper, filename, fix, allowInlineConfig, reportUnusedDisableDirectives, linter) {
|
169 | let filePath,
|
170 | fileExtension,
|
171 | processor;
|
172 |
|
173 | if (filename) {
|
174 | filePath = path.resolve(filename);
|
175 | fileExtension = path.extname(filename);
|
176 | }
|
177 |
|
178 | const effectiveFilename = filename || "<text>";
|
179 |
|
180 | debug(`Linting ${effectiveFilename}`);
|
181 | const config = configHelper.getConfig(filePath);
|
182 |
|
183 | if (config.plugins) {
|
184 | configHelper.plugins.loadAll(config.plugins);
|
185 | }
|
186 |
|
187 | const loadedPlugins = configHelper.plugins.getAll();
|
188 |
|
189 | for (const plugin in loadedPlugins) {
|
190 | if (loadedPlugins[plugin].processors && Object.keys(loadedPlugins[plugin].processors).indexOf(fileExtension) >= 0) {
|
191 | processor = loadedPlugins[plugin].processors[fileExtension];
|
192 | break;
|
193 | }
|
194 | }
|
195 |
|
196 | const autofixingEnabled = typeof fix !== "undefined" && (!processor || processor.supportsAutofix);
|
197 | const fixedResult = linter.verifyAndFix(text, config, {
|
198 | filename: effectiveFilename,
|
199 | allowInlineConfig,
|
200 | reportUnusedDisableDirectives,
|
201 | fix: !!autofixingEnabled && fix,
|
202 | preprocess: processor && (rawText => processor.preprocess(rawText, effectiveFilename)),
|
203 | postprocess: processor && (problemLists => processor.postprocess(problemLists, effectiveFilename))
|
204 | });
|
205 | const stats = calculateStatsPerFile(fixedResult.messages);
|
206 |
|
207 | const result = {
|
208 | filePath: effectiveFilename,
|
209 | messages: fixedResult.messages,
|
210 | errorCount: stats.errorCount,
|
211 | warningCount: stats.warningCount,
|
212 | fixableErrorCount: stats.fixableErrorCount,
|
213 | fixableWarningCount: stats.fixableWarningCount
|
214 | };
|
215 |
|
216 | if (fixedResult.fixed) {
|
217 | result.output = fixedResult.output;
|
218 | }
|
219 |
|
220 | if (result.errorCount + result.warningCount > 0 && typeof result.output === "undefined") {
|
221 | result.source = text;
|
222 | }
|
223 |
|
224 | return { result, config };
|
225 | }
|
226 |
|
227 | /**
|
228 | * Processes an individual file using ESLint. Files used here are known to
|
229 | * exist, so no need to check that here.
|
230 | * @param {string} filename The filename of the file being checked.
|
231 | * @param {Object} configHelper The configuration options for ESLint.
|
232 | * @param {Object} options The CLIEngine options object.
|
233 | * @param {Linter} linter Linter context
|
234 | * @returns {{rules: LintResult, config: Object}} The results for linting on this text and the fully-resolved config for it.
|
235 | * @private
|
236 | */
|
237 | function processFile(filename, configHelper, options, linter) {
|
238 |
|
239 | const text = fs.readFileSync(path.resolve(filename), "utf8");
|
240 |
|
241 | return processText(
|
242 | text,
|
243 | configHelper,
|
244 | filename,
|
245 | options.fix,
|
246 | options.allowInlineConfig,
|
247 | options.reportUnusedDisableDirectives,
|
248 | linter
|
249 | );
|
250 | }
|
251 |
|
252 | /**
|
253 | * Returns result with warning by ignore settings
|
254 | * @param {string} filePath - File path of checked code
|
255 | * @param {string} baseDir - Absolute path of base directory
|
256 | * @returns {LintResult} Result with single warning
|
257 | * @private
|
258 | */
|
259 | function createIgnoreResult(filePath, baseDir) {
|
260 | let message;
|
261 | const isHidden = /^\./u.test(path.basename(filePath));
|
262 | const isInNodeModules = baseDir && path.relative(baseDir, filePath).startsWith("node_modules");
|
263 | const isInBowerComponents = baseDir && path.relative(baseDir, filePath).startsWith("bower_components");
|
264 |
|
265 | if (isHidden) {
|
266 | message = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!<relative/path/to/filename>'\") to override.";
|
267 | } else if (isInNodeModules) {
|
268 | message = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override.";
|
269 | } else if (isInBowerComponents) {
|
270 | message = "File ignored by default. Use \"--ignore-pattern '!bower_components/*'\" to override.";
|
271 | } else {
|
272 | message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.";
|
273 | }
|
274 |
|
275 | return {
|
276 | filePath: path.resolve(filePath),
|
277 | messages: [
|
278 | {
|
279 | fatal: false,
|
280 | severity: 1,
|
281 | message
|
282 | }
|
283 | ],
|
284 | errorCount: 0,
|
285 | warningCount: 1,
|
286 | fixableErrorCount: 0,
|
287 | fixableWarningCount: 0
|
288 | };
|
289 | }
|
290 |
|
291 | /**
|
292 | * Produces rule warnings (i.e. deprecation) from configured rules
|
293 | * @param {(Array<string>|Set<string>)} usedRules - Rules configured
|
294 | * @param {Map} loadedRules - Map of loaded rules
|
295 | * @returns {Array<Object>} Contains rule warnings
|
296 | * @private
|
297 | */
|
298 | function createRuleDeprecationWarnings(usedRules, loadedRules) {
|
299 | const usedDeprecatedRules = [];
|
300 |
|
301 | usedRules.forEach(name => {
|
302 | const loadedRule = loadedRules.get(name);
|
303 |
|
304 | if (loadedRule && loadedRule.meta && loadedRule.meta.deprecated) {
|
305 | const deprecatedRule = { ruleId: name };
|
306 | const replacedBy = lodash.get(loadedRule, "meta.replacedBy", []);
|
307 |
|
308 | if (replacedBy.every(newRule => lodash.isString(newRule))) {
|
309 | deprecatedRule.replacedBy = replacedBy;
|
310 | }
|
311 |
|
312 | usedDeprecatedRules.push(deprecatedRule);
|
313 | }
|
314 | });
|
315 |
|
316 | return usedDeprecatedRules;
|
317 | }
|
318 |
|
319 | /**
|
320 | * Checks if the given message is an error message.
|
321 | * @param {Object} message The message to check.
|
322 | * @returns {boolean} Whether or not the message is an error message.
|
323 | * @private
|
324 | */
|
325 | function isErrorMessage(message) {
|
326 | return message.severity === 2;
|
327 | }
|
328 |
|
329 |
|
330 | /**
|
331 | * return the cacheFile to be used by eslint, based on whether the provided parameter is
|
332 | * a directory or looks like a directory (ends in `path.sep`), in which case the file
|
333 | * name will be the `cacheFile/.cache_hashOfCWD`
|
334 | *
|
335 | * if cacheFile points to a file or looks like a file then in will just use that file
|
336 | *
|
337 | * @param {string} cacheFile The name of file to be used to store the cache
|
338 | * @param {string} cwd Current working directory
|
339 | * @returns {string} the resolved path to the cache file
|
340 | */
|
341 | function getCacheFile(cacheFile, cwd) {
|
342 |
|
343 | /*
|
344 | * make sure the path separators are normalized for the environment/os
|
345 | * keeping the trailing path separator if present
|
346 | */
|
347 | const normalizedCacheFile = path.normalize(cacheFile);
|
348 |
|
349 | const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile);
|
350 | const looksLikeADirectory = normalizedCacheFile.slice(-1) === path.sep;
|
351 |
|
352 | /**
|
353 | * return the name for the cache file in case the provided parameter is a directory
|
354 | * @returns {string} the resolved path to the cacheFile
|
355 | */
|
356 | function getCacheFileForDirectory() {
|
357 | return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`);
|
358 | }
|
359 |
|
360 | let fileStats;
|
361 |
|
362 | try {
|
363 | fileStats = fs.lstatSync(resolvedCacheFile);
|
364 | } catch (ex) {
|
365 | fileStats = null;
|
366 | }
|
367 |
|
368 |
|
369 | /*
|
370 | * in case the file exists we need to verify if the provided path
|
371 | * is a directory or a file. If it is a directory we want to create a file
|
372 | * inside that directory
|
373 | */
|
374 | if (fileStats) {
|
375 |
|
376 | /*
|
377 | * is a directory or is a file, but the original file the user provided
|
378 | * looks like a directory but `path.resolve` removed the `last path.sep`
|
379 | * so we need to still treat this like a directory
|
380 | */
|
381 | if (fileStats.isDirectory() || looksLikeADirectory) {
|
382 | return getCacheFileForDirectory();
|
383 | }
|
384 |
|
385 | // is file so just use that file
|
386 | return resolvedCacheFile;
|
387 | }
|
388 |
|
389 | /*
|
390 | * here we known the file or directory doesn't exist,
|
391 | * so we will try to infer if its a directory if it looks like a directory
|
392 | * for the current operating system.
|
393 | */
|
394 |
|
395 | // if the last character passed is a path separator we assume is a directory
|
396 | if (looksLikeADirectory) {
|
397 | return getCacheFileForDirectory();
|
398 | }
|
399 |
|
400 | return resolvedCacheFile;
|
401 | }
|
402 |
|
403 | //------------------------------------------------------------------------------
|
404 | // Public Interface
|
405 | //------------------------------------------------------------------------------
|
406 |
|
407 | class CLIEngine {
|
408 |
|
409 | /**
|
410 | * Creates a new instance of the core CLI engine.
|
411 | * @param {CLIEngineOptions} providedOptions The options for this instance.
|
412 | * @constructor
|
413 | */
|
414 | constructor(providedOptions) {
|
415 |
|
416 | const options = Object.assign(
|
417 | Object.create(null),
|
418 | defaultOptions,
|
419 | { cwd: process.cwd() },
|
420 | providedOptions
|
421 | );
|
422 |
|
423 | /*
|
424 | * if an --ignore-path option is provided, ensure that the ignore
|
425 | * file exists and is not a directory
|
426 | */
|
427 | if (options.ignore && options.ignorePath) {
|
428 | try {
|
429 | if (!fs.statSync(options.ignorePath).isFile()) {
|
430 | throw new Error(`${options.ignorePath} is not a file`);
|
431 | }
|
432 | } catch (e) {
|
433 | e.message = `Error: Could not load file ${options.ignorePath}\nError: ${e.message}`;
|
434 | throw e;
|
435 | }
|
436 | }
|
437 |
|
438 | /**
|
439 | * Stored options for this instance
|
440 | * @type {Object}
|
441 | */
|
442 | this.options = options;
|
443 | this.linter = new Linter();
|
444 |
|
445 | // load in additional rules
|
446 | if (this.options.rulePaths) {
|
447 | const cwd = this.options.cwd;
|
448 |
|
449 | this.options.rulePaths.forEach(rulesdir => {
|
450 | debug(`Loading rules from ${rulesdir}`);
|
451 | this.linter.defineRules(loadRules(rulesdir, cwd));
|
452 | });
|
453 | }
|
454 |
|
455 | if (this.options.rules && Object.keys(this.options.rules).length) {
|
456 | const loadedRules = this.linter.getRules();
|
457 |
|
458 | // Ajv validator with default schema will mutate original object, so we must clone it recursively.
|
459 | this.options.rules = lodash.cloneDeep(this.options.rules);
|
460 |
|
461 | Object.keys(this.options.rules).forEach(name => {
|
462 | validator.validateRuleOptions(loadedRules.get(name), name, this.options.rules[name], "CLI");
|
463 | });
|
464 | }
|
465 |
|
466 | this.config = new Config(this.options, this.linter);
|
467 |
|
468 | if (this.options.cache) {
|
469 | const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
|
470 |
|
471 | /**
|
472 | * Cache used to avoid operating on files that haven't changed since the
|
473 | * last successful execution.
|
474 | * @type {Object}
|
475 | */
|
476 | this._lintResultCache = new LintResultCache(cacheFile, this.config);
|
477 | }
|
478 |
|
479 | // setup special filter for fixes
|
480 | if (this.options.fix && this.options.fixTypes && this.options.fixTypes.length > 0) {
|
481 |
|
482 | debug(`Using fix types ${this.options.fixTypes}`);
|
483 |
|
484 | // throw an error if any invalid fix types are found
|
485 | validateFixTypes(this.options.fixTypes);
|
486 |
|
487 | // convert to Set for faster lookup
|
488 | const fixTypes = new Set(this.options.fixTypes);
|
489 |
|
490 | // save original value of options.fix in case it's a function
|
491 | const originalFix = (typeof this.options.fix === "function")
|
492 | ? this.options.fix : () => this.options.fix;
|
493 |
|
494 | // create a cache of rules (but don't populate until needed)
|
495 | this._rulesCache = null;
|
496 |
|
497 | this.options.fix = lintResult => {
|
498 | const rule = this._rulesCache.get(lintResult.ruleId);
|
499 | const matches = rule.meta && fixTypes.has(rule.meta.type);
|
500 |
|
501 | return matches && originalFix(lintResult);
|
502 | };
|
503 | }
|
504 |
|
505 | }
|
506 |
|
507 | getRules() {
|
508 | return this.linter.getRules();
|
509 | }
|
510 |
|
511 | /**
|
512 | * Returns results that only contains errors.
|
513 | * @param {LintResult[]} results The results to filter.
|
514 | * @returns {LintResult[]} The filtered results.
|
515 | */
|
516 | static getErrorResults(results) {
|
517 | const filtered = [];
|
518 |
|
519 | results.forEach(result => {
|
520 | const filteredMessages = result.messages.filter(isErrorMessage);
|
521 |
|
522 | if (filteredMessages.length > 0) {
|
523 | filtered.push(
|
524 | Object.assign(result, {
|
525 | messages: filteredMessages,
|
526 | errorCount: filteredMessages.length,
|
527 | warningCount: 0,
|
528 | fixableErrorCount: result.fixableErrorCount,
|
529 | fixableWarningCount: 0
|
530 | })
|
531 | );
|
532 | }
|
533 | });
|
534 |
|
535 | return filtered;
|
536 | }
|
537 |
|
538 | /**
|
539 | * Outputs fixes from the given results to files.
|
540 | * @param {Object} report The report object created by CLIEngine.
|
541 | * @returns {void}
|
542 | */
|
543 | static outputFixes(report) {
|
544 | report.results.filter(result => Object.prototype.hasOwnProperty.call(result, "output")).forEach(result => {
|
545 | fs.writeFileSync(result.filePath, result.output);
|
546 | });
|
547 | }
|
548 |
|
549 |
|
550 | /**
|
551 | * Add a plugin by passing its configuration
|
552 | * @param {string} name Name of the plugin.
|
553 | * @param {Object} pluginobject Plugin configuration object.
|
554 | * @returns {void}
|
555 | */
|
556 | addPlugin(name, pluginobject) {
|
557 | this.config.plugins.define(name, pluginobject);
|
558 | }
|
559 |
|
560 | /**
|
561 | * Resolves the patterns passed into executeOnFiles() into glob-based patterns
|
562 | * for easier handling.
|
563 | * @param {string[]} patterns The file patterns passed on the command line.
|
564 | * @returns {string[]} The equivalent glob patterns.
|
565 | */
|
566 | resolveFileGlobPatterns(patterns) {
|
567 | return globUtils.resolveFileGlobPatterns(patterns.filter(Boolean), this.options);
|
568 | }
|
569 |
|
570 | /**
|
571 | * Executes the current configuration on an array of file and directory names.
|
572 | * @param {string[]} patterns An array of file and directory names.
|
573 | * @returns {Object} The results for all files that were linted.
|
574 | */
|
575 | executeOnFiles(patterns) {
|
576 | const options = this.options,
|
577 | lintResultCache = this._lintResultCache,
|
578 | configHelper = this.config;
|
579 | const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
|
580 |
|
581 | if (!options.cache && fs.existsSync(cacheFile)) {
|
582 | fs.unlinkSync(cacheFile);
|
583 | }
|
584 |
|
585 | const startTime = Date.now();
|
586 | const fileList = globUtils.listFilesToProcess(patterns, options);
|
587 | const allUsedRules = new Set();
|
588 | const results = fileList.map(fileInfo => {
|
589 | if (fileInfo.ignored) {
|
590 | return createIgnoreResult(fileInfo.filename, options.cwd);
|
591 | }
|
592 |
|
593 | if (options.cache) {
|
594 | const cachedLintResults = lintResultCache.getCachedLintResults(fileInfo.filename);
|
595 |
|
596 | if (cachedLintResults) {
|
597 | const resultHadMessages = cachedLintResults.messages && cachedLintResults.messages.length;
|
598 |
|
599 | if (resultHadMessages && options.fix) {
|
600 | debug(`Reprocessing cached file to allow autofix: ${fileInfo.filename}`);
|
601 | } else {
|
602 | debug(`Skipping file since it hasn't changed: ${fileInfo.filename}`);
|
603 |
|
604 | return cachedLintResults;
|
605 | }
|
606 | }
|
607 | }
|
608 |
|
609 | // if there's a cache, populate it
|
610 | if ("_rulesCache" in this) {
|
611 | this._rulesCache = this.getRules();
|
612 | }
|
613 |
|
614 | debug(`Processing ${fileInfo.filename}`);
|
615 |
|
616 | const { result, config } = processFile(fileInfo.filename, configHelper, options, this.linter);
|
617 |
|
618 | Object.keys(config.rules)
|
619 | .filter(ruleId => ConfigOps.getRuleSeverity(config.rules[ruleId]))
|
620 | .forEach(ruleId => allUsedRules.add(ruleId));
|
621 |
|
622 | return result;
|
623 | });
|
624 |
|
625 | if (options.cache) {
|
626 | results.forEach(result => {
|
627 |
|
628 | /*
|
629 | * Store the lint result in the LintResultCache.
|
630 | * NOTE: The LintResultCache will remove the file source and any
|
631 | * other properties that are difficult to serialize, and will
|
632 | * hydrate those properties back in on future lint runs.
|
633 | */
|
634 | lintResultCache.setCachedLintResults(result.filePath, result);
|
635 | });
|
636 |
|
637 | // persist the cache to disk
|
638 | lintResultCache.reconcile();
|
639 | }
|
640 |
|
641 | const stats = calculateStatsPerRun(results);
|
642 |
|
643 | const usedDeprecatedRules = createRuleDeprecationWarnings(allUsedRules, this.getRules());
|
644 |
|
645 | debug(`Linting complete in: ${Date.now() - startTime}ms`);
|
646 |
|
647 | return {
|
648 | results,
|
649 | errorCount: stats.errorCount,
|
650 | warningCount: stats.warningCount,
|
651 | fixableErrorCount: stats.fixableErrorCount,
|
652 | fixableWarningCount: stats.fixableWarningCount,
|
653 | usedDeprecatedRules
|
654 | };
|
655 | }
|
656 |
|
657 | /**
|
658 | * Executes the current configuration on text.
|
659 | * @param {string} text A string of JavaScript code to lint.
|
660 | * @param {string} filename An optional string representing the texts filename.
|
661 | * @param {boolean} warnIgnored Always warn when a file is ignored
|
662 | * @returns {Object} The results for the linting.
|
663 | */
|
664 | executeOnText(text, filename, warnIgnored) {
|
665 |
|
666 | const results = [],
|
667 | options = this.options,
|
668 | configHelper = this.config,
|
669 | ignoredPaths = new IgnoredPaths(options);
|
670 |
|
671 | // resolve filename based on options.cwd (for reporting, ignoredPaths also resolves)
|
672 |
|
673 | const resolvedFilename = filename && !path.isAbsolute(filename)
|
674 | ? path.resolve(options.cwd, filename)
|
675 | : filename;
|
676 | let usedDeprecatedRules;
|
677 |
|
678 | if (resolvedFilename && ignoredPaths.contains(resolvedFilename)) {
|
679 | if (warnIgnored) {
|
680 | results.push(createIgnoreResult(resolvedFilename, options.cwd));
|
681 | }
|
682 | usedDeprecatedRules = [];
|
683 | } else {
|
684 |
|
685 | // if there's a cache, populate it
|
686 | if ("_rulesCache" in this) {
|
687 | this._rulesCache = this.getRules();
|
688 | }
|
689 |
|
690 | const { result, config } = processText(
|
691 | text,
|
692 | configHelper,
|
693 | resolvedFilename,
|
694 | options.fix,
|
695 | options.allowInlineConfig,
|
696 | options.reportUnusedDisableDirectives,
|
697 | this.linter
|
698 | );
|
699 |
|
700 | results.push(result);
|
701 | usedDeprecatedRules = createRuleDeprecationWarnings(
|
702 | Object.keys(config.rules).filter(rule => ConfigOps.getRuleSeverity(config.rules[rule])),
|
703 | this.getRules()
|
704 | );
|
705 | }
|
706 |
|
707 | const stats = calculateStatsPerRun(results);
|
708 |
|
709 | return {
|
710 | results,
|
711 | errorCount: stats.errorCount,
|
712 | warningCount: stats.warningCount,
|
713 | fixableErrorCount: stats.fixableErrorCount,
|
714 | fixableWarningCount: stats.fixableWarningCount,
|
715 | usedDeprecatedRules
|
716 | };
|
717 | }
|
718 |
|
719 | /**
|
720 | * Returns a configuration object for the given file based on the CLI options.
|
721 | * This is the same logic used by the ESLint CLI executable to determine
|
722 | * configuration for each file it processes.
|
723 | * @param {string} filePath The path of the file to retrieve a config object for.
|
724 | * @returns {Object} A configuration object for the file.
|
725 | */
|
726 | getConfigForFile(filePath) {
|
727 | const configHelper = this.config;
|
728 |
|
729 | return configHelper.getConfig(filePath);
|
730 | }
|
731 |
|
732 | /**
|
733 | * Checks if a given path is ignored by ESLint.
|
734 | * @param {string} filePath The path of the file to check.
|
735 | * @returns {boolean} Whether or not the given path is ignored.
|
736 | */
|
737 | isPathIgnored(filePath) {
|
738 | const resolvedPath = path.resolve(this.options.cwd, filePath);
|
739 | const ignoredPaths = new IgnoredPaths(this.options);
|
740 |
|
741 | return ignoredPaths.contains(resolvedPath);
|
742 | }
|
743 |
|
744 | /**
|
745 | * Returns the formatter representing the given format or null if no formatter
|
746 | * with the given name can be found.
|
747 | * @param {string} [format] The name of the format to load or the path to a
|
748 | * custom formatter.
|
749 | * @returns {Function} The formatter function or null if not found.
|
750 | */
|
751 | getFormatter(format) {
|
752 |
|
753 | // default is stylish
|
754 | const resolvedFormatName = format || "stylish";
|
755 |
|
756 | // only strings are valid formatters
|
757 | if (typeof resolvedFormatName === "string") {
|
758 |
|
759 | // replace \ with / for Windows compatibility
|
760 | const normalizedFormatName = resolvedFormatName.replace(/\\/gu, "/");
|
761 |
|
762 | const cwd = this.options ? this.options.cwd : process.cwd();
|
763 | const namespace = naming.getNamespaceFromTerm(normalizedFormatName);
|
764 |
|
765 | let formatterPath;
|
766 |
|
767 | // if there's a slash, then it's a file
|
768 | if (!namespace && normalizedFormatName.indexOf("/") > -1) {
|
769 | formatterPath = path.resolve(cwd, normalizedFormatName);
|
770 | } else {
|
771 | try {
|
772 | const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter");
|
773 |
|
774 | formatterPath = resolver.resolve(npmFormat, `${cwd}/node_modules`);
|
775 | } catch (e) {
|
776 | formatterPath = `./formatters/${normalizedFormatName}`;
|
777 | }
|
778 | }
|
779 |
|
780 | try {
|
781 | return require(formatterPath);
|
782 | } catch (ex) {
|
783 | ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;
|
784 | throw ex;
|
785 | }
|
786 |
|
787 | } else {
|
788 | return null;
|
789 | }
|
790 | }
|
791 | }
|
792 |
|
793 | CLIEngine.version = pkg.version;
|
794 | CLIEngine.getFormatter = CLIEngine.prototype.getFormatter;
|
795 |
|
796 | module.exports = CLIEngine;
|