Skip to content

Commit

Permalink
Add propProps options to vue/no-mutating-props (vuejs#1371)
Browse files Browse the repository at this point in the history
  • Loading branch information
FlareZh committed Apr 20, 2023
1 parent 6916db0 commit efd88f7
Show file tree
Hide file tree
Showing 4 changed files with 398 additions and 83 deletions.
72 changes: 71 additions & 1 deletion docs/rules/no-mutating-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ This rule reports mutation of component props.
<template>
<div>
<input v-model="value" @click="openModal">
<button @click="pushItem">Push Item</button>
<button @click="changeId">Change ID</button>
</div>
</template>
<script>
Expand All @@ -30,11 +32,25 @@ This rule reports mutation of component props.
value: {
type: String,
required: true
},
list: {
type: Array,
required: true
},
user: {
type: Object,
required: true
}
},
methods: {
openModal() {
this.value = 'test'
},
pushItem() {
this.list.push(0)
},
changeId() {
this.user.id = 1
}
}
}
Expand All @@ -50,6 +66,8 @@ This rule reports mutation of component props.
<template>
<div>
<input :value="value" @input="$emit('input', $event.target.value)" @click="openModal">
<button @click="pushItem">Push Item</button>
<button @click="changeId">Change ID</button>
</div>
</template>
<script>
Expand All @@ -58,11 +76,25 @@ This rule reports mutation of component props.
value: {
type: String,
required: true
},
list: {
type: Array,
required: true
},
user: {
type: Object,
required: true
}
},
methods: {
openModal() {
this.$emit('input', 'test')
},
pushItem() {
this.$emit('push', 0)
},
changeId() {
this.$emit('change-id', 1)
}
}
}
Expand All @@ -88,7 +120,45 @@ This rule reports mutation of component props.

## :wrench: Options

Nothing.
```json
{
"vue/no-mutating-props": ["error", {
"propProps": true
}]
}
```

- "propProps" (`boolean`) Avoid mutating the value of a prop but leaving the reference the same. Default is `true`.

### "propProps": false

<eslint-code-block :rules="{'vue/no-mutating-props': ['error', {propProps: false}]}">

```vue
<!-- ✓ GOOD -->
<template>
<div>
<input v-model="value.id" @click="openModal">
</div>
</template>
<script>
export default {
props: {
value: {
type: Object,
required: true
}
},
methods: {
openModal() {
this.value.visible = true
}
}
}
</script>
```

</eslint-code-block>

## :books: Further Reading

Expand Down
142 changes: 95 additions & 47 deletions lib/rules/no-mutating-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
*/
'use strict'

/**
* @typedef {{name?: string, set: Set<string>}} PropsInfo
*/

const utils = require('../utils')
const { findVariable } = require('@eslint-community/eslint-utils')

Expand Down Expand Up @@ -84,6 +88,19 @@ function isVmReference(node) {
return false
}

/**
* @param { object } options
* @param { boolean } options.propProps avoid mutating the value of a prop but leaving the reference the same
*/
function parseOptions(options) {
return Object.assign(
{
propProps: true
},
options
)
}

module.exports = {
meta: {
type: 'suggestion',
Expand All @@ -94,12 +111,21 @@ module.exports = {
},
fixable: null, // or "code" or "whitespace"
schema: [
// fill in your schema
{
type: 'object',
properties: {
propProps: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},
/** @param {RuleContext} context */
create(context) {
/** @type {Map<ObjectExpression|CallExpression, Set<string>>} */
const { propProps } = parseOptions(context.options[0])
/** @type {Map<ObjectExpression|CallExpression, PropsInfo>} */
const propsMap = new Map()
/** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression } | { type: 'setup', object: CallExpression } | null } */
let vueObjectData = null
Expand Down Expand Up @@ -138,9 +164,10 @@ module.exports = {
/**
* @param {MemberExpression|Identifier} props
* @param {string} name
* @param {boolean} isRootProps
*/
function verifyMutating(props, name) {
const invalid = utils.findMutating(props)
function verifyMutating(props, name, isRootProps = false) {
const invalid = utils.findMutating(props, propProps, isRootProps)
if (invalid) {
report(invalid.node, name)
}
Expand Down Expand Up @@ -192,8 +219,9 @@ module.exports = {
/**
* @param {Identifier} prop
* @param {string[]} path
* @param {boolean} isRootProps
*/
function verifyPropVariable(prop, path) {
function verifyPropVariable(prop, path, isRootProps = false) {
const variable = findVariable(context.getScope(), prop)
if (!variable) {
return
Expand All @@ -205,7 +233,7 @@ module.exports = {
}
const id = reference.identifier

const invalid = utils.findMutating(id)
const invalid = utils.findMutating(id, propProps, isRootProps)
if (!invalid) {
continue
}
Expand Down Expand Up @@ -252,20 +280,23 @@ module.exports = {
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
Expand Down Expand Up @@ -294,22 +325,25 @@ module.exports = {
target.parent.id,
[]
)) {
verifyPropVariable(prop, path)
propsSet.add(prop.name)
if (path.length === 0) {
propsInfo.name = prop.name
} else {
propsInfo.set.add(prop.name)
}
verifyPropVariable(prop, path, propsInfo.name === 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 (
Expand Down Expand Up @@ -341,7 +375,11 @@ module.exports = {
propsParam,
[]
)) {
verifyPropVariable(prop, path)
verifyPropVariable(
prop,
path,
prop.parent.type === 'FunctionExpression'
)
}
},
/** @param {(Identifier | ThisExpression) & { parent: MemberExpression } } node */
Expand All @@ -359,7 +397,7 @@ module.exports = {
const name = utils.getStaticPropertyName(mem)
if (
name &&
/** @type {Set<string>} */ (propsMap.get(vueNode)).has(name)
/** @type {PropsInfo} */ (propsMap.get(vueNode)).set.has(name)
) {
verifyMutating(mem, name)
}
Expand All @@ -378,9 +416,9 @@ module.exports = {
const name = utils.getStaticPropertyName(mem)
if (
name &&
/** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
name
)
/** @type {PropsInfo} */ (
propsMap.get(vueObjectData.object)
).set.has(name)
) {
verifyMutating(mem, name)
}
Expand All @@ -393,14 +431,19 @@ module.exports = {
if (!isVmReference(node)) {
return
}
const name = node.name
if (
name &&
/** @type {Set<string>} */ (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 parentProperty =
parent.type === 'MemberExpression' ? parent.property : null
const name =
isRootProps && parentProperty?.type === 'Identifier'
? parentProperty.name
: node.name
if (name && (propsInfo.set.has(name) || isRootProps)) {
verifyMutating(node, name, isRootProps)
}
},
/** @param {ESNode} node */
Expand All @@ -423,12 +466,22 @@ 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)) {
if (isVmReference(first) && first.name !== propsInfo.name) {
if (!propProps && nodes.length > 1) {
return
}
name = first.name
} else if (first.type === 'ThisExpression') {
} else if (first.type === 'ThisExpression' || isVmReference(first)) {
if (!propProps && nodes.length > 2) {
return
}
const mem = nodes[1]
if (!mem) {
return
Expand All @@ -437,12 +490,7 @@ module.exports = {
} else {
return
}
if (
name &&
/** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
name
)
) {
if (name && propsInfo.set.has(name)) {
report(node, name)
}
}
Expand Down

0 comments on commit efd88f7

Please sign in to comment.