diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 742286196d14a..463245da1f5c1 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -2683,6 +2683,10 @@ impl RuleRunner for crate::rules::vue::no_multiple_slot_args::NoMultipleSlotArgs Some(&AstTypesBitset::from_types(&[AstType::CallExpression])); } +impl RuleRunner for crate::rules::vue::require_typed_ref::RequireTypedRef { + const NODE_TYPES: Option<&AstTypesBitset> = None; +} + impl RuleRunner for crate::rules::vue::valid_define_emits::ValidDefineEmits { const NODE_TYPES: Option<&AstTypesBitset> = None; } diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 496a8df74c386..6bdba84913d34 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -630,6 +630,7 @@ pub(crate) mod vue { pub mod define_emits_declaration; pub mod define_props_declaration; pub mod no_multiple_slot_args; + pub mod require_typed_ref; pub mod valid_define_emits; pub mod valid_define_props; } @@ -1215,6 +1216,7 @@ oxc_macros::declare_all_lint_rules! { vue::define_emits_declaration, vue::define_props_declaration, vue::no_multiple_slot_args, + vue::require_typed_ref, vue::valid_define_emits, vue::valid_define_props, } diff --git a/crates/oxc_linter/src/rules/vue/require_typed_ref.rs b/crates/oxc_linter/src/rules/vue/require_typed_ref.rs new file mode 100644 index 0000000000000..280a59f542aad --- /dev/null +++ b/crates/oxc_linter/src/rules/vue/require_typed_ref.rs @@ -0,0 +1,317 @@ +use oxc_ast::{AstKind, ast::Argument}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{AstNode, context::LintContext, rule::Rule}; + +fn require_typed_ref_diagnostic(span: Span, name: &str) -> OxcDiagnostic { + let msg = format!( + "Specify type parameter for `{name}` function, otherwise created variable will not be typechecked." + ); + OxcDiagnostic::warn(msg) + .with_help("Provide an explicit type parameter or an initial value.") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct RequireTypedRef; + +declare_oxc_lint!( + /// ### What it does + /// + /// Require `ref` and `shallowRef` functions to be strongly typed. + /// + /// ### Why is this bad? + /// + /// With TypeScript it is easy to prevent usage of `any` by using `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` type. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```typescript + /// const count = ref(); + /// const name = shallowRef() + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```typescript + /// const count = ref() + /// const a = ref(0) + /// ``` + RequireTypedRef, + vue, + style, +); + +impl Rule for RequireTypedRef { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call_expr) = &node.kind() else { + return; + }; + let Some(ident) = call_expr.callee.get_identifier_reference() else { + return; + }; + + let name = ident.name; + if name != "ref" && name != "shallowRef" { + return; + } + + let is_valid_first_arg = match call_expr.arguments.first() { + Some(Argument::NullLiteral(_)) | None => false, + Some(Argument::Identifier(ident)) if ident.name == "undefined" => false, + _ => true, + }; + + if is_valid_first_arg { + return; + } + + if call_expr.type_arguments.is_none() { + if let Some(variable_decl_parent) = + ctx.nodes().ancestor_kinds(node.id()).find_map(|ancestor| { + if let AstKind::VariableDeclarator(var_decl) = ancestor { + Some(var_decl) + } else { + None + } + }) + { + let id = &variable_decl_parent.id; + if id.type_annotation.is_some() { + return; + } + } + ctx.diagnostic(require_typed_ref_diagnostic(call_expr.span, &name)); + } + } + + fn should_run(&self, ctx: &crate::context::ContextHost) -> bool { + ctx.source_type().is_typescript() + } +} + +#[test] +fn test() { + use crate::tester::Tester; + use std::path::PathBuf; + + let pass = vec![ + ( + " + import { shallowRef } from 'vue' + const count = shallowRef(0) + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + " + import { ref } from 'vue' + const count = ref() + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + " + import { ref } from 'vue' + const count = ref(0) + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + " + import { ref } from 'vue' + const counter: Ref = ref() + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + " + import { ref } from 'vue' + const count = ref(0) + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + " + import { ref } from 'vue' + function useCount() { + return { + count: ref() + } + } + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + " + import { ref, defineComponent } from 'vue' + defineComponent({ + setup() { + const count = ref() + return { count } + } + }) + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + " + + ", + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parser": require("vue-eslint-parser") }, + ( + " + + ", + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parser": require("vue-eslint-parser") }, + ( + " + import { ref } from 'vue' + const count = ref() + ", + None, + None, + Some(PathBuf::from("test.js")), + ), + ]; + + let fail = vec![ + ( + " + import { ref } from 'vue' + const count = ref() + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + " + import { ref } from 'vue' + const count = ref(null) + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + " + import { ref } from 'vue' + const count = ref(undefined) + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + " + import { shallowRef } from 'vue' + const count = shallowRef() + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + " + import { ref } from 'vue' + function useCount() { + const count = ref() + return { count } + } + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + " + import { ref } from 'vue' + function useCount() { + return { + count: ref() + } + } + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + r#" + + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parser": require("vue-eslint-parser") }, + ( + r#" + + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parser": require("vue-eslint-parser") }, + ( + " + import { ref, defineComponent } from 'vue' + defineComponent({ + setup() { + const count = ref() + return { count } + } + }) + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ]; + + Tester::new(RequireTypedRef::NAME, RequireTypedRef::PLUGIN, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/vue_require_typed_ref.snap b/crates/oxc_linter/src/snapshots/vue_require_typed_ref.snap new file mode 100644 index 0000000000000..b593d28d8151d --- /dev/null +++ b/crates/oxc_linter/src/snapshots/vue_require_typed_ref.snap @@ -0,0 +1,83 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-vue(require-typed-ref): Specify type parameter for `ref` function, otherwise created variable will not be typechecked. + ╭─[require_typed_ref.tsx:3:26] + 2 │ import { ref } from 'vue' + 3 │ const count = ref() + · ───── + 4 │ + ╰──── + help: Provide an explicit type parameter or an initial value. + + ⚠ eslint-plugin-vue(require-typed-ref): Specify type parameter for `ref` function, otherwise created variable will not be typechecked. + ╭─[require_typed_ref.tsx:3:26] + 2 │ import { ref } from 'vue' + 3 │ const count = ref(null) + · ───────── + 4 │ + ╰──── + help: Provide an explicit type parameter or an initial value. + + ⚠ eslint-plugin-vue(require-typed-ref): Specify type parameter for `ref` function, otherwise created variable will not be typechecked. + ╭─[require_typed_ref.tsx:3:26] + 2 │ import { ref } from 'vue' + 3 │ const count = ref(undefined) + · ────────────── + 4 │ + ╰──── + help: Provide an explicit type parameter or an initial value. + + ⚠ eslint-plugin-vue(require-typed-ref): Specify type parameter for `shallowRef` function, otherwise created variable will not be typechecked. + ╭─[require_typed_ref.tsx:3:26] + 2 │ import { shallowRef } from 'vue' + 3 │ const count = shallowRef() + · ──────────── + 4 │ + ╰──── + help: Provide an explicit type parameter or an initial value. + + ⚠ eslint-plugin-vue(require-typed-ref): Specify type parameter for `ref` function, otherwise created variable will not be typechecked. + ╭─[require_typed_ref.tsx:4:28] + 3 │ function useCount() { + 4 │ const count = ref() + · ───── + 5 │ return { count } + ╰──── + help: Provide an explicit type parameter or an initial value. + + ⚠ eslint-plugin-vue(require-typed-ref): Specify type parameter for `ref` function, otherwise created variable will not be typechecked. + ╭─[require_typed_ref.tsx:5:23] + 4 │ return { + 5 │ count: ref() + · ───── + 6 │ } + ╰──── + help: Provide an explicit type parameter or an initial value. + + ⚠ eslint-plugin-vue(require-typed-ref): Specify type parameter for `ref` function, otherwise created variable will not be typechecked. + ╭─[require_typed_ref.tsx:4:28] + 3 │ import { ref } from 'vue' + 4 │ const count = ref() + · ───── + 5 │ + ╰──── + help: Provide an explicit type parameter or an initial value. + + ⚠ eslint-plugin-vue(require-typed-ref): Specify type parameter for `ref` function, otherwise created variable will not be typechecked. + ╭─[require_typed_ref.tsx:6:32] + 5 │ setup() { + 6 │ const count = ref() + · ───── + 7 │ } + ╰──── + help: Provide an explicit type parameter or an initial value. + + ⚠ eslint-plugin-vue(require-typed-ref): Specify type parameter for `ref` function, otherwise created variable will not be typechecked. + ╭─[require_typed_ref.tsx:5:30] + 4 │ setup() { + 5 │ const count = ref() + · ───── + 6 │ return { count } + ╰──── + help: Provide an explicit type parameter or an initial value.