Skip to content

Commit 61611ca

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

File tree

5 files changed

+657
-0
lines changed

5 files changed

+657
-0
lines changed

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 `MaybeRef` values to be unwrapped with `unref()` before using in conditions | :bulb: | :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: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
---
7+
8+
# vue/require-mayberef-unwrap
9+
10+
> require `MaybeRef` values to be unwrapped with `unref()` before using in conditions
11+
12+
- :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"]`.
13+
- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
14+
15+
## :book: Rule Details
16+
17+
`MaybeRef<T>` and `MaybeRefOrGetter<T>` are TypeScript utility types provided by Vue.
18+
They allow a value to be either a plain value **or** a `Ref<T>`. When such a variable is used in a boolean context you must first unwrap it with `unref()` so that the actual inner value is evaluated.
19+
This rule reports (and can auto-fix) places where a `MaybeRef*` value is used directly in a conditional expression, logical expression, unary operator, etc.
20+
21+
<eslint-code-block fix :rules="{'vue/require-mayberef-unwrap': ['error']}">
22+
23+
```vue
24+
<script setup lang="ts">
25+
import { ref, unref, type MaybeRef } from 'vue'
26+
27+
const maybeRef: MaybeRef<boolean> = ref(false)
28+
29+
/* ✓ GOOD */
30+
if (unref(maybeRef)) {
31+
console.log('good')
32+
}
33+
const result = unref(maybeRef) ? 'true' : 'false'
34+
35+
/* ✗ BAD */
36+
if (maybeRef) {
37+
console.log('bad')
38+
}
39+
const alt = maybeRef ? 'true' : 'false'
40+
</script>
41+
```
42+
43+
</eslint-code-block>
44+
45+
### What is considered **incorrect** ?
46+
47+
The following patterns are **incorrect**:
48+
49+
```ts
50+
// Condition without unref
51+
if (maybeRef) {}
52+
53+
// Ternary operator
54+
const result = maybeRef ? 'a' : 'b'
55+
56+
// Logical expressions
57+
const value = maybeRef || 'fallback'
58+
59+
// Unary operators
60+
const negated = !maybeRef
61+
62+
// Type queries & wrappers
63+
const t = typeof maybeRef
64+
const b = Boolean(maybeRef)
65+
```
66+
67+
### What is considered **correct** ?
68+
69+
```ts
70+
if (unref(maybeRef)) {}
71+
const result = unref(maybeRef) ? 'a' : 'b'
72+
```
73+
74+
## :wrench: Options
75+
76+
Nothing.
77+
78+
## :books: Further Reading
79+
80+
- [Guide – Reactivity – `unref`](https://vuejs.org/guide/essentials/reactivity-fundamentals.html#unref)
81+
- [API – `MaybeRef`](https://vuejs.org/api/utility-types.html#mayberef)
82+
- [API – `MaybeRefOrGetter`](https://vuejs.org/api/utility-types.html#maybereforgetter)
83+
84+
## :rocket: Version
85+
86+
This rule will be introduced in a future release of eslint-plugin-vue.
87+
88+
## :mag: Implementation
89+
90+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-mayberef-unwrap.js)
91+
- [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: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* @author 2nofa11
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const { findVariable } = require('@eslint-community/eslint-utils')
8+
const utils = require('../utils')
9+
10+
/**
11+
* Check TypeScript type node for MaybeRef/MaybeRefOrGetter
12+
* @param {import('@typescript-eslint/types').TSESTree.TypeNode | undefined} typeNode
13+
* @returns {boolean}
14+
*/
15+
function isMaybeRefTypeNode(typeNode) {
16+
if (!typeNode) return false
17+
if (
18+
typeNode.type === 'TSTypeReference' &&
19+
typeNode.typeName &&
20+
typeNode.typeName.type === 'Identifier'
21+
) {
22+
return (
23+
typeNode.typeName.name === 'MaybeRef' ||
24+
typeNode.typeName.name === 'MaybeRefOrGetter'
25+
)
26+
}
27+
if (typeNode.type === 'TSUnionType') {
28+
return typeNode.types.some((t) => isMaybeRefTypeNode(t))
29+
}
30+
return false
31+
}
32+
33+
module.exports = {
34+
meta: {
35+
type: 'problem',
36+
docs: {
37+
description:
38+
'require `MaybeRef` values to be unwrapped with `unref()` before using in conditions',
39+
categories: undefined,
40+
url: 'https://eslint.vuejs.org/rules/require-mayberef-unwrap.html'
41+
},
42+
fixable: null,
43+
hasSuggestions: true,
44+
schema: [],
45+
messages: {
46+
requireUnref:
47+
'MaybeRef should be unwrapped with `unref()` before using in conditions. Use `unref({{name}})` instead.',
48+
wrapWithUnref: 'Wrap with unref().'
49+
}
50+
},
51+
/** @param {RuleContext} context */
52+
create(context) {
53+
/**
54+
* Determine if identifier should be considered MaybeRef
55+
* @param {Identifier} node
56+
*/
57+
function isMaybeRef(node) {
58+
const variable = findVariable(utils.getScope(context, node), node)
59+
const id = variable?.defs[0]?.node?.id
60+
if (id?.type === 'Identifier' && id.typeAnnotation) {
61+
return isMaybeRefTypeNode(id.typeAnnotation.typeAnnotation)
62+
}
63+
return false
64+
}
65+
66+
/**
67+
* Reports if the identifier is a MaybeRef type
68+
* @param {Identifier} node
69+
*/
70+
function reportIfMaybeRef(node) {
71+
if (!isMaybeRef(node)) return
72+
73+
const sourceCode = context.getSourceCode()
74+
context.report({
75+
node,
76+
messageId: 'requireUnref',
77+
data: { name: node.name },
78+
suggest: [
79+
{
80+
messageId: 'wrapWithUnref',
81+
/** @param {*} fixer */
82+
fix(fixer) {
83+
return fixer.replaceText(
84+
node,
85+
`unref(${sourceCode.getText(node)})`
86+
)
87+
}
88+
}
89+
]
90+
})
91+
}
92+
93+
return {
94+
// if (maybeRef)
95+
/** @param {Identifier} node */
96+
'IfStatement>Identifier'(node) {
97+
reportIfMaybeRef(node)
98+
},
99+
// maybeRef ? x : y
100+
/** @param {Identifier & {parent: ConditionalExpression}} node */
101+
'ConditionalExpression>Identifier'(node) {
102+
if (node.parent.test === node) {
103+
reportIfMaybeRef(node)
104+
}
105+
},
106+
// !maybeRef, +maybeRef, -maybeRef, ~maybeRef, typeof maybeRef
107+
/** @param {Identifier} node */
108+
'UnaryExpression>Identifier'(node) {
109+
reportIfMaybeRef(node)
110+
},
111+
// maybeRef || other, maybeRef && other, maybeRef ?? other
112+
/** @param {Identifier & {parent: LogicalExpression}} node */
113+
'LogicalExpression>Identifier'(node) {
114+
reportIfMaybeRef(node)
115+
},
116+
// maybeRef == x, maybeRef != x, maybeRef === x, maybeRef !== x
117+
/** @param {Identifier} node */
118+
'BinaryExpression>Identifier'(node) {
119+
reportIfMaybeRef(node)
120+
},
121+
// Boolean(maybeRef), String(maybeRef)
122+
/** @param {Identifier} node */
123+
'CallExpression>Identifier'(node) {
124+
if (
125+
node.parent &&
126+
node.parent.type === 'CallExpression' &&
127+
node.parent.callee &&
128+
node.parent.callee.type === 'Identifier' &&
129+
['Boolean', 'String'].includes(node.parent.callee.name) &&
130+
node.parent.arguments[0] === node
131+
) {
132+
reportIfMaybeRef(node)
133+
}
134+
}
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)