Repository: eslint/eslint
Pull request #8340 (2017-03-26 21:38:19)

Fix: no-implicit-coercion invalid autofix with consecutive identifiers
**What is the purpose of this pull request? (put an "X" next to item)**

[x] Bug fix

**Tell us about your environment**

* **ESLint Version:** master
* **Node Version:** 7.7.4
* **npm Version:** 4.1.2

**What parser (default, Babel-ESLint, etc.) are you using?**

default

**Please show your full configuration:**

```yml
rules:
  no-implicit-coercion: error
```

**What did you do? Please include the actual source code causing the issue.**

```js
typeof+foo
```

**What did you expect to happen?**

I expected the code to get autofixed to

```js
typeof Number(foo)
```

**What actually happened? Please include the actual, raw output from ESLint.**

The code was autofixed to

```js
typeofNumber(foo)
```

<!--
    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:
-->

**What changes did you make? (Give an overview)**

Previously, the no-implicit-coercion rule would convert code like `typeof+foo` to `typeofNumber(foo)`, combining the `typeof` keyword with the following identifier and changing the semantics. This commit updates the no-implicit-coercion autofixer to prevent that from happening.

**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 @TheSavior, @mysticatea and @kaicataldo to be potential reviewers.
eslintbot: LGTM
ilyavolodin: Is `typeof+foo` even a valid syntax? What is supposed to do?
platinumazure: @ilyavolodin It's parsed as `typeof +foo`. Just two unary operators applied in succession.

I would hope nobody writes code like that, but we should be able to accept all sorts of crazy syntax while linting.

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/eslint.js b/lib/eslint.js
index 3c23a7495..a9066c4c8 100755
--- a/lib/eslint.js
+++ b/lib/eslint.js
@@ -778,17 +778,18 @@ module.exports = (function() {
         // search and apply "eslint-env *".
         const envInFile = findEslintEnv(text || textOrSourceCode.text);
 
+        config = Object.assign({}, config);
+
         if (envInFile) {
-            if (!config || !config.env) {
-                config = Object.assign({}, config || {}, { env: envInFile });
-            } else {
-                config = Object.assign({}, config);
+            if (config.env) {
                 config.env = Object.assign({}, config.env, envInFile);
+            } else {
+                config.env = envInFile;
             }
         }
 
         // process initial config to make it safe to extend
-        config = prepareConfig(config || {});
+        config = prepareConfig(config);
 
         // only do this for text
         if (text !== null) {
diff --git a/lib/rules/comma-style.js b/lib/rules/comma-style.js
index 4892ae505..fcaecc662 100644
--- a/lib/rules/comma-style.js
+++ b/lib/rules/comma-style.js
@@ -205,7 +205,11 @@ module.exports = {
                                 currentItemToken, reportItem);
                     }
 
-                    previousItemToken = item ? sourceCode.getLastToken(item) : previousItemToken;
+                    if (item) {
+                        const tokenAfterItem = sourceCode.getTokenAfter(item, astUtils.isNotClosingParenToken);
+
+                        previousItemToken = tokenAfterItem ? sourceCode.getTokenBefore(tokenAfterItem) : sourceCode.ast.tokens[sourceCode.ast.tokens.length - 1];
+                    }
                 });
 
                 /*
diff --git a/lib/rules/dot-notation.js b/lib/rules/dot-notation.js
index d55b098b2..abb9b4b48 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,20 +70,20 @@ 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;
                                 }
 
+                                const textBeforeDot = astUtils.isDecimalInteger(node.object) ? " " : "";
+
                                 return fixer.replaceTextRange(
                                     [leftBracket.range[0], rightBracket.range[1]],
-                                    `.${node.property.value}`
+                                    `${textBeforeDot}.${node.property.value}`
                                 );
                             }
                         });
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..42a86484b 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);
         }
@@ -345,6 +345,11 @@ module.exports = {
         function report(node) {
             const leftParenToken = sourceCode.getTokenBefore(node);
             const rightParenToken = sourceCode.getTokenAfter(node);
+            const tokenBeforeLeftParen = sourceCode.getTokenBefore(leftParenToken);
+
+            if (tokenBeforeLeftParen && astUtils.isArrowToken(tokenBeforeLeftParen) && astUtils.isOpeningBraceToken(sourceCode.getFirstToken(node))) {
+                return;
+            }
 
             context.report({
                 node,
@@ -481,7 +486,7 @@ module.exports = {
                 }
 
                 if (node.body.type !== "BlockStatement") {
-                    if (sourceCode.getFirstToken(node.body).value !== "{" && hasExcessParens(node.body) && precedence(node.body) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
+                    if (hasExcessParens(node.body) && precedence(node.body) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
                         report(node.body);
                         return;
                     }
diff --git a/lib/rules/no-implicit-coercion.js b/lib/rules/no-implicit-coercion.js
index 8e8db9879..387b3dae4 100644
--- a/lib/rules/no-implicit-coercion.js
+++ b/lib/rules/no-implicit-coercion.js
@@ -6,6 +6,7 @@
 "use strict";
 
 const astUtils = require("../ast-utils");
+const esUtils = require("esutils");
 
 //------------------------------------------------------------------------------
 // Helpers
@@ -197,19 +198,31 @@ module.exports = {
         */
         function report(node, recommendation, shouldFix) {
             shouldFix = typeof shouldFix === "undefined" ? true : shouldFix;
-            const reportObj = {
+
+            context.report({
                 node,
                 message: "use `{{recommendation}}` instead.",
                 data: {
                     recommendation
+                },
+                fix(fixer) {
+                    if (!shouldFix) {
+                        return null;
+                    }
+
+                    const tokenBefore = sourceCode.getTokenBefore(node);
+
+                    if (
+                        tokenBefore &&
+                        tokenBefore.range[1] === node.range[0] &&
+                        esUtils.code.isIdentifierPartES6(tokenBefore.value.slice(-1).charCodeAt(0)) &&
+                        esUtils.code.isIdentifierPartES6(recommendation.charCodeAt(0))
+                    ) {
+                        return fixer.replaceText(node, ` ${recommendation}`);
+                    }
+                    return fixer.replaceText(node, recommendation);
                 }
-            };
-
-            if (shouldFix) {
-                reportObj.fix = fixer => fixer.replaceText(node, recommendation);
-            }
-
-            context.report(reportObj);
+            });
         }
 
         return {
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/eslint.js b/tests/lib/eslint.js
index 406302a69..9a1456cb1 100644
--- a/tests/lib/eslint.js
+++ b/tests/lib/eslint.js
@@ -3425,6 +3425,16 @@ describe("eslint", () => {
 
             assert(result.length === 0);
         });
+
+
+        it("should not modify config object passed as argument", () => {
+            const config = {};
+
+            Object.freeze(config);
+            assert.doesNotThrow(() => {
+                eslint.verify("var foo", config);
+            });
+        });
     });
 
     describe("Variables and references", () => {
diff --git a/tests/lib/rules/comma-style.js b/tests/lib/rules/comma-style.js
index 8dcbbc883..98c765c6b 100644
--- a/tests/lib/rules/comma-style.js
+++ b/tests/lib/rules/comma-style.js
@@ -569,6 +569,11 @@ ruleTester.run("comma-style", rule, {
                 message: BAD_LN_BRK_MSG,
                 type: "Identifier"
             }]
+        },
+        {
+            code: "[(foo),\n,\nbar]",
+            output: "[(foo),,\nbar]",
+            errors: [{ message: BAD_LN_BRK_MSG }]
         }
     ]
 });
