Skip to content

Draft:Add new vue/require-mayberef-unwrap rule #2798

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/happy-corners-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-vue': minor
---

Added new [`vue/require-mayberef-unwrap`](https://eslint.vuejs.org/rules/require-mayberef-unwrap.html) rule
2 changes: 2 additions & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Rules in this category are enabled for all presets provided by eslint-plugin-vue
| [vue/no-watch-after-await] | disallow asynchronously registered `watch` | | :three::hammer: |
| [vue/prefer-import-from-vue] | enforce import from 'vue' instead of import from '@vue/*' | :wrench: | :three::hammer: |
| [vue/require-component-is] | require `v-bind:is` of `<component>` elements | | :three::two::warning: |
| [vue/require-mayberef-unwrap] | require `MaybeRef` values to be unwrapped with `unref()` before using in conditions | :bulb: | :three::warning: |
| [vue/require-prop-type-constructor] | require prop type to be a constructor | :wrench: | :three::two::hammer: |
| [vue/require-render-return] | enforce render function to always return value | | :three::two::warning: |
| [vue/require-slots-as-functions] | enforce properties of `$slots` to be used as a function | | :three::warning: |
Expand Down Expand Up @@ -564,6 +565,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
[vue/require-explicit-slots]: ./require-explicit-slots.md
[vue/require-expose]: ./require-expose.md
[vue/require-macro-variable-name]: ./require-macro-variable-name.md
[vue/require-mayberef-unwrap]: ./require-mayberef-unwrap.md
[vue/require-name-property]: ./require-name-property.md
[vue/require-prop-comment]: ./require-prop-comment.md
[vue/require-prop-type-constructor]: ./require-prop-type-constructor.md
Expand Down
91 changes: 91 additions & 0 deletions docs/rules/require-mayberef-unwrap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/require-mayberef-unwrap
description: require `MaybeRef` values to be unwrapped with `unref()` before using in conditions
---

# vue/require-mayberef-unwrap

> require `MaybeRef` values to be unwrapped with `unref()` before using in conditions

- :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"]`.
- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).

## :book: Rule Details

`MaybeRef<T>` and `MaybeRefOrGetter<T>` are TypeScript utility types provided by Vue.
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.
This rule reports (and can auto-fix) places where a `MaybeRef*` value is used directly in a conditional expression, logical expression, unary operator, etc.

<eslint-code-block fix :rules="{'vue/require-mayberef-unwrap': ['error']}">

```vue
<script setup lang="ts">
import { ref, unref, type MaybeRef } from 'vue'

const maybeRef: MaybeRef<boolean> = ref(false)

/* ✓ GOOD */
if (unref(maybeRef)) {
console.log('good')
}
const result = unref(maybeRef) ? 'true' : 'false'

/* ✗ BAD */
if (maybeRef) {
console.log('bad')
}
const alt = maybeRef ? 'true' : 'false'
</script>
```

</eslint-code-block>

### What is considered **incorrect** ?

The following patterns are **incorrect**:

```ts
// Condition without unref
if (maybeRef) {}

// Ternary operator
const result = maybeRef ? 'a' : 'b'

// Logical expressions
const value = maybeRef || 'fallback'

// Unary operators
const negated = !maybeRef

// Type queries & wrappers
const t = typeof maybeRef
const b = Boolean(maybeRef)
```

### What is considered **correct** ?

```ts
if (unref(maybeRef)) {}
const result = unref(maybeRef) ? 'a' : 'b'
```

## :wrench: Options

Nothing.

## :books: Further Reading

