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