Repository: eslint/eslint (js)

Issue:
Bug: eslint --fix breaks code when airbnb no-duplicate-imports happens
### Environment

Node version:  v14.17.1
npm version: 6.14.13
Local ESLint version: "^8.10.0"
Global ESLint version: 
Operating System: macOS BigSur 11.6


### What parser are you using?

Default (Espree)

### What did you do?

<details>
<summary>Configuration</summary>

```
{
    "env": {
        "es2021": true,
        "node": true
    },
    "extends": [
        "plugin:react/recommended",
        "airbnb"
    ],
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": "latest",
        "sourceType": "module"
    },
    "plugins": [
        "react"
    ],
    "rules": {
    }
}
```
</details>
<details>
<summary>package.json</summary>

```
{
  "name": "AwesomeProject",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint ."
  },
  "dependencies": {
    "react": "17.0.2",
    "react-native": "0.67.3"
  },
  "devDependencies": {
    "@babel/core": "^7.17.5",
    "@babel/runtime": "^7.17.2",
    "@react-native-community/eslint-config": "^3.0.1",
    "babel-jest": "^27.5.1",
    "eslint": "^8.10.0",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-plugin-import": "^2.25.4",
    "eslint-plugin-jsx-a11y": "^6.5.1",
    "eslint-plugin-react": "^7.29.2",
    "eslint-plugin-react-hooks": "^4.3.0",
    "jest": "^27.5.1",
    "metro-react-native-babel-preset": "^0.69.0",
    "react-test-renderer": "17.0.2"
  },
  "jest": {
    "preset": "react-native"
  }
}
```
</details>

```js

import {
  StyleSheet,
  View,
  TextInput,
  ImageBackground,
  Image,
  TouchableOpacity,
  SafeAreaView
} from 'react-native';
import { I18nManager } from 'react-native';
```


### What did you expect to happen?

expected it to format to:

```
import {
  StyleSheet,
  View,
  TextInput,
  ImageBackground,
  Image,
  TouchableOpacity,
  SafeAreaView,
  I18nManager
} from 'react-native';

```

### What actually happened?

```
import {
  StyleSheet,
  View,
  TextInput,
  ImageBackground,
  Image,
  TouchableOpacity,
  SafeAreaView, // <<< notice the comma here and in the line after
, I18nManager } from 'react-native'; 
```

### Participation

- [ ] I am willing to submit a pull request for this issue.

### Additional comments

when adding the airbnb extension it tries to merge both imports

Pull request:
fix: extend the autofix range in comma-dangle to ensure the last element
<!--
    Thank you for contributing!

    ESLint adheres to the [JS Foundation Code of Conduct](https://eslint.org/conduct).
-->

#### Prerequisites checklist

- [x] I have read the [contributing guidelines](https://github.com/eslint/eslint/blob/HEAD/CONTRIBUTING.md).

#### What is the purpose of this pull request? (put an "X" next to an item)

<!--
    The following template is intentionally not a markdown checkbox list for the reasons
    explained in https://github.com/eslint/eslint/pull/12848#issuecomment-580302888
-->

[ ] Documentation update
[x] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/HEAD/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/HEAD/templates/rule-proposal.md))
[ ] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/HEAD/templates/rule-change-proposal.md))
[ ] Add autofix to a rule
[ ] Add a CLI option
[ ] Add something to the core
[ ] Other, please explain:

Fixes #15660

<!--
    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)

Extended the fix range in the `comma-dangle` rule to ensure that the element after which the comma is added/removed stays the last element after that autofix pass.

#### Is there anything you'd like reviewers to focus on?

<!-- markdownlint-disable-file MD004 -->


Patch:
diff --git a/README.md b/README.md
index 1c845f18e..e513321c6 100644
--- a/README.md
+++ b/README.md
@@ -294,7 +294,7 @@ The following companies, organizations, and individuals support ESLint's ongoing
 <p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="undefined"></a></p><h3>Gold Sponsors</h3>
 <p><a href="https://contra.com"><img src="https://images.opencollective.com/contra1/c70f93f/logo.png" alt="Contra" height="96"></a> <a href="https://nx.dev"><img src="https://images.opencollective.com/nx/0efbe42/logo.png" alt="Nx (by Nrwl)" height="96"></a> <a href="https://www.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="96"></a> <a href="https://substack.com/"><img src="https://avatars.githubusercontent.com/u/53023767?v=4" alt="Substack" height="96"></a></p><h3>Silver Sponsors</h3>
 <p><a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a></p><h3>Bronze Sponsors</h3>
-<p><a href="https://launchdarkly.com"><img src="https://images.opencollective.com/launchdarkly/574bb9e/logo.png" alt="launchdarkly" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://www.vpsserver.com"><img src="https://images.opencollective.com/vpsservercom/logo.png" alt="VPS Server" height="32"></a> <a href="https://icons8.com"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8: free icons, photos, illustrations, and music" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Practice Ignition" height="32"></a></p>
+<p><a href="https://launchdarkly.com"><img src="https://images.opencollective.com/launchdarkly/574bb9e/logo.png" alt="launchdarkly" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://www.vpsserver.com"><img src="https://images.opencollective.com/vpsservercom/logo.png" alt="VPS Server" height="32"></a> <a href="https://icons8.com"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8: free icons, photos, illustrations, and music" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Ignition" height="32"></a></p>
 <!--sponsorsend-->
 
 ## <a name="technology-sponsors"></a>Technology Sponsors
