Repository: eslint/eslint
Pull request #12485 (2019-10-23 23:37:57)

Fix: operator-assignment removes and duplicates comments
<!--
    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)**

[X] Bug fix

This PR fixes `operator-assignment` to prevent autofix if it would remove or duplicate comments.

<!--
    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.)
-->

**Tell us about your environment**

* **ESLint Version:**  6.5.1
* **Node Version:** 10.16.0
* **npm Version:** 6.9.0

**What parser (default, Babel-ESLint, etc.) are you using?**

default

**Please show your full configuration:**

<details>
<summary>Configuration</summary>

<!-- Paste your configuration below: -->
```js
module.exports = {
  parserOptions: {
    ecmaVersion: 2015,
  }
};
```

</details>

**What did you do? Please include the actual source code causing the issue.**

[Online Demo Link 1](https://eslint.org/demo#eyJ0ZXh0IjoiLyogZXNsaW50IG9wZXJhdG9yLWFzc2lnbm1lbnQ6IFtcImVycm9yXCIsIFwiYWx3YXlzXCJdKi9cblxueCA9IC8qIGNvbW1lbnQgKi8geCArIHk7XG5cbnggPSB4IC8vIGNvbW1lbnQgXG5cdCsgeTsiLCJvcHRpb25zIjp7InBhcnNlck9wdGlvbnMiOnsiZWNtYVZlcnNpb24iOjYsInNvdXJjZVR5cGUiOiJzY3JpcHQiLCJlY21hRmVhdHVyZXMiOnt9fSwicnVsZXMiOnt9LCJlbnYiOnt9fX0=)

```js
/* eslint operator-assignment: ["error", "always"]*/

x = /* comment */ x + y;

x = x // comment 
	+ y;
```

[Online Demo Link 2](https://eslint.org/demo#eyJ0ZXh0IjoiLyogZXNsaW50IG9wZXJhdG9yLWFzc2lnbm1lbnQ6IFtcImVycm9yXCIsIFwibmV2ZXJcIl0qL1xuXG54IC8qIGNvbW1lbnQgKi8gKz0geTtcblxueCAvLyBjb21tZW50XG5cdCs9IHk7Iiwib3B0aW9ucyI6eyJwYXJzZXJPcHRpb25zIjp7ImVjbWFWZXJzaW9uIjo2LCJzb3VyY2VUeXBlIjoic2NyaXB0IiwiZWNtYUZlYXR1cmVzIjp7fX0sInJ1bGVzIjp7fSwiZW52Ijp7fX19)

```js
/* eslint operator-assignment: ["error", "never"]*/

x /* comment */ += y;

x // comment
	+= y;
```

**What did you expect to happen?**

Not to remove/duplicate comments.

**What actually happened? Please include the actual, raw output from ESLint.**

```js
/* eslint operator-assignment: ["error", "always"]*/

x += y;

x += y;
```

```js
/* eslint operator-assignment: ["error", "never"]*/

x /* comment */ = x /* comment */ + y;

x // comment
	= x // comment
	+ y;
```

<!--
    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)**

Check for the comments in the ranges there are going to be removed or copied to prevent fixing.

**Is there anything you'd like reviewers to focus on?**



Patch:
diff --git a/README.md b/README.md
index f28cfb230..fd0c14668 100644
--- a/README.md
+++ b/README.md
@@ -265,7 +265,7 @@ The following companies, organizations, and individuals support ESLint's ongoing
 <h3>Gold Sponsors</h3>
 <p><a href="https://www.shopify.com"><img src="https://images.opencollective.com/shopify/eeb91aa/logo.png" alt="Shopify" height="96"></a> <a href="http://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://badoo.com/team?utm_source=eslint"><img src="https://images.opencollective.com/badoo/2826a3b/logo.png" alt="Badoo" 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://opensource.facebook.com"><img src="https://images.opencollective.com/fbopensource/fbb8a5b/logo.png" alt="Facebook Open Source" height="96"></a></p><h3>Silver Sponsors</h3>
 <p><a href="https://www.ampproject.org/"><img src="https://images.opencollective.com/amp/c8a3b25/logo.png" alt="AMP Project" height="64"></a></p><h3>Bronze Sponsors</h3>
-<p><a href="https://uxplanet.org/top-ui-ux-design-agencies-user-experience-firms-8c54697e290"><img src="https://images.opencollective.com/ui-ux-design-agencies/cae5dfe/logo.png" alt="UI UX Design Agencies" height="32"></a> <a href="https://www.bugsnag.com/platforms/react-error-reporting?utm_source=MobX&utm_medium=Website&utm_content=open-source&utm_campaign=2019-community&utm_term=20190913"><img src="https://images.opencollective.com/bugsnag-stability-monitoring/c2cef36/logo.png" alt="Bugsnag Stability Monitoring" height="32"></a> <a href="https://www.minitool.com"><img src="https://images.opencollective.com/minitool-software-ltd/6d68e16/logo.png" alt="MiniTool Software Ltd" height="32"></a> <a href="https://www.codacy.com/?utm_source=eslint&utm_medium=cpm&utm_campaign=eslint-sponsorship"><img src="https://images.opencollective.com/codacy/ed22716/logo.png" alt="Codacy" height="32"></a> <a href="https://mixpanel.com"><img src="https://images.opencollective.com/mixpanel/cd682f7/logo.png" alt="Mixpanel" 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/0b37d14/logo.png" alt="Free Icons by Icons8" height="32"></a> <a href="https://www.crosswordsolver.com"><img src="https://images.opencollective.com/crosswordsolver/d4481d6/logo.png" alt="Crosswordsolver" height="32"></a> <a href="https://clay.global"><img src="https://images.opencollective.com/clayglobal/2468f34/logo.png" alt="clay" height="32"></a> <a href="https://discordapp.com"><img src="https://images.opencollective.com/discordapp/7e3d9a9/logo.png" alt="Discord" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://tekhattan.com"><img src="https://images.opencollective.com/tekhattan/bc73c28/logo.png" alt="TekHattan" height="32"></a> <a href="https://www.marfeel.com/"><img src="https://images.opencollective.com/marfeel/4b88e30/logo.png" alt="Marfeel" height="32"></a> <a href="http://www.firesticktricks.com"><img src="https://images.opencollective.com/fire-stick-tricks/b8fbe2c/logo.png" alt="Fire Stick Tricks" height="32"></a> <a href="https://jsheroes.io/"><img src="https://images.opencollective.com/jsheroes1/9fedf0b/logo.png" alt="JSHeroes " height="32"></a></p>
+<p><a href="https://uxplanet.org/top-ui-ux-design-agencies-user-experience-firms-8c54697e290"><img src="https://images.opencollective.com/ui-ux-design-agencies/cae5dfe/logo.png" alt="UI UX Design Agencies" height="32"></a> <a href="https://www.bugsnag.com/platforms?utm_source=Open Collective&utm_medium=Website&utm_content=open-source&utm_campaign=2019-community&utm_term="><img src="https://images.opencollective.com/bugsnag-stability-monitoring/c2cef36/logo.png" alt="Bugsnag Stability Monitoring" height="32"></a> <a href="https://www.minitool.com"><img src="https://images.opencollective.com/minitool-software-ltd/6d68e16/logo.png" alt="MiniTool Software Ltd" height="32"></a> <a href="https://www.codacy.com/?utm_source=eslint&utm_medium=cpm&utm_campaign=eslint-sponsorship"><img src="https://images.opencollective.com/codacy/ed22716/logo.png" alt="Codacy" height="32"></a> <a href="https://mixpanel.com"><img src="https://images.opencollective.com/mixpanel/cd682f7/logo.png" alt="Mixpanel" 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/0b37d14/logo.png" alt="Free Icons by Icons8" height="32"></a> <a href="https://www.crosswordsolver.com"><img src="https://images.opencollective.com/crosswordsolver/d4481d6/logo.png" alt="Crosswordsolver" height="32"></a> <a href="https://clay.global"><img src="https://images.opencollective.com/clayglobal/2468f34/logo.png" alt="clay" height="32"></a> <a href="https://discordapp.com"><img src="https://images.opencollective.com/discordapp/7e3d9a9/logo.png" alt="Discord" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://tekhattan.com"><img src="https://images.opencollective.com/tekhattan/bc73c28/logo.png" alt="TekHattan" height="32"></a> <a href="https://www.marfeel.com/"><img src="https://images.opencollective.com/marfeel/4b88e30/logo.png" alt="Marfeel" height="32"></a> <a href="http://www.firesticktricks.com"><img src="https://images.opencollective.com/fire-stick-tricks/b8fbe2c/logo.png" alt="Fire Stick Tricks" height="32"></a> <a href="https://jsheroes.io/"><img src="https://images.opencollective.com/jsheroes1/9fedf0b/logo.png" alt="JSHeroes " height="32"></a></p>
 <!--sponsorsend-->
 
 ## <a name="technology-sponsors"></a>Technology Sponsors
diff --git a/docs/rules/no-param-reassign.md b/docs/rules/no-param-reassign.md
index f88d211e2..2ee312c2b 100644
--- a/docs/rules/no-param-reassign.md
+++ b/docs/rules/no-param-reassign.md
@@ -42,7 +42,7 @@ function foo(bar) {
 
 ## Options
 
-This rule takes one option, an object, with a boolean property `"props"` and an array `"ignorePropertyModificationsFor"`. `"props"` is `false` by default. If `"props"` is set to `true`, this rule warns against the modification of parameter properties unless they're included in `"ignorePropertyModificationsFor"`, which is an empty array by default.
+This rule takes one option, an object, with a boolean property `"props"`, and  arrays `"ignorePropertyModificationsFor"` and `"ignorePropertyModificationsForRegex"`. `"props"` is `false` by default. If `"props"` is set to `true`, this rule warns against the modification of parameter properties unless they're included in `"ignorePropertyModificationsFor"` or `"ignorePropertyModificationsForRegex"`, which is an empty array by default.
 
 ### props
 
@@ -124,6 +124,24 @@ function foo(bar) {
 }
 ```
 
+Examples of **correct** code for the `{ "props": true }` option with `"ignorePropertyModificationsForRegex"` set:
+
+```js
+/*eslint no-param-reassign: ["error", { "props": true, "ignorePropertyModificationsForRegex": ["^bar"] }]*/
+
+function foo(bar) {
+    barVar.prop = "value";
+}
+
+function foo(bar) {
+    delete barrito.aaa;
+}
+
+function foo(bar) {
+    bar_.aaa++;
+}
+```
+
 
 ## When Not To Use It
 
diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js
index 17ed8d9f0..8afd26270 100644
--- a/lib/cli-engine/cli-engine.js
+++ b/lib/cli-engine/cli-engine.js
@@ -667,11 +667,14 @@ class CLIEngine {
     addPlugin(name, pluginObject) {
         const {
             additionalPluginPool,
-            configArrayFactory
+            configArrayFactory,
+            lastConfigArrays
         } = internalSlotsMap.get(this);
 
         additionalPluginPool.set(name, pluginObject);
         configArrayFactory.clearCache();
+        lastConfigArrays.length = 1;
+        lastConfigArrays[0] = configArrayFactory.getConfigArrayForFile();
     }
 
     /**
diff --git a/lib/rules/dot-location.js b/lib/rules/dot-location.js
index 812988efa..d483e217a 100644
--- a/lib/rules/dot-location.js
+++ b/lib/rules/dot-location.js
@@ -47,19 +47,18 @@ module.exports = {
 
         /**
          * Reports if the dot between object and property is on the correct loccation.
-         * @param {ASTNode} obj The object owning the property.
-         * @param {ASTNode} prop The property of the object.
-         * @param {ASTNode} node The corresponding node of the token.
+         * @param {ASTNode} node The `MemberExpression` node.
          * @returns {void}
          */
-        function checkDotLocation(obj, prop, node) {
-            const dot = sourceCode.getTokenBefore(prop);
+        function checkDotLocation(node) {
+            const property = node.property;
+            const dot = sourceCode.getTokenBefore(property);
 
             // `obj` expression can be parenthesized, but those paren tokens are not a part of the `obj` node.
             const tokenBeforeDot = sourceCode.getTokenBefore(dot);
 
             const textBeforeDot = sourceCode.getText().slice(tokenBeforeDot.range[1], dot.range[0]);
-            const textAfterDot = sourceCode.getText().slice(dot.range[1], prop.range[0]);
+            const textAfterDot = sourceCode.getText().slice(dot.range[1], property.range[0]);
 
             if (onObject) {
                 if (!astUtils.isTokenOnSameLine(tokenBeforeDot, dot)) {
@@ -69,15 +68,15 @@ module.exports = {
                         node,
                         loc: dot.loc,
                         messageId: "expectedDotAfterObject",
-                        fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], prop.range[0]], `${neededTextAfterToken}.${textBeforeDot}${textAfterDot}`)
+                        fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], property.range[0]], `${neededTextAfterToken}.${textBeforeDot}${textAfterDot}`)
                     });
                 }
-            } else if (!astUtils.isTokenOnSameLine(dot, prop)) {
+            } else if (!astUtils.isTokenOnSameLine(dot, property)) {
                 context.report({
                     node,
                     loc: dot.loc,
                     messageId: "expectedDotBeforeProperty",
-                    fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], prop.range[0]], `${textBeforeDot}${textAfterDot}.`)
+                    fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], property.range[0]], `${textBeforeDot}${textAfterDot}.`)
                 });
             }
         }
@@ -89,7 +88,7 @@ module.exports = {
          */
         function checkNode(node) {
             if (!node.computed) {
-                checkDotLocation(node.object, node.property, node);
+                checkDotLocation(node);
             }
         }
 
diff --git a/lib/rules/no-param-reassign.js b/lib/rules/no-param-reassign.js
index edaaef537..d65eb3476 100644
--- a/lib/rules/no-param-reassign.js
+++ b/lib/rules/no-param-reassign.js
@@ -45,6 +45,13 @@ module.exports = {
                                     type: "string"
                                 },
                                 uniqueItems: true
+                            },
+                            ignorePropertyModificationsForRegex: {
+                                type: "array",
+                                items: {
+                                    type: "string"
+                                },
+                                uniqueItems: true
                             }
                         },
                         additionalProperties: false
@@ -57,6 +64,7 @@ module.exports = {
     create(context) {
         const props = context.options[0] && context.options[0].props;
         const ignoredPropertyAssignmentsFor = context.options[0] && context.options[0].ignorePropertyModificationsFor || [];
+        const ignoredPropertyAssignmentsForRegex = context.options[0] && context.options[0].ignorePropertyModificationsForRegex || [];
 
         /**
          * Checks whether or not the reference modifies properties of its variable.
@@ -136,6 +144,19 @@ module.exports = {
             return false;
         }
 
+        /**
+         * Tests that an identifier name matches any of the ignored property assignments.
+         * First we test strings in ignoredPropertyAssignmentsFor.
+         * Then we instantiate and test RegExp objects from ignoredPropertyAssignmentsForRegex strings.
+         * @param {string} identifierName A string that describes the name of an identifier to
+         * ignore property assignments for.
+         * @returns {boolean} Whether the string matches an ignored property assignment regular expression or not.
+         */
+        function isIgnoredPropertyAssignment(identifierName) {
+            return ignoredPropertyAssignmentsFor.includes(identifierName) ||
+                ignoredPropertyAssignmentsForRegex.some(ignored => new RegExp(ignored, "u").test(identifierName));
+        }
+
         /**
          * Reports a reference if is non initializer and writable.
          * @param {Reference} reference A reference to check.
@@ -157,7 +178,7 @@ module.exports = {
             ) {
                 if (reference.isWrite()) {
                     context.report({ node: identifier, message: "Assignment to function parameter '{{name}}'.", data: { name: identifier.name } });
-                } else if (props && isModifyingProp(reference) && ignoredPropertyAssignmentsFor.indexOf(identifier.name) === -1) {
+                } else if (props && isModifyingProp(reference) && !isIgnoredPropertyAssignment(identifier.name)) {
                     context.report({ node: identifier, message: "Assignment to property of function parameter '{{name}}'.", data: { name: identifier.name } });
                 }
             }
diff --git a/lib/rules/operator-assignment.js b/lib/rules/operator-assignment.js
index 63982cf39..e294668b4 100644
--- a/lib/rules/operator-assignment.js
+++ b/lib/rules/operator-assignment.js
@@ -150,6 +150,11 @@ module.exports = {
                                 const leftText = sourceCode.getText().slice(node.range[0], equalsToken.range[0]);
                                 const rightText = sourceCode.getText().slice(operatorToken.range[1], node.right.range[1]);
 
+                                // Check for comments that would be removed.
+                                if (sourceCode.commentsExistBetween(equalsToken, operatorToken)) {
+                                    return null;
+                                }
+
                                 return fixer.replaceText(node, `${leftText}${expr.operator}=${rightText}`);
                             }
                             return null;
@@ -182,11 +187,17 @@ module.exports = {
                     messageId: "unexpected",
                     fix(fixer) {
                         if (canBeFixed(node.left)) {
+                            const firstToken = sourceCode.getFirstToken(node);
                             const operatorToken = getOperatorToken(node);
                             const leftText = sourceCode.getText().slice(node.range[0], operatorToken.range[0]);
                             const newOperator = node.operator.slice(0, -1);
                             let rightText;
 
+                            // Check for comments that would be duplicated.
+                            if (sourceCode.commentsExistBetween(firstToken, operatorToken)) {
+                                return null;
+                            }
+
                             // If this change would modify precedence (e.g. `foo *= bar + 1` => `foo = foo * (bar + 1)`), parenthesize the right side.
                             if (
                                 astUtils.getPrecedence(node.right) <= astUtils.getPrecedence({ type: "BinaryExpression", operator: newOperator }) &&
@@ -194,7 +205,17 @@ module.exports = {
                             ) {
                                 rightText = `${sourceCode.text.slice(operatorToken.range[1], node.right.range[0])}(${sourceCode.getText(node.right)})`;
                             } else {
-                                rightText = sourceCode.text.slice(operatorToken.range[1], node.range[1]);
+                                const firstRightToken = sourceCode.getFirstToken(node.right);
+                                let rightTextPrefix = "";
+
+                                if (
+                                    operatorToken.range[1] === firstRightToken.range[0] &&
+                                    !astUtils.canTokensBeAdjacent(newOperator, firstRightToken)
+                                ) {
+                                    rightTextPrefix = " "; // foo+=+bar -> foo= foo+ +bar
+                                }
+
+                                rightText = `${rightTextPrefix}${sourceCode.text.slice(operatorToken.range[1], node.range[1])}`;
                             }
 
                             return fixer.replaceText(node, `${leftText}= ${leftText}${newOperator}${rightText}`);
diff --git a/package.json b/package.json
index 778365194..94a10b969 100644
--- a/package.json
+++ b/package.json
@@ -52,9 +52,9 @@
     "debug": "^4.0.1",
     "doctrine": "^3.0.0",
     "eslint-scope": "^5.0.0",
-    "eslint-utils": "^1.4.2",
+    "eslint-utils": "^1.4.3",
     "eslint-visitor-keys": "^1.1.0",
-    "espree": "^6.1.1",
+    "espree": "^6.1.2",
     "esquery": "^1.0.1",
     "esutils": "^2.0.2",
     "file-entry-cache": "^5.0.1",
@@ -86,7 +86,7 @@
   "devDependencies": {
     "@babel/core": "^7.4.3",
     "@babel/preset-env": "^7.4.3",
-    "acorn": "^7.0.0",
+    "acorn": "^7.1.0",
     "babel-loader": "^8.0.5",
     "chai": "^4.0.1",
     "cheerio": "^0.22.0",


Test patch:
diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js
index 7c2ce871c..2cdc81301 100644
--- a/tests/lib/cli-engine/cli-engine.js
+++ b/tests/lib/cli-engine/cli-engine.js
@@ -4022,6 +4022,14 @@ describe("CLIEngine", () => {
 
             assert(engine.getRules().has("node/no-deprecated-api"), "node/no-deprecated-api is present");
         });
+
+        it("should expose the rules of the plugin that is added by 'addPlugin'.", () => {
+            const engine = new CLIEngine({ plugins: ["foo"] });
+
+            engine.addPlugin("foo", require("eslint-plugin-node"));
+
+            assert(engine.getRules().has("foo/no-deprecated-api"), "foo/no-deprecated-api is present");
+        });
     });
 
     describe("resolveFileGlobPatterns", () => {
diff --git a/tests/lib/rules/no-obj-calls.js b/tests/lib/rules/no-obj-calls.js
index d5bcf0638..db7261568 100644
--- a/tests/lib/rules/no-obj-calls.js
+++ b/tests/lib/rules/no-obj-calls.js
@@ -61,6 +61,10 @@ ruleTester.run("no-obj-calls", rule, {
         {
             code: "function foo() { var Atomics = bar(); var baz = Atomics(5); }",
             globals: { Atomics: false }
+        },
+        {
+            code: "var construct = typeof Reflect !== \"undefined\" ? Reflect.construct : undefined; construct();",
+            globals: { Reflect: false }
         }
     ],
     invalid: [
diff --git a/tests/lib/rules/no-param-reassign.js b/tests/lib/rules/no-param-reassign.js
index fec1e4168..5bb70e6aa 100644
--- a/tests/lib/rules/no-param-reassign.js
+++ b/tests/lib/rules/no-param-reassign.js
@@ -45,6 +45,11 @@ ruleTester.run("no-param-reassign", rule, {
         { code: "function foo(a) { for (a.b of arr); }", options: [{ props: true, ignorePropertyModificationsFor: ["a"] }], parserOptions: { ecmaVersion: 6 } },
         { code: "function foo(a, z) { a.b = 0; x.y = 0; }", options: [{ props: true, ignorePropertyModificationsFor: ["a", "x"] }] },
         { code: "function foo(a) { a.b.c = 0;}", options: [{ props: true, ignorePropertyModificationsFor: ["a"] }] },
+        { code: "function foo(aFoo) { aFoo.b = 0; }", options: [{ props: true, ignorePropertyModificationsForRegex: ["^a.*$"] }] },
+        { code: "function foo(aFoo) { ++aFoo.b; }", options: [{ props: true, ignorePropertyModificationsForRegex: ["^a.*$"] }] },
+        { code: "function foo(aFoo) { delete aFoo.b; }", options: [{ props: true, ignorePropertyModificationsForRegex: ["^a.*$"] }] },
+        { code: "function foo(a, z) { aFoo.b = 0; x.y = 0; }", options: [{ props: true, ignorePropertyModificationsForRegex: ["^a.*$", "^x.*$"] }] },
+        { code: "function foo(aFoo) { aFoo.b.c = 0;}", options: [{ props: true, ignorePropertyModificationsForRegex: ["^a.*$"] }] },
         {
             code: "function foo(a) { ({ [a]: variable } = value) }",
             options: [{ props: true }],
@@ -152,6 +157,18 @@ ruleTester.run("no-param-reassign", rule, {
             parserOptions: { ecmaVersion: 6 },
             errors: [{ message: "Assignment to property of function parameter 'bar'." }]
         },
+        {
+            code: "function foo(bar) { [bar.a] = []; }",
+            options: [{ props: true, ignorePropertyModificationsForRegex: ["^a.*$"] }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [{ message: "Assignment to property of function parameter 'bar'." }]
+        },
+        {
+            code: "function foo(bar) { [bar.a] = []; }",
+            options: [{ props: true, ignorePropertyModificationsForRegex: ["^B.*$"] }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [{ message: "Assignment to property of function parameter 'bar'." }]
+        },
         {
             code: "function foo(bar) { ({foo: bar.a} = {}); }",
             options: [{ props: true }],
diff --git a/tests/lib/rules/operator-assignment.js b/tests/lib/rules/operator-assignment.js
index fe7b326a1..2f8753253 100644
--- a/tests/lib/rules/operator-assignment.js
+++ b/tests/lib/rules/operator-assignment.js
@@ -191,6 +191,86 @@ ruleTester.run("operator-assignment", rule, {
         code: "foo[5] = foo[5] / baz",
         output: "foo[5] /= baz", // this is ok because 5 is a literal, so toString won't get called
         errors: EXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "/*1*/x/*2*/./*3*/y/*4*/= x.y +/*5*/z/*6*/./*7*/w/*8*/;",
+        output: "/*1*/x/*2*/./*3*/y/*4*/+=/*5*/z/*6*/./*7*/w/*8*/;", // these comments are preserved
+        options: ["always"],
+        errors: EXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "x // 1\n . // 2\n y // 3\n = x.y + //4\n z //5\n . //6\n w;",
+        output: "x // 1\n . // 2\n y // 3\n += //4\n z //5\n . //6\n w;", // these comments are preserved
+        options: ["always"],
+        errors: EXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "x = /*1*/ x + y",
+        output: null, // not fixed; fixing would remove this comment
+        options: ["always"],
+        errors: EXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "x = //1\n x + y",
+        output: null, // not fixed; fixing would remove this comment
+        options: ["always"],
+        errors: EXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "x.y = x/*1*/.y + z",
+        output: null, // not fixed; fixing would remove this comment
+        options: ["always"],
+        errors: EXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "x.y = x. //1\n y + z",
+        output: null, // not fixed; fixing would remove this comment
+        options: ["always"],
+        errors: EXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "x = x /*1*/ + y",
+        output: null, // not fixed; fixing would remove this comment
+        options: ["always"],
+        errors: EXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "x = x //1\n + y",
+        output: null, // not fixed; fixing would remove this comment
+        options: ["always"],
+        errors: EXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "/*1*/x +=/*2*/y/*3*/;",
+        output: "/*1*/x = x +/*2*/y/*3*/;", // these comments are preserved and not duplicated
+        options: ["never"],
+        errors: UNEXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "x +=//1\n y",
+        output: "x = x +//1\n y", // this comment is preserved and not duplicated
+        options: ["never"],
+        errors: UNEXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "(/*1*/x += y)",
+        output: "(/*1*/x = x + y)", // this comment is preserved and not duplicated
+        options: ["never"],
+        errors: UNEXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "x/*1*/+=  y",
+        output: null, // not fixed; fixing would duplicate this comment
+        options: ["never"],
+        errors: UNEXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "x //1\n +=  y",
+        output: null, // not fixed; fixing would duplicate this comment
+        options: ["never"],
+        errors: UNEXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "(/*1*/x) +=  y",
+        output: null, // not fixed; fixing would duplicate this comment
+        options: ["never"],
+        errors: UNEXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "x/*1*/.y +=  z",
+        output: null, // not fixed; fixing would duplicate this comment
+        options: ["never"],
+        errors: UNEXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "x.//1\n y +=  z",
+        output: null, // not fixed; fixing would duplicate this comment
+        options: ["never"],
+        errors: UNEXPECTED_OPERATOR_ASSIGNMENT
     }, {
         code: "(foo.bar) ^= ((((((((((((((((baz))))))))))))))))",
         output: "(foo.bar) = (foo.bar) ^ ((((((((((((((((baz))))))))))))))))",
@@ -230,6 +310,31 @@ ruleTester.run("operator-assignment", rule, {
         output: "foo = foo * (bar + 1)",
         options: ["never"],
         errors: UNEXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "foo+=-bar",
+        output: "foo= foo+-bar", // tokens can be adjacent
+        options: ["never"],
+        errors: UNEXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "foo+=+bar",
+        output: "foo= foo+ +bar", // tokens cannot be adjacent, insert a space between
+        options: ["never"],
+        errors: UNEXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "foo+= +bar",
+        output: "foo= foo+ +bar", // tokens cannot be adjacent, but there is already a space between
+        options: ["never"],
+        errors: UNEXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "foo+=/**/+bar",
+        output: "foo= foo+/**/+bar", // tokens cannot be adjacent, but there is a comment between
+        options: ["never"],
+        errors: UNEXPECTED_OPERATOR_ASSIGNMENT
+    }, {
+        code: "foo+=+bar===baz",
+        output: "foo= foo+(+bar===baz)", // tokens cannot be adjacent, but the right side will be parenthesised
+        options: ["never"],
+        errors: UNEXPECTED_OPERATOR_ASSIGNMENT
     }]
 
 });