diff --git a/tools/eslint/README.md b/tools/eslint/README.md index 7304f1b708..2f0f1648e1 100644 --- a/tools/eslint/README.md +++ b/tools/eslint/README.md @@ -9,11 +9,11 @@ # ESLint -[Website](http://eslint.org) | -[Configuring](http://eslint.org/docs/user-guide/configuring) | -[Rules](http://eslint.org/docs/rules/) | -[Contributing](http://eslint.org/docs/developer-guide/contributing) | -[Reporting Bugs](http://eslint.org/docs/developer-guide/contributing/reporting-bugs) | +[Website](https://eslint.org) | +[Configuring](https://eslint.org/docs/user-guide/configuring) | +[Rules](https://eslint.org/docs/rules/) | +[Contributing](https://eslint.org/docs/developer-guide/contributing) | +[Reporting Bugs](https://eslint.org/docs/developer-guide/contributing/reporting-bugs) | [Code of Conduct](https://js.foundation/community/code-of-conduct) | [Twitter](https://twitter.com/geteslint) | [Mailing List](https://groups.google.com/group/eslint) | @@ -88,17 +88,17 @@ After running `eslint --init`, you'll have a `.eslintrc` file in your directory. } ``` -The names `"semi"` and `"quotes"` are the names of [rules](http://eslint.org/docs/rules) in ESLint. The first value is the error level of the rule and can be one of these values: +The names `"semi"` and `"quotes"` are the names of [rules](https://eslint.org/docs/rules) in ESLint. The first value is the error level of the rule and can be one of these values: * `"off"` or `0` - turn the rule off * `"warn"` or `1` - turn the rule on as a warning (doesn't affect exit code) * `"error"` or `2` - turn the rule on as an error (exit code will be 1) -The three error levels allow you fine-grained control over how ESLint applies rules (for more configuration options and details, see the [configuration docs](http://eslint.org/docs/user-guide/configuring)). +The three error levels allow you fine-grained control over how ESLint applies rules (for more configuration options and details, see the [configuration docs](https://eslint.org/docs/user-guide/configuring)). ## Sponsors -* Site search ([eslint.org](http://eslint.org)) is sponsored by [Algolia](https://www.algolia.com) +* Site search ([eslint.org](https://eslint.org)) is sponsored by [Algolia](https://www.algolia.com) ## Team @@ -145,10 +145,10 @@ ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/comm Before filing an issue, please be sure to read the guidelines for what you're reporting: -* [Bug Report](http://eslint.org/docs/developer-guide/contributing/reporting-bugs) -* [Propose a New Rule](http://eslint.org/docs/developer-guide/contributing/new-rules) -* [Proposing a Rule Change](http://eslint.org/docs/developer-guide/contributing/rule-changes) -* [Request a Change](http://eslint.org/docs/developer-guide/contributing/changes) +* [Bug Report](https://eslint.org/docs/developer-guide/contributing/reporting-bugs) +* [Propose a New Rule](https://eslint.org/docs/developer-guide/contributing/new-rules) +* [Proposing a Rule Change](https://eslint.org/docs/developer-guide/contributing/rule-changes) +* [Request a Change](https://eslint.org/docs/developer-guide/contributing/changes) ## Semantic Versioning Policy @@ -171,7 +171,6 @@ ESLint follows [semantic versioning](http://semver.org). However, due to the nat * Major release (likely to break your lint build) * `eslint:recommended` is updated. * A new option to an existing rule that results in ESLint reporting more errors by default. - * An existing rule is removed. * An existing formatter is removed. * Part of the public API is removed or changed in an incompatible way. @@ -195,11 +194,11 @@ Despite being slower, we believe that ESLint is fast enough to replace JSHint wi ### I heard ESLint is going to replace JSCS? -Yes. Since we are solving the same problems, ESLint and JSCS teams have decided to join forces and work together in the development of ESLint instead of competing with each other. You can read more about this in both [ESLint](http://eslint.org/blog/2016/04/welcoming-jscs-to-eslint) and [JSCS](https://medium.com/@markelog/jscs-end-of-the-line-bc9bf0b3fdb2#.u76sx334n) announcements. +Yes. Since we are solving the same problems, ESLint and JSCS teams have decided to join forces and work together in the development of ESLint instead of competing with each other. You can read more about this in both [ESLint](https://eslint.org/blog/2016/04/welcoming-jscs-to-eslint) and [JSCS](https://medium.com/@markelog/jscs-end-of-the-line-bc9bf0b3fdb2#.u76sx334n) announcements. ### So, should I stop using JSCS and start using ESLint? -Maybe, depending on how much you need it. [JSCS has reached end of life](http://eslint.org/blog/2016/07/jscs-end-of-life), but if it is working for you then there is no reason to move yet. We are still working to smooth the transition. You can see our progress [here](https://github.com/eslint/eslint/milestones/JSCS%20Compatibility). We’ll announce when all of the changes necessary to support JSCS users in ESLint are complete and will start encouraging JSCS users to switch to ESLint at that time. +Maybe, depending on how much you need it. [JSCS has reached end of life](https://eslint.org/blog/2016/07/jscs-end-of-life), but if it is working for you then there is no reason to move yet. We are still working to smooth the transition. You can see our progress [here](https://github.com/eslint/eslint/milestones/JSCS%20Compatibility). We’ll announce when all of the changes necessary to support JSCS users in ESLint are complete and will start encouraging JSCS users to switch to ESLint at that time. If you are having issues with JSCS, you can try to move to ESLint. We are focusing our time and energy on JSCS compatibility issues. @@ -210,17 +209,17 @@ ESLint does both traditional linting (looking for problematic patterns) and styl ### Does ESLint support JSX? -Yes, ESLint natively supports parsing JSX syntax (this must be enabled in [configuration](http://eslint.org/docs/user-guide/configuring).). Please note that supporting JSX syntax *is not* the same as supporting React. React applies specific semantics to JSX syntax that ESLint doesn't recognize. We recommend using [eslint-plugin-react](https://www.npmjs.com/package/eslint-plugin-react) if you are using React and want React semantics. +Yes, ESLint natively supports parsing JSX syntax (this must be enabled in [configuration](https://eslint.org/docs/user-guide/configuring).). Please note that supporting JSX syntax *is not* the same as supporting React. React applies specific semantics to JSX syntax that ESLint doesn't recognize. We recommend using [eslint-plugin-react](https://www.npmjs.com/package/eslint-plugin-react) if you are using React and want React semantics. ### What about ECMAScript 6 support? -ESLint has full support for ECMAScript 6. By default, this support is off. You can enable ECMAScript 6 syntax and global variables through [configuration](http://eslint.org/docs/user-guide/configuring). +ESLint has full support for ECMAScript 6. By default, this support is off. You can enable ECMAScript 6 syntax and global variables through [configuration](https://eslint.org/docs/user-guide/configuring). ### What about experimental features? ESLint doesn't natively support experimental ECMAScript language features. You can use [babel-eslint](https://github.com/babel/babel-eslint) to use any option available in Babel. -Once a language feature has been adopted into the ECMAScript standard (stage 4 according to the [TC39 process](https://tc39.github.io/process-document/)), we will accept issues and pull requests related to the new feature, subject to our [contributing guidelines](http://eslint.org/docs/developer-guide/contributing). Until then, please use the appropriate parser and plugin(s) for your experimental feature. +Once a language feature has been adopted into the ECMAScript standard (stage 4 according to the [TC39 process](https://tc39.github.io/process-document/)), we will accept issues and pull requests related to the new feature, subject to our [contributing guidelines](https://eslint.org/docs/developer-guide/contributing). Until then, please use the appropriate parser and plugin(s) for your experimental feature. ### Where to ask for help? diff --git a/tools/eslint/bin/eslint.js b/tools/eslint/bin/eslint.js index 7c05ad3b6e..1a298047ae 100755 --- a/tools/eslint/bin/eslint.js +++ b/tools/eslint/bin/eslint.js @@ -43,10 +43,13 @@ process.once("uncaughtException", err => { if (typeof err.messageTemplate === "string" && err.messageTemplate.length > 0) { const template = lodash.template(fs.readFileSync(path.resolve(__dirname, `../messages/${err.messageTemplate}.txt`), "utf-8")); + const pkg = require("../package.json"); console.error("\nOops! Something went wrong! :("); - console.error(`\n${template(err.messageData || {})}`); + console.error(`\nESLint: ${pkg.version}.\n${template(err.messageData || {})}`); } else { + + console.error(err.message); console.error(err.stack); } diff --git a/tools/eslint/conf/category-list.json b/tools/eslint/conf/category-list.json index b5020c1f00..5427667b09 100644 --- a/tools/eslint/conf/category-list.json +++ b/tools/eslint/conf/category-list.json @@ -10,12 +10,12 @@ ], "deprecated": { "name": "Deprecated", - "description": "These rules have been deprecated and replaced by newer rules:", + "description": "These rules have been deprecated in accordance with the [deprecation policy](/docs/user-guide/rule-deprecation), and replaced by newer rules:", "rules": [] }, "removed": { "name": "Removed", - "description": "These rules from older versions of ESLint have been replaced by newer rules:", + "description": "These rules from older versions of ESLint (before the [deprecation policy](/docs/user-guide/rule-deprecation) existed) have been replaced by newer rules:", "rules": [ { "removed": "generator-star", "replacedBy": ["generator-star-spacing"] }, { "removed": "global-strict", "replacedBy": ["strict"] }, diff --git a/tools/eslint/conf/config-schema.js b/tools/eslint/conf/config-schema.js index 4ef958c40d..626e1d54c5 100644 --- a/tools/eslint/conf/config-schema.js +++ b/tools/eslint/conf/config-schema.js @@ -12,7 +12,9 @@ const baseConfigProperties = { parserOptions: { type: "object" }, plugins: { type: "array" }, rules: { type: "object" }, - settings: { type: "object" } + settings: { type: "object" }, + + ecmaFeatures: { type: "object" } // deprecated; logs a warning when used }; const overrideProperties = Object.assign( diff --git a/tools/eslint/conf/default-cli-options.js b/tools/eslint/conf/default-cli-options.js index 6dfdb54421..6e4a113003 100644 --- a/tools/eslint/conf/default-cli-options.js +++ b/tools/eslint/conf/default-cli-options.js @@ -23,5 +23,6 @@ module.exports = { cacheLocation: "", cacheFile: ".eslintcache", fix: false, - allowInlineConfig: true + allowInlineConfig: true, + reportUnusedDisableDirectives: false }; diff --git a/tools/eslint/conf/eslint-recommended.js b/tools/eslint/conf/eslint-recommended.js index 1a2e2f8e7f..a6fc9adf56 100755 --- a/tools/eslint/conf/eslint-recommended.js +++ b/tools/eslint/conf/eslint-recommended.js @@ -6,13 +6,10 @@ "use strict"; -/* eslint sort-keys: ["error", "asc"], quote-props: ["error", "consistent"] */ -/* eslint-disable sort-keys */ +/* eslint sort-keys: ["error", "asc"] */ module.exports = { rules: { - - /* eslint-enable sort-keys */ "accessor-pairs": "off", "array-bracket-newline": "off", "array-bracket-spacing": "off", @@ -25,28 +22,29 @@ module.exports = { "block-spacing": "off", "brace-style": "off", "callback-return": "off", - "camelcase": "off", + camelcase: "off", "capitalized-comments": "off", "class-methods-use-this": "off", "comma-dangle": "off", "comma-spacing": "off", "comma-style": "off", - "complexity": "off", + complexity: "off", "computed-property-spacing": "off", "consistent-return": "off", "consistent-this": "off", "constructor-super": "error", - "curly": "off", + curly: "off", "default-case": "off", "dot-location": "off", "dot-notation": "off", "eol-last": "off", - "eqeqeq": "off", + eqeqeq: "off", "for-direction": "off", "func-call-spacing": "off", "func-name-matching": "off", "func-names": "off", "func-style": "off", + "function-paren-newline": "off", "generator-star-spacing": "off", "getter-return": "off", "global-require": "off", @@ -55,7 +53,7 @@ module.exports = { "id-blacklist": "off", "id-length": "off", "id-match": "off", - "indent": "off", + indent: "off", "indent-legacy": "off", "init-declarations": "off", "jsx-quotes": "off", @@ -234,13 +232,13 @@ module.exports = { "prefer-spread": "off", "prefer-template": "off", "quote-props": "off", - "quotes": "off", - "radix": "off", + quotes: "off", + radix: "off", "require-await": "off", "require-jsdoc": "off", "require-yield": "error", "rest-spread-spacing": "off", - "semi": "off", + semi: "off", "semi-spacing": "off", "semi-style": "off", "sort-imports": "off", @@ -252,7 +250,7 @@ module.exports = { "space-infix-ops": "off", "space-unary-ops": "off", "spaced-comment": "off", - "strict": "off", + strict: "off", "switch-colon-spacing": "off", "symbol-description": "off", "template-curly-spacing": "off", @@ -265,6 +263,6 @@ module.exports = { "wrap-iife": "off", "wrap-regex": "off", "yield-star-spacing": "off", - "yoda": "off" + yoda: "off" } }; diff --git a/tools/eslint/lib/ast-utils.js b/tools/eslint/lib/ast-utils.js index 98775f5ef0..47cc71990f 100644 --- a/tools/eslint/lib/ast-utils.js +++ b/tools/eslint/lib/ast-utils.js @@ -626,6 +626,9 @@ module.exports = { // // setup... // return function foo() { ... }; // })(); + // obj.foo = (() => + // function foo() { ... } + // )(); case "ReturnStatement": { const func = getUpperFunction(parent); @@ -635,6 +638,12 @@ module.exports = { node = func.parent; break; } + case "ArrowFunctionExpression": + if (node !== parent.body || !isCallee(parent)) { + return true; + } + node = parent.parent; + break; // e.g. // var obj = { foo() { ... } }; @@ -655,16 +664,15 @@ module.exports = { // [Foo = function() { ... }] = a; case "AssignmentExpression": case "AssignmentPattern": - if (parent.right === node) { - if (parent.left.type === "MemberExpression") { - return false; - } - if (isAnonymous && - parent.left.type === "Identifier" && - startsWithUpperCase(parent.left.name) - ) { - return false; - } + if (parent.left.type === "MemberExpression") { + return false; + } + if ( + isAnonymous && + parent.left.type === "Identifier" && + startsWithUpperCase(parent.left.name) + ) { + return false; } return true; @@ -809,19 +817,14 @@ module.exports = { return 17; case "CallExpression": - - // IIFE is allowed to have parens in any position (#655) - if (node.callee.type === "FunctionExpression") { - return -1; - } return 18; case "NewExpression": return 19; - // no default + default: + return 20; } - return 20; }, /** diff --git a/tools/eslint/lib/cli-engine.js b/tools/eslint/lib/cli-engine.js index 1abc1fd2c6..38c49cb31d 100644 --- a/tools/eslint/lib/cli-engine.js +++ b/tools/eslint/lib/cli-engine.js @@ -56,6 +56,7 @@ const debug = require("debug")("eslint:cli-engine"); * @property {string[]} plugins An array of plugins to load. * @property {Object} rules An object of rules to use. * @property {string[]} rulePaths An array of directories to load custom rules from. + * @property {boolean} reportUnusedDisableDirectives `true` adds reports for unused eslint-disable directives */ /** @@ -137,20 +138,15 @@ function calculateStatsPerRun(results) { * @param {string} filename An optional string representing the texts filename. * @param {boolean|Function} fix Indicates if fixes should be processed. * @param {boolean} allowInlineConfig Allow/ignore comments that change config. + * @param {boolean} reportUnusedDisableDirectives Allow/ignore comments that change config. * @param {Linter} linter Linter context * @returns {LintResult} The results for linting on this text. * @private */ -function processText(text, configHelper, filename, fix, allowInlineConfig, linter) { - - // clear all existing settings for a new file - linter.reset(); - +function processText(text, configHelper, filename, fix, allowInlineConfig, reportUnusedDisableDirectives, linter) { let filePath, - messages, fileExtension, - processor, - fixedResult; + processor; if (filename) { filePath = path.resolve(filename); @@ -174,51 +170,29 @@ function processText(text, configHelper, filename, fix, allowInlineConfig, linte } } - if (processor) { - debug("Using processor"); - const parsedBlocks = processor.preprocess(text, filename); - const unprocessedMessages = []; - - parsedBlocks.forEach(block => { - unprocessedMessages.push(linter.verify(block, config, { - filename, - allowInlineConfig - })); - }); - - // TODO(nzakas): Figure out how fixes might work for processors - - messages = processor.postprocess(unprocessedMessages, filename); - - } else { + const autofixingEnabled = typeof fix !== "undefined" && (!processor || processor.supportsAutofix); - if (fix) { - fixedResult = linter.verifyAndFix(text, config, { - filename, - allowInlineConfig, - fix - }); - messages = fixedResult.messages; - } else { - messages = linter.verify(text, config, { - filename, - allowInlineConfig - }); - } - } + const fixedResult = linter.verifyAndFix(text, config, { + filename, + allowInlineConfig, + reportUnusedDisableDirectives, + fix: !!autofixingEnabled && fix, + preprocess: processor && (rawText => processor.preprocess(rawText, filename)), + postprocess: processor && (problemLists => processor.postprocess(problemLists, filename)) + }); - const stats = calculateStatsPerFile(messages); + const stats = calculateStatsPerFile(fixedResult.messages); const result = { filePath: filename, - messages, + messages: fixedResult.messages, errorCount: stats.errorCount, warningCount: stats.warningCount, fixableErrorCount: stats.fixableErrorCount, fixableWarningCount: stats.fixableWarningCount }; - if (fixedResult && fixedResult.fixed) { + if (fixedResult.fixed) { result.output = fixedResult.output; } @@ -242,7 +216,15 @@ function processText(text, configHelper, filename, fix, allowInlineConfig, linte function processFile(filename, configHelper, options, linter) { const text = fs.readFileSync(path.resolve(filename), "utf8"), - result = processText(text, configHelper, filename, options.fix, options.allowInlineConfig, linter); + result = processText( + text, + configHelper, + filename, + options.fix, + options.allowInlineConfig, + options.reportUnusedDisableDirectives, + linter + ); return result; @@ -372,6 +354,8 @@ function getCacheFile(cacheFile, cwd) { return resolvedCacheFile; } +const configHashCache = new WeakMap(); + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -493,11 +477,9 @@ class CLIEngine { * @returns {Object} The results for all files that were linted. */ executeOnFiles(patterns) { - const results = [], - options = this.options, + const options = this.options, fileCache = this._fileCache, configHelper = this.config; - let prevConfig; // the previous configuration used const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd); if (!options.cache && fs.existsSync(cacheFile)) { @@ -512,42 +494,18 @@ class CLIEngine { function hashOfConfigFor(filename) { const config = configHelper.getConfig(filename); - if (!prevConfig) { - prevConfig = {}; + if (!configHashCache.has(config)) { + configHashCache.set(config, hash(`${pkg.version}_${stringify(config)}`)); } - // reuse the previously hashed config if the config hasn't changed - if (prevConfig.config !== config) { - - /* - * config changed so we need to calculate the hash of the config - * and the hash of the plugins being used - */ - prevConfig.config = config; - - const eslintVersion = pkg.version; - - prevConfig.hash = hash(`${eslintVersion}_${stringify(config)}`); - } - - return prevConfig.hash; + return configHashCache.get(config); } - /** - * Executes the linter on a file defined by the `filename`. Skips - * unsupported file extensions and any files that are already linted. - * @param {string} filename The resolved filename of the file to be linted - * @param {boolean} warnIgnored always warn when a file is ignored - * @param {Linter} linter Linter context - * @returns {void} - */ - function executeOnFile(filename, warnIgnored, linter) { - let hashOfConfig, - descriptor; - - if (warnIgnored) { - results.push(createIgnoreResult(filename, options.cwd)); - return; + const startTime = Date.now(); + const fileList = globUtil.listFilesToProcess(this.resolveFileGlobPatterns(patterns), options); + const results = fileList.map(fileInfo => { + if (fileInfo.ignored) { + return createIgnoreResult(fileInfo.filename, options.cwd); } if (options.cache) { @@ -557,15 +515,12 @@ class CLIEngine { * with the metadata and the flag that determines if * the file has changed */ - descriptor = fileCache.getFileDescriptor(filename); - const meta = descriptor.meta || {}; - - hashOfConfig = hashOfConfigFor(filename); - - const changed = descriptor.changed || meta.hashOfConfig !== hashOfConfig; + const descriptor = fileCache.getFileDescriptor(fileInfo.filename); + const hashOfConfig = hashOfConfigFor(fileInfo.filename); + const changed = descriptor.changed || descriptor.meta.hashOfConfig !== hashOfConfig; if (!changed) { - debug(`Skipping file since hasn't changed: ${filename}`); + debug(`Skipping file since hasn't changed: ${fileInfo.filename}`); /* * Add the the cached results (always will be 0 error and @@ -573,63 +528,45 @@ class CLIEngine { * failed, in order to guarantee that next execution will * process those files as well. */ - results.push(descriptor.meta.results); - - // move to the next file - return; + return descriptor.meta.results; } } - debug(`Processing ${filename}`); + debug(`Processing ${fileInfo.filename}`); - const res = processFile(filename, configHelper, options, linter); - - if (options.cache) { + return processFile(fileInfo.filename, configHelper, options, this.linter); + }); - /* - * if a file contains errors or warnings we don't want to - * store the file in the cache so we can guarantee that - * next execution will also operate on this file - */ - if (res.errorCount > 0 || res.warningCount > 0) { - debug(`File has problems, skipping it: ${filename}`); + if (options.cache) { + results.forEach(result => { + if (result.messages.length) { - // remove the entry from the cache - fileCache.removeEntry(filename); + /* + * if a file contains errors or warnings we don't want to + * store the file in the cache so we can guarantee that + * next execution will also operate on this file + */ + fileCache.removeEntry(result.filePath); } else { /* * since the file passed we store the result here - * TODO: check this as we might not need to store the - * successful runs as it will always should be 0 errors and - * 0 warnings. + * TODO: it might not be necessary to store the results list in the cache, + * since it should always be 0 errors/warnings */ - descriptor.meta.hashOfConfig = hashOfConfig; - descriptor.meta.results = res; - } - } - - results.push(res); - } + const descriptor = fileCache.getFileDescriptor(result.filePath); - const startTime = Date.now(); - - - patterns = this.resolveFileGlobPatterns(patterns); - const fileList = globUtil.listFilesToProcess(patterns, options); - - fileList.forEach(fileInfo => { - executeOnFile(fileInfo.filename, fileInfo.ignored, this.linter); - }); - - const stats = calculateStatsPerRun(results); - - if (options.cache) { + descriptor.meta.hashOfConfig = hashOfConfigFor(result.filePath); + descriptor.meta.results = result; + } + }); // persist the cache to disk fileCache.reconcile(); } + const stats = calculateStatsPerRun(results); + debug(`Linting complete in: ${Date.now() - startTime}ms`); return { @@ -665,7 +602,17 @@ class CLIEngine { results.push(createIgnoreResult(filename, options.cwd)); } } else { - results.push(processText(text, configHelper, filename, options.fix, options.allowInlineConfig, this.linter)); + results.push( + processText( + text, + configHelper, + filename, + options.fix, + options.allowInlineConfig, + options.reportUnusedDisableDirectives, + this.linter + ) + ); } const stats = calculateStatsPerRun(results); @@ -713,7 +660,6 @@ class CLIEngine { */ getFormatter(format) { - let formatterPath; // default is stylish format = format || "stylish"; @@ -724,6 +670,8 @@ class CLIEngine { // replace \ with / for Windows compatibility format = format.replace(/\\/g, "/"); + let formatterPath; + // if there's a slash, then it's a file if (format.indexOf("/") > -1) { const cwd = this.options ? this.options.cwd : process.cwd(); diff --git a/tools/eslint/lib/cli.js b/tools/eslint/lib/cli.js index d398477184..0c8a7d62fb 100644 --- a/tools/eslint/lib/cli.js +++ b/tools/eslint/lib/cli.js @@ -64,7 +64,8 @@ function translateOptions(cliOptions) { cacheFile: cliOptions.cacheFile, cacheLocation: cliOptions.cacheLocation, fix: cliOptions.fix && (cliOptions.quiet ? quietFixPredicate : true), - allowInlineConfig: cliOptions.inlineConfig + allowInlineConfig: cliOptions.inlineConfig, + reportUnusedDisableDirectives: cliOptions.reportUnusedDisableDirectives }; } diff --git a/tools/eslint/lib/code-path-analysis/code-path-analyzer.js b/tools/eslint/lib/code-path-analysis/code-path-analyzer.js index 539b5e18b3..899f240bc4 100644 --- a/tools/eslint/lib/code-path-analysis/code-path-analyzer.js +++ b/tools/eslint/lib/code-path-analysis/code-path-analyzer.js @@ -154,7 +154,8 @@ function forwardCurrentToHead(analyzer, node) { analyzer.emitter.emit( "onCodePathSegmentEnd", currentSegment, - node); + node + ); } } } @@ -175,7 +176,8 @@ function forwardCurrentToHead(analyzer, node) { analyzer.emitter.emit( "onCodePathSegmentStart", headSegment, - node); + node + ); } } } @@ -202,7 +204,8 @@ function leaveFromCurrentSegment(analyzer, node) { analyzer.emitter.emit( "onCodePathSegmentEnd", currentSegment, - node); + node + ); } } @@ -369,7 +372,8 @@ function processCodePathToEnter(analyzer, node) { case "SwitchStatement": state.pushSwitchContext( node.cases.some(isCaseNode), - astUtils.getLabel(node)); + astUtils.getLabel(node) + ); break; case "TryStatement": diff --git a/tools/eslint/lib/code-path-analysis/code-path-segment.js b/tools/eslint/lib/code-path-analysis/code-path-segment.js index db1eba4560..9de4264b13 100644 --- a/tools/eslint/lib/code-path-analysis/code-path-segment.js +++ b/tools/eslint/lib/code-path-analysis/code-path-segment.js @@ -15,43 +15,6 @@ const debug = require("./debug-helpers"); // Helpers //------------------------------------------------------------------------------ -/** - * Replaces unused segments with the previous segments of each unused segment. - * - * @param {CodePathSegment[]} segments - An array of segments to replace. - * @returns {CodePathSegment[]} The replaced array. - */ -function flattenUnusedSegments(segments) { - const done = Object.create(null); - const retv = []; - - for (let i = 0; i < segments.length; ++i) { - const segment = segments[i]; - - // Ignores duplicated. - if (done[segment.id]) { - continue; - } - - // Use previous segments if unused. - if (!segment.internal.used) { - for (let j = 0; j < segment.allPrevSegments.length; ++j) { - const prevSegment = segment.allPrevSegments[j]; - - if (!done[prevSegment.id]) { - done[prevSegment.id] = true; - retv.push(prevSegment); - } - } - } else { - done[segment.id] = true; - retv.push(segment); - } - } - - return retv; -} - /** * Checks whether or not a given segment is reachable. * @@ -163,8 +126,9 @@ class CodePathSegment { static newNext(id, allPrevSegments) { return new CodePathSegment( id, - flattenUnusedSegments(allPrevSegments), - allPrevSegments.some(isReachable)); + CodePathSegment.flattenUnusedSegments(allPrevSegments), + allPrevSegments.some(isReachable) + ); } /** @@ -175,7 +139,7 @@ class CodePathSegment { * @returns {CodePathSegment} The created segment. */ static newUnreachable(id, allPrevSegments) { - const segment = new CodePathSegment(id, flattenUnusedSegments(allPrevSegments), false); + const segment = new CodePathSegment(id, CodePathSegment.flattenUnusedSegments(allPrevSegments), false); // In `if (a) return a; foo();` case, the unreachable segment preceded by // the return statement is not used but must not be remove. @@ -237,6 +201,43 @@ class CodePathSegment { static markPrevSegmentAsLooped(segment, prevSegment) { segment.internal.loopedPrevSegments.push(prevSegment); } + + /** + * Replaces unused segments with the previous segments of each unused segment. + * + * @param {CodePathSegment[]} segments - An array of segments to replace. + * @returns {CodePathSegment[]} The replaced array. + */ + static flattenUnusedSegments(segments) { + const done = Object.create(null); + const retv = []; + + for (let i = 0; i < segments.length; ++i) { + const segment = segments[i]; + + // Ignores duplicated. + if (done[segment.id]) { + continue; + } + + // Use previous segments if unused. + if (!segment.internal.used) { + for (let j = 0; j < segment.allPrevSegments.length; ++j) { + const prevSegment = segment.allPrevSegments[j]; + + if (!done[prevSegment.id]) { + done[prevSegment.id] = true; + retv.push(prevSegment); + } + } + } else { + done[segment.id] = true; + retv.push(segment); + } + } + + return retv; + } } module.exports = CodePathSegment; diff --git a/tools/eslint/lib/code-path-analysis/code-path-state.js b/tools/eslint/lib/code-path-analysis/code-path-state.js index 7c8abb2071..068caca9e6 100644 --- a/tools/eslint/lib/code-path-analysis/code-path-state.js +++ b/tools/eslint/lib/code-path-analysis/code-path-state.js @@ -169,6 +169,9 @@ function removeConnection(prevSegments, nextSegments) { * @returns {void} */ function makeLooped(state, fromSegments, toSegments) { + fromSegments = CodePathSegment.flattenUnusedSegments(fromSegments); + toSegments = CodePathSegment.flattenUnusedSegments(toSegments); + const end = Math.min(fromSegments.length, toSegments.length); for (let i = 0; i < end; ++i) { @@ -843,21 +846,23 @@ class CodePathState { * This segment will leave at the end of this finally block. */ const segments = forkContext.makeNext(-1, -1); - let j; for (let i = 0; i < forkContext.count; ++i) { const prevSegsOfLeavingSegment = [headOfLeavingSegments[i]]; - for (j = 0; j < returned.segmentsList.length; ++j) { + for (let j = 0; j < returned.segmentsList.length; ++j) { prevSegsOfLeavingSegment.push(returned.segmentsList[j][i]); } - for (j = 0; j < thrown.segmentsList.length; ++j) { + for (let j = 0; j < thrown.segmentsList.length; ++j) { prevSegsOfLeavingSegment.push(thrown.segmentsList[j][i]); } - segments.push(CodePathSegment.newNext( - this.idGenerator.next(), - prevSegsOfLeavingSegment)); + segments.push( + CodePathSegment.newNext( + this.idGenerator.next(), + prevSegsOfLeavingSegment + ) + ); } this.pushForkContext(true); @@ -982,7 +987,6 @@ class CodePathState { const forkContext = this.forkContext; const brokenForkContext = this.popBreakContext().brokenForkContext; - let choiceContext; // Creates a looped path. switch (context.type) { @@ -992,11 +996,12 @@ class CodePathState { makeLooped( this, forkContext.head, - context.continueDestSegments); + context.continueDestSegments + ); break; case "DoWhileStatement": { - choiceContext = this.popChoiceContext(); + const choiceContext = this.popChoiceContext(); if (!choiceContext.processed) { choiceContext.trueForkContext.add(forkContext.head); @@ -1013,7 +1018,8 @@ class CodePathState { makeLooped( this, segmentsList[i], - context.entrySegments); + context.entrySegments + ); } break; } @@ -1024,7 +1030,8 @@ class CodePathState { makeLooped( this, forkContext.head, - context.leftSegments); + context.leftSegments + ); break; /* istanbul ignore next */ @@ -1149,7 +1156,8 @@ class CodePathState { finalizeTestSegmentsOfFor( context, choiceContext, - forkContext.head); + forkContext.head + ); } else { context.endOfInitSegments = forkContext.head; } @@ -1180,13 +1188,15 @@ class CodePathState { makeLooped( this, context.endOfUpdateSegments, - context.testSegments); + context.testSegments + ); } } else if (context.testSegments) { finalizeTestSegmentsOfFor( context, choiceContext, - forkContext.head); + forkContext.head + ); } else { context.endOfInitSegments = forkContext.head; } diff --git a/tools/eslint/lib/code-path-analysis/code-path.js b/tools/eslint/lib/code-path-analysis/code-path.js index 6ef07b4a2d..709a111189 100644 --- a/tools/eslint/lib/code-path-analysis/code-path.js +++ b/tools/eslint/lib/code-path-analysis/code-path.js @@ -51,7 +51,8 @@ class CodePath { Object.defineProperty( this, "internal", - { value: new CodePathState(new IdGenerator(`${id}_`), onLooped) }); + { value: new CodePathState(new IdGenerator(`${id}_`), onLooped) } + ); // Adds this into `childCodePaths` of `upper`. if (upper) { @@ -205,7 +206,7 @@ class CodePath { // Call the callback when the first time. if (!skippedSegment) { - callback.call(this, segment, controller); // eslint-disable-line callback-return + callback.call(this, segment, controller); if (segment === lastSegment) { controller.skip(); } diff --git a/tools/eslint/lib/code-path-analysis/fork-context.js b/tools/eslint/lib/code-path-analysis/fork-context.js index 7423c13199..4fae6bbb1e 100644 --- a/tools/eslint/lib/code-path-analysis/fork-context.js +++ b/tools/eslint/lib/code-path-analysis/fork-context.js @@ -254,7 +254,8 @@ class ForkContext { return new ForkContext( parentContext.idGenerator, parentContext, - (forkLeavingPath ? 2 : 1) * parentContext.count); + (forkLeavingPath ? 2 : 1) * parentContext.count + ); } } diff --git a/tools/eslint/lib/config.js b/tools/eslint/lib/config.js index 2db87d9426..c20522f6b3 100644 --- a/tools/eslint/lib/config.js +++ b/tools/eslint/lib/config.js @@ -24,7 +24,7 @@ const debug = require("debug")("eslint:config"); // Constants //------------------------------------------------------------------------------ -const PERSONAL_CONFIG_DIR = os.homedir() || null; +const PERSONAL_CONFIG_DIR = os.homedir(); const SUBCONFIG_SEP = ":"; //------------------------------------------------------------------------------ @@ -148,15 +148,13 @@ class Config { getPersonalConfig() { if (typeof this.personalConfig === "undefined") { let config; + const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR); - if (PERSONAL_CONFIG_DIR) { - const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR); - - if (filename) { - debug("Using personal config"); - config = ConfigFile.load(filename, this); - } + if (filename) { + debug("Using personal config"); + config = ConfigFile.load(filename, this); } + this.personalConfig = config || null; } @@ -351,10 +349,8 @@ class Config { config = ConfigOps.merge(config, { parser: this.parser }); } - // Step 4: Apply environments to the config if present - if (config.env) { - config = ConfigOps.applyEnvironments(config, this.linterContext.environments); - } + // Step 4: Apply environments to the config + config = ConfigOps.applyEnvironments(config, this.linterContext.environments); this.configCache.setMergedConfig(vector, config); diff --git a/tools/eslint/lib/config/autoconfig.js b/tools/eslint/lib/config/autoconfig.js index 88204a7a45..6614a1bc48 100644 --- a/tools/eslint/lib/config/autoconfig.js +++ b/tools/eslint/lib/config/autoconfig.js @@ -269,10 +269,8 @@ class Registry { * @returns {Registry} New registry with errorCount populated */ lintSourceCode(sourceCodes, config, cb) { - let ruleSetIdx, - lintedRegistry; + let lintedRegistry = new Registry(); - lintedRegistry = new Registry(); lintedRegistry.rules = Object.assign({}, this.rules); const ruleSets = lintedRegistry.buildRuleSets(); @@ -287,7 +285,7 @@ class Registry { filenames.forEach(filename => { debug(`Linting file: ${filename}`); - ruleSetIdx = 0; + let ruleSetIdx = 0; ruleSets.forEach(ruleSet => { const lintConfig = Object.assign({}, config, { rules: ruleSet }); diff --git a/tools/eslint/lib/config/config-file.js b/tools/eslint/lib/config/config-file.js index 3c790cf3be..87412dd2a2 100644 --- a/tools/eslint/lib/config/config-file.js +++ b/tools/eslint/lib/config/config-file.js @@ -3,8 +3,6 @@ * @author Nicholas C. Zakas */ -/* eslint no-use-before-define: 0 */ - "use strict"; //------------------------------------------------------------------------------ @@ -418,6 +416,8 @@ function applyExtends(config, configContext, filePath, relativeTo) { ); } debug(`Loading ${parentPath}`); + + // eslint-disable-next-line no-use-before-define return ConfigOps.merge(load(parentPath, configContext, relativeTo), previousValue); } catch (e) { @@ -502,8 +502,8 @@ function resolve(filePath, relativeTo) { if (filePath.startsWith("plugin:")) { const configFullName = filePath; - const pluginName = filePath.substr(7, filePath.lastIndexOf("/") - 7); - const configName = filePath.substr(filePath.lastIndexOf("/") + 1, filePath.length - filePath.lastIndexOf("/") - 1); + const pluginName = filePath.slice(7, filePath.lastIndexOf("/")); + const configName = filePath.slice(filePath.lastIndexOf("/") + 1); normalizedPackageName = normalizePackageName(pluginName, "eslint-plugin"); debug(`Attempting to resolve ${normalizedPackageName}`); @@ -546,7 +546,7 @@ function loadFromDisk(resolvedPath, configContext) { } // validate the configuration before continuing - validator.validate(config, resolvedPath, configContext.linterContext.rules, configContext.linterContext.environments); + validator.validate(config, resolvedPath.configFullName, configContext.linterContext.rules, configContext.linterContext.environments); /* * If an `extends` property is defined, it represents a configuration file to use as diff --git a/tools/eslint/lib/config/config-initializer.js b/tools/eslint/lib/config/config-initializer.js index d344fa0ac7..47139e06e3 100644 --- a/tools/eslint/lib/config/config-initializer.js +++ b/tools/eslint/lib/config/config-initializer.js @@ -65,6 +65,7 @@ function writeFile(config, format) { * @param {string} moduleName The module name to get. * @returns {Object} The peer dependencies of the given module. * This object is the object of `peerDependencies` field of `package.json`. + * Returns null if npm was not found. */ function getPeerDependencies(moduleName) { let result = getPeerDependencies.cache.get(moduleName); @@ -356,7 +357,8 @@ function hasESLintVersionConflict(answers) { // Get the required range of ESLint version. const configName = getStyleGuideName(answers); const moduleName = `eslint-config-${configName}@latest`; - const requiredESLintVersionRange = getPeerDependencies(moduleName).eslint; + const peerDependencies = getPeerDependencies(moduleName) || {}; + const requiredESLintVersionRange = peerDependencies.eslint; if (!requiredESLintVersionRange) { return false; @@ -380,7 +382,6 @@ function hasESLintVersionConflict(answers) { * @returns {Promise} The promise with the result of the prompt */ function promptUser() { - let config; return inquirer.prompt([ { @@ -467,7 +468,8 @@ function promptUser() { earlyAnswers.styleguide = "airbnb-base"; } - config = getConfigForStyleGuide(earlyAnswers.styleguide, earlyAnswers.installESLint); + const config = getConfigForStyleGuide(earlyAnswers.styleguide, earlyAnswers.installESLint); + writeFile(config, earlyAnswers.format); return void 0; @@ -527,7 +529,8 @@ function promptUser() { if (earlyAnswers.source === "auto") { const combinedAnswers = Object.assign({}, earlyAnswers, secondAnswers); - config = processAnswers(combinedAnswers); + const config = processAnswers(combinedAnswers); + installModules(config); writeFile(config, earlyAnswers.format); @@ -573,7 +576,8 @@ function promptUser() { ]).then(answers => { const totalAnswers = Object.assign({}, earlyAnswers, secondAnswers, answers); - config = processAnswers(totalAnswers); + const config = processAnswers(totalAnswers); + installModules(config); writeFile(config, answers.format); }); diff --git a/tools/eslint/lib/config/config-ops.js b/tools/eslint/lib/config/config-ops.js index d169e60dcf..4ed5ec6b02 100644 --- a/tools/eslint/lib/config/config-ops.js +++ b/tools/eslint/lib/config/config-ops.js @@ -193,25 +193,25 @@ module.exports = { }, /** - * Converts new-style severity settings (off, warn, error) into old-style - * severity settings (0, 1, 2) for all rules. Assumption is that severity - * values have already been validated as correct. - * @param {Object} config The config object to normalize. - * @returns {void} + * Normalizes the severity value of a rule's configuration to a number + * @param {(number|string|[number, ...*]|[string, ...*])} ruleConfig A rule's configuration value, generally + * received from the user. A valid config value is either 0, 1, 2, the string "off" (treated the same as 0), + * the string "warn" (treated the same as 1), the string "error" (treated the same as 2), or an array + * whose first element is one of the above values. Strings are matched case-insensitively. + * @returns {(0|1|2)} The numeric severity value if the config value was valid, otherwise 0. */ - normalize(config) { + getRuleSeverity(ruleConfig) { + const severityValue = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig; - if (config.rules) { - Object.keys(config.rules).forEach(ruleId => { - const ruleConfig = config.rules[ruleId]; + if (severityValue === 0 || severityValue === 1 || severityValue === 2) { + return severityValue; + } - if (typeof ruleConfig === "string") { - config.rules[ruleId] = RULE_SEVERITY[ruleConfig.toLowerCase()] || 0; - } else if (Array.isArray(ruleConfig) && typeof ruleConfig[0] === "string") { - ruleConfig[0] = RULE_SEVERITY[ruleConfig[0].toLowerCase()] || 0; - } - }); + if (typeof severityValue === "string") { + return RULE_SEVERITY[severityValue.toLowerCase()] || 0; } + + return 0; }, /** diff --git a/tools/eslint/lib/config/config-validator.js b/tools/eslint/lib/config/config-validator.js index fb5a965641..22bc1efbd0 100644 --- a/tools/eslint/lib/config/config-validator.js +++ b/tools/eslint/lib/config/config-validator.js @@ -10,6 +10,7 @@ //------------------------------------------------------------------------------ const ajv = require("../util/ajv"), + lodash = require("lodash"), configSchema = require("../../conf/config-schema.js"), util = require("util"); @@ -179,6 +180,25 @@ function formatErrors(errors) { }).map(message => `\t- ${message}.\n`).join(""); } +/** + * Emits a deprecation warning containing a given filepath. A new deprecation warning is emitted + * for each unique file path, but repeated invocations with the same file path have no effect. + * No warnings are emitted if the `--no-deprecation` or `--no-warnings` Node runtime flags are active. + * @param {string} source The name of the configuration source to report the warning for. + * @returns {void} + */ +const emitEcmaFeaturesWarning = lodash.memoize(source => { + + /* + * util.deprecate seems to be the only way to emit a warning in Node 4.x while respecting the --no-warnings flag. + * (In Node 6+, process.emitWarning could be used instead.) + */ + util.deprecate( + () => {}, + `[eslint] The 'ecmaFeatures' config file property is deprecated, and has no effect. (found in ${source})` + )(); +}); + /** * Validates the top level properties of the config object. * @param {Object} config The config object to validate. @@ -189,7 +209,11 @@ function validateConfigSchema(config, source) { validateSchema = validateSchema || ajv.compile(configSchema); if (!validateSchema(config)) { - throw new Error(`${source}:\n\tESLint configuration is invalid:\n${formatErrors(validateSchema.errors)}`); + throw new Error(`ESLint configuration in ${source} is invalid:\n${formatErrors(validateSchema.errors)}`); + } + + if (Object.prototype.hasOwnProperty.call(config, "ecmaFeatures")) { + emitEcmaFeaturesWarning(source); } } diff --git a/tools/eslint/lib/config/plugins.js b/tools/eslint/lib/config/plugins.js index adfd8a1bbe..9884f36039 100644 --- a/tools/eslint/lib/config/plugins.js +++ b/tools/eslint/lib/config/plugins.js @@ -43,7 +43,7 @@ class Plugins { * @returns {string} The name of the plugin without prefix. */ static removePrefix(pluginName) { - return pluginName.startsWith(PLUGIN_NAME_PREFIX) ? pluginName.substring(PLUGIN_NAME_PREFIX.length) : pluginName; + return pluginName.startsWith(PLUGIN_NAME_PREFIX) ? pluginName.slice(PLUGIN_NAME_PREFIX.length) : pluginName; } /** diff --git a/tools/eslint/lib/formatters/codeframe.js b/tools/eslint/lib/formatters/codeframe.js index ed50ae608d..0b97a0d818 100644 --- a/tools/eslint/lib/formatters/codeframe.js +++ b/tools/eslint/lib/formatters/codeframe.js @@ -47,7 +47,7 @@ function formatFilePath(filePath, line, column) { */ function formatMessage(message, parentResult) { const type = (message.fatal || message.severity === 2) ? chalk.red("error") : chalk.yellow("warning"); - const msg = `${chalk.bold(message.message.replace(/\.$/, ""))}`; + const msg = `${chalk.bold(message.message.replace(/([^ ])\.$/, "$1"))}`; const ruleId = message.fatal ? "" : chalk.dim(`(${message.ruleId})`); const filePath = formatFilePath(parentResult.filePath, message.line, message.column); const sourceCode = parentResult.output ? parentResult.output : parentResult.source; diff --git a/tools/eslint/lib/formatters/html-template-message.html b/tools/eslint/lib/formatters/html-template-message.html index 0683172748..66f49ff49d 100644 --- a/tools/eslint/lib/formatters/html-template-message.html +++ b/tools/eslint/lib/formatters/html-template-message.html @@ -3,6 +3,6 @@ <%= severityName %> <%- message %> - <%= ruleId %> + <%= ruleId %> diff --git a/tools/eslint/lib/formatters/html-template-page.html b/tools/eslint/lib/formatters/html-template-page.html index 39e15562e9..4016576fa0 100644 --- a/tools/eslint/lib/formatters/html-template-page.html +++ b/tools/eslint/lib/formatters/html-template-page.html @@ -1,5 +1,7 @@ + + ESLint Report