diff --git a/lib/rules/comma-dangle.js b/lib/rules/comma-dangle.js
index 232b27cb3..063e1cb69 100644
--- a/lib/rules/comma-dangle.js
+++ b/lib/rules/comma-dangle.js
@@ -243,8 +243,18 @@ module.exports = {
                     node: lastItem,
                     loc: trailingToken.loc,
                     messageId: "unexpected",
-                    fix(fixer) {
-                        return fixer.remove(trailingToken);
+                    *fix(fixer) {
+                        yield fixer.remove(trailingToken);
+
+                        /*
+                         * Extend the range of the fix to include surrounding tokens to ensure
+                         * that the element after which the comma is removed stays _last_.
+                         * This intentionally makes conflicts in fix ranges with rules that may be
+                         * adding or removing elements in the same autofix pass.
+                         * https://github.com/eslint/eslint/issues/15660
+                         */
+                        yield fixer.insertTextBefore(sourceCode.getTokenBefore(trailingToken), "");
+                        yield fixer.insertTextAfter(sourceCode.getTokenAfter(trailingToken), "");
                     }
                 });
             }
@@ -282,8 +292,18 @@ module.exports = {
                         end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end)
                     },
                     messageId: "missing",
-                    fix(fixer) {
-                        return fixer.insertTextAfter(trailingToken, ",");
+                    *fix(fixer) {
+                        yield fixer.insertTextAfter(trailingToken, ",");
+
+                        /*
+                         * Extend the range of the fix to include surrounding tokens to ensure
+                         * that the element after which the comma is inserted stays _last_.
+                         * This intentionally makes conflicts in fix ranges with rules that may be
+                         * adding or removing elements in the same autofix pass.
+                         * https://github.com/eslint/eslint/issues/15660
+                         */
+                        yield fixer.insertTextBefore(trailingToken, "");
+                        yield fixer.insertTextAfter(sourceCode.getTokenAfter(trailingToken), "");
                     }
                 });
             }


Test patch:
diff --git a/tests/lib/rules/comma-dangle.js b/tests/lib/rules/comma-dangle.js
index 2ef7a008e..b02a74a55 100644
--- a/tests/lib/rules/comma-dangle.js
+++ b/tests/lib/rules/comma-dangle.js
@@ -10,6 +10,7 @@
 //------------------------------------------------------------------------------
 
 const path = require("path"),
+    { unIndent } = require("../../_utils"),
     rule = require("../../../lib/rules/comma-dangle"),
     { RuleTester } = require("../../../lib/rule-tester");
 
@@ -35,6 +36,29 @@ function parser(name) {
 
 const ruleTester = new RuleTester();
 
+ruleTester.defineRule("add-named-import", {
+    meta: {
+        fixable: "code"
+    },
+    create(context) {
+        return {
+            ImportDeclaration(node) {
+                const sourceCode = context.getSourceCode();
+                const closingBrace = sourceCode.getLastToken(node, token => token.value === "}");
+                const addComma = sourceCode.getTokenBefore(closingBrace).value !== ",";
+
+                context.report({
+                    message: "Add I18nManager.",
+                    node,
+                    fix(fixer) {
+                        return fixer.insertTextBefore(closingBrace, `${addComma ? "," : ""}I18nManager`);
+                    }
+                });
+            }
+        };
+    }
+});
+
 ruleTester.run("comma-dangle", rule, {
     valid: [
         "var foo = { bar: 'baz' }",
@@ -1766,6 +1790,66 @@ let d = 0;export {d,};
             output: "foo(a)",
             parserOptions: { ecmaVersion: 8 },
             errors: [{ messageId: "unexpected" }]
+        },
+
+        // https://github.com/eslint/eslint/issues/15660
+        {
+            code: unIndent`
+                /*eslint add-named-import:1*/
+                import {
+                    StyleSheet,
+                    View,
+                    TextInput,
+                    ImageBackground,
+                    Image,
+                    TouchableOpacity,
+                    SafeAreaView
+                } from 'react-native';
+            `,
+            output: unIndent`
+                /*eslint add-named-import:1*/
+                import {
+                    StyleSheet,
+                    View,
+                    TextInput,
+                    ImageBackground,
+                    Image,
+                    TouchableOpacity,
+                    SafeAreaView,
+                } from 'react-native';
+            `,
+            options: [{ imports: "always-multiline" }],
+            parserOptions: { ecmaVersion: 6, sourceType: "module" },
+            errors: 2
+        },
+        {
+            code: unIndent`
+                /*eslint add-named-import:1*/
+                import {
+                    StyleSheet,
+                    View,
+                    TextInput,
+                    ImageBackground,
+                    Image,
+                    TouchableOpacity,
+                    SafeAreaView,
+                } from 'react-native';
+            `,
+            output: unIndent`
+                /*eslint add-named-import:1*/
+                import {
+                    StyleSheet,
+                    View,
+                    TextInput,
+                    ImageBackground,
+                    Image,
+                    TouchableOpacity,
+                    SafeAreaView
+                } from 'react-native';
+            `,
+            options: [{ imports: "never" }],
+            parserOptions: { ecmaVersion: 6, sourceType: "module" },
+            errors: 2
         }
     ]
 });