Skip to content

Commit 81ce0ce

Browse files
DemivanFloEdelmann
andauthored
Add vue/require-typed-ref rule (#2204)
Co-authored-by: Flo Edelmann <[email protected]>
1 parent 11f3f9f commit 81ce0ce

File tree

7 files changed

+400
-0
lines changed

7 files changed

+400
-0
lines changed

docs/rules/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ For example:
265265
| [vue/require-macro-variable-name](./require-macro-variable-name.md) | require a certain macro variable name | :bulb: | :hammer: |
266266
| [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | :bulb: | :hammer: |
267267
| [vue/require-prop-comment](./require-prop-comment.md) | require props to have a comment | | :hammer: |
268+
| [vue/require-typed-ref](./require-typed-ref.md) | require `ref` and `shallowRef` functions to be strongly typed | | :hammer: |
268269
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: | :lipstick: |
269270
| [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components | | :hammer: |
270271
| [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: | :hammer: |

docs/rules/require-typed-ref.md

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/require-typed-ref
5+
description: require `ref` and `shallowRef` functions to be strongly typed
6+
---
7+
# vue/require-typed-ref
8+
9+
> require `ref` and `shallowRef` functions to be strongly typed
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
13+
## :book: Rule Details
14+
15+
This rule disallows calling `ref()` or `shallowRef()` functions without generic type parameter or an argument when using TypeScript.
16+
17+
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.
18+
19+
<eslint-code-block :rules="{'vue/require-typed-ref': ['error']}">
20+
21+
```vue
22+
<script setup lang="ts">
23+
import { ref, shallowRef, type Ref } from 'vue'
24+
25+
/* ✗ BAD */
26+
const count = ref() // Returns Ref<any> that is not type checked
27+
count.value = '50' // Should be a type error, but it is not
28+
29+
const count = shallowRef()
30+
31+
/* ✓ GOOD */
32+
const count = ref<number>()
33+
const count = ref(0)
34+
const count: Ref<number | undefined> = ref()
35+
</script>
36+
```
37+
38+
</eslint-code-block>
39+
40+
## :wrench: Options
41+
42+
Nothing.
43+
44+
## :mag: Implementation
45+
46+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-typed-ref.js)
47+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-typed-ref.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ module.exports = {
189189
'require-render-return': require('./rules/require-render-return'),
190190
'require-slots-as-functions': require('./rules/require-slots-as-functions'),
191191
'require-toggle-inside-transition': require('./rules/require-toggle-inside-transition'),
192+
'require-typed-ref': require('./rules/require-typed-ref'),
192193
'require-v-for-key': require('./rules/require-v-for-key'),
193194
'require-valid-default-prop': require('./rules/require-valid-default-prop'),
194195
'return-in-computed-property': require('./rules/return-in-computed-property'),

lib/rules/require-typed-ref.js

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* @author Ivan Demchuk <https://github.com/Demivan>
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const { iterateDefineRefs } = require('../utils/ref-object-references')
8+
const utils = require('../utils')
9+
10+
/**
11+
* @param {Expression|SpreadElement} node
12+
*/
13+
function isNullOrUndefined(node) {
14+
return (
15+
(node.type === 'Literal' && node.value === null) ||
16+
(node.type === 'Identifier' && node.name === 'undefined')
17+
)
18+
}
19+
20+
/**
21+
* @typedef {import('../utils/ref-object-references').RefObjectReferences} RefObjectReferences
22+
*/
23+
24+
module.exports = {
25+
meta: {
26+
type: 'suggestion',
27+
docs: {
28+
description:
29+
'require `ref` and `shallowRef` functions to be strongly typed',
30+
categories: undefined,
31+
url: 'https://eslint.vuejs.org/rules/require-typed-ref.html'
32+
},
33+
fixable: null,
34+
messages: {
35+
noType:
36+
'Specify type parameter for `{{name}}` function, otherwise created variable will not by typechecked.'
37+
},
38+
schema: []
39+
},
40+
/** @param {RuleContext} context */
41+
create(context) {
42+
const filename = context.getFilename()
43+
if (!utils.isVueFile(filename) && !utils.isTypeScriptFile(filename)) {
44+
return {}
45+
}
46+
47+
const scriptSetup = utils.getScriptSetupElement(context)
48+
if (
49+
scriptSetup &&
50+
!utils.hasAttribute(scriptSetup, 'lang', 'ts') &&
51+
!utils.hasAttribute(scriptSetup, 'lang', 'typescript')
52+
) {
53+
return {}
54+
}
55+
56+
const defines = iterateDefineRefs(context.getScope())
57+
58+
/**
59+
* @param {string} name
60+
* @param {CallExpression} node
61+
*/
62+
function report(name, node) {
63+
context.report({
64+
node,
65+
messageId: 'noType',
66+
data: {
67+
name
68+
}
69+
})
70+
}
71+
72+
return {
73+
Program() {
74+
for (const ref of defines) {
75+
if (ref.name !== 'ref' && ref.name !== 'shallowRef') {
76+
continue
77+
}
78+
79+
if (
80+
ref.node.arguments.length > 0 &&
81+
!isNullOrUndefined(ref.node.arguments[0])
82+
) {
83+
continue
84+
}
85+
86+
if (ref.node.typeParameters == null) {
87+
if (
88+
ref.node.parent.type === 'VariableDeclarator' &&
89+
ref.node.parent.id.type === 'Identifier'
90+
) {
91+
if (ref.node.parent.id.typeAnnotation == null) {
92+
report(ref.name, ref.node)
93+
}
94+
} else {
95+
report(ref.name, ref.node)
96+
}
97+
}
98+
}
99+
}
100+
}
101+
}
102+
}

lib/utils/index.js

+9
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,8 @@ module.exports = {
998998
return null
999999
},
10001000

1001+
isTypeScriptFile,
1002+
10011003
isVueFile,
10021004

10031005
/**
@@ -2416,6 +2418,13 @@ function getVExpressionContainer(node) {
24162418
return n
24172419
}
24182420

2421+
/**
2422+
* @param {string} path
2423+
*/
2424+
function isTypeScriptFile(path) {
2425+
return path.endsWith('.ts') || path.endsWith('.tsx') || path.endsWith('.mts')
2426+
}
2427+
24192428
// ------------------------------------------------------------------------------
24202429
// Vue Helpers
24212430
// ------------------------------------------------------------------------------

lib/utils/ref-object-references.js

+1
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ function getGlobalScope(context) {
251251
}
252252

253253
module.exports = {
254+
iterateDefineRefs,
254255
extractRefObjectReferences,
255256
extractReactiveVariableReferences
256257
}

0 commit comments

Comments
 (0)