Repository: eslint/eslint
Pull request #8326 (2017-03-26 03:52:24)

Fix: don't generate invalid options in config-rule
**What is the purpose of this pull request? (put an "X" next to item)**

[x] Bug fix

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (http://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

**Tell us about your environment**

* **ESLint Version:** master
* **Node Version:** 7.7.4
* **npm Version:** 4.1.2

**What did you do? Please include the actual source code causing the issue.**

```js
const ruleConfigs = require("eslint/lib/config/config-rule").createCoreRuleConfigs();
```

**What did you expect to happen?**

I expected all the configs in `ruleConfigs` to be valid.

**What actually happened? Please include the actual, raw output from ESLint.**

Some of the configs were invalid. For example:

```js
console.log(ruleConfigs["id-match"]);
// => [ 2, [ 2, { properties: true } ], [ 2, { properties: false } ] ]
```

**What changes did you make? (Give an overview)**

Previously, the `config-rule` logic for generating rule options had a bug where it could generate invalid options. For example, it could generate these options for the `id-match` rule:

```json
[2, { "properties": true }]
```

This is invalid because the first option in `id-match` must be a string, and the second option must be an object. The config generator doesn't handle strings, but it was incorrectly skipping the string option and trying to fill in the config with an object after realizing that it couldn't generate a string. As a result, it placed the object where the string was supposed to go, so an invalid set of options was created. A similar issue can appear when generating configs for the `max-statements` rule.

Luckily, the `id-match` and `max-statements` rule are still able to run when passed an invalid config, and the invalid config never results in fewer errors being reported than the default, so the invalid options wouldn't ever get output to a config file. However, this is only the case due to implementation details in the two rules, and a new rule could be introduced in the future that crashes when given an invalid schema, so this is still worth fixing.

This commit fixes the issue by stopping early when the option generator reaches an option that it doesn't know how to fill in.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular


mention-bot: @not-an-aardvark, thanks for your PR! By analyzing the history of the files in this pull request, we identified @IanVS, @mysticatea and @gyandeeps to be potential reviewers.
eslintbot: LGTM
eslintbot: LGTM
eslintbot: LGTM

Patch:
diff --git a/lib/config/config-rule.js b/lib/config/config-rule.js
index bd7aa13b9..a8a073caa 100644
--- a/lib/config/config-rule.js
+++ b/lib/config/config-rule.js
@@ -223,7 +223,7 @@ class RuleConfigSet {
     /**
     * Add rule configurations from a schema object
     * @param  {Object} obj Schema item with type === "object"
-    * @returns {void}
+    * @returns {boolean} true if at least one schema for the object could be generated, false otherwise
     */
     addObject(obj) {
         const objectConfigSet = {
@@ -258,7 +258,10 @@ class RuleConfigSet {
 
         if (objectConfigSet.objectConfigs.length > 0) {
             this.ruleConfigs = this.ruleConfigs.concat(combineArrays(this.ruleConfigs, objectConfigSet.objectConfigs));
+            return true;
         }
+
+        return false;
     }
 }
 
@@ -271,20 +274,21 @@ function generateConfigsFromSchema(schema) {
     const configSet = new RuleConfigSet();
 
     if (Array.isArray(schema)) {
-        schema.forEach(opt => {
+        for (const opt of schema) {
             if (opt.enum) {
                 configSet.addEnums(opt.enum);
-            }
-
-            if (opt.type && opt.type === "object") {
-                configSet.addObject(opt);
-            }
+            } else if (opt.type && opt.type === "object") {
+                if (!configSet.addObject(opt)) {
+                    break;
+                }
 
-            if (opt.oneOf) {
+            // TODO (IanVS): support oneOf
+            } else {
 
-                // TODO (IanVS): not yet implemented
+                // If we don't know how to fill in this option, don't fill in any of the following options.
+                break;
             }
-        });
+        }
     }
     configSet.addErrorSeverity();
     return configSet.ruleConfigs;
diff --git a/lib/rules/dot-notation.js b/lib/rules/dot-notation.js
index d55b098b2..3f5eae0bb 100644
--- a/lib/rules/dot-notation.js
+++ b/lib/rules/dot-notation.js
@@ -4,6 +4,12 @@
  */
 "use strict";
 
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const astUtils = require("../ast-utils");
+
 //------------------------------------------------------------------------------
 // Rule Definition
 //------------------------------------------------------------------------------
@@ -64,12 +70,10 @@ module.exports = {
                                 propertyValue: JSON.stringify(node.property.value)
                             },
                             fix(fixer) {
-                                const leftBracket = sourceCode.getTokenBefore(node.property);
-                                const rightBracket = sourceCode.getTokenAfter(node.property);
-                                const textBeforeProperty = sourceCode.text.slice(leftBracket.range[1], node.property.range[0]);
-                                const textAfterProperty = sourceCode.text.slice(node.property.range[1], rightBracket.range[0]);
+                                const leftBracket = sourceCode.getTokenAfter(node.object, astUtils.isOpeningBracketToken);
+                                const rightBracket = sourceCode.getLastToken(node);
 
-                                if (textBeforeProperty.trim() || textAfterProperty.trim()) {
+                                if (sourceCode.getFirstTokenBetween(leftBracket, rightBracket, { includeComments: true, filter: astUtils.isCommentToken })) {
 
                                     // Don't perform any fixes if there are comments inside the brackets.
                                     return null;
diff --git a/lib/rules/no-else-return.js b/lib/rules/no-else-return.js
index 6ba0b2426..68ab4c760 100644
--- a/lib/rules/no-else-return.js
+++ b/lib/rules/no-else-return.js
@@ -9,6 +9,7 @@
 // Requirements
 //------------------------------------------------------------------------------
 
+const astUtils = require("../ast-utils");
 const FixTracker = require("../util/fix-tracker");
 
 //------------------------------------------------------------------------------
@@ -199,9 +200,11 @@ module.exports = {
             let consequents,
                 alternate;
 
-            // Only "top-level" if statements are checked, meaning the first `if`
-            // in a `if-else-if-...` chain.
-            if (parent.type === "IfStatement" && parent.alternate === node) {
+            /*
+             * Fixing this would require splitting one statement into two, so no error should
+             * be reported if this node is in a position where only one statement is allowed.
+             */
+            if (!astUtils.STATEMENT_LIST_PARENTS.has(parent.type)) {
                 return;
             }
 
diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js
index eed0d401d..42c2bda99 100644
--- a/lib/rules/no-extra-parens.js
+++ b/lib/rules/no-extra-parens.js
@@ -318,7 +318,7 @@ module.exports = {
             }
 
             // If the parens are preceded by a keyword (e.g. `typeof(0)`), a space should be inserted (`typeof 0`)
-            const precededByKeyword = tokenBeforeLeftParen.type === "Keyword";
+            const precededByIdentiferPart = esUtils.code.isIdentifierPartES6(tokenBeforeLeftParen.value.slice(-1).charCodeAt(0));
 
             // However, a space should not be inserted unless the first character of the token is an identifier part
             // e.g. `typeof([])` should be fixed to `typeof[]`
@@ -331,7 +331,7 @@ module.exports = {
             const startsWithUnaryPlus = firstToken.type === "Punctuator" && firstToken.value === "+";
             const startsWithUnaryMinus = firstToken.type === "Punctuator" && firstToken.value === "-";
 
-            return (precededByKeyword && startsWithIdentifierPart) ||
+            return (precededByIdentiferPart && startsWithIdentifierPart) ||
                 (precededByUnaryPlus && startsWithUnaryPlus) ||
                 (precededByUnaryMinus && startsWithUnaryMinus);
         }
diff --git a/lib/rules/object-shorthand.js b/lib/rules/object-shorthand.js
index 1d32d1722..1d3d9dae1 100644
--- a/lib/rules/object-shorthand.js
+++ b/lib/rules/object-shorthand.js
@@ -230,7 +230,10 @@ module.exports = {
                 const functionToken = sourceCode.getTokens(node.value).find(token => token.type === "Keyword" && token.value === "function");
                 const tokenBeforeParams = node.value.generator ? sourceCode.getTokenAfter(functionToken) : functionToken;
 
-                return fixer.replaceTextRange([firstKeyToken.range[0], tokenBeforeParams.range[1]], keyPrefix + keyText);
+                return fixer.replaceTextRange(
+                    [firstKeyToken.range[0], node.range[1]],
+                    keyPrefix + keyText + sourceCode.text.slice(tokenBeforeParams.range[1], node.value.range[1])
+                );
             }
             const arrowToken = sourceCode.getTokens(node.value).find(token => token.value === "=>");
             const tokenBeforeArrow = sourceCode.getTokenBefore(arrowToken);
@@ -238,7 +241,10 @@ module.exports = {
             const oldParamText = sourceCode.text.slice(sourceCode.getFirstToken(node.value, node.value.async ? 1 : 0).range[0], tokenBeforeArrow.range[1]);
             const newParamText = hasParensAroundParameters ? oldParamText : `(${oldParamText})`;
 
-            return fixer.replaceTextRange([firstKeyToken.range[0], arrowToken.range[1]], keyPrefix + keyText + newParamText);
+            return fixer.replaceTextRange(
+                [firstKeyToken.range[0], node.range[1]],
+                keyPrefix + keyText + newParamText + sourceCode.text.slice(arrowToken.range[1], node.value.range[1])
+            );
 
         }
 
diff --git a/lib/rules/quotes.js b/lib/rules/quotes.js
index 1085ea33b..b1117b85f 100644
--- a/lib/rules/quotes.js
+++ b/lib/rules/quotes.js
@@ -209,6 +209,7 @@ module.exports = {
 
                 // LiteralPropertyName.
                 case "Property":
+                case "MethodDefinition":
                     return parent.key === node && !parent.computed;
 
                 // ModuleSpecifier.


Test patch:
diff --git a/tests/fixtures/config-rule/schemas.js b/tests/fixtures/config-rule/schemas.js
index 7eb77fdae..d74bcce1d 100644
--- a/tests/fixtures/config-rule/schemas.js
+++ b/tests/fixtures/config-rule/schemas.js
@@ -69,6 +69,22 @@ module.exports = {
         "additionalProperties": false
     }],
 
+    mixedObjectWithNothingEnum: [{
+        "type": "object",
+        "properties": {},
+        "additionalProperties": false
+    },
+    {
+        "enum": ["always", "never"]
+    }],
+
+    mixedStringEnum: [{
+        "type": "string"
+    },
+    {
+        "enum": ["always", "never"]
+    }],
+
     oneOf: [{
         "oneOf": [
             {
diff --git a/tests/lib/config/config-rule.js b/tests/lib/config/config-rule.js
index a801e91c8..f41e3bfc6 100644
--- a/tests/lib/config/config-rule.js
+++ b/tests/lib/config/config-rule.js
@@ -230,7 +230,7 @@ describe("ConfigRule", () => {
             });
         });
 
-        describe("for a schema with an enum and an object with no usable properties", () => {
+        describe("for a schema with an enum followed by an object with no usable properties", () => {
             before(() => {
                 actualConfigs = ConfigRule.generateConfigsFromSchema(schema.mixedEnumObjectWithNothing);
             });
@@ -242,6 +242,30 @@ describe("ConfigRule", () => {
             });
         });
 
+        describe("for a schema with an enum preceded by an object with no usable properties", () => {
+            before(() => {
+                actualConfigs = ConfigRule.generateConfigsFromSchema(schema.mixedObjectWithNothingEnum);
+            });
+
+            it("should not create a config for the enum", () => {
+                const expectedConfigs = [2];
+
+                assert.sameDeepMembers(actualConfigs, expectedConfigs);
+            });
+        });
+
+        describe("for a schema with an enum preceded by a string", () => {
+            before(() => {
+                actualConfigs = ConfigRule.generateConfigsFromSchema(schema.mixedStringEnum);
+            });
+
+            it("should not create a config for the enum", () => {
+                const expectedConfigs = [2];
+
+                assert.sameDeepMembers(actualConfigs, expectedConfigs);
+            });
+        });
+
         describe("for a schema with oneOf", () => {
 
             before(() => {
diff --git a/tests/lib/rules/dot-notation.js b/tests/lib/rules/dot-notation.js
index 83b212efd..e228899e1 100644
--- a/tests/lib/rules/dot-notation.js
+++ b/tests/lib/rules/dot-notation.js
@@ -153,6 +153,21 @@ ruleTester.run("dot-notation", rule, {
             options: [{ allowKeywords: false }],
             errors: [{ message: ".while is a syntax error." }],
             output: null // Not fixed due to comment
+        },
+        {
+            code: "foo[('bar')]",
+            output: "foo.bar",
+            errors: [{ message: "[\"bar\"] is better written in dot notation." }]
+        },
+        {
+            code: "foo[(null)]",
+            output: "foo.null",
+            errors: [{ message: "[null] is better written in dot notation." }]
+        },
+        {
+            code: "(foo)['bar']",
+            output: "(foo).bar",
+            errors: [{ message: "[\"bar\"] is better written in dot notation." }]
         }
     ]
 });
diff --git a/tests/lib/rules/no-else-return.js b/tests/lib/rules/no-else-return.js
index e99b83d6c..6aea5c867 100644
--- a/tests/lib/rules/no-else-return.js
+++ b/tests/lib/rules/no-else-return.js
@@ -27,7 +27,22 @@ ruleTester.run("no-else-return", rule, {
         "function foo() { if (true) notAReturn(); else return y; }",
         "function foo() {if (x) { notAReturn(); } else if (y) { return true; } else { notAReturn(); } }",
         "function foo() {if (x) { return true; } else if (y) { notAReturn() } else { notAReturn(); } }",
-        "if (0) { if (0) {} else {} } else {}"
+        "if (0) { if (0) {} else {} } else {}",
+        `
+            function foo() {
+                if (foo)
+                    if (bar) return;
+                    else baz;
+                else qux;
+            }
+        `,
+        `
+            function foo() {
+                while (foo)
+                    if (bar) return;
+                    else baz;
+            }
+        `
     ],
     invalid: [
         {
diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js
index 349c14994..9b12ccf81 100644
--- a/tests/lib/rules/no-extra-parens.js
+++ b/tests/lib/rules/no-extra-parens.js
@@ -952,6 +952,13 @@ ruleTester.run("no-extra-parens", rule, {
             "AssignmentExpression",
             1,
             { parserOptions: { ecmaVersion: 2015 } }
+        ),
+        invalid(
+            "for (foo of(bar));",
+            "for (foo of bar);",
+            "Identifier",
+            1,
+            { parserOptions: { ecmaVersion: 2015 } }
         )
     ]
 });
diff --git a/tests/lib/rules/object-shorthand.js b/tests/lib/rules/object-shorthand.js
index 8ef707d52..d961849ae 100644
--- a/tests/lib/rules/object-shorthand.js
+++ b/tests/lib/rules/object-shorthand.js
@@ -973,6 +973,17 @@ ruleTester.run("object-shorthand", rule, {
             `,
             options: ["always", { avoidExplicitReturnArrows: true }],
             errors: [METHOD_ERROR]
+        },
+        {
+            code: "({ a: (function(){ return foo; }) })",
+            output: "({ a(){ return foo; } })",
+            errors: [METHOD_ERROR]
+        },
+        {
+            code: "({ a: (() => { return foo; }) })",
+            output: "({ a() { return foo; } })",
+            options: ["always", { avoidExplicitReturnArrows: true }],
+            errors: [METHOD_ERROR]
         }
     ]
 });
diff --git a/tests/lib/rules/quotes.js b/tests/lib/rules/quotes.js
index 98ccd043e..3201e594c 100644
--- a/tests/lib/rules/quotes.js
+++ b/tests/lib/rules/quotes.js
@@ -68,8 +68,10 @@ ruleTester.run("quotes", rule, {
         { code: "import a from \"a\"; import b from 'b';", options: ["backtick"], parserOptions: { sourceType: "module" } },
         { code: "export * from \"a\"; export * from 'b';", options: ["backtick"], parserOptions: { sourceType: "module" } },
 
-        // `backtick` should not warn property names (not computed).
-        { code: "var obj = {\"key0\": 0, 'key1': 1};", options: ["backtick"], parserOptions: { ecmaVersion: 6 } }
+        // `backtick` should not warn property/method names (not computed).
+        { code: "var obj = {\"key0\": 0, 'key1': 1};", options: ["backtick"], parserOptions: { ecmaVersion: 6 } },
+        { code: "class Foo { 'bar'(){} }", options: ["backtick"], parserOptions: { ecmaVersion: 6 } },
+        { code: "class Foo { static ''(){} }", options: ["backtick"], parserOptions: { ecmaVersion: 6 } }
     ],
     invalid: [
         {
@@ -205,7 +207,7 @@ ruleTester.run("quotes", rule, {
             errors: [{ message: "Strings must use backtick.", type: "Literal" }]
         },
 
-        // `backtick` should not warn computed property names.
+        // `backtick` should warn computed property names.
         {
             code: "var obj = {[\"key0\"]: 0, ['key1']: 1};",
             output: "var obj = {[`key0`]: 0, [`key1`]: 1};",
@@ -216,6 +218,16 @@ ruleTester.run("quotes", rule, {
                 { message: "Strings must use backtick.", type: "Literal" }
             ]
         },
+        {
+            code: "class Foo { ['a'](){} static ['b'](){} }",
+            output: "class Foo { [`a`](){} static [`b`](){} }",
+            options: ["backtick"],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                { message: "Strings must use backtick.", type: "Literal" },
+                { message: "Strings must use backtick.", type: "Literal" }
+            ]
+        },
 
         // https://github.com/eslint/eslint/issues/7084
         {