diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index b141c6692d6de..a4ee2647e74e8 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -13,8 +13,8 @@ use ignore::{gitignore::Gitignore, overrides::OverrideBuilder}; use oxc_allocator::AllocatorPool; use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler, OxcDiagnostic}; use oxc_linter::{ - AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, ExternalLinter, InvalidFilterKind, - LintFilter, LintOptions, LintService, LintServiceOptions, Linter, Oxlintrc, + AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, ExternalLinter, ExternalPluginStore, + InvalidFilterKind, LintFilter, LintOptions, LintService, LintServiceOptions, Linter, Oxlintrc, }; use rustc_hash::{FxHashMap, FxHashSet}; use serde_json::Value; @@ -178,13 +178,22 @@ impl Runner for LintRunner { GraphicalReportHandler::new() }; + let mut external_plugin_store = ExternalPluginStore::default(); + let search_for_nested_configs = !disable_nested_config && // If the `--config` option is explicitly passed, we should not search for nested config files // as the passed config file takes absolute precedence. basic_options.config.is_none(); let nested_configs = if search_for_nested_configs { - match Self::get_nested_configs(stdout, &handler, &filters, &paths, external_linter) { + match Self::get_nested_configs( + stdout, + &handler, + &filters, + &paths, + external_linter, + &mut external_plugin_store, + ) { Ok(v) => v, Err(v) => return v, } @@ -203,21 +212,25 @@ impl Runner for LintRunner { } else { None }; - let config_builder = - match ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, external_linter) { - Ok(builder) => builder, - Err(e) => { - print_and_flush_stdout( - stdout, - &format!( - "Failed to parse configuration file.\n{}\n", - render_report(&handler, &OxcDiagnostic::error(e.to_string())) - ), - ); - return CliRunResult::InvalidOptionConfig; - } + let config_builder = match ConfigStoreBuilder::from_oxlintrc( + false, + oxlintrc, + external_linter, + &mut external_plugin_store, + ) { + Ok(builder) => builder, + Err(e) => { + print_and_flush_stdout( + stdout, + &format!( + "Failed to parse configuration file.\n{}\n", + render_report(&handler, &OxcDiagnostic::error(e.to_string())) + ), + ); + return CliRunResult::InvalidOptionConfig; } - .with_filters(&filters); + } + .with_filters(&filters); if let Some(basic_config_file) = oxlintrc_for_print { let config_file = config_builder.resolve_final_config_file(basic_config_file); @@ -269,10 +282,13 @@ impl Runner for LintRunner { _ => None, }; - let linter = - Linter::new(LintOptions::default(), ConfigStore::new(lint_config, nested_configs)) - .with_fix(fix_options.fix_kind()) - .with_report_unused_directives(report_unused_directives); + let linter = Linter::new( + LintOptions::default(), + ConfigStore::new(lint_config, nested_configs, external_plugin_store), + self.external_linter, + ) + .with_fix(fix_options.fix_kind()) + .with_report_unused_directives(report_unused_directives); let tsconfig = basic_options.tsconfig; if let Some(path) = tsconfig.as_ref() { @@ -406,6 +422,7 @@ impl LintRunner { filters: &Vec, paths: &Vec>, external_linter: Option<&ExternalLinter>, + external_plugin_store: &mut ExternalPluginStore, ) -> Result, CliRunResult> { // TODO(perf): benchmark whether or not it is worth it to store the configurations on a // per-file or per-directory basis, to avoid calling `.parent()` on every path. @@ -436,8 +453,12 @@ impl LintRunner { // iterate over each config and build the ConfigStore for (dir, oxlintrc) in nested_oxlintrc { // TODO(refactor): clean up all of the error handling in this function - let builder = match ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, external_linter) - { + let builder = match ConfigStoreBuilder::from_oxlintrc( + false, + oxlintrc, + external_linter, + external_plugin_store, + ) { Ok(builder) => builder, Err(e) => { print_and_flush_stdout( diff --git a/crates/oxc_language_server/src/linter/isolated_lint_handler.rs b/crates/oxc_language_server/src/linter/isolated_lint_handler.rs index e00fc4193235d..b2d8f33e0090b 100644 --- a/crates/oxc_language_server/src/linter/isolated_lint_handler.rs +++ b/crates/oxc_language_server/src/linter/isolated_lint_handler.rs @@ -67,7 +67,7 @@ impl IsolatedLintHandler { config_store: ConfigStore, options: &IsolatedLintHandlerOptions, ) -> Self { - let linter = Linter::new(lint_options, config_store); + let linter = Linter::new(lint_options, config_store, None); let lint_service_options = LintServiceOptions::new(options.root_path.clone()) .with_cross_module(options.use_cross_module); diff --git a/crates/oxc_language_server/src/linter/server_linter.rs b/crates/oxc_language_server/src/linter/server_linter.rs index 08a7b40808dc0..0fe3571bdb8f8 100644 --- a/crates/oxc_language_server/src/linter/server_linter.rs +++ b/crates/oxc_language_server/src/linter/server_linter.rs @@ -8,7 +8,10 @@ use rustc_hash::{FxBuildHasher, FxHashMap}; use tokio::sync::Mutex; use tower_lsp_server::lsp_types::Uri; -use oxc_linter::{AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, LintOptions, Oxlintrc}; +use oxc_linter::{ + AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, ExternalPluginStore, LintOptions, + Oxlintrc, +}; use tower_lsp_server::UriExt; use crate::linter::{ @@ -48,8 +51,13 @@ impl ServerLinter { }; // clone because we are returning it for ignore builder - let config_builder = - ConfigStoreBuilder::from_oxlintrc(false, oxlintrc.clone(), None).unwrap_or_default(); + let config_builder = ConfigStoreBuilder::from_oxlintrc( + false, + oxlintrc.clone(), + None, + &mut ExternalPluginStore::default(), + ) + .unwrap_or_default(); // TODO(refactor): pull this into a shared function, because in oxlint we have the same functionality. let use_nested_config = options.use_nested_configs(); @@ -82,6 +90,7 @@ impl ServerLinter { } else { FxHashMap::default() }, + ExternalPluginStore::default(), ); let isolated_linter = IsolatedLintHandler::new( @@ -123,8 +132,12 @@ impl ServerLinter { warn!("Skipping invalid config file: {}", file_path.display()); continue; }; - let Ok(config_store_builder) = ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, None) - else { + let Ok(config_store_builder) = ConfigStoreBuilder::from_oxlintrc( + false, + oxlintrc, + None, + &mut ExternalPluginStore::default(), + ) else { warn!("Skipping config (builder failed): {}", file_path.display()); continue; }; diff --git a/crates/oxc_linter/src/config/config_builder.rs b/crates/oxc_linter/src/config/config_builder.rs index 9f155d0ad69a1..002d14aa2c02d 100644 --- a/crates/oxc_linter/src/config/config_builder.rs +++ b/crates/oxc_linter/src/config/config_builder.rs @@ -10,12 +10,14 @@ use rustc_hash::FxHashMap; use oxc_span::{CompactStr, format_compact_str}; use crate::{ - AllowWarnDeny, LintConfig, LintFilter, LintFilterKind, Oxlintrc, RuleCategory, RuleEnum, + AllowWarnDeny, ExternalPluginStore, LintConfig, LintFilter, LintFilterKind, Oxlintrc, + RuleCategory, RuleEnum, config::{ ESLintRule, LintPlugins, OxlintOverrides, OxlintRules, overrides::OxlintOverride, plugins::BuiltinLintPlugins, }, external_linter::ExternalLinter, + external_plugin_store::{ExternalRuleId, ExternalRuleLookupError}, rules::RULES, }; @@ -24,6 +26,7 @@ use super::{Config, categories::OxlintCategories}; #[must_use = "You dropped your builder without building a Linter! Did you mean to call .build()?"] pub struct ConfigStoreBuilder { pub(super) rules: FxHashMap, + pub(super) external_rules: FxHashMap, config: LintConfig, categories: OxlintCategories, overrides: OxlintOverrides, @@ -47,11 +50,12 @@ impl ConfigStoreBuilder { pub fn empty() -> Self { let config = LintConfig::default(); let rules = FxHashMap::default(); + let external_rules = FxHashMap::default(); let categories: OxlintCategories = OxlintCategories::default(); let overrides = OxlintOverrides::default(); let extended_paths = Vec::new(); - Self { rules, config, categories, overrides, extended_paths } + Self { rules, external_rules, config, categories, overrides, extended_paths } } /// Warn on all rules in all plugins and categories, including those in `nursery`. @@ -64,8 +68,9 @@ impl ConfigStoreBuilder { let overrides = OxlintOverrides::default(); let categories: OxlintCategories = OxlintCategories::default(); let rules = RULES.iter().map(|rule| (rule.clone(), AllowWarnDeny::Warn)).collect(); + let external_rules = FxHashMap::default(); let extended_paths = Vec::new(); - Self { rules, config, categories, overrides, extended_paths } + Self { rules, external_rules, config, categories, overrides, extended_paths } } /// Create a [`ConfigStoreBuilder`] from a loaded or manually built [`Oxlintrc`]. @@ -90,6 +95,7 @@ impl ConfigStoreBuilder { start_empty: bool, oxlintrc: Oxlintrc, external_linter: Option<&ExternalLinter>, + external_plugin_store: &mut ExternalPluginStore, ) -> Result { // TODO: this can be cached to avoid re-computing the same oxlintrc fn resolve_oxlintrc_config( @@ -146,13 +152,13 @@ impl ConfigStoreBuilder { let resolver = oxc_resolver::Resolver::new(ResolveOptions::default()); #[expect(clippy::missing_panics_doc, reason = "infallible")] let oxlintrc_dir_path = oxlintrc.path.parent().unwrap(); - for plugin_specifier in &plugins.external { Self::load_external_plugin( oxlintrc_dir_path, plugin_specifier, external_linter, &resolver, + external_plugin_store, )?; } } @@ -179,8 +185,14 @@ impl ConfigStoreBuilder { path: Some(oxlintrc.path), }; - let mut builder = - Self { rules, config, categories, overrides: oxlintrc.overrides, extended_paths }; + let mut builder = Self { + rules, + external_rules: FxHashMap::default(), + config, + categories, + overrides: oxlintrc.overrides, + extended_paths, + }; for filter in oxlintrc.categories.filters() { builder = builder.with_filter(&filter); @@ -189,7 +201,15 @@ impl ConfigStoreBuilder { { let all_rules = builder.get_all_rules(); - oxlintrc.rules.override_rules(&mut builder.rules, &all_rules); + oxlintrc + .rules + .override_rules( + &mut builder.rules, + &mut builder.external_rules, + &all_rules, + external_plugin_store, + ) + .map_err(ConfigBuilderError::ExternalRuleLookupError)?; } Ok(builder) @@ -338,15 +358,17 @@ impl ConfigStoreBuilder { plugins = plugins.union(BuiltinLintPlugins::JEST); } - // TODO: js rules filteing - let mut rules: Vec<_> = self .rules .into_iter() .filter(|(r, _)| plugins.contains(r.plugin_name().into())) .collect(); rules.sort_unstable_by_key(|(r, _)| r.id()); - Config::new(rules, self.categories, self.config, self.overrides) + + let mut external_rules: Vec<_> = self.external_rules.into_iter().collect(); + external_rules.sort_unstable_by_key(|(r, _)| *r); + + Config::new(rules, external_rules, self.categories, self.config, self.overrides) } /// Warn for all correctness rules in the given set of plugins. @@ -399,6 +421,7 @@ impl ConfigStoreBuilder { _plugin_specifier: &str, _external_linter: &ExternalLinter, _resolver: &Resolver, + _external_plugin_store: &mut ExternalPluginStore, ) -> Result<(), ConfigBuilderError> { unreachable!() } @@ -409,6 +432,7 @@ impl ConfigStoreBuilder { plugin_specifier: &str, external_linter: &ExternalLinter, resolver: &Resolver, + external_plugin_store: &mut ExternalPluginStore, ) -> Result<(), ConfigBuilderError> { use crate::PluginLoadResult; @@ -421,16 +445,27 @@ impl ConfigStoreBuilder { // TODO: We should support paths which are not valid UTF-8. How? let plugin_path = resolved.full_path().to_str().unwrap().to_string(); - let result = tokio::task::block_in_place(move || { - tokio::runtime::Handle::current().block_on((external_linter.load_plugin)(plugin_path)) - }) - .map_err(|e| ConfigBuilderError::PluginLoadFailed { - plugin_specifier: plugin_specifier.to_string(), - error: e.to_string(), - })?; + if external_plugin_store.is_plugin_registered(&plugin_path) { + return Ok(()); + } + + let result = { + let plugin_path = plugin_path.clone(); + tokio::task::block_in_place(move || { + tokio::runtime::Handle::current() + .block_on((external_linter.load_plugin)(plugin_path)) + }) + .map_err(|e| ConfigBuilderError::PluginLoadFailed { + plugin_specifier: plugin_specifier.to_string(), + error: e.to_string(), + }) + }?; match result { - PluginLoadResult::Success => Ok(()), + PluginLoadResult::Success { name, offset, rules } => { + external_plugin_store.register_plugin(plugin_path, name, offset, rules); + Ok(()) + } PluginLoadResult::Failure(e) => Err(ConfigBuilderError::PluginLoadFailed { plugin_specifier: plugin_specifier.to_string(), error: e, @@ -452,7 +487,8 @@ impl TryFrom for ConfigStoreBuilder { #[inline] fn try_from(oxlintrc: Oxlintrc) -> Result { - Self::from_oxlintrc(false, oxlintrc, None) + let mut external_plugin_store = ExternalPluginStore::default(); + Self::from_oxlintrc(false, oxlintrc, None, &mut external_plugin_store) } } @@ -481,6 +517,7 @@ pub enum ConfigBuilderError { plugin_specifier: String, error: String, }, + ExternalRuleLookupError(ExternalRuleLookupError), NoExternalLinterConfigured, } @@ -509,6 +546,7 @@ impl Display for ConfigBuilderError { f.write_str("Failed to load external plugin because no external linter was configured. This means the Oxlint binary was executed directly rather than via napi bindings.")?; Ok(()) } + ConfigBuilderError::ExternalRuleLookupError(e) => std::fmt::Display::fmt(&e, f), } } } @@ -743,7 +781,11 @@ mod test { "#, ) .unwrap(); - let builder = ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, None).unwrap(); + let builder = { + let mut external_plugin_store = ExternalPluginStore::default(); + ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, None, &mut external_plugin_store) + .unwrap() + }; for (rule, severity) in &builder.rules { let name = rule.name(); let plugin = rule.plugin_name(); @@ -912,14 +954,18 @@ mod test { #[test] fn test_extends_invalid() { - let invalid_config = ConfigStoreBuilder::from_oxlintrc( - true, - Oxlintrc::from_file(&PathBuf::from( - "fixtures/extends_config/extends_invalid_config.json", - )) - .unwrap(), - None, - ); + let invalid_config = { + let mut external_plugin_store = ExternalPluginStore::default(); + ConfigStoreBuilder::from_oxlintrc( + true, + Oxlintrc::from_file(&PathBuf::from( + "fixtures/extends_config/extends_invalid_config.json", + )) + .unwrap(), + None, + &mut external_plugin_store, + ) + }; let err = invalid_config.unwrap_err(); assert!(matches!(err, ConfigBuilderError::InvalidConfigFile { .. })); if let ConfigBuilderError::InvalidConfigFile { file, reason } = err { @@ -1072,18 +1118,26 @@ mod test { } fn config_store_from_path(path: &str) -> Config { + let mut external_plugin_store = ExternalPluginStore::default(); ConfigStoreBuilder::from_oxlintrc( true, Oxlintrc::from_file(&PathBuf::from(path)).unwrap(), None, + &mut external_plugin_store, ) .unwrap() .build() } fn config_store_from_str(s: &str) -> Config { - ConfigStoreBuilder::from_oxlintrc(true, serde_json::from_str(s).unwrap(), None) - .unwrap() - .build() + let mut external_plugin_store = ExternalPluginStore::default(); + ConfigStoreBuilder::from_oxlintrc( + true, + serde_json::from_str(s).unwrap(), + None, + &mut external_plugin_store, + ) + .unwrap() + .build() } } diff --git a/crates/oxc_linter/src/config/config_store.rs b/crates/oxc_linter/src/config/config_store.rs index ea6a2f1b3cd58..5563671979cc5 100644 --- a/crates/oxc_linter/src/config/config_store.rs +++ b/crates/oxc_linter/src/config/config_store.rs @@ -10,6 +10,7 @@ use super::{ }; use crate::{ AllowWarnDeny, LintPlugins, + external_plugin_store::{ExternalPluginStore, ExternalRuleId}, rules::{RULES, RuleEnum}, }; @@ -19,11 +20,17 @@ pub struct ResolvedLinterState { // TODO: Arc + Vec -> SyncVec? It would save a pointer dereference. pub rules: Arc<[(RuleEnum, AllowWarnDeny)]>, pub config: Arc, + + pub external_rules: Arc<[(ExternalRuleId, AllowWarnDeny)]>, } impl Clone for ResolvedLinterState { fn clone(&self) -> Self { - Self { rules: Arc::clone(&self.rules), config: Arc::clone(&self.config) } + Self { + rules: Arc::clone(&self.rules), + config: Arc::clone(&self.config), + external_rules: Arc::clone(&self.external_rules), + } } } @@ -49,6 +56,7 @@ pub struct Config { impl Config { pub fn new( rules: Vec<(RuleEnum, AllowWarnDeny)>, + external_rules: Vec<(ExternalRuleId, AllowWarnDeny)>, categories: OxlintCategories, config: LintConfig, overrides: OxlintOverrides, @@ -64,6 +72,13 @@ impl Config { .into_boxed_slice(), ), config: Arc::new(config), + external_rules: Arc::from( + external_rules + .into_iter() + .filter(|(_, sev)| sev.is_warn_deny()) + .collect::>() + .into_boxed_slice(), + ), }, base_rules: rules, categories, @@ -83,7 +98,11 @@ impl Config { self.base.rules.len() } - pub fn apply_overrides(&self, path: &Path) -> ResolvedLinterState { + pub fn apply_overrides( + &self, + path: &Path, + external_plugin_store: &ExternalPluginStore, + ) -> ResolvedLinterState { if self.overrides.is_empty() { return self.base.clone(); } @@ -135,7 +154,8 @@ impl Config { .cloned() .collect::>(); - // TODO: external rules. + // TODO(camc314): this should come from the base. + let mut external_rules = FxHashMap::default(); for override_config in overrides_to_apply { if let Some(override_plugins) = &override_config.plugins { @@ -151,7 +171,19 @@ impl Config { } if !override_config.rules.is_empty() { - override_config.rules.override_rules(&mut rules, &all_rules); + // TODO(camc314): this needs refactoring such that this call is infallible + // note: errors from this FN call can currently only come from custom plugins (unmatched rules) + // so it's ok to to have the `unwrap` for now + #[expect(clippy::missing_panics_doc)] + override_config + .rules + .override_rules( + &mut rules, + &mut external_rules, + &all_rules, + external_plugin_store, + ) + .unwrap(); } if let Some(override_env) = &override_config.env { @@ -179,7 +211,17 @@ impl Config { let rules = rules.into_iter().filter(|(_, severity)| severity.is_warn_deny()).collect::>(); - ResolvedLinterState { rules: Arc::from(rules.into_boxed_slice()), config } + + let external_rules = external_rules + .into_iter() + .filter(|(_, severity)| severity.is_warn_deny()) + .collect::>(); + + ResolvedLinterState { + rules: Arc::from(rules.into_boxed_slice()), + config, + external_rules: Arc::from(external_rules.into_boxed_slice()), + } } } @@ -192,11 +234,20 @@ impl Config { pub struct ConfigStore { base: Config, nested_configs: FxHashMap, + external_plugin_store: Arc, } impl ConfigStore { - pub fn new(base_config: Config, nested_configs: FxHashMap) -> Self { - Self { base: base_config, nested_configs } + pub fn new( + base_config: Config, + nested_configs: FxHashMap, + external_plugin_store: ExternalPluginStore, + ) -> Self { + Self { + base: base_config, + nested_configs, + external_plugin_store: Arc::new(external_plugin_store), + } } pub fn number_of_rules(&self) -> Option { @@ -220,7 +271,7 @@ impl ConfigStore { &self.base }; - Config::apply_overrides(resolved_config, path) + Config::apply_overrides(resolved_config, path, &self.external_plugin_store) } fn get_nearest_config(&self, path: &Path) -> Option<&Config> { @@ -243,7 +294,7 @@ mod test { use super::{ConfigStore, OxlintOverrides}; use crate::{ - AllowWarnDeny, BuiltinLintPlugins, LintPlugins, RuleEnum, + AllowWarnDeny, BuiltinLintPlugins, ExternalPluginStore, LintPlugins, RuleEnum, config::{ LintConfig, OxlintEnv, OxlintGlobals, OxlintSettings, categories::OxlintCategories, config_store::Config, @@ -270,8 +321,15 @@ mod test { "rules": {} }]); let store = ConfigStore::new( - Config::new(base_rules, OxlintCategories::default(), LintConfig::default(), overrides), + Config::new( + base_rules, + vec![], + OxlintCategories::default(), + LintConfig::default(), + overrides, + ), FxHashMap::default(), + ExternalPluginStore::default(), ); let rules_for_source_file = store.resolve("App.tsx".as_ref()); @@ -292,8 +350,15 @@ mod test { "rules": {} }]); let store = ConfigStore::new( - Config::new(base_rules, OxlintCategories::default(), LintConfig::default(), overrides), + Config::new( + base_rules, + vec![], + OxlintCategories::default(), + LintConfig::default(), + overrides, + ), FxHashMap::default(), + ExternalPluginStore::default(), ); let rules_for_source_file = store.resolve("App.tsx".as_ref()); @@ -315,8 +380,15 @@ mod test { }]); let store = ConfigStore::new( - Config::new(base_rules, OxlintCategories::default(), LintConfig::default(), overrides), + Config::new( + base_rules, + vec![], + OxlintCategories::default(), + LintConfig::default(), + overrides, + ), FxHashMap::default(), + ExternalPluginStore::default(), ); assert_eq!(store.number_of_rules(), Some(1)); @@ -338,8 +410,15 @@ mod test { }]); let store = ConfigStore::new( - Config::new(base_rules, OxlintCategories::default(), LintConfig::default(), overrides), + Config::new( + base_rules, + vec![], + OxlintCategories::default(), + LintConfig::default(), + overrides, + ), FxHashMap::default(), + ExternalPluginStore::default(), ); assert_eq!(store.number_of_rules(), Some(1)); @@ -361,8 +440,15 @@ mod test { }]); let store = ConfigStore::new( - Config::new(base_rules, OxlintCategories::default(), LintConfig::default(), overrides), + Config::new( + base_rules, + vec![], + OxlintCategories::default(), + LintConfig::default(), + overrides, + ), FxHashMap::default(), + ExternalPluginStore::default(), ); assert_eq!(store.number_of_rules(), Some(1)); @@ -393,8 +479,9 @@ mod test { }]); let store = ConfigStore::new( - Config::new(vec![], OxlintCategories::default(), base_config, overrides), + Config::new(vec![], vec![], OxlintCategories::default(), base_config, overrides), FxHashMap::default(), + ExternalPluginStore::default(), ); assert_eq!(store.base.base.config.plugins.builtin, BuiltinLintPlugins::IMPORT); @@ -436,8 +523,9 @@ mod test { }]); let store = ConfigStore::new( - Config::new(vec![], OxlintCategories::default(), base_config, overrides), + Config::new(vec![], vec![], OxlintCategories::default(), base_config, overrides), FxHashMap::default(), + ExternalPluginStore::default(), ); assert!(!store.base.base.config.env.contains("React")); @@ -463,8 +551,9 @@ mod test { }]); let store = ConfigStore::new( - Config::new(vec![], OxlintCategories::default(), base_config, overrides), + Config::new(vec![], vec![], OxlintCategories::default(), base_config, overrides), FxHashMap::default(), + ExternalPluginStore::default(), ); assert!(store.base.base.config.env.contains("es2024")); @@ -491,8 +580,9 @@ mod test { }]); let store = ConfigStore::new( - Config::new(vec![], OxlintCategories::default(), base_config, overrides), + Config::new(vec![], vec![], OxlintCategories::default(), base_config, overrides), FxHashMap::default(), + ExternalPluginStore::default(), ); assert!(!store.base.base.config.globals.is_enabled("React")); assert!(!store.base.base.config.globals.is_enabled("Secret")); @@ -524,8 +614,9 @@ mod test { }]); let store = ConfigStore::new( - Config::new(vec![], OxlintCategories::default(), base_config, overrides), + Config::new(vec![], vec![], OxlintCategories::default(), base_config, overrides), FxHashMap::default(), + ExternalPluginStore::default(), ); assert!(store.base.base.config.globals.is_enabled("React")); assert!(store.base.base.config.globals.is_enabled("Secret")); diff --git a/crates/oxc_linter/src/config/mod.rs b/crates/oxc_linter/src/config/mod.rs index 978f55c37800b..10f31e7e0d3be 100644 --- a/crates/oxc_linter/src/config/mod.rs +++ b/crates/oxc_linter/src/config/mod.rs @@ -54,7 +54,7 @@ mod test { use serde::Deserialize; use super::Oxlintrc; - use crate::rules::RULES; + use crate::{ExternalPluginStore, rules::RULES}; #[test] fn test_from_file() { @@ -112,7 +112,17 @@ mod test { env::current_dir().unwrap().join("fixtures/eslint_config_vitest_replace.json"); let config = Oxlintrc::from_file(&fixture_path).unwrap(); let mut set = FxHashMap::default(); - config.rules.override_rules(&mut set, &RULES); + let mut external_rules_for_override = FxHashMap::default(); + let external_linter_store = ExternalPluginStore::default(); + config + .rules + .override_rules( + &mut set, + &mut external_rules_for_override, + &RULES, + &external_linter_store, + ) + .unwrap(); let (rule, _) = set.into_iter().next().unwrap(); assert_eq!(rule.name(), "no-disabled-tests"); diff --git a/crates/oxc_linter/src/config/rules.rs b/crates/oxc_linter/src/config/rules.rs index a264fa6c8d735..66d36eef5fc00 100644 --- a/crates/oxc_linter/src/config/rules.rs +++ b/crates/oxc_linter/src/config/rules.rs @@ -11,7 +11,8 @@ use serde::{ use oxc_diagnostics::{Error, OxcDiagnostic}; use crate::{ - AllowWarnDeny, + AllowWarnDeny, BuiltinLintPlugins, ExternalPluginStore, + external_plugin_store::{ExternalRuleId, ExternalRuleLookupError}, rules::{RULES, RuleEnum}, utils::{is_eslint_rule_adapted_to_typescript, is_jest_rule_adapted_to_vitest}, }; @@ -58,9 +59,16 @@ pub struct ESLintRule { } impl OxlintRules { - pub(crate) fn override_rules(&self, rules_for_override: &mut RuleSet, all_rules: &[RuleEnum]) { + pub(crate) fn override_rules( + &self, + rules_for_override: &mut RuleSet, + external_rules_for_override: &mut FxHashMap, + all_rules: &[RuleEnum], + external_plugin_store: &ExternalPluginStore, + ) -> Result<(), ExternalRuleLookupError> { use itertools::Itertools; let mut rules_to_replace = vec![]; + let mut external_rules_to_replace = vec![]; let lookup = self.rules.iter().into_group_map_by(|r| r.rule_name.as_str()); @@ -79,14 +87,20 @@ impl OxlintRules { let config = rule_config.config.clone().unwrap_or_default(); let severity = rule_config.severity; - let rule = rules_map.get(&plugin_name).copied().or_else(|| { - all_rules - .iter() - .find(|r| r.name() == rule_name && r.plugin_name() == plugin_name) - }); - - if let Some(rule) = rule { - rules_to_replace.push((rule.read_json(config), severity)); + // TODO(camc314): remove the `plugin_name == "eslint"` + if plugin_name == "eslint" || !BuiltinLintPlugins::from(plugin_name).is_empty() { + let rule = rules_map.get(&plugin_name).copied().or_else(|| { + all_rules + .iter() + .find(|r| r.name() == rule_name && r.plugin_name() == plugin_name) + }); + if let Some(rule) = rule { + rules_to_replace.push((rule.read_json(config), severity)); + } + } else { + let external_rule_id = + external_plugin_store.lookup_rule_id(plugin_name, rule_name)?; + external_rules_to_replace.push((external_rule_id, severity)); } } } @@ -95,6 +109,12 @@ impl OxlintRules { let _ = rules_for_override.remove(&rule); rules_for_override.insert(rule, severity); } + for (external_rule_id, severity) in external_rules_to_replace { + let _ = external_rules_for_override.remove(&external_rule_id); + external_rules_for_override.insert(external_rule_id, severity); + } + + Ok(()) } } @@ -290,11 +310,12 @@ impl ESLintRule { #[cfg(test)] #[expect(clippy::default_trait_access)] mod test { + use rustc_hash::FxHashMap; use serde::Deserialize; use serde_json::{Value, json}; use crate::{ - AllowWarnDeny, + AllowWarnDeny, ExternalPluginStore, rules::{RULES, RuleEnum}, }; @@ -344,7 +365,11 @@ mod test { fn r#override(rules: &mut RuleSet, rules_rc: &Value) { let rules_config = OxlintRules::deserialize(rules_rc).unwrap(); - rules_config.override_rules(rules, &RULES); + let mut external_rules_for_override = FxHashMap::default(); + let external_linter_store = ExternalPluginStore::default(); + rules_config + .override_rules(rules, &mut external_rules_for_override, &RULES, &external_linter_store) + .unwrap(); } #[test] diff --git a/crates/oxc_linter/src/external_linter.rs b/crates/oxc_linter/src/external_linter.rs index 1598e5ca0c84b..62f7c917c6e8a 100644 --- a/crates/oxc_linter/src/external_linter.rs +++ b/crates/oxc_linter/src/external_linter.rs @@ -17,19 +17,17 @@ pub type ExternalLinterLoadPluginCb = Arc< >; pub type ExternalLinterCb = Arc< - dyn Fn() -> Pin< - Box>> + Send>, - >, + dyn Fn(String, Vec) -> Result<(), Box> + Sync + Send, >; #[derive(Clone, Debug, Deserialize, Serialize)] pub enum PluginLoadResult { - Success, + Success { name: String, offset: usize, rules: Vec }, Failure(String), } #[derive(Clone)] -#[expect(dead_code)] +#[cfg_attr(any(not(feature = "oxlint2"), feature = "disable_oxlint2"), expect(dead_code))] pub struct ExternalLinter { pub(crate) load_plugin: ExternalLinterLoadPluginCb, pub(crate) run: ExternalLinterCb, diff --git a/crates/oxc_linter/src/external_plugin_store.rs b/crates/oxc_linter/src/external_plugin_store.rs new file mode 100644 index 0000000000000..aa3f852edadd5 --- /dev/null +++ b/crates/oxc_linter/src/external_plugin_store.rs @@ -0,0 +1,144 @@ +use std::fmt; + +use rustc_hash::{FxHashMap, FxHashSet}; + +use nonmax::NonMaxU32; +use oxc_index::{Idx, IndexVec}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ExternalPluginId(NonMaxU32); + +impl Idx for ExternalPluginId { + #[expect(clippy::cast_possible_truncation)] + fn from_usize(idx: usize) -> Self { + assert!(idx < u32::MAX as usize); + // SAFETY: We just checked `idx` is valid for `NonMaxU32` + Self(unsafe { NonMaxU32::new_unchecked(idx as u32) }) + } + + fn index(self) -> usize { + self.0.get() as usize + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ExternalRuleId(NonMaxU32); + +impl Idx for ExternalRuleId { + #[expect(clippy::cast_possible_truncation)] + fn from_usize(idx: usize) -> Self { + assert!(idx < u32::MAX as usize); + // SAFETY: We just checked `idx` is valid for `NonMaxU32` + Self(unsafe { NonMaxU32::new_unchecked(idx as u32) }) + } + + fn index(self) -> usize { + self.0.get() as usize + } +} + +impl ExternalRuleId { + #[inline] + pub fn as_u32(self) -> u32 { + self.0.get() + } +} + +#[derive(Debug, Default)] +pub struct ExternalPluginStore { + registered_plugin_specifiers: FxHashSet, + + plugins: IndexVec, + plugin_names: FxHashMap, + rules: IndexVec, +} + +impl ExternalPluginStore { + pub fn is_plugin_registered(&self, plugin_specifier: &str) -> bool { + self.registered_plugin_specifiers.contains(plugin_specifier) + } + + /// # Panics + /// Panics if you use it wrong + pub fn register_plugin( + &mut self, + plugin_specifier: String, + plugin_name: String, + offset: usize, + rules: Vec, + ) { + let registered_plugin_specifier_newly_inserted = + self.registered_plugin_specifiers.insert(plugin_specifier); + assert!(registered_plugin_specifier_newly_inserted); + + let plugin_id: ExternalPluginId = self + .plugins + .push(ExternalPlugin { name: plugin_name.clone(), rules: FxHashMap::default() }); + self.plugin_names.insert(plugin_name, plugin_id); + + assert!( + offset == self.rules.len(), + "register_plugin: expected offset {}, but rule table is currently {} long", + offset, + self.rules.len() + ); + + for rule_name in rules { + let rule_id = self.rules.push(ExternalRule { name: rule_name.clone(), plugin_id }); + self.plugins[plugin_id].rules.insert(rule_name, rule_id); + } + } + + /// # Errors + /// Returns an error if the plugin, or rule could not be found + pub fn lookup_rule_id( + &self, + plugin_name: &str, + rule_name: &str, + ) -> Result { + let plugin_id: &ExternalPluginId = self.plugin_names.get(plugin_name).ok_or_else(|| { + ExternalRuleLookupError::PluginNotFound { plugin: plugin_name.to_string() } + })?; + + self.plugins[*plugin_id].rules.get(rule_name).copied().ok_or_else(|| { + ExternalRuleLookupError::RuleNotFound { + plugin: plugin_name.to_string(), + rule: rule_name.to_string(), + } + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExternalRuleLookupError { + PluginNotFound { plugin: String }, + RuleNotFound { plugin: String, rule: String }, +} + +impl fmt::Display for ExternalRuleLookupError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ExternalRuleLookupError::PluginNotFound { plugin } => { + write!(f, "Plugin '{plugin}' not found",) + } + ExternalRuleLookupError::RuleNotFound { plugin, rule } => { + write!(f, "Rule '{rule}' not found in plugin '{plugin}'") + } + } + } +} + +impl std::error::Error for ExternalRuleLookupError {} + +#[derive(Debug, Default)] +#[expect(dead_code)] +struct ExternalPlugin { + name: String, + rules: FxHashMap, +} + +#[derive(Debug, Default, PartialEq, Eq)] +struct ExternalRule { + name: String, + plugin_id: ExternalPluginId, +} diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index b07fc31f4fcc4..82d89ecf32e72 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -9,6 +9,7 @@ mod config; mod context; mod disable_directives; mod external_linter; +mod external_plugin_store; mod fixer; mod frameworks; mod globals; @@ -36,6 +37,7 @@ pub use crate::{ external_linter::{ ExternalLinter, ExternalLinterCb, ExternalLinterLoadPluginCb, PluginLoadResult, }, + external_plugin_store::ExternalPluginStore, fixer::FixKind, frameworks::FrameworkFlags, loader::LINTABLE_EXTENSIONS, @@ -68,15 +70,20 @@ fn size_asserts() { } #[derive(Debug, Clone)] +#[expect(clippy::struct_field_names)] pub struct Linter { options: LintOptions, - // config: Arc, config: ConfigStore, + external_linter: Option, } impl Linter { - pub fn new(options: LintOptions, config: ConfigStore) -> Self { - Self { options, config } + pub fn new( + options: LintOptions, + config: ConfigStore, + external_linter: Option, + ) -> Self { + Self { options, config, external_linter } } /// Set the kind of auto fixes to apply. @@ -109,7 +116,7 @@ impl Linter { semantic: Rc>, module_record: Arc, ) -> Vec> { - let ResolvedLinterState { rules, config } = self.config.resolve(path); + let ResolvedLinterState { rules, config, external_rules } = self.config.resolve(path); let ctx_host = Rc::new(ContextHost::new(path, semantic, module_record, self.options, config)); @@ -189,6 +196,24 @@ impl Linter { } } + if !external_rules.is_empty() { + if let Some(external_linter) = self.external_linter.as_ref() { + let result = (external_linter.run)( + #[expect(clippy::missing_panics_doc)] + path.to_str().unwrap().to_string(), + external_rules.iter().map(|(rule_id, _)| rule_id.as_u32()).collect(), + ); + match result { + Ok(()) => { + // TODO: report diagnostics + } + Err(_err) => { + // TODO: report diagnostic + } + } + } + } + if let Some(severity) = self.options.report_unused_directive { if severity.is_warn_deny() { ctx_host.report_unused_directives(severity.into()); diff --git a/crates/oxc_linter/src/tester.rs b/crates/oxc_linter/src/tester.rs index fea38019a2cd9..49c686afcd2b6 100644 --- a/crates/oxc_linter/src/tester.rs +++ b/crates/oxc_linter/src/tester.rs @@ -16,6 +16,7 @@ use serde_json::{Value, json}; use crate::{ AllowWarnDeny, BuiltinLintPlugins, ConfigStore, ConfigStoreBuilder, LintPlugins, LintService, LintServiceOptions, Linter, Oxlintrc, RuleEnum, + external_plugin_store::ExternalPluginStore, fixer::{FixKind, Fixer}, options::LintOptions, rules::RULES, @@ -501,6 +502,7 @@ impl Tester { ) -> TestResult { let allocator = Allocator::default(); let rule = self.find_rule().read_json(rule_config.unwrap_or_default()); + let mut external_plugin_store = ExternalPluginStore::default(); let linter = Linter::new( self.lint_options, ConfigStore::new( @@ -511,6 +513,7 @@ impl Tester { true, Oxlintrc::deserialize(v).unwrap(), None, + &mut external_plugin_store, ) .unwrap() }) @@ -520,7 +523,9 @@ impl Tester { .with_rule(rule, AllowWarnDeny::Warn) .build(), FxHashMap::default(), + external_plugin_store, ), + None, ) .with_fix(fix_kind.into()); diff --git a/napi/oxlint2/src/bindings.d.ts b/napi/oxlint2/src/bindings.d.ts index 660e57b5a1699..59d667cd4f991 100644 --- a/napi/oxlint2/src/bindings.d.ts +++ b/napi/oxlint2/src/bindings.d.ts @@ -4,6 +4,6 @@ export type JsLoadPluginCb = ((arg: string) => Promise) export type JsRunCb = - (() => void) + ((arg0: string, arg1: Array) => void) export declare function lint(loadPlugin: JsLoadPluginCb, run: JsRunCb): Promise diff --git a/napi/oxlint2/src/index.js b/napi/oxlint2/src/index.js index b3e9e69fddfa2..881abbb8384d5 100644 --- a/napi/oxlint2/src/index.js +++ b/napi/oxlint2/src/index.js @@ -1,21 +1,59 @@ import { lint } from './bindings.js'; +class PluginRegistry { + registeredPlugins = new Set(); + + registeredRules = []; + + isPluginRegistered(path) { + return this.registeredPlugins.has(path); + } + + registerPlugin(path, plugin) { + // TODO: use a validation library to assert the shape of the plugin + this.registeredPlugins.add(path); + const ret = { + name: plugin.meta.name, + offset: this.registeredRules.length, + rules: [], + }; + + for (const [ruleName, rule] of Object.entries(plugin.rules)) { + ret.rules.push(ruleName); + this.registeredRules.push(rule); + } + + return ret; + } + + *getRules(ruleIds) { + for (const ruleId of ruleIds) { + yield this.registeredRules[ruleId]; + } + } +} + class Linter { - pluginRegistry = new Map(); + pluginRegistry = new PluginRegistry(); run() { return lint(this.loadPlugin.bind(this), this.lint.bind(this)); } + /** + * @param {string} pluginName The name of the plugin we're loading + */ loadPlugin = async (pluginName) => { - if (this.pluginRegistry.has(pluginName)) { - return JSON.stringify({ Success: null }); + if (this.pluginRegistry.isPluginRegistered(pluginName)) { + return JSON.stringify({ + Failure: 'This plugin has already been registered', + }); } try { - const plugin = await import(pluginName); - this.pluginRegistry.set(pluginName, plugin); - return JSON.stringify({ Success: null }); + const { default: plugin } = await import(pluginName); + const ret = this.pluginRegistry.registerPlugin(pluginName, plugin); + return JSON.stringify({ Success: ret }); } catch (error) { const errorMessage = 'message' in error && typeof error.message === 'string' ? error.message @@ -24,8 +62,15 @@ class Linter { } }; - lint = async () => { - throw new Error('unimplemented'); + // TODO(camc314): why do we have to destructure here? + // In `./bindings.d.ts`, it doesn't indicate that we have to (typed as `(filePath: string, ruleIds: number[]))` + lint = ([filePath, ruleIds]) => { + if (typeof filePath !== 'string' || filePath.length === 0) { + throw new Error('expected filePath to be a non-zero length string'); + } + if (!Array.isArray(ruleIds) || ruleIds.length === 0) { + throw new Error('Expected `ruleIds` to be a non-zero len array'); + } }; } @@ -33,7 +78,6 @@ async function main() { const linter = new Linter(); const result = await linter.run(); - if (!result) { process.exit(1); } diff --git a/napi/oxlint2/src/lib.rs b/napi/oxlint2/src/lib.rs index bf74ab23e05ea..6f2981d28bc21 100644 --- a/napi/oxlint2/src/lib.rs +++ b/napi/oxlint2/src/lib.rs @@ -1,9 +1,13 @@ use std::{ process::{ExitCode, Termination}, - sync::Arc, + sync::{Arc, mpsc::channel}, }; -use napi::{Status, bindgen_prelude::Promise, threadsafe_function::ThreadsafeFunction}; +use napi::{ + Status, + bindgen_prelude::Promise, + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, +}; use napi_derive::napi; use oxlint::{ @@ -12,7 +16,7 @@ use oxlint::{ }; #[napi] -pub type JsRunCb = ThreadsafeFunction<(), /* no input */ (), (), Status, false>; +pub type JsRunCb = ThreadsafeFunction<(String, Vec), (), (String, Vec), Status, false>; #[napi] pub type JsLoadPluginCb = ThreadsafeFunction< @@ -25,11 +29,33 @@ pub type JsLoadPluginCb = ThreadsafeFunction< fn wrap_run(cb: JsRunCb) -> ExternalLinterCb { let cb = Arc::new(cb); - Arc::new(move || { - Box::pin({ - let cb = Arc::clone(&cb); - async move { cb.call_async(()).await.map_err(Into::into) } - }) + Arc::new(move |file_path: String, rule_ids: Vec| { + let cb = Arc::clone(&cb); + + let (tx, rx) = channel(); + + let status = cb.call_with_return_value( + (file_path, rule_ids), + ThreadsafeFunctionCallMode::NonBlocking, + move |result, _env| { + let _ = match &result { + Ok(()) => tx.send(Ok(())), + Err(e) => tx.send(Err(e.to_string())), + }; + + result + }, + ); + + if status != Status::Ok { + return Err(format!("Failed to schedule callback: {status:?}").into()); + } + + match rx.recv() { + Ok(Ok(())) => Ok(()), + Ok(Err(e)) => Err(format!("Callback reported error: {e}").into()), + Err(e) => Err(format!("Callback did not respond: {e}").into()), + } }) } @@ -40,9 +66,7 @@ fn wrap_load_plugin(cb: JsLoadPluginCb) -> ExternalLinterLoadPluginCb { let cb = Arc::clone(&cb); async move { let result = cb.call_async(plugin_name).await?.into_future().await?; - let plugin_load_result: PluginLoadResult = serde_json::from_str(&result)?; - Ok(plugin_load_result) } }) diff --git a/napi/oxlint2/test/__snapshots__/e2e.test.ts.snap b/napi/oxlint2/test/__snapshots__/e2e.test.ts.snap index c1fb796c01c97..1fb8bbdc27c0a 100644 --- a/napi/oxlint2/test/__snapshots__/e2e.test.ts.snap +++ b/napi/oxlint2/test/__snapshots__/e2e.test.ts.snap @@ -30,3 +30,10 @@ exports[`cli options for bundling > should report an error if a custom plugin ca | Cannot find module './test_plugin' " `; + +exports[`cli options for bundling > should report an error if a rule is not found within a custom plugin 1`] = ` +"Failed to parse configuration file. + + x Rule 'unknown-rule' not found in plugin 'basic-custom-plugin' +" +`; diff --git a/napi/oxlint2/test/e2e.test.ts b/napi/oxlint2/test/e2e.test.ts index 4e35cd5fb644d..2e99206e55bf1 100644 --- a/napi/oxlint2/test/e2e.test.ts +++ b/napi/oxlint2/test/e2e.test.ts @@ -22,14 +22,18 @@ function normalizeOutput(output: string): string { describe('cli options for bundling', () => { it('should lint a directory without errors', async () => { - const { stdout, exitCode } = await runOxlint('test/fixtures/built_in_no_errors'); + const { stdout, exitCode } = await runOxlint( + 'test/fixtures/built_in_no_errors', + ); expect(exitCode).toBe(0); expect(normalizeOutput(stdout)).toMatchSnapshot(); }); it('should lint a directory with errors', async () => { - const { stdout, exitCode } = await runOxlint('test/fixtures/built_in_errors'); + const { stdout, exitCode } = await runOxlint( + 'test/fixtures/built_in_errors', + ); expect(exitCode).toBe(1); expect(normalizeOutput(stdout)).toMatchSnapshot(); @@ -52,4 +56,13 @@ describe('cli options for bundling', () => { expect(exitCode).toBe(1); expect(normalizeOutput(stdout)).toMatchSnapshot(); }); + + it('should report an error if a rule is not found within a custom plugin', async () => { + const { stdout, exitCode } = await runOxlint( + 'test/fixtures/custom_plugin_missing_rule', + ); + + expect(exitCode).toBe(1); + expect(normalizeOutput(stdout)).toMatchSnapshot(); + }); }); diff --git a/napi/oxlint2/test/fixtures/basic_custom_plugin/.oxlintrc.json b/napi/oxlint2/test/fixtures/basic_custom_plugin/.oxlintrc.json index 287c08debcc8b..97cb03b2bcd7d 100644 --- a/napi/oxlint2/test/fixtures/basic_custom_plugin/.oxlintrc.json +++ b/napi/oxlint2/test/fixtures/basic_custom_plugin/.oxlintrc.json @@ -1,5 +1,8 @@ { "plugins": [ "./test_plugin" - ] + ], + "rules": { + "basic-custom-plugin/no-debugger": "error" + } } diff --git a/napi/oxlint2/test/fixtures/basic_custom_plugin/test_plugin/index.js b/napi/oxlint2/test/fixtures/basic_custom_plugin/test_plugin/index.js index 4f00b08c16ffa..2d0676aded1a2 100644 --- a/napi/oxlint2/test/fixtures/basic_custom_plugin/test_plugin/index.js +++ b/napi/oxlint2/test/fixtures/basic_custom_plugin/test_plugin/index.js @@ -1,3 +1,14 @@ export default { - rules: {}, + meta: { + name: "basic-custom-plugin", + }, + rules: { + "no-debugger": (_context) => { + return { + DebuggerStatement(_debuggerStatement) { + throw new Error("unimplemented"); + }, + }; + }, + }, }; diff --git a/napi/oxlint2/test/fixtures/custom_plugin_missing_rule/.oxlintrc.json b/napi/oxlint2/test/fixtures/custom_plugin_missing_rule/.oxlintrc.json new file mode 100644 index 0000000000000..21032f3ec8189 --- /dev/null +++ b/napi/oxlint2/test/fixtures/custom_plugin_missing_rule/.oxlintrc.json @@ -0,0 +1,7 @@ +{ + "plugins": ["./test_plugin"], + "categories": { "correctness": "off" }, + "rules": { + "basic-custom-plugin/unknown-rule": "error" + } +} diff --git a/napi/oxlint2/test/fixtures/custom_plugin_missing_rule/test_plugin/index.js b/napi/oxlint2/test/fixtures/custom_plugin_missing_rule/test_plugin/index.js new file mode 100644 index 0000000000000..3fa2dc3dd5c40 --- /dev/null +++ b/napi/oxlint2/test/fixtures/custom_plugin_missing_rule/test_plugin/index.js @@ -0,0 +1,6 @@ +export default { + meta: { + name: "basic-custom-plugin", + }, + rules: {}, +}; diff --git a/napi/playground/src/lib.rs b/napi/playground/src/lib.rs index 700576e807eb4..52ddb50054346 100644 --- a/napi/playground/src/lib.rs +++ b/napi/playground/src/lib.rs @@ -29,7 +29,10 @@ use oxc::{ }; use oxc_formatter::{FormatOptions, Formatter}; use oxc_index::Idx; -use oxc_linter::{ConfigStore, ConfigStoreBuilder, LintOptions, Linter, ModuleRecord, Oxlintrc}; +use oxc_linter::{ + ConfigStore, ConfigStoreBuilder, ExternalPluginStore, LintOptions, Linter, ModuleRecord, + Oxlintrc, +}; use oxc_napi::{Comment, OxcError, convert_utf8_to_utf16}; use crate::options::{OxcLinterOptions, OxcOptions, OxcRunOptions}; @@ -288,15 +291,21 @@ impl Oxc { let oxlintrc = Oxlintrc::from_string(&linter_options.config.as_ref().unwrap().to_string()) .unwrap_or_default(); - let config_builder = - ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, None).unwrap_or_default(); + let config_builder = ConfigStoreBuilder::from_oxlintrc( + false, + oxlintrc, + None, + &mut ExternalPluginStore::default(), + ) + .unwrap_or_default(); config_builder.build() } else { ConfigStoreBuilder::default().build() }; let linter_ret = Linter::new( LintOptions::default(), - ConfigStore::new(lint_config, FxHashMap::default()), + ConfigStore::new(lint_config, FxHashMap::default(), ExternalPluginStore::default()), + None, ) .run(path, Rc::clone(&semantic), Arc::clone(module_record)); self.diagnostics.extend(linter_ret.into_iter().map(|e| e.error)); diff --git a/tasks/benchmark/benches/linter.rs b/tasks/benchmark/benches/linter.rs index 429555a6aa6d7..5097fcb481ca7 100644 --- a/tasks/benchmark/benches/linter.rs +++ b/tasks/benchmark/benches/linter.rs @@ -4,7 +4,10 @@ use rustc_hash::FxHashMap; use oxc_allocator::Allocator; use oxc_benchmark::{BenchmarkId, Criterion, criterion_group, criterion_main}; -use oxc_linter::{ConfigStore, ConfigStoreBuilder, FixKind, LintOptions, Linter, ModuleRecord}; +use oxc_linter::{ + ConfigStore, ConfigStoreBuilder, ExternalPluginStore, FixKind, LintOptions, Linter, + ModuleRecord, +}; use oxc_parser::Parser; use oxc_semantic::SemanticBuilder; use oxc_tasks_common::TestFiles; @@ -30,7 +33,8 @@ fn bench_linter(criterion: &mut Criterion) { let lint_config = ConfigStoreBuilder::all().build(); let linter = Linter::new( LintOptions::default(), - ConfigStore::new(lint_config, FxHashMap::default()), + ConfigStore::new(lint_config, FxHashMap::default(), ExternalPluginStore::default()), + None, ) .with_fix(FixKind::All); group.bench_function(id, |b| {