Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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)

AltStyle によって変換されたページ (->オリジナル) /