diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 362a0e1820abc..eb664d94b4c0a 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -4090,6 +4090,12 @@ impl RuleRunner for crate::rules::vue::no_required_prop_with_default::NoRequired const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; } +impl RuleRunner for crate::rules::vue::no_this_in_before_route_enter::NoThisInBeforeRouteEnter { + const NODE_TYPES: Option<&AstTypesBitset> = + Some(&AstTypesBitset::from_types(&[AstType::ExportDefaultDeclaration])); + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; +} + impl RuleRunner for crate::rules::vue::prefer_import_from_vue::PreferImportFromVue { const NODE_TYPES: Option<&AstTypesBitset> = None; const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::RunOnce; diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 0210cbd00fc30..ebce9a787c3ee 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -693,6 +693,7 @@ pub(crate) mod vue { pub mod no_import_compiler_macros; pub mod no_multiple_slot_args; pub mod no_required_prop_with_default; + pub mod no_this_in_before_route_enter; pub mod prefer_import_from_vue; pub mod require_default_export; pub mod require_typed_ref; @@ -1344,6 +1345,7 @@ oxc_macros::declare_all_lint_rules! { vue::no_import_compiler_macros, vue::no_multiple_slot_args, vue::no_required_prop_with_default, + vue::no_this_in_before_route_enter, vue::prefer_import_from_vue, vue::require_default_export, vue::require_typed_ref, diff --git a/crates/oxc_linter/src/rules/vue/no_this_in_before_route_enter.rs b/crates/oxc_linter/src/rules/vue/no_this_in_before_route_enter.rs new file mode 100644 index 0000000000000..d8777d1cceef3 --- /dev/null +++ b/crates/oxc_linter/src/rules/vue/no_this_in_before_route_enter.rs @@ -0,0 +1,349 @@ +use oxc_ast::{ + AstKind, + ast::{ExportDefaultDeclarationKind, Expression, Function, ObjectPropertyKind, ThisExpression}, +}; +use oxc_ast_visit::Visit; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_semantic::ScopeFlags; +use oxc_span::Span; + +use crate::{AstNode, context::LintContext, rule::Rule}; + +fn no_this_in_before_route_enter_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("`beforeRouteEnter` does NOT have access to `this` component instance.") + .with_help("Use the callback's `vm` parameter instead of `this` in `beforeRouteEnter`.") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoThisInBeforeRouteEnter; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow this usage in a beforeRouteEnter method. + /// + /// ### Why is this bad? + /// + /// Because lack of this in the beforeRouteEnter. + /// This behavior isn't obvious, so it's pretty easy to make a TypeError. Especially while refactoring. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// export default { + /// beforeRouteEnter(to, from, next) { + /// this.a; // Error: 'this' is not available + /// next(); + /// } + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// export default { + /// beforeRouteEnter(to, from, next) { + /// // anything without this + /// } + /// } + /// ``` + NoThisInBeforeRouteEnter, + vue, + correctness, +); + +impl Rule for NoThisInBeforeRouteEnter { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::ExportDefaultDeclaration(export_default_decl) = node.kind() else { return }; + let ExportDefaultDeclarationKind::ObjectExpression(obj_expr) = + &export_default_decl.declaration + else { + return; + }; + + let before_route_enter_prop = obj_expr.properties.iter().find_map(|prop| { + if let ObjectPropertyKind::ObjectProperty(obj_prop) = prop + && let Some(key_name) = obj_prop.key.static_name() + && key_name == "beforeRouteEnter" + { + Some(obj_prop) + } else { + None + } + }); + + if let Some(before_route_enter_prop) = before_route_enter_prop { + let function_body = match &before_route_enter_prop.value { + Expression::FunctionExpression(func_expr) => func_expr.body.as_ref(), + _ => return, + }; + + let Some(function_body) = function_body else { + return; + }; + + let mut finder = ThisFinder::new(); + finder.visit_function_body(function_body); + for span in finder.found_this_expressions { + ctx.diagnostic(no_this_in_before_route_enter_diagnostic(span)); + } + } + } + + fn should_run(&self, ctx: &crate::context::ContextHost) -> bool { + ctx.file_extension().is_some_and(|ext| ext == "vue") + } +} + +struct ThisFinder { + found_this_expressions: Vec, +} + +impl ThisFinder { + fn new() -> Self { + Self { found_this_expressions: Vec::new() } + } +} + +impl<'a> Visit<'a> for ThisFinder { + fn visit_this_expression(&mut self, expr: &ThisExpression) { + self.found_this_expressions.push(expr.span); + } + + fn visit_function(&mut self, _func: &Function<'a>, _flags: ScopeFlags) {} +} + +#[test] +fn test() { + use crate::tester::Tester; + use std::path::PathBuf; + + let pass = vec![ + ( + r#" + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + r#" + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + r#" + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + r#" + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + r" + ", + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + r" + ", + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + r" + export default { + beforeRouteEnter(to, from, next) { + this.a; + } + }; + ", + None, + None, + Some(PathBuf::from("test.js")), + ), + ]; + + let fail = vec![ + ( + r#" + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + r#" + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + r#" + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + r#" + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), + // this inside if condition + ( + r#" + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + r#" + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), + ]; + + Tester::new(NoThisInBeforeRouteEnter::NAME, NoThisInBeforeRouteEnter::PLUGIN, pass, fail) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/vue_no_this_in_before_route_enter.snap b/crates/oxc_linter/src/snapshots/vue_no_this_in_before_route_enter.snap new file mode 100644 index 0000000000000..ec0ae6a42b12d --- /dev/null +++ b/crates/oxc_linter/src/snapshots/vue_no_this_in_before_route_enter.snap @@ -0,0 +1,74 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-vue(no-this-in-before-route-enter): `beforeRouteEnter` does NOT have access to `this` component instance. + ╭─[no_this_in_before_route_enter.tsx:10:17] + 9 │ beforeRouteEnter() { + 10 │ this.xxx(); + · ──── + 11 │ } + ╰──── + help: Use the callback's `vm` parameter instead of `this` in `beforeRouteEnter`. + + ⚠ eslint-plugin-vue(no-this-in-before-route-enter): `beforeRouteEnter` does NOT have access to `this` component instance. + ╭─[no_this_in_before_route_enter.tsx:10:17] + 9 │ beforeRouteEnter: function() { + 10 │ this.method(); + · ──── + 11 │ } + ╰──── + help: Use the callback's `vm` parameter instead of `this` in `beforeRouteEnter`. + + ⚠ eslint-plugin-vue(no-this-in-before-route-enter): `beforeRouteEnter` does NOT have access to `this` component instance. + ╭─[no_this_in_before_route_enter.tsx:10:17] + 9 │ beforeRouteEnter() { + 10 │ this.attr = this.method(); + · ──── + 11 │ } + ╰──── + help: Use the callback's `vm` parameter instead of `this` in `beforeRouteEnter`. + + ⚠ eslint-plugin-vue(no-this-in-before-route-enter): `beforeRouteEnter` does NOT have access to `this` component instance. + ╭─[no_this_in_before_route_enter.tsx:10:29] + 9 │ beforeRouteEnter() { + 10 │ this.attr = this.method(); + · ──── + 11 │ } + ╰──── + help: Use the callback's `vm` parameter instead of `this` in `beforeRouteEnter`. + + ⚠ eslint-plugin-vue(no-this-in-before-route-enter): `beforeRouteEnter` does NOT have access to `this` component instance. + ╭─[no_this_in_before_route_enter.tsx:10:17] + 9 │ beforeRouteEnter: function() { + 10 │ this.attr = this.method(); + · ──── + 11 │ } + ╰──── + help: Use the callback's `vm` parameter instead of `this` in `beforeRouteEnter`. + + ⚠ eslint-plugin-vue(no-this-in-before-route-enter): `beforeRouteEnter` does NOT have access to `this` component instance. + ╭─[no_this_in_before_route_enter.tsx:10:29] + 9 │ beforeRouteEnter: function() { + 10 │ this.attr = this.method(); + · ──── + 11 │ } + ╰──── + help: Use the callback's `vm` parameter instead of `this` in `beforeRouteEnter`. + + ⚠ eslint-plugin-vue(no-this-in-before-route-enter): `beforeRouteEnter` does NOT have access to `this` component instance. + ╭─[no_this_in_before_route_enter.tsx:10:21] + 9 │ beforeRouteEnter() { + 10 │ if (this.method()) {} + · ──── + 11 │ } + ╰──── + help: Use the callback's `vm` parameter instead of `this` in `beforeRouteEnter`. + + ⚠ eslint-plugin-vue(no-this-in-before-route-enter): `beforeRouteEnter` does NOT have access to `this` component instance. + ╭─[no_this_in_before_route_enter.tsx:10:29] + 9 │ beforeRouteEnter: function() { + 10 │ if (true) { this.method(); } + · ──── + 11 │ } + ╰──── + help: Use the callback's `vm` parameter instead of `this` in `beforeRouteEnter`.