diff --git a/lib/rules/no-mutating-props.js b/lib/rules/no-mutating-props.js
index 70559026b..93855fd4e 100644
--- a/lib/rules/no-mutating-props.js
+++ b/lib/rules/no-mutating-props.js
@@ -11,6 +11,34 @@ const { findVariable } = require('eslint-utils')
// Rule Definition
// ------------------------------------------------------------------------------
+// https://github.com/vuejs/vue-next/blob/7c11c58faf8840ab97b6449c98da0296a60dddd8/packages/shared/src/globalsWhitelist.ts
+const GLOBALS_WHITE_LISTED = new Set([
+ 'Infinity',
+ 'undefined',
+ 'NaN',
+ 'isFinite',
+ 'isNaN',
+ 'parseFloat',
+ 'parseInt',
+ 'decodeURI',
+ 'decodeURIComponent',
+ 'encodeURI',
+ 'encodeURIComponent',
+ 'Math',
+ 'Number',
+ 'Date',
+ 'Array',
+ 'Object',
+ 'Boolean',
+ 'String',
+ 'RegExp',
+ 'Map',
+ 'Set',
+ 'JSON',
+ 'Intl',
+ 'BigInt'
+])
+
module.exports = {
meta: {
type: 'suggestion',
@@ -191,12 +219,43 @@ module.exports = {
}
}
+ function* extractDefineVariableNames() {
+ const globalScope = context.getSourceCode().scopeManager.globalScope
+ if (globalScope) {
+ for (const variable of globalScope.variables) {
+ if (variable.defs.length) {
+ yield variable.name
+ }
+ }
+ const moduleScope = globalScope.childScopes.find(
+ (scope) => scope.type === 'module'
+ )
+ for (const variable of (moduleScope && moduleScope.variables) || []) {
+ if (variable.defs.length) {
+ yield variable.name
+ }
+ }
+ }
+ }
+
return utils.compositingVisitors(
{},
utils.defineScriptSetupVisitor(context, {
onDefinePropsEnter(node, props) {
+ const defineVariableNames = new Set(extractDefineVariableNames())
+
const propsSet = new Set(
- props.map((p) => p.propName).filter(utils.isDef)
+ props
+ .map((p) => p.propName)
+ .filter(
+ /**
+ * @returns {propName is string}
+ */
+ (propName) =>
+ utils.isDef(propName) &&
+ !GLOBALS_WHITE_LISTED.has(propName) &&
+ !defineVariableNames.has(propName)
+ )
)
propsMap.set(node, propsSet)
vueObjectData = {
@@ -337,12 +396,22 @@ module.exports = {
}
},
/** @param {ESNode} node */
- "VAttribute[directive=true][key.name.name='model'] VExpressionContainer > *"(
+ "VAttribute[directive=true]:matches([key.name.name='model'], [key.name.name='bind']) VExpressionContainer > *"(
node
) {
if (!vueObjectData) {
return
}
+ let attr = node.parent
+ while (attr && attr.type !== 'VAttribute') {
+ attr = attr.parent
+ }
+ if (attr && attr.directive && attr.key.name.name === 'bind') {
+ if (!attr.key.modifiers.some((mod) => mod.name === 'sync')) {
+ return
+ }
+ }
+
const nodes = utils.getMemberChaining(node)
const first = nodes[0]
let name
diff --git a/tests/lib/rules/no-mutating-props.js b/tests/lib/rules/no-mutating-props.js
index c0d6c2736..5c976baa2 100644
--- a/tests/lib/rules/no-mutating-props.js
+++ b/tests/lib/rules/no-mutating-props.js
@@ -331,6 +331,28 @@ ruleTester.run('no-mutating-props', rule, {
}
}
`
+ },
+
+ {
+ // script setup with shadow
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+ `
}
],
@@ -582,6 +604,42 @@ ruleTester.run('no-mutating-props', rule, {
}
]
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Unexpected mutation of "prop" prop.',
+ line: 4
+ },
+ {
+ message: 'Unexpected mutation of "prop" prop.',
+ line: 5
+ },
+ {
+ message: 'Unexpected mutation of "prop" prop.',
+ line: 10
+ }
+ ]
+ },
// setup
{
@@ -817,6 +875,43 @@ ruleTester.run('no-mutating-props', rule, {
line: 6
}
]
+ },
+
+ {
+ // script setup with shadow
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Unexpected mutation of "bar" prop.',
+ line: 4
+ },
+ {
+ message: 'Unexpected mutation of "window" prop.',
+ line: 5
+ },
+ {
+ message: 'Unexpected mutation of "Infinity" prop.',
+ line: 6
+ }
+ ]
}
]
})