Skip to content

Commit b34b1a9

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

File tree

5 files changed

+640
-0
lines changed

5 files changed

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

0 commit comments

Comments
 (0)