Skip to content

Commit

Permalink
Add vue/require-typed-ref rule (#2204)
Browse files Browse the repository at this point in the history
Co-authored-by: Flo Edelmann <git@flo-edelmann.de>
  • Loading branch information
Demivan and FloEdelmann committed Jun 9, 2023
1 parent 11f3f9f commit 81ce0ce
Show file tree
Hide file tree
Showing 7 changed files with 400 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ For example:
| [vue/require-macro-variable-name](./require-macro-variable-name.md) | require a certain macro variable name | :bulb: | :hammer: |
| [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | :bulb: | :hammer: |
| [vue/require-prop-comment](./require-prop-comment.md) | require props to have a comment | | :hammer: |
| [vue/require-typed-ref](./require-typed-ref.md) | require `ref` and `shallowRef` functions to be strongly typed | | :hammer: |
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: | :lipstick: |
| [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components | | :hammer: |
| [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: | :hammer: |
Expand Down
47 changes: 47 additions & 0 deletions docs/rules/require-typed-ref.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/require-typed-ref
description: require `ref` and `shallowRef` functions to be strongly typed
---
# vue/require-typed-ref

> require `ref` and `shallowRef` functions to be strongly typed
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>

## :book: Rule Details

This rule disallows calling `ref()` or `shallowRef()` functions without generic type parameter or an argument when using TypeScript.

With TypeScript it is easy to prevent usage of `any` by using [`noImplicitAny`](https://www.typescriptlang.org/tsconfig#noImplicitAny). Unfortunately this rule is easily bypassed with Vue `ref()` function. Calling `ref()` function without a generic parameter or an initial value leads to ref having `Ref<any>` type.

<eslint-code-block :rules="{'vue/require-typed-ref': ['error']}">

```vue
<script setup lang="ts">
import { ref, shallowRef, type Ref } from 'vue'
/* ✗ BAD */
const count = ref() // Returns Ref<any> that is not type checked
count.value = '50' // Should be a type error, but it is not
const count = shallowRef()
/* ✓ GOOD */
const count = ref<number>()
const count = ref(0)
const count: Ref<number | undefined> = ref()
</script>
```

</eslint-code-block>

## :wrench: Options

Nothing.

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-typed-ref.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-typed-ref.js)
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ module.exports = {
'require-render-return': require('./rules/require-render-return'),
'require-slots-as-functions': require('./rules/require-slots-as-functions'),
'require-toggle-inside-transition': require('./rules/require-toggle-inside-transition'),
'require-typed-ref': require('./rules/require-typed-ref'),
'require-v-for-key': require('./rules/require-v-for-key'),
'require-valid-default-prop': require('./rules/require-valid-default-prop'),
'return-in-computed-property': require('./rules/return-in-computed-property'),
Expand Down
102 changes: 102 additions & 0 deletions lib/rules/require-typed-ref.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @author Ivan Demchuk <https://github.com/Demivan>
* See LICENSE file in root directory for full license.
*/
'use strict'

const { iterateDefineRefs } = require('../utils/ref-object-references')
const utils = require('../utils')

/**
* @param {Expression|SpreadElement} node
*/
function isNullOrUndefined(node) {
return (
(node.type === 'Literal' && node.value === null) ||
(node.type === 'Identifier' && node.name === 'undefined')
)
}

/**
* @typedef {import('../utils/ref-object-references').RefObjectReferences} RefObjectReferences
*/

module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'require `ref` and `shallowRef` functions to be strongly typed',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/require-typed-ref.html'
},
fixable: null,
messages: {
noType:
'Specify type parameter for `{{name}}` function, otherwise created variable will not by typechecked.'
},
schema: []
},
/** @param {RuleContext} context */
create(context) {
const filename = context.getFilename()
if (!utils.isVueFile(filename) && !utils.isTypeScriptFile(filename)) {
return {}
}

const scriptSetup = utils.getScriptSetupElement(context)
if (
scriptSetup &&
!utils.hasAttribute(scriptSetup, 'lang', 'ts') &&
!utils.hasAttribute(scriptSetup, 'lang', 'typescript')
) {
return {}
}

const defines = iterateDefineRefs(context.getScope())

/**
* @param {string} name
* @param {CallExpression} node
*/
function report(name, node) {
context.report({
node,
messageId: 'noType',
data: {
name
}
})
}

return {
Program() {
for (const ref of defines) {
if (ref.name !== 'ref' && ref.name !== 'shallowRef') {
continue
}

if (
ref.node.arguments.length > 0 &&
!isNullOrUndefined(ref.node.arguments[0])
) {
continue
}

if (ref.node.typeParameters == null) {
if (
ref.node.parent.type === 'VariableDeclarator' &&
ref.node.parent.id.type === 'Identifier'
) {
if (ref.node.parent.id.typeAnnotation == null) {
report(ref.name, ref.node)
}
} else {
report(ref.name, ref.node)
}
}
}
}
}
}
}
9 changes: 9 additions & 0 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,8 @@ module.exports = {
return null
},

isTypeScriptFile,

isVueFile,

/**
Expand Down Expand Up @@ -2416,6 +2418,13 @@ function getVExpressionContainer(node) {
return n
}

/**
* @param {string} path
*/
function isTypeScriptFile(path) {
return path.endsWith('.ts') || path.endsWith('.tsx') || path.endsWith('.mts')
}

// ------------------------------------------------------------------------------
// Vue Helpers
// ------------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions lib/utils/ref-object-references.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ function getGlobalScope(context) {
}

module.exports = {
iterateDefineRefs,
extractRefObjectReferences,
extractReactiveVariableReferences
}
Expand Down

0 comments on commit 81ce0ce

Please sign in to comment.