Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"overrides": [
{
"files": ["*.test.ts"],
"rules": {
"no-const-assign": "error"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": ["./base_override.json"],
"overrides": [
{
"files": ["*.test.ts"],
"rules": {
"no-const-assign": "off"
}
}
]
}
40 changes: 39 additions & 1 deletion crates/oxc_linter/src/config/config_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ impl ConfigStoreBuilder {

let (extends, extends_paths) = resolve_oxlintrc_config(extends_oxlintrc)?;

oxlintrc = oxlintrc.merge(extends);
oxlintrc = oxlintrc.merge(&extends);
extended_paths.extend(extends_paths);
}

Expand Down Expand Up @@ -1240,6 +1240,44 @@ mod test {
assert!(config.rules().is_empty());
}

#[test]
fn test_extends_overrides_precedence() {
// Test that current config's overrides take priority over extended config's overrides
// This is consistent with how base-level rules work (current overrides extended)

// Load the oxlintrc that extends a base config
let current_oxlintrc = Oxlintrc::from_file(&PathBuf::from(
"fixtures/extends_config/overrides/current_override.json",
))
.unwrap();

// Build the config with from_oxlintrc which will handle extends
let mut external_plugin_store = ExternalPluginStore::default();
let builder = ConfigStoreBuilder::from_oxlintrc(
false, // start_empty = false to get default rules
current_oxlintrc,
None,
&mut external_plugin_store,
)
.unwrap();

let config = builder.build(&external_plugin_store).unwrap();

// Apply overrides for a foo.test.ts file (matches both overrides)
let resolved = config.apply_overrides(Path::new("foo.test.ts"));

// The no-const-assign rule should be "off" (disabled, not present in rules)
// because current config's override sets it to "off", which should take priority
// over the extended config's override which sets it to "error"
let no_const_assign_rule =
resolved.rules.iter().find(|(rule, _)| rule.name() == "no-const-assign");

assert!(
no_const_assign_rule.is_none(),
"no-const-assign should be disabled (off) by current config's override, not error from extended config"
);
}

fn config_store_from_path(path: &str) -> Config {
let mut external_plugin_store = ExternalPluginStore::default();
ConfigStoreBuilder::from_oxlintrc(
Expand Down
6 changes: 3 additions & 3 deletions crates/oxc_linter/src/config/oxlintrc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ impl Oxlintrc {
/// Merges two [Oxlintrc] files together
/// [Self] takes priority over `other`
Copy link
Member

@overlookmotel overlookmotel Nov 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment still accurate? If not, please update it.

Given that this is a bit confusing (hence why implementation was wrong before this PR), it'd also be helpful to expand this comment with an example of what merging looks like - show who wins when self and other both have the same property.

#[must_use]
pub fn merge(&self, other: Oxlintrc) -> Oxlintrc {
pub fn merge(&self, other: &Oxlintrc) -> Oxlintrc {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing other from Oxlintrc to &Oxlintrc introduces the necessity for an additional clone. The caller has an owned Oxlintrc, so this is unnecessary.

I know taking an owned Oxlintrc is a bit weird, but given that we have one, we may as well avoid the clone.

Could you revert that change please?

let mut categories = other.categories.clone();
categories.extend(self.categories.iter());

Expand All @@ -242,8 +242,8 @@ impl Oxlintrc {
let env = self.env.clone();
let globals = self.globals.clone();

let mut overrides = self.overrides.clone();
overrides.extend(other.overrides);
let mut overrides = other.overrides.clone();
overrides.extend(self.overrides.clone());

let plugins = match (self.plugins, other.plugins) {
(Some(self_plugins), Some(other_plugins)) => Some(self_plugins | other_plugins),
Expand Down
Loading