diff --git a/crates/oxc_linter/src/context/host.rs b/crates/oxc_linter/src/context/host.rs index f285ce5e6..12e1e74a8 100644 --- a/crates/oxc_linter/src/context/host.rs +++ b/crates/oxc_linter/src/context/host.rs @@ -209,6 +209,12 @@ impl<'a> ContextHost<'a> { self } + + /// Returns the framework hints for the target file. + #[inline] + pub fn frameworks(&self) -> FrameworkFlags { + self.frameworks + } } impl<'a> From> for Vec> { diff --git a/crates/oxc_linter/src/frameworks.rs b/crates/oxc_linter/src/frameworks.rs index 77d34ec98..54f429ffb 100644 --- a/crates/oxc_linter/src/frameworks.rs +++ b/crates/oxc_linter/src/frameworks.rs @@ -35,6 +35,7 @@ bitflags! { const Jest = 1 << 9; const Vitest = 1 << 10; const OtherTest = 1 << 11; + /// Flag for if any test frameworks are used, such as Jest or Vitest. const Test = Self::Jest.bits() | Self::Vitest.bits() | Self::OtherTest.bits(); } } diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index b2ad5345d..69665affb 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -27,6 +27,7 @@ use config::LintConfig; use context::ContextHost; use options::LintOptions; use oxc_semantic::{AstNode, Semantic}; +use utils::iter_possible_jest_call_node; pub use crate::{ builder::LinterBuilder, @@ -140,6 +141,14 @@ impl Linter { } } + if ctx_host.frameworks().is_test() && self.options.plugins.has_test() { + for jest_node in iter_possible_jest_call_node(semantic) { + for (rule, ctx) in &rules { + rule.run_on_jest_node(&jest_node, ctx); + } + } + } + ctx_host.take_diagnostics() } diff --git a/crates/oxc_linter/src/rule.rs b/crates/oxc_linter/src/rule.rs index 27f1db528..cd4b06aa2 100644 --- a/crates/oxc_linter/src/rule.rs +++ b/crates/oxc_linter/src/rule.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; use crate::{ context::{ContextHost, LintContext}, + utils::PossibleJestNode, AllowWarnDeny, AstNode, FixKind, RuleEnum, }; @@ -35,6 +36,18 @@ pub trait Rule: Sized + Default + fmt::Debug { #[inline] fn run_once(&self, ctx: &LintContext) {} + /// Run on each Jest node (e.g. `it`, `describe`, `test`, `expect`, etc.). + /// This is only called if the Jest plugin is enabled and the file is a test file. + /// It should be used to run rules that are specific to Jest or Vitest. + #[expect(unused_variables)] + #[inline] + fn run_on_jest_node<'a, 'c>( + &self, + jest_node: &PossibleJestNode<'a, 'c>, + ctx: &'c LintContext<'a>, + ) { + } + /// Check if a rule should be run at all. /// /// You usually do not need to implement this function. If you do, use it to diff --git a/crates/oxc_macros/src/declare_all_lint_rules.rs b/crates/oxc_macros/src/declare_all_lint_rules.rs index b6e429b9f..0394bbdaf 100644 --- a/crates/oxc_macros/src/declare_all_lint_rules.rs +++ b/crates/oxc_macros/src/declare_all_lint_rules.rs @@ -61,7 +61,12 @@ pub fn declare_all_lint_rules(metadata: AllLintRulesMeta) -> TokenStream { let expanded = quote! { #(pub use self::#use_stmts::#struct_names;)* - use crate::{context::{ContextHost, LintContext}, rule::{Rule, RuleCategory, RuleFixMeta, RuleMeta}, AstNode}; + use crate::{ + context::{ContextHost, LintContext}, + rule::{Rule, RuleCategory, RuleFixMeta, RuleMeta}, + utils::PossibleJestNode, + AstNode + }; use oxc_semantic::SymbolId; #[derive(Debug, Clone)] @@ -134,6 +139,16 @@ pub fn declare_all_lint_rules(metadata: AllLintRulesMeta) -> TokenStream { } } + pub(super) fn run_on_jest_node<'a, 'c>( + &self, + jest_node: &PossibleJestNode<'a, 'c>, + ctx: &'c LintContext<'a>, + ) { + match self { + #(Self::#struct_names(rule) => rule.run_on_jest_node(jest_node, ctx)),* + } + } + pub(super) fn should_run(&self, ctx: &ContextHost) -> bool { match self { #(Self::#struct_names(rule) => rule.should_run(ctx)),*