Repository: eslint/eslint (js)

Issue:
Rule ´prefer-spread´ breaks ´Array.apply´ code
**Tell us about your environment**

* **ESLint Version:** 5.12.1
* **Node Version:** 10.15.0
* **npm Version:** 6.5.0

**What parser (default, Babel-ESLint, etc.) are you using?** `babel-eslint` v. 10.0.1

**Please show your full configuration:**

<details>
<summary>Configuration</summary>

<!-- Paste your configuration below: -->
# `package.json`
```json
{
  "devDependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.2.2",
    "@babel/plugin-proposal-class-properties": "^7.2.3",
    "@babel/plugin-proposal-decorators": "^7.2.3",
    "@babel/plugin-proposal-do-expressions": "^7.2.0",
    "@babel/plugin-proposal-export-default-from": "^7.2.0",
    "@babel/plugin-proposal-export-namespace-from": "^7.2.0",
    "@babel/plugin-proposal-function-bind": "^7.2.0",
    "@babel/plugin-proposal-function-sent": "^7.2.0",
    "@babel/plugin-proposal-json-strings": "^7.2.0",
    "@babel/plugin-proposal-logical-assignment-operators": "^7.2.0",
    "@babel/plugin-proposal-nullish-coalescing-operator": "^7.2.0",
    "@babel/plugin-proposal-numeric-separator": "^7.2.0",
    "@babel/plugin-proposal-optional-chaining": "^7.2.0",
    "@babel/plugin-proposal-pipeline-operator": "^7.2.0",
    "@babel/plugin-proposal-throw-expressions": "^7.2.0",
    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
    "@babel/plugin-syntax-import-meta": "^7.2.0",
    "@babel/polyfill": "^7.2.5",
    "@babel/preset-env": "^7.2.3",
    "@babel/preset-react": "^7.0.0",
    "@babel/runtime": "^7.2.0",
    "babel-eslint": "^10.0.1",
    "babel-loader": "^8.0.0",
    "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
    "eslint": "^5.11.1",
    "eslint-config-airbnb": "^17.1.0",
    "eslint-plugin-import": "^2.14.0",
    "eslint-plugin-jsx-a11y": "^6.1.2",
    "eslint-plugin-react": "^7.11.1",
    "file-loader": "^2.0.0",
    "html-webpack-plugin": "^3.2.0",
    "json-loader": "^0.5.7",
    "node-sass": "^4.11.0",
    "postcss-loader": "^3.0.0",
    "sass-loader": "^7.1.0",
    "svg-url-loader": "^2.3.2",
    "uglifyjs-webpack-plugin": "^2.1.0",
    "webpack": "^4.28.2",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.14"
  }
}
```

# `.eslintrc`
```json
{
  "parser": "babel-eslint",
  "extends": "airbnb",
  "rules": {
    "max-len": ["error", 180],
    "semi": [2, "always"],
    "quotes": [2, "single"],
    "indent": [1, "tab", {
      "SwitchCase": 1
    }],
    "react/jsx-indent": [1, "tab"],
    "react/jsx-one-expression-per-line": 0,
    "no-tabs": 0,
    "react/react-in-jsx-scope": 0,
    "jsx-quotes": [2, "prefer-single"],
    "react/jsx-curly-spacing": ["error", "always"],
    "react/jsx-indent-props": [2, "tab"],
    "comma-dangle": [1, "always-multiline"],
    "no-console": 0,
    "no-underscore-dangle": 0,
    "react/prefer-stateless-function": 0,
    "padded-blocks": 0,
    "no-script-url": 0,
    "react/forbid-prop-types": 0,
    "react/require-default-props": 0,
    "arrow-parens": [2, "as-needed"],
    "no-return-assign": 0,
    "react/sort-comp": 0,
    "linebreak-style": 0,
    "template-curly-spacing": ["error", "always"],
    "no-trailing-spaces": ["error", {
      "skipBlankLines": true
    }],
    "no-confusing-arrow": 0,
    "jsx-a11y/no-static-element-interactions": 0,
    "import/prefer-default-export": 0,
    "no-nested-ternary": 0,
    "jsx-a11y/anchor-is-valid": 0,
    "object-curly-newline": 0,
    "space-before-function-paren": [1, "always"],
    "react/jsx-tag-spacing": [0, "never"]
  },
  "globals": {
    "_": true,
    "log": true
  },
  "env": {
    "browser": true,
    "es6": true,
    "node": true,
    "jest": true
  },
  "settings": {
    "import/resolver": {
      "node": {
        "moduleDirectory": [
          "node_modules",
          "src"
        ]
      }
    }
  }
}

```


