Skip to content

Commit

Permalink
Add shallowOnly option to vue/no-mutating-props (#2135)
Browse files Browse the repository at this point in the history
Co-authored-by: Yosuke Ota <otameshiyo23@gmail.com>
  • Loading branch information
FlareZh and ota-meshi committed May 10, 2023
1 parent d58fb19 commit 3cbb1b3
Show file tree
Hide file tree
Showing 3 changed files with 416 additions and 47 deletions.
72 changes: 71 additions & 1 deletion docs/rules/no-mutating-props.md
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", {
"shallowOnly": false
}]
}
```

- "shallowOnly" (`boolean`) Enables mutating the value of a prop but leaving the reference the same. Default is `false`.

### "shallowOnly": true

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

```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
159 changes: 115 additions & 44 deletions lib/rules/no-mutating-props.js
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.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',
Expand All @@ -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<ObjectExpression|CallExpression, Set<string>>} */
const { shallowOnly } = 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,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)
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -246,26 +276,43 @@ module.exports = {
}
}

/**
* Is shallowOnly false or the prop reassigned
* @param {Exclude<ReturnType<typeof utils.findMutating>, 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
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -359,7 +409,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 +428,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 +443,18 @@ 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 name =
(isRootProps &&
parent.type === 'MemberExpression' &&
utils.getStaticPropertyName(parent)) ||
node.name
if (name && (propsInfo.set.has(name) || isRootProps)) {
verifyMutating(node, name, isRootProps)
}
},
/** @param {ESNode} node */
Expand All @@ -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<string>} */ (propsMap.get(vueObjectData.object)).has(
name
)
) {
report(node, name)
}
report(node, name)
}
})
)
Expand Down

0 comments on commit 3cbb1b3

Please sign in to comment.