From 9b4f0cf0990896c3b73ccdcb137a4570321a089e Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Wed, 31 May 2023 16:50:22 +0300 Subject: [PATCH 01/18] Initial implementation of require-typed-ref rule --- lib/index.js | 1 + lib/rules/require-typed-ref.js | 60 ++++++++++++++ lib/utils/ref-object-references.js | 1 + tests/lib/rules/require-typed-ref.js | 120 +++++++++++++++++++++++++++ 4 files changed, 182 insertions(+) create mode 100644 lib/rules/require-typed-ref.js create mode 100644 tests/lib/rules/require-typed-ref.js diff --git a/lib/index.js b/lib/index.js index 8ac5d9383..42a223de6 100644 --- a/lib/index.js +++ b/lib/index.js @@ -188,6 +188,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'), diff --git a/lib/rules/require-typed-ref.js b/lib/rules/require-typed-ref.js new file mode 100644 index 000000000..ca56071a3 --- /dev/null +++ b/lib/rules/require-typed-ref.js @@ -0,0 +1,60 @@ +/** + * @author Ivan Demchuk + * See LICENSE file in root directory for full license. + */ +'use strict' + +const { + iterateDefineRefs +} = require('../utils/ref-object-references') +const utils = require('../utils') + +/** + * @typedef {import('../utils/ref-object-references').RefObjectReferences} RefObjectReferences + */ + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'enforce declaration style of `defineProps`', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/define-props-declaration.html' + }, + fixable: null, + messages: { + noType: 'Specify type parameter for `ref`, otherwise it will be `any`.' + }, + schema: [] + }, + /** @param {RuleContext} context */ + create(context) { + const scriptSetup = utils.getScriptSetupElement(context) + if (scriptSetup && !utils.hasAttribute(scriptSetup, 'lang', 'ts')) { + return {} + } + + const defines = iterateDefineRefs(context.getScope()) + + return { + Program() { + for (const ref of defines) { + if (ref.node.parent.type !== 'VariableDeclarator' || ref.node.parent.id.type !== 'Identifier') { + continue + } + + if (ref.node.arguments.length > 0) { + continue + } + + if (ref.node.typeParameters == null && ref.node.parent.id.typeAnnotation == null) { + context.report({ + node: ref.node, + messageId: 'noType', + }) + } + } + }, + } + } +} diff --git a/lib/utils/ref-object-references.js b/lib/utils/ref-object-references.js index ceecd89e3..49404dad0 100644 --- a/lib/utils/ref-object-references.js +++ b/lib/utils/ref-object-references.js @@ -251,6 +251,7 @@ function getGlobalScope(context) { } module.exports = { + iterateDefineRefs, extractRefObjectReferences, extractReactiveVariableReferences } diff --git a/tests/lib/rules/require-typed-ref.js b/tests/lib/rules/require-typed-ref.js new file mode 100644 index 000000000..54eccacdc --- /dev/null +++ b/tests/lib/rules/require-typed-ref.js @@ -0,0 +1,120 @@ +/** + * @author Ivan Demchuk + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/require-typed-ref') + +const tester = new RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { ecmaVersion: 2020, sourceType: 'module' } +}) + +tester.run('require-typed-ref', rule, { + valid: [ + ` + import { ref } from 'vue' + const count = ref(0) + `, + ` + import { shallowRef } from 'vue' + const count = shallowRef(0) + `, + ` + import { ref } from 'vue' + const count = ref() + `, + ` + import { ref } from 'vue' + const count = ref(0) + `, + ` + import { ref } from 'vue' + const counter: Ref = ref() + `, + ` + import { ref } from 'vue' + const count = ref(0) + `, + { + parser: require.resolve('vue-eslint-parser'), + filename: 'test.vue', + code: ` + + ` + } + ], + invalid: [ + { + code: ` + import { ref } from 'vue' + const count = ref() + `, + errors: [ + { + messageId: 'noType', + line: 3, + column: 23, + endLine: 3, + endColumn: 28 + } + ] + }, + { + code: ` + import { shallowRef } from 'vue' + const count = shallowRef() + `, + errors: [ + { + messageId: 'noType', + line: 3, + column: 23, + endLine: 3, + endColumn: 35 + } + ] + }, + { + code: ` + import { ref } from 'vue' + function useCount() { + const count = ref() + return { count } + } + `, + errors: [ + { + messageId: 'noType', + line: 4, + column: 25, + endLine: 4, + endColumn: 30 + } + ] + }, + { + parser: require.resolve('vue-eslint-parser'), + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'noType', + line: 4, + column: 25, + endLine: 4, + endColumn: 30 + } + ] + } + ] +}) From a18ff999c0393f327b1f681ef16a68fbb5152127 Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Fri, 2 Jun 2023 10:42:31 +0300 Subject: [PATCH 02/18] Improve type detection --- lib/rules/require-typed-ref.js | 36 ++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/lib/rules/require-typed-ref.js b/lib/rules/require-typed-ref.js index ca56071a3..14e5dbedf 100644 --- a/lib/rules/require-typed-ref.js +++ b/lib/rules/require-typed-ref.js @@ -4,9 +4,7 @@ */ 'use strict' -const { - iterateDefineRefs -} = require('../utils/ref-object-references') +const { iterateDefineRefs } = require('../utils/ref-object-references') const utils = require('../utils') /** @@ -23,7 +21,7 @@ module.exports = { }, fixable: null, messages: { - noType: 'Specify type parameter for `ref`, otherwise it will be `any`.' + noType: 'Specify type parameter for `{{name}}`, otherwise it will be `any`.' }, schema: [] }, @@ -36,22 +34,34 @@ module.exports = { const defines = iterateDefineRefs(context.getScope()) + /** + * @param {string} name + * @param {CallExpression} node + */ + function report(name, node) { + context.report({ + node: node, + messageId: 'noType', + data: { + name: name + } + }) + } + return { Program() { for (const ref of defines) { - if (ref.node.parent.type !== 'VariableDeclarator' || ref.node.parent.id.type !== 'Identifier') { - continue - } - if (ref.node.arguments.length > 0) { continue } - if (ref.node.typeParameters == null && ref.node.parent.id.typeAnnotation == null) { - context.report({ - node: ref.node, - messageId: 'noType', - }) + 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) + } } } }, From 4097a97aac6b1f19d0ca8a7c4a8485d100e6f15b Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Fri, 2 Jun 2023 11:57:56 +0300 Subject: [PATCH 03/18] Add filename check --- lib/rules/require-typed-ref.js | 7 +- lib/utils/index.js | 9 +++ tests/lib/rules/require-typed-ref.js | 96 +++++++++++++++++++++------- 3 files changed, 89 insertions(+), 23 deletions(-) diff --git a/lib/rules/require-typed-ref.js b/lib/rules/require-typed-ref.js index 14e5dbedf..4c3b915d2 100644 --- a/lib/rules/require-typed-ref.js +++ b/lib/rules/require-typed-ref.js @@ -27,8 +27,13 @@ module.exports = { }, /** @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')) { + if (scriptSetup && !utils.hasAttribute(scriptSetup, 'lang', 'ts') && !utils.hasAttribute(scriptSetup, 'lang', 'typescript')) { return {} } diff --git a/lib/utils/index.js b/lib/utils/index.js index ef8cd0ee9..b3020c888 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -998,6 +998,8 @@ module.exports = { return null }, + isTypeScriptFile, + isVueFile, /** @@ -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 // ------------------------------------------------------------------------------ diff --git a/tests/lib/rules/require-typed-ref.js b/tests/lib/rules/require-typed-ref.js index 54eccacdc..a4361dd66 100644 --- a/tests/lib/rules/require-typed-ref.js +++ b/tests/lib/rules/require-typed-ref.js @@ -11,45 +11,75 @@ const tester = new RuleTester({ parserOptions: { ecmaVersion: 2020, sourceType: 'module' } }) +// Note: Need to specify filename for each test, +// as only TypeScript files are being checked tester.run('require-typed-ref', rule, { - valid: [ - ` - import { ref } from 'vue' - const count = ref(0) - `, - ` + valid: [{ + filename: 'test.ts', + code: ` import { shallowRef } from 'vue' const count = shallowRef(0) - `, ` + }, + { + filename: 'test.ts', + code: ` import { ref } from 'vue' const count = ref() - `, ` + }, + { + filename: 'test.ts', + code: ` import { ref } from 'vue' const count = ref(0) - `, ` + }, + { + filename: 'test.ts', + code: ` import { ref } from 'vue' const counter: Ref = ref() - `, ` + }, + { + filename: 'test.ts', + code: ` import { ref } from 'vue' const count = ref(0) + ` + }, + { + filename: 'test.ts', + code: ` + import { ref } from 'vue' + function useCount() { + return { + count: ref() + } + } + ` + }, + { + filename: 'test.vue', + parser: require.resolve('vue-eslint-parser'), + code: ` + + ` + }, + { + filename: 'test.js', + code: ` + import { ref } from 'vue' + const count = ref() `, - { - parser: require.resolve('vue-eslint-parser'), - filename: 'test.vue', - code: ` - - ` - } - ], + }], invalid: [ { + filename: 'test.ts', code: ` import { ref } from 'vue' const count = ref() @@ -65,6 +95,7 @@ tester.run('require-typed-ref', rule, { ] }, { + filename: 'test.ts', code: ` import { shallowRef } from 'vue' const count = shallowRef() @@ -80,6 +111,7 @@ tester.run('require-typed-ref', rule, { ] }, { + filename: 'test.ts', code: ` import { ref } from 'vue' function useCount() { @@ -98,8 +130,28 @@ tester.run('require-typed-ref', rule, { ] }, { - parser: require.resolve('vue-eslint-parser'), + filename: 'test.ts', + code: ` + import { ref } from 'vue' + function useCount() { + return { + count: ref() + } + } + `, + errors: [ + { + messageId: 'noType', + line: 5, + column: 20, + endLine: 5, + endColumn: 25 + } + ] + }, + { filename: 'test.vue', + parser: require.resolve('vue-eslint-parser'), code: ` + ` + }, + { + filename: 'test.js', + code: ` import { ref } from 'vue' const count = ref() - - ` - }, - { - filename: 'test.js', - code: ` - import { ref } from 'vue' - const count = ref() - `, - }], + ` + } + ], invalid: [ { filename: 'test.ts', From 2eb0f6a558d87a33b433beb7d157ceb3a6109479 Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Fri, 2 Jun 2023 14:01:31 +0300 Subject: [PATCH 05/18] Improve error messages --- lib/rules/require-typed-ref.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rules/require-typed-ref.js b/lib/rules/require-typed-ref.js index 2ff071292..eef54cf55 100644 --- a/lib/rules/require-typed-ref.js +++ b/lib/rules/require-typed-ref.js @@ -16,14 +16,14 @@ module.exports = { type: 'suggestion', docs: { description: - 'enforce `ref` and `shallowRef` functions to be strongly typed', + '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}}`, otherwise it will be `any`.' + 'Specify type parameter for `{{name}}` function, otherwise created variable will not by typechecked.' }, schema: [] }, From cab4092a6ef6e580b7c8c5ce7581aa5a55a206b9 Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Fri, 2 Jun 2023 14:09:01 +0300 Subject: [PATCH 06/18] Add documentation --- docs/rules/require-typed-ref.md | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/rules/require-typed-ref.md diff --git a/docs/rules/require-typed-ref.md b/docs/rules/require-typed-ref.md new file mode 100644 index 000000000..597026ca7 --- /dev/null +++ b/docs/rules/require-typed-ref.md @@ -0,0 +1,43 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/require-typed-ref +description: xxx +--- +# vue/require-typed-ref + +> require `ref` and `shallowRef` functions to be strongly typed + +- :exclamation: ***This rule has not been released yet.*** + +## :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 `no-implicit-any`. 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` type. + + + +```vue + +``` + + + +## :wrench: Options + +Nothing. + From 4989f9c311f3c4591571ce574d1f66ffcc3dff1d Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Fri, 2 Jun 2023 14:14:02 +0300 Subject: [PATCH 07/18] Add requireExplicitType option --- lib/rules/require-typed-ref.js | 20 ++++++++++++++++++-- tests/lib/rules/require-typed-ref.js | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/rules/require-typed-ref.js b/lib/rules/require-typed-ref.js index eef54cf55..56298756c 100644 --- a/lib/rules/require-typed-ref.js +++ b/lib/rules/require-typed-ref.js @@ -25,10 +25,26 @@ module.exports = { noType: 'Specify type parameter for `{{name}}` function, otherwise created variable will not by typechecked.' }, - schema: [] + schema: [ + { + type: 'object', + properties: { + requireExplicitType: { + type: 'boolean' + } + }, + additionalProperties: false + } + ] }, /** @param {RuleContext} context */ create(context) { + let requireExplicitType = false + const option = context.options[0] + if (option) { + requireExplicitType = option.requireExplicitType + } + const filename = context.getFilename() if (!utils.isVueFile(filename) && !utils.isTypeScriptFile(filename)) { return {} @@ -62,7 +78,7 @@ module.exports = { return { Program() { for (const ref of defines) { - if (ref.node.arguments.length > 0) { + if (ref.node.arguments.length > 0 && !requireExplicitType) { continue } diff --git a/tests/lib/rules/require-typed-ref.js b/tests/lib/rules/require-typed-ref.js index 1b55dbe9f..5076c08bb 100644 --- a/tests/lib/rules/require-typed-ref.js +++ b/tests/lib/rules/require-typed-ref.js @@ -96,6 +96,23 @@ tester.run('require-typed-ref', rule, { } ] }, + { + filename: 'test.ts', + code: ` + import { ref } from 'vue' + const count = ref(0) + `, + options: [{ requireExplicitType: true }], + errors: [ + { + messageId: 'noType', + line: 3, + column: 23, + endLine: 3, + endColumn: 29 + } + ] + }, { filename: 'test.ts', code: ` From c0c6932937ca1ab7d1945311266355e5b1a89b4e Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Fri, 2 Jun 2023 14:18:34 +0300 Subject: [PATCH 08/18] Fix docs build --- docs/rules/index.md | 1 + docs/rules/require-typed-ref.md | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/rules/index.md b/docs/rules/index.md index c3e4b8ed7..9b7d52e76 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -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 `