Skip to content

Commit 9f991a4

Browse files
authored
fix(linter): reverse extends overrides priority (#14939)
Fixes #14925
1 parent 9eda70f commit 9f991a4

File tree

4 files changed

+63
-4
lines changed

4 files changed

+63
-4
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"overrides": [
3+
{
4+
"files": ["*.test.ts"],
5+
"rules": {
6+
"no-const-assign": "error"
7+
}
8+
}
9+
]
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": ["./base_override.json"],
3+
"overrides": [
4+
{
5+
"files": ["*.test.ts"],
6+
"rules": {
7+
"no-const-assign": "off"
8+
}
9+
}
10+
]
11+
}

crates/oxc_linter/src/config/config_builder.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ impl ConfigStoreBuilder {
137137

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

140-
oxlintrc = oxlintrc.merge(extends);
140+
oxlintrc = oxlintrc.merge(&extends);
141141
extended_paths.extend(extends_paths);
142142
}
143143

@@ -1240,6 +1240,44 @@ mod test {
12401240
assert!(config.rules().is_empty());
12411241
}
12421242

1243+
#[test]
1244+
fn test_extends_overrides_precedence() {
1245+
// Test that current config's overrides take priority over extended config's overrides
1246+
// This is consistent with how base-level rules work (current overrides extended)
1247+
1248+
// Load the oxlintrc that extends a base config
1249+
let current_oxlintrc = Oxlintrc::from_file(&PathBuf::from(
1250+
"fixtures/extends_config/overrides/current_override.json",
1251+
))
1252+
.unwrap();
1253+
1254+
// Build the config with from_oxlintrc which will handle extends
1255+
let mut external_plugin_store = ExternalPluginStore::default();
1256+
let builder = ConfigStoreBuilder::from_oxlintrc(
1257+
false, // start_empty = false to get default rules
1258+
current_oxlintrc,
1259+
None,
1260+
&mut external_plugin_store,
1261+
)
1262+
.unwrap();
1263+
1264+
let config = builder.build(&external_plugin_store).unwrap();
1265+
1266+
// Apply overrides for a foo.test.ts file (matches both overrides)
1267+
let resolved = config.apply_overrides(Path::new("foo.test.ts"));
1268+
1269+
// The no-const-assign rule should be "off" (disabled, not present in rules)
1270+
// because current config's override sets it to "off", which should take priority
1271+
// over the extended config's override which sets it to "error"
1272+
let no_const_assign_rule =
1273+
resolved.rules.iter().find(|(rule, _)| rule.name() == "no-const-assign");
1274+
1275+
assert!(
1276+
no_const_assign_rule.is_none(),
1277+
"no-const-assign should be disabled (off) by current config's override, not error from extended config"
1278+
);
1279+
}
1280+
12431281
fn config_store_from_path(path: &str) -> Config {
12441282
let mut external_plugin_store = ExternalPluginStore::default();
12451283
ConfigStoreBuilder::from_oxlintrc(

crates/oxc_linter/src/config/oxlintrc.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ impl Oxlintrc {
218218
/// Merges two [Oxlintrc] files together
219219
/// [Self] takes priority over `other`
220220
#[must_use]
221-
pub fn merge(&self, other: Oxlintrc) -> Oxlintrc {
221+
pub fn merge(&self, other: &Oxlintrc) -> Oxlintrc {
222222
let mut categories = other.categories.clone();
223223
categories.extend(self.categories.iter());
224224

@@ -242,8 +242,8 @@ impl Oxlintrc {
242242
let env = self.env.clone();
243243
let globals = self.globals.clone();
244244

245-
let mut overrides = self.overrides.clone();
246-
overrides.extend(other.overrides);
245+
let mut overrides = other.overrides.clone();
246+
overrides.extend(self.overrides.clone());
247247

248248
let plugins = match (self.plugins, other.plugins) {
249249
(Some(self_plugins), Some(other_plugins)) => Some(self_plugins | other_plugins),

0 commit comments

Comments
 (0)