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
fix(linter): support nested extending (#9472)
- part of #9307

Allows for using the `extends` keyword to chain multiple extended config files together: for example, a package config that extends a base config that extends a config from `node_modules` and so on.

I don't really love what we're having to do here with using both `override_rules` and `ConfigStoreBuilder.with_rule`. I think there is a refactoring opportunity to merge some functionality together here, but I don't want to refactor and optimize before we have a more comprehensive test suite.
  • Loading branch information
camchenry committed Mar 4, 2025
commit 5ecda01c7fdd4e8f99fc4dd8dc4ffb884bc83051
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["../extends_rules_config.json"]
}
7 changes: 7 additions & 0 deletions apps/oxlint/src/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1022,4 +1022,11 @@ mod test {
let args = &["--config", "extends_rules_config.json", "console.js"];
Tester::new().with_cwd("fixtures/extends_config".into()).test_and_snapshot(args);
}

#[test]
fn test_extends_extends_config() {
// Check that using a config that extends a config which extends a config works
let args = &["--config", "relative_paths/extends_extends_config.json", "console.js"];
Tester::new().with_cwd("fixtures/extends_config".into()).test_and_snapshot(args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
source: apps/oxlint/src/tester.rs
---
##########
arguments: --config relative_paths/extends_extends_config.json console.js
working directory: fixtures/extends_config
----------

x ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-console.html\eslint(no-console)]8;;\: eslint(no-console): Unexpected console statement.
,-[console.js:1:1]
1 | console.log("test");
: ^^^^^^^^^^^
`----
help: Delete this console statement.

Found 0 warnings and 1 error.
Finished in <variable>ms on 1 file with 102 rules using 1 threads.
----------
CLI result: LintFoundErrors
----------
24 changes: 19 additions & 5 deletions crates/oxc_linter/src/config/config_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,23 +109,30 @@ impl ConfigStoreBuilder {
}

{
let all_rules = builder.cache.borrow();

if !extends.is_empty() {
let config_file_path = builder.config.path.as_ref().and_then(|p| p.parent());
let config_path = builder.config.path.clone();
let config_path_parent = config_path.as_ref().and_then(|p| p.parent());

for path in &extends {
// resolve path relative to config path
let path = match config_file_path {
let path = match config_path_parent {
Some(config_file_path) => &config_file_path.join(path),
None => path,
};
// TODO: throw an error if this is a self-referential extend
// TODO(perf): use a global config cache to avoid re-parsing the same file multiple times
match Oxlintrc::from_file(path) {
Ok(extended_config) => {
// TODO(refactor): can we merge this together? seems redundant to use `override_rules` and then
// use `ConfigStoreBuilder`, but we don't have a better way of loading rules from config files other than that.
// Use `override_rules` to apply rule configurations and add/remove rules as needed
extended_config
.rules
.override_rules(&mut builder.rules, all_rules.as_slice());
.override_rules(&mut builder.rules, &builder.cache.borrow());
// Use `ConfigStoreBuilder` to load extended config files and then apply rules from those
let extended_config_store =
ConfigStoreBuilder::from_oxlintrc(true, extended_config)?;
builder = builder.with_rules(extended_config_store.rules);
}
Err(err) => {
return Err(ConfigBuilderError::InvalidConfigFile {
Expand All @@ -137,6 +144,8 @@ impl ConfigStoreBuilder {
}
}

let all_rules = builder.cache.borrow();

oxlintrc_rules.override_rules(&mut builder.rules, all_rules.as_slice());
}

Expand Down Expand Up @@ -185,6 +194,11 @@ impl ConfigStoreBuilder {
self
}

pub(crate) fn with_rules<R: IntoIterator<Item = RuleWithSeverity>>(mut self, rules: R) -> Self {
self.rules.extend(rules);
self
}

pub fn with_filters<I: IntoIterator<Item = LintFilter>>(mut self, filters: I) -> Self {
for filter in filters {
self = self.with_filter(filter);
Expand Down
Loading