diff --git a/Cargo.lock b/Cargo.lock index 00ec0e5872991..538844421185d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1296,6 +1296,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -1640,6 +1646,7 @@ dependencies = [ "markdown", "memchr", "mime_guess", + "nohash-hasher", "once_cell", "oxc_allocator", "oxc_ast", diff --git a/Cargo.toml b/Cargo.toml index 592331a330472..055a61d6be02e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,6 +147,7 @@ memoffset = "0.9.1" miette = { version = "7.2.0", features = ["fancy-no-syscall"] } mimalloc = "0.1.43" mime_guess = "2.0.5" +nohash-hasher = "0.2.0" nonmax = "0.5.5" num-bigint = "0.4.6" num-traits = "0.2.19" diff --git a/crates/oxc_linter/Cargo.toml b/crates/oxc_linter/Cargo.toml index 06d8c4af99e7d..122cfc9980fc9 100644 --- a/crates/oxc_linter/Cargo.toml +++ b/crates/oxc_linter/Cargo.toml @@ -45,6 +45,7 @@ language-tags = { workspace = true } lazy_static = { workspace = true } memchr = { workspace = true } mime_guess = { workspace = true } +nohash-hasher = { workspace = true } once_cell = { workspace = true } phf = { workspace = true, features = ["macros"] } rayon = { workspace = true } diff --git a/crates/oxc_linter/src/context/host.rs b/crates/oxc_linter/src/context/host.rs index b84d91146735e..32376793da66c 100644 --- a/crates/oxc_linter/src/context/host.rs +++ b/crates/oxc_linter/src/context/host.rs @@ -11,7 +11,7 @@ use crate::{ utils, FrameworkFlags, RuleWithSeverity, }; -use super::{plugin_name_to_prefix, LintContext}; +use super::{plugin_name_to_prefix, LintContext, RuleState}; /// Stores shared information about a file being linted. /// @@ -46,6 +46,8 @@ pub struct ContextHost<'a> { pub(super) config: Arc, pub(super) frameworks: FrameworkFlags, pub(super) plugins: LintPlugins, + + pub(super) state: RuleState, } impl<'a> ContextHost<'a> { @@ -55,6 +57,7 @@ impl<'a> ContextHost<'a> { file_path: P, semantic: Rc>, options: LintOptions, + rules: &Vec, ) -> Self { // We should always check for `semantic.cfg()` being `Some` since we depend on it and it is // unwrapped without any runtime checks after construction. @@ -77,6 +80,7 @@ impl<'a> ContextHost<'a> { config: Arc::new(LintConfig::default()), frameworks: options.framework_hints, plugins: options.plugins, + state: RuleState::new(rules), } .sniff_for_frameworks() } @@ -116,6 +120,7 @@ impl<'a> ContextHost<'a> { LintContext { parent: self, diagnostics: RefCell::new(Vec::with_capacity(DIAGNOSTICS_INITIAL_CAPACITY)), + current_rule_id: rule.id(), current_rule_name: rule_name, current_plugin_name: plugin_name, current_plugin_prefix: plugin_name_to_prefix(plugin_name), @@ -132,6 +137,7 @@ impl<'a> ContextHost<'a> { LintContext { parent: Rc::clone(&self), diagnostics: RefCell::new(Vec::with_capacity(DIAGNOSTICS_INITIAL_CAPACITY)), + current_rule_id: 0, current_rule_name: "", current_plugin_name: "eslint", current_plugin_prefix: "eslint", diff --git a/crates/oxc_linter/src/context/mod.rs b/crates/oxc_linter/src/context/mod.rs index c4c8ead2d43cc..71412f7f3df5f 100644 --- a/crates/oxc_linter/src/context/mod.rs +++ b/crates/oxc_linter/src/context/mod.rs @@ -1,7 +1,12 @@ #![allow(rustdoc::private_intra_doc_links)] // useful for intellisense mod host; +mod rule_state; -use std::{cell::RefCell, path::Path, rc::Rc}; +use std::{ + cell::{RefCell, RefMut}, + path::Path, + rc::Rc, +}; use oxc_cfg::ControlFlowGraph; use oxc_diagnostics::{OxcDiagnostic, Severity}; @@ -15,10 +20,11 @@ use crate::{ disable_directives::DisableDirectives, fixer::{FixKind, Message, RuleFix, RuleFixer}, javascript_globals::GLOBALS, - AllowWarnDeny, FrameworkFlags, OxlintEnv, OxlintGlobals, OxlintSettings, + AllowWarnDeny, FrameworkFlags, OxlintEnv, OxlintGlobals, OxlintSettings, RuleMeta, }; pub use host::ContextHost; +pub(crate) use rule_state::RuleState; #[derive(Clone)] #[must_use] @@ -32,6 +38,7 @@ pub struct LintContext<'a> { diagnostics: RefCell>>, // states + current_rule_id: usize, current_plugin_name: &'static str, current_plugin_prefix: &'static str, current_rule_name: &'static str, @@ -134,6 +141,12 @@ impl<'a> LintContext<'a> { &self.parent.config.globals } + #[inline] + #[must_use] + pub fn state(&self) -> RefMut<'_, R::State> { + self.parent.state.get_mut::(self.current_rule_id) + } + /// Runtime environments turned on/off by the user. /// /// Examples of environments are `builtin`, `browser`, `node`, etc. diff --git a/crates/oxc_linter/src/context/rule_state.rs b/crates/oxc_linter/src/context/rule_state.rs new file mode 100644 index 0000000000000..8e3edf3266e2d --- /dev/null +++ b/crates/oxc_linter/src/context/rule_state.rs @@ -0,0 +1,40 @@ +use std::{ + any::Any, + cell::{RefCell, RefMut}, +}; + +use nohash_hasher::IntMap; + +use crate::{rules::RuleEnum, RuleMeta}; + +#[must_use] +pub struct RuleState { + inner: IntMap>>, +} + +impl RuleState { + pub fn new(rules: I) -> Self + where + R: AsRef, + I: IntoIterator, + { + let inner = rules + .into_iter() + .map(|rule| { + let rule = rule.as_ref(); + (rule.id(), rule.new_state()) + }) + .collect(); + Self { inner } + } + + pub fn get_raw_mut(&self, rule_id: usize) -> RefMut<'_, dyn Any> { + self.inner[&rule_id].borrow_mut() + } + + pub fn get_mut(&self, rule_id: usize) -> RefMut<'_, R::State> { + RefMut::map(self.get_raw_mut(rule_id), |state| { + state.downcast_mut().expect("downcast failed") + }) + } +} diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index ca28492762dbc..96a2f86c71398 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -115,8 +115,9 @@ impl Linter { } pub fn run<'a>(&self, path: &Path, semantic: Rc>) -> Vec> { - let ctx_host = - Rc::new(ContextHost::new(path, semantic, self.options).with_config(&self.config)); + let ctx_host = Rc::new( + ContextHost::new(path, semantic, self.options, &self.rules).with_config(&self.config), + ); let rules = self .rules diff --git a/crates/oxc_linter/src/rule.rs b/crates/oxc_linter/src/rule.rs index 4d45b793db9e6..50bdc367ef310 100644 --- a/crates/oxc_linter/src/rule.rs +++ b/crates/oxc_linter/src/rule.rs @@ -55,6 +55,13 @@ pub trait RuleMeta { /// What kind of auto-fixing can this rule do? const FIX: RuleFixMeta = RuleFixMeta::None; + /// Data stored between each call to [run](`Rule::run`), + /// [run_on_symbol](`Rule::run_on_symbol`), and + /// [run_once](`Rule::run_once`). State is reset between files. + /// + /// By default, this is the unit type (aka `()`). + type State: Default + 'static; + fn documentation() -> Option<&'static str> { None } @@ -261,6 +268,12 @@ impl Deref for RuleWithSeverity { } } +impl AsRef for RuleWithSeverity { + fn as_ref(&self) -> &RuleEnum { + &self.rule + } +} + impl RuleWithSeverity { pub fn new(rule: RuleEnum, severity: AllowWarnDeny) -> Self { Self { rule, severity } diff --git a/crates/oxc_linter/src/rules/react/jsx_boolean_value.rs b/crates/oxc_linter/src/rules/react/jsx_boolean_value.rs index 3a949bdff6152..1be09561f92a0 100644 --- a/crates/oxc_linter/src/rules/react/jsx_boolean_value.rs +++ b/crates/oxc_linter/src/rules/react/jsx_boolean_value.rs @@ -66,7 +66,7 @@ declare_oxc_lint!( /// ``` JsxBooleanValue, style, - fix, + fix ); impl Rule for JsxBooleanValue { diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_structured_clone.rs b/crates/oxc_linter/src/rules/unicorn/prefer_structured_clone.rs index a7405db022a55..d6dc19acdd0ae 100644 --- a/crates/oxc_linter/src/rules/unicorn/prefer_structured_clone.rs +++ b/crates/oxc_linter/src/rules/unicorn/prefer_structured_clone.rs @@ -53,7 +53,7 @@ declare_oxc_lint!( /// ``` PreferStructuredClone, style, - pending, + pending ); impl Rule for PreferStructuredClone { diff --git a/crates/oxc_linter/src/utils/jest.rs b/crates/oxc_linter/src/utils/jest.rs index 21d6c8c5d1e56..17621b0aa3d5d 100644 --- a/crates/oxc_linter/src/utils/jest.rs +++ b/crates/oxc_linter/src/utils/jest.rs @@ -321,8 +321,13 @@ mod test { let semantic_ret = Rc::new(semantic_ret); let build_ctx = |path: &'static str| { - Rc::new(ContextHost::new(path, Rc::clone(&semantic_ret), LintOptions::default())) - .spawn_for_test() + Rc::new(ContextHost::new( + path, + Rc::clone(&semantic_ret), + LintOptions::default(), + &vec![], + )) + .spawn_for_test() }; let ctx = build_ctx("foo.js"); diff --git a/crates/oxc_macros/src/declare_all_lint_rules.rs b/crates/oxc_macros/src/declare_all_lint_rules.rs index b6e429b9f42dc..da78307f30f6c 100644 --- a/crates/oxc_macros/src/declare_all_lint_rules.rs +++ b/crates/oxc_macros/src/declare_all_lint_rules.rs @@ -139,6 +139,12 @@ pub fn declare_all_lint_rules(metadata: AllLintRulesMeta) -> TokenStream { #(Self::#struct_names(rule) => rule.should_run(ctx)),* } } + + pub(crate) fn new_state(&self) -> Box> { + match self { + #(Self::#struct_names(_) => Box::new(std::cell::RefCell::new(<#struct_names as RuleMeta>::State::default()))),* + } + } } impl std::hash::Hash for RuleEnum { diff --git a/crates/oxc_macros/src/declare_oxc_lint.rs b/crates/oxc_macros/src/declare_oxc_lint.rs index 8f80bcbe30cf4..81f1e5fdca546 100644 --- a/crates/oxc_macros/src/declare_oxc_lint.rs +++ b/crates/oxc_macros/src/declare_oxc_lint.rs @@ -13,6 +13,7 @@ pub struct LintRuleMeta { /// Describes what auto-fixing capabilities the rule has fix: Option, documentation: String, + state: Option, pub used_in_test: bool, } @@ -45,10 +46,17 @@ impl Parse for LintRuleMeta { None }; + let state = if input.peek(Token!(,)) { + input.parse::()?; + Some(input.parse()?) + } else { + None + }; + // Ignore the rest input.parse::()?; - Ok(Self { name: struct_name, category, fix, documentation, used_in_test: false }) + Ok(Self { name: struct_name, category, fix, documentation, state, used_in_test: false }) } } @@ -57,7 +65,7 @@ fn rule_name_converter() -> Converter { } pub fn declare_oxc_lint(metadata: LintRuleMeta) -> TokenStream { - let LintRuleMeta { name, category, fix, documentation, used_in_test } = metadata; + let LintRuleMeta { name, category, fix, documentation, state, used_in_test } = metadata; let canonical_name = rule_name_converter().convert(name.to_string()); let category = match category.to_string().as_str() { @@ -83,6 +91,12 @@ pub fn declare_oxc_lint(metadata: LintRuleMeta) -> TokenStream { Some(quote! { use crate::{rule::{RuleCategory, RuleMeta, RuleFixMeta}, fixer::FixKind}; }) }; + let state_type = if let Some(ty) = state { + quote! { type State = #ty; } + } else { + quote! { type State = (); } + }; + let output = quote! { #import_statement @@ -93,6 +107,8 @@ pub fn declare_oxc_lint(metadata: LintRuleMeta) -> TokenStream { #fix + #state_type + fn documentation() -> Option<&'static str> { Some(#documentation) }