diff --git a/tests/lib/rules/dot-notation.js b/tests/lib/rules/dot-notation.js
index 83b212efd..0c28c4393 100644
--- a/tests/lib/rules/dot-notation.js
+++ b/tests/lib/rules/dot-notation.js
@@ -153,6 +153,26 @@ 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." }]
+        },
+        {
+            code: "1['toString']",
+            output: "1 .toString",
+            errors: [{ message: "[\"toString\"] 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..967d2df05 100644
--- a/tests/lib/rules/no-extra-parens.js
+++ b/tests/lib/rules/no-extra-parens.js
@@ -410,6 +410,22 @@ ruleTester.run("no-extra-parens", rule, {
         {
             code: "const A = class extends (B=C) {}",
             parserOptions: { ecmaVersion: 2015 }
+        },
+        {
+            code: "() => ({ foo: 1 })",
+            parserOptions: { ecmaVersion: 2015 }
+        },
+        {
+            code: "() => ({ foo: 1 }).foo",
+            parserOptions: { ecmaVersion: 2015 }
+        },
+        {
+            code: "() => ({ foo: 1 }.foo().bar).baz.qux()",
+            parserOptions: { ecmaVersion: 2015 }
+        },
+        {
+            code: "() => ({ foo: 1 }.foo().bar + baz)",
+            parserOptions: { ecmaVersion: 2015 }
         }
     ],
 
@@ -952,6 +968,25 @@ 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 } }
+        ),
+        {
+            code: "() => (({ foo: 1 }).foo)",
+            output: "() => ({ foo: 1 }).foo",
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [
+
+                // 2 errors are reported, but fixing one gets rid of the other
+                { message: "Gratuitous parentheses around expression.", type: "MemberExpression" },
+                { message: "Gratuitous parentheses around expression.", type: "ObjectExpression" }
+            ]
+
+        }
     ]
 });
diff --git a/tests/lib/rules/no-implicit-coercion.js b/tests/lib/rules/no-implicit-coercion.js
index 3b1f8e89c..5ea5f5fa4 100644
--- a/tests/lib/rules/no-implicit-coercion.js
+++ b/tests/lib/rules/no-implicit-coercion.js
@@ -221,6 +221,16 @@ ruleTester.run("no-implicit-coercion", rule, {
             parserOptions: { ecmaVersion: 6 },
             errors: [{ message: "use `String(foo)` instead.", type: "BinaryExpression" }],
             output: "var a = String(foo)"
+        },
+        {
+            code: "typeof+foo",
+            output: "typeof Number(foo)",
+            errors: [{ message: "use `Number(foo)` instead.", type: "UnaryExpression" }]
+        },
+        {
+            code: "typeof +foo",
+            output: "typeof Number(foo)",
+            errors: [{ message: "use `Number(foo)` instead.", type: "UnaryExpression" }]
         }
     ]
 });
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
         {