</details>

**What did you do? Please include the actual source code causing the issue, as well as the command that you used to run ESLint.**

<!-- Paste the source code below: -->
```js
// `index.js`
Array.apply(null, { length: 10 }).map((e, i) => ++i); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
```

<!-- Paste the command you used to run ESLint: -->
```bash
eslint --fix index.js
```

**What did you expect to happen?**
No changes

**What actually happened? Please include the actual, raw output from ESLint.**
```js
// `index.js` becomes
Array(...{ length: 10 }).map((e, i) => ++i); // TypeError: ({length:10}) is not iterable
```

**Are you willing to submit a pull request to fix this bug?**
Maybe.

Pull request:
Update: remove prefer-spread autofix (fixes #11330)
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

**What is the purpose of this pull request? (put an "X" next to item)**

[ ] Documentation update
[ ] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md))
[ x] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-change-proposal.md))
[ ] Add autofixing to a rule
[ ] Add a CLI option
[ ] Add something to the core
[ ] Other, please explain:

<!--
    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 (https://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)**
fixes #11330 

**Is there anything you'd like reviewers to focus on?**

no.


Patch:
diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md
index 1eed790a2..6fa35ce2e 100644
--- a/docs/user-guide/configuring.md
+++ b/docs/user-guide/configuring.md
@@ -23,7 +23,7 @@ Please note that supporting JSX syntax is not the same as supporting React. Reac
 By the same token, supporting ES6 syntax is not the same as supporting new ES6 globals (e.g., new types such as
 `Set`).
 For ES6 syntax, use `{ "parserOptions": { "ecmaVersion": 6 } }`; for new ES6 global variables, use `{ "env":
-{ "es6": true } }` (this setting enables ES6 syntax automatically).
+{ "es6": true } }`. `{ env: { es6: true } }` enables ES6 syntax automatically, but `{ parserOptions: { ecmaVersion: 6 } }` does not enable ES6 globals automatically.
 Parser options are set in your `.eslintrc.*` file by using the `parserOptions` property. The available options are:
 
 * `ecmaVersion` - set to 3, 5 (default), 6, 7, 8, 9, or 10 to specify the version of ECMAScript syntax you want to use. You can also set to 2015 (same as 6), 2016 (same as 7), 2017 (same as 8), 2018 (same as 9), or 2019 (same as 10) to use the year-based naming.
diff --git a/lib/rules/prefer-destructuring.js b/lib/rules/prefer-destructuring.js
index 119fae560..dec93d51f 100644
--- a/lib/rules/prefer-destructuring.js
+++ b/lib/rules/prefer-destructuring.js
@@ -19,6 +19,8 @@ module.exports = {
             url: "https://eslint.org/docs/rules/prefer-destructuring"
         },
 
+        fixable: "code",
+
         schema: [
             {
 
@@ -130,10 +132,55 @@ module.exports = {
          *
          * @param {ASTNode} reportNode the node to report
          * @param {string} type the type of destructuring that should have been done
+         * @param {Function|null} fix the fix function or null to pass to context.report
          * @returns {void}
          */
-        function report(reportNode, type) {
-            context.report({ node: reportNode, message: "Use {{type}} destructuring.", data: { type } });
+        function report(reportNode, type, fix) {
+            context.report({
+                node: reportNode,
+                message: "Use {{type}} destructuring.",
+                data: { type },
+                fix
+            });
+        }
+
+        /**
+         * Determines if a node should be fixed into object destructuring
+         *
+         * The fixer only fixes the simplest case of object destructuring,
+         * like: `let x = a.x`;
+         *
+         * Assignment expression is not fixed.
+         * Array destructuring is not fixed.
+         * Renamed property is not fixed.
+         *
+         * @param {ASTNode} node the the node to evaluate
+         * @returns {boolean} whether or not the node should be fixed
+         */
+        function shouldFix(node) {
+            return node.type === "VariableDeclarator" &&
+                node.id.type === "Identifier" &&
+                node.init.type === "MemberExpression" &&
+                node.id.name === node.init.property.name;
+        }
+
+        /**
+         * Fix a node into object destructuring.
+         * This function only handles the simplest case of object destructuring,
+         * see {@link shouldFix}.
+         *
+         * @param {SourceCodeFixer} fixer the fixer object
+         * @param {ASTNode} node the node to be fixed.
+         * @returns {Object} a fix for the node
+         */
+        function fixIntoObjectDestructuring(fixer, node) {
+            const rightNode = node.init;
+            const sourceCode = context.getSourceCode();
+
+            return fixer.replaceText(
+                node,
+                `{${rightNode.property.name}} = ${sourceCode.getText(rightNode.object)}`
+            );
         }
 
         /**
@@ -155,13 +202,17 @@ module.exports = {
 
             if (isArrayIndexAccess(rightNode)) {
                 if (shouldCheck(reportNode.type, "array")) {
-                    report(reportNode, "array");
+                    report(reportNode, "array", null);
                 }
                 return;
             }
 
+            const fix = shouldFix(reportNode)
+                ? fixer => fixIntoObjectDestructuring(fixer, reportNode)
+                : null;
+
             if (shouldCheck(reportNode.type, "object") && enforceForRenamedProperties) {
-                report(reportNode, "object");
+                report(reportNode, "object", fix);
                 return;
             }
 
@@ -172,7 +223,7 @@ module.exports = {
                     (property.type === "Literal" && leftNode.name === property.value) ||
                     (property.type === "Identifier" && leftNode.name === property.name && !rightNode.computed)
                 ) {
-                    report(reportNode, "object");
+                    report(reportNode, "object", fix);
                 }
             }
         }
diff --git a/lib/rules/prefer-spread.js b/lib/rules/prefer-spread.js
index 790fd3b82..5f3a47732 100644
--- a/lib/rules/prefer-spread.js
+++ b/lib/rules/prefer-spread.js
@@ -59,7 +59,7 @@ module.exports = {
         },
 
         schema: [],
-        fixable: "code"
+        fixable: null
     },
 
     create(context) {
@@ -78,18 +78,7 @@ module.exports = {
                 if (isValidThisArg(expectedThis, thisArg, sourceCode)) {
                     context.report({
                         node,
-                        message: "Use the spread operator instead of '.apply()'.",
-                        fix(fixer) {
-                            if (expectedThis && expectedThis.type !== "Identifier") {
-
-                                // Don't fix cases where the `this` value could be a computed expression.
-                                return null;
-                            }
-
-                            const propertyDot = sourceCode.getFirstTokenBetween(applied, node.callee.property, token => token.value === ".");
-
-                            return fixer.replaceTextRange([propertyDot.range[0], node.range[1]], `(...${sourceCode.getText(node.arguments[1])})`);
-                        }
+                        message: "Use the spread operator instead of '.apply()'."
                     });
                 }
             }


Test patch:
diff --git a/tests/lib/rules/prefer-destructuring.js b/tests/lib/rules/prefer-destructuring.js
index a0e4aa79b..5470079ef 100644
--- a/tests/lib/rules/prefer-destructuring.js
+++ b/tests/lib/rules/prefer-destructuring.js
@@ -133,12 +133,21 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         "class Foo extends Bar { static foo() {var foo = super.foo} }",
         "foo = bar[foo];",
-        "var foo = bar[foo];"
+        "var foo = bar[foo];",
+        {
+            code: "var {foo: {bar}} = object;",
+            options: [{ object: true }]
+        },
+        {
+            code: "var {bar} = object.foo;",
+            options: [{ object: true }]
+        }
     ],
 
     invalid: [
         {
             code: "var foo = array[0];",
+            output: null,
             errors: [{
                 message: "Use array destructuring.",
                 type: "VariableDeclarator"
@@ -146,6 +155,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "foo = array[0];",
+            output: null,
             errors: [{
                 message: "Use array destructuring.",
                 type: "AssignmentExpression"
@@ -153,6 +163,15 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "var foo = object.foo;",
+            output: "var {foo} = object;",
+            errors: [{
+                message: "Use object destructuring.",
+                type: "VariableDeclarator"
+            }]
+        },
+        {
+            code: "var foo = object.bar.foo;",
+            output: "var {foo} = object.bar;",
             errors: [{
                 message: "Use object destructuring.",
                 type: "VariableDeclarator"
@@ -160,6 +179,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "var foobar = object.bar;",
+            output: null,
             options: [{ VariableDeclarator: { object: true } }, { enforceForRenamedProperties: true }],
             errors: [{
                 message: "Use object destructuring.",
@@ -168,6 +188,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "var foobar = object.bar;",
+            output: null,
             options: [{ object: true }, { enforceForRenamedProperties: true }],
             errors: [{
                 message: "Use object destructuring.",
@@ -176,6 +197,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "var foo = object[bar];",
+            output: null,
             options: [{ VariableDeclarator: { object: true } }, { enforceForRenamedProperties: true }],
             errors: [{
                 message: "Use object destructuring.",
@@ -184,6 +206,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "var foo = object[bar];",
+            output: null,
             options: [{ object: true }, { enforceForRenamedProperties: true }],
             errors: [{
                 message: "Use object destructuring.",
@@ -192,6 +215,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "var foo = object['foo'];",
+            output: null,
             errors: [{
                 message: "Use object destructuring.",
                 type: "VariableDeclarator"
@@ -199,6 +223,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "foo = object.foo;",
+            output: null,
             errors: [{
                 message: "Use object destructuring.",
                 type: "AssignmentExpression"
@@ -206,6 +231,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "foo = object['foo'];",
+            output: null,
             errors: [{
                 message: "Use object destructuring.",
                 type: "AssignmentExpression"
@@ -213,6 +239,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "var foo = array[0];",
+            output: null,
             options: [{ VariableDeclarator: { array: true } }, { enforceForRenamedProperties: true }],
             errors: [{
                 message: "Use array destructuring.",
@@ -221,6 +248,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "foo = array[0];",
+            output: null,
             options: [{ AssignmentExpression: { array: true } }],
             errors: [{
                 message: "Use array destructuring.",
@@ -229,6 +257,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "var foo = array[0];",
+            output: null,
             options: [
                 {
                     VariableDeclarator: { array: true },
@@ -243,6 +272,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "var foo = array[0];",
+            output: null,
             options: [
                 {
                     VariableDeclarator: { array: true },
@@ -256,6 +286,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "foo = array[0];",
+            output: null,
             options: [
                 {
                     VariableDeclarator: { array: false },
@@ -269,6 +300,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "foo = object.foo;",
+            output: null,
             options: [
                 {
                     VariableDeclarator: { array: true, object: false },
@@ -282,6 +314,7 @@ ruleTester.run("prefer-destructuring", rule, {
         },
         {
             code: "class Foo extends Bar { static foo() {var bar = super.foo.bar} }",
+            output: "class Foo extends Bar { static foo() {var {bar} = super.foo} }",
             errors: [{
                 message: "Use object destructuring.",
                 type: "VariableDeclarator"
diff --git a/tests/lib/rules/prefer-spread.js b/tests/lib/rules/prefer-spread.js
index 7e9681770..1e11dd6c4 100644
--- a/tests/lib/rules/prefer-spread.js
+++ b/tests/lib/rules/prefer-spread.js
@@ -44,50 +44,34 @@ ruleTester.run("prefer-spread", rule, {
     invalid: [
         {
             code: "foo.apply(undefined, args);",
-            output: "foo(...args);",
             errors
         },
         {
             code: "foo.apply(void 0, args);",
-            output: "foo(...args);",
             errors
         },
         {
             code: "foo.apply(null, args);",
-            output: "foo(...args);",
             errors
         },
         {
             code: "obj.foo.apply(obj, args);",
-            output: "obj.foo(...args);",
             errors
         },
         {
-
-            // Not fixed: a.b.c might activate getters
             code: "a.b.c.foo.apply(a.b.c, args);",
-            output: null,
             errors
         },
         {
-
-            // Not fixed: a.b(x, y).c might activate getters
             code: "a.b(x, y).c.foo.apply(a.b(x, y).c, args);",
-            output: null,
             errors
         },
         {
-
-            // Not fixed (not an identifier)
             code: "[].concat.apply([ ], args);",
-            output: null,
             errors
         },
         {
-
-            // Not fixed (not an identifier)
             code: "[].concat.apply([\n/*empty*/\n], args);",
-            output: null,
             errors
         }
     ]