Skip to content

Commit 236ff7e

Browse files
committed
Add new vue/require-mayberef-unwrap rule
1 parent 3d9e15e commit 236ff7e

File tree

6 files changed

+778
-0
lines changed

6 files changed

+778
-0
lines changed

.changeset/happy-corners-shop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-vue': minor
3+
---
4+
5+
Added new [`vue/require-mayberef-unwrap`](https://eslint.vuejs.org/rules/require-mayberef-unwrap.html) rule

docs/rules/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ Rules in this category are enabled for all presets provided by eslint-plugin-vue
9797
| [vue/no-watch-after-await] | disallow asynchronously registered `watch` | | :three::hammer: |
9898
| [vue/prefer-import-from-vue] | enforce import from 'vue' instead of import from '@vue/*' | :wrench: | :three::hammer: |
9999
| [vue/require-component-is] | require `v-bind:is` of `<component>` elements | | :three::two::warning: |
100+
| [vue/require-mayberef-unwrap] | require unwrapping `MaybeRef` values with `unref()` in conditions | :wrench: | :three::warning: |
100101
| [vue/require-prop-type-constructor] | require prop type to be a constructor | :wrench: | :three::two::hammer: |
101102
| [vue/require-render-return] | enforce render function to always return value | | :three::two::warning: |
102103
| [vue/require-slots-as-functions] | enforce properties of `$slots` to be used as a function | | :three::warning: |
@@ -564,6 +565,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
564565
[vue/require-explicit-slots]: ./require-explicit-slots.md
565566
[vue/require-expose]: ./require-expose.md
566567
[vue/require-macro-variable-name]: ./require-macro-variable-name.md
568+
[vue/require-mayberef-unwrap]: ./require-mayberef-unwrap.md
567569
[vue/require-name-property]: ./require-name-property.md
568570
[vue/require-prop-comment]: ./require-prop-comment.md
569571
[vue/require-prop-type-constructor]: ./require-prop-type-constructor.md

docs/rules/require-mayberef-unwrap.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/require-mayberef-unwrap
5+
description: require `MaybeRef` values to be unwrapped with `unref()` before using in conditions
6+
since: v10.3.0
7+
---
8+
9+
# vue/require-mayberef-unwrap
10+
11+
> require unwrapping `MaybeRef` values with `unref()` in conditions
12+
13+
- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
14+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fix-problems) can automatically fix some of the problems reported by this rule.
15+
16+
## :book: Rule Details
17+
18+
This rule reports cases where a `MaybeRef` value is used incorrectly in conditions.
19+
You must use `unref()` to access the inner value.
20+
21+
<eslint-code-block fix :rules="{'vue/require-mayberef-unwrap': ['error']}">
22+
23+
```vue
24+
<script lang="ts">
25+
import { ref, unref, type MaybeRef } from 'vue'
26+
27+
export default {
28+
setup() {
29+
const maybeRef: MaybeRef<boolean> = ref(false)
30+
31+
/* ✓ GOOD */
32+
if (unref(maybeRef)) {
33+
console.log('good')
34+
}
35+
const result = unref(maybeRef) ? 'true' : 'false'
36+
37+
/* ✗ BAD */
38+
if (maybeRef) {
39+
console.log('bad')
40+
}
41+
const alt = maybeRef ? 'true' : 'false'
42+
43+
return {
44+
maybeRef
45+
}
46+
}
47+
}
48+
</script>
49+
```
50+
51+
</eslint-code-block>
52+
53+
## :wrench: Options
54+
55+
Nothing.
56+
57+
This rule also applies to `MaybeRefOrGetter` values in addition to `MaybeRef`.
58+
59+
## :books: Further Reading
60+
61+
- [Guide – Reactivity – `unref`](https://vuejs.org/guide/essentials/reactivity-fundamentals.html#unref)
62+
- [API – `MaybeRef`](https://vuejs.org/api/utility-types.html#mayberef)
63+
- [API – `MaybeRefOrGetter`](https://vuejs.org/api/utility-types.html#maybereforgetter)
64+
65+
## :rocket: Version
66+
67+
This rule was introduced in eslint-plugin-vue v10.3.0
68+
69+
## :mag: Implementation
70+
71+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-mayberef-unwrap.js)
72+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-mayberef-unwrap.js)

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ const plugin = {
223223
'require-explicit-slots': require('./rules/require-explicit-slots'),
224224
'require-expose': require('./rules/require-expose'),
225225
'require-macro-variable-name': require('./rules/require-macro-variable-name'),
226+
'require-mayberef-unwrap': require('./rules/require-mayberef-unwrap'),
226227
'require-name-property': require('./rules/require-name-property'),
227228
'require-prop-comment': require('./rules/require-prop-comment'),
228229
'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),

lib/rules/require-mayberef-unwrap.js

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/**
2+
* @author 2nofa11
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
/**
10+
* Check TypeScript type node for MaybeRef/MaybeRefOrGetter
11+
* @param {import('@typescript-eslint/types').TSESTree.TypeNode | undefined} typeNode
12+
* @returns {boolean}
13+
*/
14+
function isMaybeRefTypeNode(typeNode) {
15+
if (!typeNode) return false
16+
if (
17+
typeNode.type === 'TSTypeReference' &&
18+
typeNode.typeName &&
19+
typeNode.typeName.type === 'Identifier'
20+
) {
21+
return (
22+
typeNode.typeName.name === 'MaybeRef' ||
23+
typeNode.typeName.name === 'MaybeRefOrGetter'
24+
)
25+
}
26+
if (typeNode.type === 'TSUnionType') {
27+
return typeNode.types.some((t) => isMaybeRefTypeNode(t))
28+
}
29+
return false
30+
}
31+
32+
module.exports = {
33+
meta: {
34+
type: 'problem',
35+
docs: {
36+
description:
37+
'require `MaybeRef` values to be unwrapped with `unref()` before using in conditions',
38+
categories: undefined,
39+
url: 'https://eslint.vuejs.org/rules/require-mayberef-unwrap.html'
40+
},
41+
fixable: 'code',
42+
schema: [],
43+
messages: {
44+
requireUnref:
45+
'MaybeRef should be unwrapped with `unref()` before using in conditions. Use `unref({{name}})` instead.'
46+
}
47+
},
48+
/** @param {RuleContext} context */
49+
create(context) {
50+
const filename = context.getFilename()
51+
if (!utils.isVueFile(filename) && !utils.isTypeScriptFile(filename)) {
52+
return {}
53+
}
54+
55+
/** @type {Map<string, Set<string>>} */
56+
const maybeRefPropsMap = new Map()
57+
58+
/**
59+
* Determine if identifier should be considered MaybeRef
60+
* @param {Identifier} node
61+
*/
62+
function isMaybeRef(node) {
63+
const variable = utils.findVariableByIdentifier(context, node)
64+
if (!variable) {
65+
return false
66+
}
67+
68+
const definition = variable.defs[0]
69+
if (definition.type !== 'Variable') {
70+
return false
71+
}
72+
73+
const id = definition.node?.id
74+
if (!id || id.type !== 'Identifier' || !id.typeAnnotation) {
75+
return false
76+
}
77+
78+
return isMaybeRefTypeNode(id.typeAnnotation.typeAnnotation)
79+
}
80+
81+
/**
82+
* Check if MemberExpression accesses a MaybeRef prop
83+
* @param {Identifier} objectNode
84+
* @param {string} propertyName
85+
*/
86+
function isMaybeRefPropsAccess(objectNode, propertyName) {
87+
if (!propertyName) {
88+
return false
89+
}
90+
91+
const variable = utils.findVariableByIdentifier(context, objectNode)
92+
if (!variable) {
93+
return false
94+
}
95+
96+
const maybeRefProps = maybeRefPropsMap.get(variable.name)
97+
return maybeRefProps ? maybeRefProps.has(propertyName) : false
98+
}
99+
100+
/**
101+
* Reports if the identifier is a MaybeRef type
102+
* @param {Identifier} node
103+
* @param {string} [customName] Custom name for error message
104+
*/
105+
function reportIfMaybeRef(node, customName) {
106+
if (!isMaybeRef(node)) {
107+
return
108+
}
109+
110+
const sourceCode = context.getSourceCode()
111+
context.report({
112+
node,
113+
messageId: 'requireUnref',
114+
data: { name: customName || node.name },
115+
fix(fixer) {
116+
return fixer.replaceText(node, `unref(${sourceCode.getText(node)})`)
117+
}
118+
})
119+
}
120+
121+
/**
122+
* Reports if the MemberExpression accesses a MaybeRef prop
123+
* @param {MemberExpression} node
124+
*/
125+
function reportIfMaybeRefProps(node) {
126+
if (node.object.type !== 'Identifier') {
127+
return
128+
}
129+
130+
const propertyName = utils.getStaticPropertyName(node)
131+
if (!propertyName) {
132+
return
133+
}
134+
135+
if (!isMaybeRefPropsAccess(node.object, propertyName)) {
136+
return
137+
}
138+
139+
const sourceCode = context.getSourceCode()
140+
context.report({
141+
node: node.property,
142+
messageId: 'requireUnref',
143+
data: { name: `${node.object.name}.${propertyName}` },
144+
fix(fixer) {
145+
return fixer.replaceText(node, `unref(${sourceCode.getText(node)})`)
146+
}
147+
})
148+
}
149+
150+
return utils.compositingVisitors(
151+
{
152+
// if (maybeRef)
153+
/** @param {Identifier} node */
154+
'IfStatement>Identifier'(node) {
155+
reportIfMaybeRef(node)
156+
},
157+
// maybeRef ? x : y
158+
/** @param {Identifier & {parent: ConditionalExpression}} node */
159+
'ConditionalExpression>Identifier'(node) {
160+
if (node.parent.test !== node) {
161+
return
162+
}
163+
reportIfMaybeRef(node)
164+
},
165+
// !maybeRef, +maybeRef, -maybeRef, ~maybeRef, typeof maybeRef
166+
/** @param {Identifier} node */
167+
'UnaryExpression>Identifier'(node) {
168+
reportIfMaybeRef(node)
169+
},
170+
// maybeRef || other, maybeRef && other, maybeRef ?? other
171+
/** @param {Identifier & {parent: LogicalExpression}} node */
172+
'LogicalExpression>Identifier'(node) {
173+
reportIfMaybeRef(node)
174+
},
175+
// maybeRef == x, maybeRef != x, maybeRef === x, maybeRef !== x
176+
/** @param {Identifier} node */
177+
'BinaryExpression>Identifier'(node) {
178+
reportIfMaybeRef(node)
179+
},
180+
// Boolean(maybeRef), String(maybeRef)
181+
/** @param {Identifier} node */
182+
'CallExpression>Identifier'(node) {
183+
const parent = node.parent
184+
if (parent?.type !== 'CallExpression') return
185+
186+
const callee = parent.callee
187+
if (callee?.type !== 'Identifier') return
188+
189+
if (!['Boolean', 'String'].includes(callee.name)) return
190+
191+
if (parent.arguments[0] === node) {
192+
reportIfMaybeRef(node)
193+
}
194+
},
195+
// props.maybeRefProp
196+
/** @param {MemberExpression} node */
197+
MemberExpression(node) {
198+
reportIfMaybeRefProps(node)
199+
}
200+
},
201+
utils.defineScriptSetupVisitor(context, {
202+
onDefinePropsEnter(node, props) {
203+
if (
204+
!node.parent ||
205+
node.parent.type !== 'VariableDeclarator' ||
206+
node.parent.init !== node
207+
) {
208+
return
209+
}
210+
211+
const propsParam = node.parent.id
212+
if (propsParam.type !== 'Identifier') {
213+
return
214+
}
215+
216+
const maybeRefProps = new Set()
217+
for (const prop of props) {
218+
if (prop.type !== 'type' || !prop.node) {
219+
continue
220+
}
221+
222+
if (
223+
prop.node.type !== 'TSPropertySignature' ||
224+
!prop.node.typeAnnotation
225+
) {
226+
continue
227+
}
228+
229+
const typeAnnotation = prop.node.typeAnnotation.typeAnnotation
230+
if (isMaybeRefTypeNode(typeAnnotation)) {
231+
maybeRefProps.add(prop.propName)
232+
}
233+
}
234+
235+
if (maybeRefProps.size > 0) {
236+
maybeRefPropsMap.set(propsParam.name, maybeRefProps)
237+
}
238+
}
239+
}),
240+
utils.defineVueVisitor(context, {
241+
onSetupFunctionEnter(node) {
242+
const propsParam = utils.skipDefaultParamValue(node.params[0])
243+
if (!propsParam || propsParam.type !== 'Identifier') {
244+
return
245+
}
246+
247+
if (!propsParam.typeAnnotation) {
248+
return
249+
}
250+
251+
const typeAnnotation = propsParam.typeAnnotation.typeAnnotation
252+
const maybeRefProps = new Set()
253+
254+
if (typeAnnotation.type === 'TSTypeLiteral') {
255+
for (const member of typeAnnotation.members) {
256+
if (
257+
member.type === 'TSPropertySignature' &&
258+
member.key &&
259+
member.key.type === 'Identifier' &&
260+
member.typeAnnotation &&
261+
isMaybeRefTypeNode(member.typeAnnotation.typeAnnotation)
262+
) {
263+
maybeRefProps.add(member.key.name)
264+
}
265+
}
266+
}
267+
268+
if (maybeRefProps.size > 0) {
269+
maybeRefPropsMap.set(propsParam.name, maybeRefProps)
270+
}
271+
},
272+
onVueObjectExit() {
273+
maybeRefPropsMap.clear()
274+
}
275+
})
276+
)
277+
}
278+
}

0 commit comments

Comments
 (0)