diff --git a/docs/rules/no-mutating-props.md b/docs/rules/no-mutating-props.md
index 960ebe900..62c76680d 100644
--- a/docs/rules/no-mutating-props.md
+++ b/docs/rules/no-mutating-props.md
@@ -22,6 +22,8 @@ This rule reports mutation of component props.
+
+
+```
+
+
## :books: Further Reading
diff --git a/lib/rules/no-mutating-props.js b/lib/rules/no-mutating-props.js
index f79613d9c..61461010b 100644
--- a/lib/rules/no-mutating-props.js
+++ b/lib/rules/no-mutating-props.js
@@ -4,6 +4,10 @@
*/
'use strict'
+/**
+ * @typedef {{name?: string, set: Set}} PropsInfo
+ */
+
const utils = require('../utils')
const { findVariable } = require('@eslint-community/eslint-utils')
@@ -84,6 +88,19 @@ function isVmReference(node) {
return false
}
+/**
+ * @param { object } options
+ * @param { boolean } options.shallowOnly Enables mutating the value of a prop but leaving the reference the same
+ */
+function parseOptions(options) {
+ return Object.assign(
+ {
+ shallowOnly: false
+ },
+ options
+ )
+}
+
module.exports = {
meta: {
type: 'suggestion',
@@ -94,12 +111,21 @@ module.exports = {
},
fixable: null, // or "code" or "whitespace"
schema: [
- // fill in your schema
+ {
+ type: 'object',
+ properties: {
+ shallowOnly: {
+ type: 'boolean'
+ }
+ },
+ additionalProperties: false
+ }
]
},
/** @param {RuleContext} context */
create(context) {
- /** @type {Map>} */
+ const { shallowOnly } = parseOptions(context.options[0])
+ /** @type {Map} */
const propsMap = new Map()
/** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression } | { type: 'setup', object: CallExpression } | null } */
let vueObjectData = null
@@ -138,10 +164,11 @@ module.exports = {
/**
* @param {MemberExpression|Identifier} props
* @param {string} name
+ * @param {boolean} isRootProps
*/
- function verifyMutating(props, name) {
+ function verifyMutating(props, name, isRootProps = false) {
const invalid = utils.findMutating(props)
- if (invalid) {
+ if (invalid && isShallowOnlyInvalid(invalid, isRootProps)) {
report(invalid.node, name)
}
}
@@ -210,6 +237,9 @@ module.exports = {
continue
}
let name
+ if (!isShallowOnlyInvalid(invalid, path.length === 0)) {
+ continue
+ }
if (path.length === 0) {
if (invalid.pathNodes.length === 0) {
continue
@@ -246,26 +276,43 @@ module.exports = {
}
}
+ /**
+ * Is shallowOnly false or the prop reassigned
+ * @param {Exclude, null>} invalid
+ * @param {boolean} isRootProps
+ * @return {boolean}
+ */
+ function isShallowOnlyInvalid(invalid, isRootProps) {
+ return (
+ !shallowOnly ||
+ (invalid.pathNodes.length === (isRootProps ? 1 : 0) &&
+ ['assignment', 'update'].includes(invalid.kind))
+ )
+ }
+
return utils.compositingVisitors(
{},
utils.defineScriptSetupVisitor(context, {
onDefinePropsEnter(node, props) {
const defineVariableNames = new Set(extractDefineVariableNames())
- const propsSet = new Set(
- 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)
+ const propsInfo = {
+ name: '',
+ set: new Set(
+ 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, propsInfo)
vueObjectData = {
type: 'setup',
object: node
@@ -294,22 +341,25 @@ module.exports = {
target.parent.id,
[]
)) {
+ if (path.length === 0) {
+ propsInfo.name = prop.name
+ } else {
+ propsInfo.set.add(prop.name)
+ }
verifyPropVariable(prop, path)
- propsSet.add(prop.name)
}
}
}),
utils.defineVueVisitor(context, {
onVueObjectEnter(node) {
- propsMap.set(
- node,
- new Set(
+ propsMap.set(node, {
+ set: new Set(
utils
.getComponentPropsFromOptions(node)
.map((p) => p.propName)
.filter(utils.isDef)
)
- )
+ })
},
onVueObjectExit(node, { type }) {
if (
@@ -359,7 +409,7 @@ module.exports = {
const name = utils.getStaticPropertyName(mem)
if (
name &&
- /** @type {Set} */ (propsMap.get(vueNode)).has(name)
+ /** @type {PropsInfo} */ (propsMap.get(vueNode)).set.has(name)
) {
verifyMutating(mem, name)
}
@@ -378,9 +428,9 @@ module.exports = {
const name = utils.getStaticPropertyName(mem)
if (
name &&
- /** @type {Set} */ (propsMap.get(vueObjectData.object)).has(
- name
- )
+ /** @type {PropsInfo} */ (
+ propsMap.get(vueObjectData.object)
+ ).set.has(name)
) {
verifyMutating(mem, name)
}
@@ -393,14 +443,18 @@ module.exports = {
if (!isVmReference(node)) {
return
}
- const name = node.name
- if (
- name &&
- /** @type {Set} */ (propsMap.get(vueObjectData.object)).has(
- name
- )
- ) {
- verifyMutating(node, name)
+ const propsInfo = /** @type {PropsInfo} */ (
+ propsMap.get(vueObjectData.object)
+ )
+ const isRootProps = !!node.name && propsInfo.name === node.name
+ const parent = node.parent
+ const name =
+ (isRootProps &&
+ parent.type === 'MemberExpression' &&
+ utils.getStaticPropertyName(parent)) ||
+ node.name
+ if (name && (propsInfo.set.has(name) || isRootProps)) {
+ verifyMutating(node, name, isRootProps)
}
},
/** @param {ESNode} node */
@@ -423,28 +477,45 @@ module.exports = {
return
}
+ const propsInfo = /** @type {PropsInfo} */ (
+ propsMap.get(vueObjectData.object)
+ )
+
const nodes = utils.getMemberChaining(node)
const first = nodes[0]
let name
if (isVmReference(first)) {
- name = first.name
+ if (first.name === propsInfo.name) {
+ // props variable
+ if (shallowOnly && nodes.length > 2) {
+ return
+ }
+ name = (nodes[1] && getPropertyNameText(nodes[1])) || first.name
+ } else {
+ if (shallowOnly && nodes.length > 1) {
+ return
+ }
+ name = first.name
+ if (!name || !propsInfo.set.has(name)) {
+ return
+ }
+ }
} else if (first.type === 'ThisExpression') {
+ if (shallowOnly && nodes.length > 2) {
+ return
+ }
const mem = nodes[1]
if (!mem) {
return
}
name = utils.getStaticPropertyName(mem)
+ if (!name || !propsInfo.set.has(name)) {
+ return
+ }
} else {
return
}
- if (
- name &&
- /** @type {Set} */ (propsMap.get(vueObjectData.object)).has(
- name
- )
- ) {
- report(node, name)
- }
+ report(node, name)
}
})
)
diff --git a/tests/lib/rules/no-mutating-props.js b/tests/lib/rules/no-mutating-props.js
index 7338d9374..7c4884847 100644
--- a/tests/lib/rules/no-mutating-props.js
+++ b/tests/lib/rules/no-mutating-props.js
@@ -181,6 +181,37 @@ ruleTester.run('no-mutating-props', rule, {
`
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ options: [{ shallowOnly: true }]
+ },
// setup
{
@@ -325,6 +356,43 @@ ruleTester.run('no-mutating-props', rule, {
`
},
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ shallowOnly: true }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ shallowOnly: true }]
+ },
+
{
// script setup with shadow
filename: 'test.vue',
@@ -642,6 +710,63 @@ ruleTester.run('no-mutating-props', rule, {
}
]
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: [{ shallowOnly: true }],
+ errors: [
+ {
+ message: 'Unexpected mutation of "prop1" prop.',
+ line: 4
+ },
+ {
+ message: 'Unexpected mutation of "prop2" prop.',
+ line: 5
+ },
+ {
+ message: 'Unexpected mutation of "prop5" prop.',
+ line: 8
+ },
+ {
+ message: 'Unexpected mutation of "prop6" prop.',
+ line: 9
+ },
+ {
+ message: 'Unexpected mutation of "prop10" prop.',
+ line: 13
+ },
+ {
+ message: 'Unexpected mutation of "prop10" prop.',
+ line: 22
+ }
+ ]
+ },
// setup
{
@@ -820,6 +945,24 @@ ruleTester.run('no-mutating-props', rule, {
errors: ['Unexpected mutation of "[a]" prop.']
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Unexpected mutation of "[foo]" prop.',
+ line: 7
+ }
+ ]
+ },
{
filename: 'test.vue',
code: `
@@ -839,7 +982,7 @@ ruleTester.run('no-mutating-props', rule, {
line: 3
},
{
- message: 'Unexpected mutation of "props" prop.',
+ message: 'Unexpected mutation of "value" prop.',
line: 4
}
]
@@ -898,6 +1041,45 @@ ruleTester.run('no-mutating-props', rule, {
}
]
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ options: [{ shallowOnly: true }],
+ errors: [
+ {
+ message: 'Unexpected mutation of "a" prop.',
+ line: 5
+ },
+ {
+ message: 'Unexpected mutation of "b" prop.',
+ line: 6
+ },
+ {
+ message: 'Unexpected mutation of "d" prop.',
+ line: 8
+ },
+ {
+ message: 'Unexpected mutation of "a" prop.',
+ line: 11
+ }
+ ]
+ },
{
// script setup with shadow
@@ -911,13 +1093,15 @@ ruleTester.run('no-mutating-props', rule, {
`,
errors: [
@@ -932,6 +1116,50 @@ ruleTester.run('no-mutating-props', rule, {
{
message: 'Unexpected mutation of "Infinity" prop.',
line: 6
+ },
+ {
+ message: 'Unexpected mutation of "obj" prop.',
+ line: 18
+ }
+ ]
+ },
+
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+
+
+
+ `,
+ options: [{ shallowOnly: true }],
+ errors: [
+ {
+ message: 'Unexpected mutation of "a" prop.',
+ line: 3
+ },
+ {
+ message: 'Unexpected mutation of "a" prop.',
+ line: 4
+ },
+ {
+ message: 'Unexpected mutation of "a" prop.',
+ line: 7
+ },
+ {
+ message: 'Unexpected mutation of "a" prop.',
+ line: 15
}
]
}