Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hot-beers-help.md
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2068,6 +2068,27 @@ 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 (
callExpr.type === 'CallExpression' &&
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 }
Expand Down Expand Up @@ -2128,6 +2149,14 @@ module.exports = {
}
}
}
if (this.isObjectAssignCall(target, node)) {
// Object.assign(xxx, {})
return {
kind: 'call',
node: target,
pathNodes
}
}
break
}
case 'MemberExpression': {
Expand Down
41 changes: 41 additions & 0 deletions tests/lib/rules/no-mutating-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>
`
}
],

Expand Down Expand Up @@ -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
}
]
}
]
})
49 changes: 49 additions & 0 deletions tests/lib/rules/no-side-effects-in-computed-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>`
Expand Down Expand Up @@ -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
}
]
}
]
})