- [Guide – Reactivity – `unref`](https://vuejs.org/guide/essentials/reactivity-fundamentals.html#unref)
- [API – `MaybeRef`](https://vuejs.org/api/utility-types.html#mayberef)
- [API – `MaybeRefOrGetter`](https://vuejs.org/api/utility-types.html#maybereforgetter)

## :rocket: Version

This rule will be introduced in a future release of eslint-plugin-vue.

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-mayberef-unwrap.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-mayberef-unwrap.js)
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ const plugin = {
'require-explicit-slots': require('./rules/require-explicit-slots'),
'require-expose': require('./rules/require-expose'),
'require-macro-variable-name': require('./rules/require-macro-variable-name'),
'require-mayberef-unwrap': require('./rules/require-mayberef-unwrap'),
'require-name-property': require('./rules/require-name-property'),
'require-prop-comment': require('./rules/require-prop-comment'),
'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
Expand Down
137 changes: 137 additions & 0 deletions lib/rules/require-mayberef-unwrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* @author 2nofa11
* See LICENSE file in root directory for full license.
*/
'use strict'

const { findVariable } = require('@eslint-community/eslint-utils')
const utils = require('../utils')

/**
* Check TypeScript type node for MaybeRef/MaybeRefOrGetter
* @param {import('@typescript-eslint/types').TSESTree.TypeNode | undefined} typeNode
* @returns {boolean}
*/
function isMaybeRefTypeNode(typeNode) {
if (!typeNode) return false
if (
typeNode.type === 'TSTypeReference' &&
typeNode.typeName &&
typeNode.typeName.type === 'Identifier'
) {
return (
typeNode.typeName.name === 'MaybeRef' ||
typeNode.typeName.name === 'MaybeRefOrGetter'
)
}
if (typeNode.type === 'TSUnionType') {
return typeNode.types.some((t) => isMaybeRefTypeNode(t))
}
return false
}

module.exports = {
meta: {
type: 'problem',
docs: {
description:
'require `MaybeRef` values to be unwrapped with `unref()` before using in conditions',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/require-mayberef-unwrap.html'
},
fixable: null,
hasSuggestions: true,
schema: [],
messages: {
requireUnref:
'MaybeRef should be unwrapped with `unref()` before using in conditions. Use `unref({{name}})` instead.',
wrapWithUnref: 'Wrap with unref().'
}
},
/** @param {RuleContext} context */
create(context) {
/**
* Determine if identifier should be considered MaybeRef
* @param {Identifier} node
*/
function isMaybeRef(node) {
const variable = findVariable(utils.getScope(context, node), node)
const id = variable?.defs[0]?.node?.id
if (id?.type === 'Identifier' && id.typeAnnotation) {
return isMaybeRefTypeNode(id.typeAnnotation.typeAnnotation)
}
return false
}

/**
* Reports if the identifier is a MaybeRef type
* @param {Identifier} node
*/
function reportIfMaybeRef(node) {
if (!isMaybeRef(node)) return

const sourceCode = context.getSourceCode()
context.report({
node,
messageId: 'requireUnref',
data: { name: node.name },
suggest: [
{
messageId: 'wrapWithUnref',
/** @param {*} fixer */
fix(fixer) {
return fixer.replaceText(
node,
`unref(${sourceCode.getText(node)})`
)
}
}
]
})
}

return {
// if (maybeRef)
/** @param {Identifier} node */
'IfStatement>Identifier'(node) {
reportIfMaybeRef(node)
},
// maybeRef ? x : y
/** @param {Identifier & {parent: ConditionalExpression}} node */
'ConditionalExpression>Identifier'(node) {
if (node.parent.test === node) {
reportIfMaybeRef(node)
}
},
// !maybeRef, +maybeRef, -maybeRef, ~maybeRef, typeof maybeRef
/** @param {Identifier} node */
'UnaryExpression>Identifier'(node) {
reportIfMaybeRef(node)
},
// maybeRef || other, maybeRef && other, maybeRef ?? other
/** @param {Identifier & {parent: LogicalExpression}} node */
'LogicalExpression>Identifier'(node) {
reportIfMaybeRef(node)
},
// maybeRef == x, maybeRef != x, maybeRef === x, maybeRef !== x
/** @param {Identifier} node */
'BinaryExpression>Identifier'(node) {
reportIfMaybeRef(node)
},
// Boolean(maybeRef), String(maybeRef)
/** @param {Identifier} node */
'CallExpression>Identifier'(node) {
if (
node.parent &&
node.parent.type === 'CallExpression' &&
node.parent.callee &&
node.parent.callee.type === 'Identifier' &&
['Boolean', 'String'].includes(node.parent.callee.name) &&
node.parent.arguments[0] === node
) {
reportIfMaybeRef(node)
}
}
}
}
}
Loading