Skip to content

Commit ff47c73

Browse files
authored
Fix false positives and false negatives in vue/no-mutating-props rule. (#1715)
1 parent e0dddb1 commit ff47c73

File tree

2 files changed

+166
-2
lines changed

2 files changed

+166
-2
lines changed

lib/rules/no-mutating-props.js

+71-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,34 @@ const { findVariable } = require('eslint-utils')
1111
// Rule Definition
1212
// ------------------------------------------------------------------------------
1313

14+
// https://github.com/vuejs/vue-next/blob/7c11c58faf8840ab97b6449c98da0296a60dddd8/packages/shared/src/globalsWhitelist.ts
15+
const GLOBALS_WHITE_LISTED = new Set([
16+
'Infinity',
17+
'undefined',
18+
'NaN',
19+
'isFinite',
20+
'isNaN',
21+
'parseFloat',
22+
'parseInt',
23+
'decodeURI',
24+
'decodeURIComponent',
25+
'encodeURI',
26+
'encodeURIComponent',
27+
'Math',
28+
'Number',
29+
'Date',
30+
'Array',
31+
'Object',
32+
'Boolean',
33+
'String',
34+
'RegExp',
35+
'Map',
36+
'Set',
37+
'JSON',
38+
'Intl',
39+
'BigInt'
40+
])
41+
1442
module.exports = {
1543
meta: {
1644
type: 'suggestion',
@@ -191,12 +219,43 @@ module.exports = {
191219
}
192220
}
193221

222+
function* extractDefineVariableNames() {
223+
const globalScope = context.getSourceCode().scopeManager.globalScope
224+
if (globalScope) {
225+
for (const variable of globalScope.variables) {
226+
if (variable.defs.length) {
227+
yield variable.name
228+
}
229+
}
230+
const moduleScope = globalScope.childScopes.find(
231+
(scope) => scope.type === 'module'
232+
)
233+
for (const variable of (moduleScope && moduleScope.variables) || []) {
234+
if (variable.defs.length) {
235+
yield variable.name
236+
}
237+
}
238+
}
239+
}
240+
194241
return utils.compositingVisitors(
195242
{},
196243
utils.defineScriptSetupVisitor(context, {
197244
onDefinePropsEnter(node, props) {
245+
const defineVariableNames = new Set(extractDefineVariableNames())
246+
198247
const propsSet = new Set(
199-
props.map((p) => p.propName).filter(utils.isDef)
248+
props
249+
.map((p) => p.propName)
250+
.filter(
251+
/**
252+
* @returns {propName is string}
253+
*/
254+
(propName) =>
255+
utils.isDef(propName) &&
256+
!GLOBALS_WHITE_LISTED.has(propName) &&
257+
!defineVariableNames.has(propName)
258+
)
200259
)
201260
propsMap.set(node, propsSet)
202261
vueObjectData = {
@@ -337,12 +396,22 @@ module.exports = {
337396
}
338397
},
339398
/** @param {ESNode} node */
340-
"VAttribute[directive=true][key.name.name='model'] VExpressionContainer > *"(
399+
"VAttribute[directive=true]:matches([key.name.name='model'], [key.name.name='bind']) VExpressionContainer > *"(
341400
node
342401
) {
343402
if (!vueObjectData) {
344403
return
345404
}
405+
let attr = node.parent
406+
while (attr && attr.type !== 'VAttribute') {
407+
attr = attr.parent
408+
}
409+
if (attr && attr.directive && attr.key.name.name === 'bind') {
410+
if (!attr.key.modifiers.some((mod) => mod.name === 'sync')) {
411+
return
412+
}
413+
}
414+
346415
const nodes = utils.getMemberChaining(node)
347416
const first = nodes[0]
348417
let name

tests/lib/rules/no-mutating-props.js

+95
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,28 @@ ruleTester.run('no-mutating-props', rule, {
331331
}
332332
}
333333
</script>`
334+
},
335+
336+
{
337+
// script setup with shadow
338+
filename: 'test.vue',
339+
code: `
340+
<template>
341+
<input v-model="foo">
342+
<input v-model="bar">
343+
<input v-model="Infinity">
344+
</template>
345+
<script setup>
346+
import { ref } from 'vue'
347+
import { bar } from './my-script'
348+
defineProps({
349+
foo: String,
350+
bar: String,
351+
Infinity: Number
352+
})
353+
const foo = ref('')
354+
</script>
355+
`
334356
}
335357
],
336358

@@ -582,6 +604,42 @@ ruleTester.run('no-mutating-props', rule, {
582604
}
583605
]
584606
},
607+
{
608+
filename: 'test.vue',
609+
code: `
610+
<template>
611+
<div>
612+
<MyComponent :data.sync="this.prop" />
613+
<MyComponent :data.sync="prop" />
614+
<MyComponent :data="this.prop" />
615+
<MyComponent :data="prop" />
616+
<template v-for="prop of data">
617+
<MyComponent :data.sync="prop" />
618+
<MyComponent :data.sync="this.prop" />
619+
</template>
620+
</div>
621+
</template>
622+
<script>
623+
export default {
624+
props: ['prop']
625+
}
626+
</script>
627+
`,
628+
errors: [
629+
{
630+
message: 'Unexpected mutation of "prop" prop.',
631+
line: 4
632+
},
633+
{
634+
message: 'Unexpected mutation of "prop" prop.',
635+
line: 5
636+
},
637+
{
638+
message: 'Unexpected mutation of "prop" prop.',
639+
line: 10
640+
}
641+
]
642+
},
585643

586644
// setup
587645
{
@@ -817,6 +875,43 @@ ruleTester.run('no-mutating-props', rule, {
817875
line: 6
818876
}
819877
]
878+
},
879+
880+
{
881+
// script setup with shadow
882+
filename: 'test.vue',
883+
code: `
884+
<template>
885+
<input v-model="foo">
886+
<input v-model="bar">
887+
<input v-model="window">
888+
<input v-model="Infinity">
889+
</template>
890+
<script setup>
891+
import { ref } from 'vue'
892+
const { Infinity } = defineProps({
893+
foo: String,
894+
bar: String,
895+
Infinity: String,
896+
window: String,
897+
})
898+
const foo = ref('')
899+
</script>
900+
`,
901+
errors: [
902+
{
903+
message: 'Unexpected mutation of "bar" prop.',
904+
line: 4
905+
},
906+
{
907+
message: 'Unexpected mutation of "window" prop.',
908+
line: 5
909+
},
910+
{
911+
message: 'Unexpected mutation of "Infinity" prop.',
912+
line: 6
913+
}
914+
]
820915
}
821916
]
822917
})

0 commit comments

Comments
 (0)