Repository: vuejs/eslint-plugin-vue (JavaScript)

Issue:
`vue/no-mutating-props` should warn on Object.assign with prop as first argument
<!--
  Before proposing rule changes, please make sure it hasn't been posted already.
  You can see all open propositions here:
  https://github.com/vuejs/eslint-plugin-vue/issues?q=is%3Aopen+is%3Aissue+label%3A%22new+rule+proposition%22
-->

**What rule do you want to change?**
`vue/no-mutating-props` should detect Object.assign on props with the prop as first argument as this `target` is mutated by the function

**Does this change cause the rule to produce more or fewer warnings?**
The rule would produce more warnings, due to detecting more unintended mutations on props

**How will the change be implemented? (New option, new default behavior, etc.)?**
I'd say this should be a default behavior for this rule

**Please provide some example code that this change will affect:**

<!-- Put your code examples here -->
```javascript
this.$emit(
    'update:modelValue',
    Object.assign(this.modelValue, {[this.colname]: val}),
)
```
```vue
<input @input="$emit('update:modelValue', Object.assign(modelValue, {[colname]: $event.target.value}))" />
```

**What does the rule currently do for this code?**
This code is currently ignored by this rule!

**What will the rule do after it's changed?**
The rule should detect this kind of code and warn about it

**Additional context**
[MDN Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
<!-- Add any other context or screenshots about the feature request here. -->


Discussion:

Patch:
diff --git a/.changeset/hot-beers-help.md b/.changeset/hot-beers-help.md
new file mode 100644
index 000000000..b32ab0ad2
--- /dev/null
+++ b/.changeset/hot-beers-help.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-vue': minor
+---
+
+Changed `vue/no-mutating-props` and `vue/no-side-effects-in-computed-properties` rules to detect `Object.assign` mutations
diff --git a/lib/utils/index.js b/lib/utils/index.js
index e47dc4acf..dabdc2ba3 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -2068,6 +2068,26 @@ module.exports = {
     return false
   },
 
+  /**
+   * Check if a call expression is Object.assign with the given node as first argument
+   * @param {CallExpression} callExpr
+   * @param {ASTNode} targetNode
+   * @returns {boolean}
+   */
+  isObjectAssignCall(callExpr, targetNode) {
+    const { callee, arguments: args } = callExpr
+
+    return (
+      args.length > 0 &&
+      args[0] === targetNode &&
+      callee?.type === 'MemberExpression' &&
+      callee.object?.type === 'Identifier' &&
+      callee.object.name === 'Object' &&
+      callee.property?.type === 'Identifier' &&
+      callee.property.name === 'assign'
+    )
+  },
+
   /**
    * @param {MemberExpression|Identifier} props
    * @returns { { kind: 'assignment' | 'update' | 'call' , node: ESNode, pathNodes: MemberExpression[] } | null }
@@ -2128,6 +2148,14 @@ module.exports = {
               }
             }
           }
+          if (this.isObjectAssignCall(target, node)) {
+            // Object.assign(xxx, {})
+            return {
+              kind: 'call',
+              node: target,
+              pathNodes
+            }
+          }
           break
         }
         case 'MemberExpression': {


Test patch:
diff --git a/tests/lib/rules/no-mutating-props.js b/tests/lib/rules/no-mutating-props.js
index d4584b57c..1448ef22a 100644
--- a/tests/lib/rules/no-mutating-props.js
+++ b/tests/lib/rules/no-mutating-props.js
@@ -413,6 +413,22 @@ ruleTester.run('no-mutating-props', rule, {
         const foo = ref('')
         </script>
       `
+    },
+    {
+      // Object.assign not mutating the prop
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            props: ['data'],
+            methods: {
+              update() {
+                return Object.assign({}, this.data, { extra: 'value' })
+              }
+            }
+          }
+        </script>
+      `
     }
   ],
 
@@ -1420,6 +1436,31 @@ ruleTester.run('no-mutating-props', rule, {
           endColumn: 21
         }
       ]
+    },
+    {
+      // Object.assign mutating the prop as first argument
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            props: ['data'],
+            methods: {
+              update() {
+                return Object.assign(this.data, { extra: 'value' })
+              }
+            }
+          }
+        </script>
+      `,
+      errors: [
+        {
+          message: 'Unexpected mutation of "data" prop.',
+          line: 7,
+          column: 24,
+          endLine: 7,
+          endColumn: 68
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-side-effects-in-computed-properties.js b/tests/lib/rules/no-side-effects-in-computed-properties.js
index 2d0a124a4..7ba23f875 100644
--- a/tests/lib/rules/no-side-effects-in-computed-properties.js
+++ b/tests/lib/rules/no-side-effects-in-computed-properties.js
@@ -259,6 +259,7 @@ ruleTester.run('no-side-effects-in-computed-properties', rule, {
           })
           const test18 = computed(() => (console.log('a'), true))
           const test19 = computed(() => utils.reverse(foo.array))
+          const test20 = computed(() => Object.assign({}, foo.data, { extra: 'value' }))
         }
       }
       </script>`
@@ -889,6 +890,54 @@ ruleTester.run('no-side-effects-in-computed-properties', rule, {
           endColumn: 59
         }
       ]
+    },
+    {
+      // Object.assign mutating the prop as first argument in computed properties
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {ref, computed} from 'vue'
+      export default {
+        setup() {
+          const foo = useFoo()
+
+          const test1 = computed(() => Object.assign(foo.data, { extra: 'value' }))
+          const test2 = computed(() => {
+            return Object.assign(foo.user, foo.updates)
+          })
+          const test3 = computed({
+            get() {
+              Object.assign(foo.settings, { theme: 'dark' })
+              return foo.settings
+            }
+          })
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'Unexpected side effect in computed function.',
+          line: 8,
+          column: 40,
+          endLine: 8,
+          endColumn: 83
+        },
+        {
+          message: 'Unexpected side effect in computed function.',
+          line: 10,
+          column: 20,
+          endLine: 10,
+          endColumn: 56
+        },
+        {
+          message: 'Unexpected side effect in computed function.',
+          line: 14,
+          column: 15,
+          endLine: 14,
+          endColumn: 61
+        }
+      ]
     }
   ]
 })