Skip to content

Commit c0e224a

Browse files
committed
refactor(linter): store ExternalRuleId in OxlintOverrides not raw names (#12502)
fixes #12653
1 parent 40c988c commit c0e224a

File tree

12 files changed

+362
-127
lines changed

12 files changed

+362
-127
lines changed

apps/oxlint/src/lint.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,19 @@ impl LintRunner {
276276
|| nested_configs.values().any(|config| config.plugins().has_import());
277277
let mut options = LintServiceOptions::new(self.cwd).with_cross_module(use_cross_module);
278278

279-
let lint_config = config_builder.build();
279+
let lint_config = match config_builder.build(&external_plugin_store) {
280+
Ok(config) => config,
281+
Err(e) => {
282+
print_and_flush_stdout(
283+
stdout,
284+
&format!(
285+
"Failed to build configuration.\n{}\n",
286+
render_report(&handler, &OxcDiagnostic::error(e.to_string()))
287+
),
288+
);
289+
return CliRunResult::InvalidOptionConfig;
290+
}
291+
};
280292

281293
let report_unused_directives = match inline_config_options.report_unused_directives {
282294
ReportUnusedDirectives::WithoutSeverity(true) => Some(AllowWarnDeny::Warn),
@@ -489,7 +501,19 @@ impl LintRunner {
489501
}
490502
.with_filters(filters);
491503

492-
let config = builder.build();
504+
let config = match builder.build(external_plugin_store) {
505+
Ok(config) => config,
506+
Err(e) => {
507+
print_and_flush_stdout(
508+
stdout,
509+
&format!(
510+
"Failed to build configuration.\n{}\n",
511+
render_report(handler, &OxcDiagnostic::error(e.to_string()))
512+
),
513+
);
514+
return Err(CliRunResult::InvalidOptionConfig);
515+
}
516+
};
493517
nested_configs.insert(dir.to_path_buf(), config);
494518
}
495519

crates/oxc_language_server/src/linter/server_linter.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ impl ServerLinter {
6767
&& nested_configs.pin().values().any(|config| config.plugins().has_import()));
6868

6969
extended_paths.extend(config_builder.extended_paths.clone());
70-
let base_config = config_builder.build();
70+
let external_plugin_store = ExternalPluginStore::default();
71+
let base_config = config_builder.build(&external_plugin_store).unwrap_or_else(|err| {
72+
warn!("Failed to build config: {err}");
73+
ConfigStoreBuilder::empty().build(&external_plugin_store).unwrap()
74+
});
7175

7276
let lint_options = LintOptions {
7377
fix: options.fix_kind(),
@@ -142,7 +146,12 @@ impl ServerLinter {
142146
continue;
143147
};
144148
extended_paths.extend(config_store_builder.extended_paths.clone());
145-
nested_configs.pin().insert(dir_path.to_path_buf(), config_store_builder.build());
149+
let external_plugin_store = ExternalPluginStore::default();
150+
let config = config_store_builder.build(&external_plugin_store).unwrap_or_else(|err| {
151+
warn!("Failed to build nested config for {}: {:?}", dir_path.display(), err);
152+
ConfigStoreBuilder::empty().build(&external_plugin_store).unwrap()
153+
});
154+
nested_configs.pin().insert(dir_path.to_path_buf(), config);
146155
}
147156

148157
(nested_configs, extended_paths)

crates/oxc_linter/src/config/config_builder.rs

Lines changed: 106 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::{
55

66
use itertools::Itertools;
77
use oxc_resolver::Resolver;
8-
use rustc_hash::FxHashMap;
8+
use rustc_hash::{FxHashMap, FxHashSet};
99

1010
use oxc_span::{CompactStr, format_compact_str};
1111

@@ -21,7 +21,11 @@ use crate::{
2121
rules::RULES,
2222
};
2323

24-
use super::{Config, categories::OxlintCategories};
24+
use super::{
25+
Config,
26+
categories::OxlintCategories,
27+
config_store::{ResolvedOxlintOverride, ResolvedOxlintOverrideRules, ResolvedOxlintOverrides},
28+
};
2529

2630
#[must_use = "You dropped your builder without building a Linter! Did you mean to call .build()?"]
2731
pub struct ConfigStoreBuilder {
@@ -144,35 +148,36 @@ impl ConfigStoreBuilder {
144148

145149
let (oxlintrc, extended_paths) = resolve_oxlintrc_config(oxlintrc)?;
146150

151+
// Collect external plugins from both base config and overrides
152+
let mut external_plugins = FxHashSet::default();
153+
147154
if let Some(base_plugins) = oxlintrc.plugins.as_ref() {
148-
let mut external_plugins = base_plugins.external.clone();
149-
for r#override in &oxlintrc.overrides {
150-
if let Some(override_plugins) = &r#override.plugins {
151-
external_plugins.extend(override_plugins.external.iter().cloned());
152-
}
155+
external_plugins.extend(base_plugins.external.iter().cloned());
156+
}
157+
158+
for r#override in &oxlintrc.overrides {
159+
if let Some(override_plugins) = &r#override.plugins {
160+
external_plugins.extend(override_plugins.external.iter().cloned());
153161
}
162+
}
154163

155-
if !external_plugins.is_empty() {
156-
let external_linter =
157-
external_linter.ok_or(ConfigBuilderError::NoExternalLinterConfigured)?;
158-
159-
let resolver = Resolver::default();
160-
161-
#[expect(
162-
clippy::missing_panics_doc,
163-
reason = "oxlintrc.path is always a file path"
164-
)]
165-
let oxlintrc_dir = oxlintrc.path.parent().unwrap();
166-
167-
for plugin_specifier in &external_plugins {
168-
Self::load_external_plugin(
169-
oxlintrc_dir,
170-
plugin_specifier,
171-
external_linter,
172-
&resolver,
173-
external_plugin_store,
174-
)?;
175-
}
164+
if !external_plugins.is_empty() {
165+
let external_linter =
166+
external_linter.ok_or(ConfigBuilderError::NoExternalLinterConfigured)?;
167+
168+
let resolver = Resolver::default();
169+
170+
#[expect(clippy::missing_panics_doc, reason = "oxlintrc.path is always a file path")]
171+
let oxlintrc_dir = oxlintrc.path.parent().unwrap();
172+
173+
for plugin_specifier in &external_plugins {
174+
Self::load_external_plugin(
175+
oxlintrc_dir,
176+
plugin_specifier,
177+
external_linter,
178+
&resolver,
179+
external_plugin_store,
180+
)?;
176181
}
177182
}
178183
let plugins = oxlintrc.plugins.unwrap_or_default();
@@ -320,11 +325,19 @@ impl ConfigStoreBuilder {
320325
/// re-configured, while those that are not are added. Affects rules where `query` returns
321326
/// `true`.
322327
fn get_all_rules(&self) -> Vec<RuleEnum> {
323-
if self.config.plugins.builtin.is_all() {
324-
RULES.clone()
328+
self.get_all_rules_for_plugins(None)
329+
}
330+
331+
fn get_all_rules_for_plugins(&self, override_plugins: Option<&LintPlugins>) -> Vec<RuleEnum> {
332+
let mut builtin_plugins = if let Some(override_plugins) = override_plugins {
333+
self.config.plugins.builtin | override_plugins.builtin
325334
} else {
326-
let mut builtin_plugins = self.plugins().builtin;
335+
self.config.plugins.builtin
336+
};
327337

338+
if builtin_plugins.is_all() {
339+
RULES.clone()
340+
} else {
328341
// we need to include some jest rules when vitest is enabled, see [`VITEST_COMPATIBLE_JEST_RULES`]
329342
if builtin_plugins.contains(BuiltinLintPlugins::VITEST) {
330343
builtin_plugins = builtin_plugins.union(BuiltinLintPlugins::JEST);
@@ -359,7 +372,13 @@ impl ConfigStoreBuilder {
359372
}
360373
}
361374

362-
pub fn build(self) -> Config {
375+
/// Builds a [`Config`] from the current state of the builder.
376+
/// # Errors
377+
/// Returns [`ConfigBuilderError::UnknownRules`] if there are rules that could not be matched.
378+
pub fn build(
379+
mut self,
380+
external_plugin_store: &ExternalPluginStore,
381+
) -> Result<Config, ConfigBuilderError> {
363382
// When a plugin gets disabled before build(), rules for that plugin aren't removed until
364383
// with_filters() gets called. If the user never calls it, those now-undesired rules need
365384
// to be taken out.
@@ -370,6 +389,11 @@ impl ConfigStoreBuilder {
370389
plugins = plugins.union(BuiltinLintPlugins::JEST);
371390
}
372391

392+
let overrides = std::mem::take(&mut self.overrides);
393+
let resolved_overrides = self
394+
.resolve_overrides(overrides, external_plugin_store)
395+
.map_err(ConfigBuilderError::ExternalRuleLookupError)?;
396+
373397
let mut rules: Vec<_> = self
374398
.rules
375399
.into_iter()
@@ -380,7 +404,47 @@ impl ConfigStoreBuilder {
380404
let mut external_rules: Vec<_> = self.external_rules.into_iter().collect();
381405
external_rules.sort_unstable_by_key(|(r, _)| *r);
382406

383-
Config::new(rules, external_rules, self.categories, self.config, self.overrides)
407+
Ok(Config::new(rules, external_rules, self.categories, self.config, resolved_overrides))
408+
}
409+
410+
fn resolve_overrides(
411+
&self,
412+
overrides: OxlintOverrides,
413+
external_plugin_store: &ExternalPluginStore,
414+
) -> Result<ResolvedOxlintOverrides, ExternalRuleLookupError> {
415+
let resolved = overrides
416+
.into_iter()
417+
.map(|override_config| {
418+
let mut builtin_rules = Vec::new();
419+
let mut external_rules = Vec::new();
420+
let mut rules_map = FxHashMap::default();
421+
let mut external_rules_map = FxHashMap::default();
422+
423+
let all_rules = self.get_all_rules_for_plugins(override_config.plugins.as_ref());
424+
425+
// Resolve rules for this override
426+
override_config.rules.override_rules(
427+
&mut rules_map,
428+
&mut external_rules_map,
429+
&all_rules,
430+
external_plugin_store,
431+
)?;
432+
433+
// Convert to vectors
434+
builtin_rules.extend(rules_map.into_iter());
435+
external_rules.extend(external_rules_map.into_iter());
436+
437+
Ok(ResolvedOxlintOverride {
438+
files: override_config.files,
439+
env: override_config.env,
440+
globals: override_config.globals,
441+
plugins: override_config.plugins,
442+
rules: ResolvedOxlintOverrideRules { builtin_rules, external_rules },
443+
})
444+
})
445+
.collect::<Result<Vec<_>, _>>()?;
446+
447+
Ok(ResolvedOxlintOverrides::new(resolved))
384448
}
385449

386450
/// Warn for all correctness rules in the given set of plugins.
@@ -707,8 +771,11 @@ mod test {
707771
let mut desired_plugins = LintPlugins::default();
708772
desired_plugins.builtin.set(BuiltinLintPlugins::TYPESCRIPT, false);
709773

710-
let linter =
711-
ConfigStoreBuilder::default().with_builtin_plugins(desired_plugins.builtin).build();
774+
let external_plugin_store = ExternalPluginStore::default();
775+
let linter = ConfigStoreBuilder::default()
776+
.with_builtin_plugins(desired_plugins.builtin)
777+
.build(&external_plugin_store)
778+
.unwrap();
712779
for (rule, _) in linter.base.rules.iter() {
713780
let name = rule.name();
714781
let plugin = rule.plugin_name();
@@ -1129,7 +1196,8 @@ mod test {
11291196
&mut external_plugin_store,
11301197
)
11311198
.unwrap()
1132-
.build()
1199+
.build(&external_plugin_store)
1200+
.unwrap()
11331201
}
11341202

11351203
fn config_store_from_str(s: &str) -> Config {
@@ -1141,6 +1209,7 @@ mod test {
11411209
&mut external_plugin_store,
11421210
)
11431211
.unwrap()
1144-
.build()
1212+
.build(&external_plugin_store)
1213+
.unwrap()
11451214
}
11461215
}

0 commit comments

Comments
 (0)