Skip to content
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

Add vue/require-typed-ref rule #2204

Merged
merged 18 commits into from
Jun 9, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ For example:
| [vue/require-expose](./require-expose.md) | require declare public properties using `expose` | :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
56 changes: 56 additions & 0 deletions docs/rules/require-typed-ref.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
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

```json
{
"vue/require-typed-ref": ["error", {
"requireExplicitType": false
}]
}
```

- `requireExplicitType` ... If `true`, requires `ref` and `shallowRef` functions to have type specified, even if there is a argument. Requires to write `const count = ref<number>(0)`.
default `false`

## :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 @@ -188,6 +188,7 @@
'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 Expand Up @@ -250,7 +251,7 @@
},
environments: {
// TODO Remove in the next major version
/** @deprecated */

Check warning on line 254 in lib/index.js

View workflow job for this annotation

GitHub Actions / Lint

Unexpected 'todo' comment: 'TODO Remove in the next major version'
'setup-compiler-macros': {
globals: {
defineProps: 'readonly',
Expand Down
119 changes: 119 additions & 0 deletions lib/rules/require-typed-ref.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* @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) {
if (node.type === 'Literal' && node.value === null) {
return true
}

if (node.type === 'Identifier' && node.name === 'undefined') {
return true
}

return false
Demivan marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @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: [
{
type: 'object',
properties: {
requireExplicitType: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},
/** @param {RuleContext} context */
create(context) {
const { requireExplicitType = false } = context.options[0] ?? {}

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]) &&
!requireExplicitType
) {
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)
Demivan marked this conversation as resolved.
Show resolved Hide resolved
} 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