diff --git a/apps/oxlint/fixtures/extends_config/relative_paths/extends_extends_config.json b/apps/oxlint/fixtures/extends_config/relative_paths/extends_extends_config.json new file mode 100644 index 0000000000000..a9c86df9c235d --- /dev/null +++ b/apps/oxlint/fixtures/extends_config/relative_paths/extends_extends_config.json @@ -0,0 +1,3 @@ +{ + "extends": ["../extends_rules_config.json"] +} diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index 9c7547e8f2727..9c75325d25e65 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -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); + } } diff --git a/apps/oxlint/src/snapshots/fixtures__extends_config_--config relative_paths__extends_extends_config.json console.js@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__extends_config_--config relative_paths__extends_extends_config.json console.js@oxlint.snap new file mode 100644 index 0000000000000..4caff171f72c2 --- /dev/null +++ b/apps/oxlint/src/snapshots/fixtures__extends_config_--config relative_paths__extends_extends_config.json console.js@oxlint.snap @@ -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 ms on 1 file with 102 rules using 1 threads. +---------- +CLI result: LintFoundErrors +---------- diff --git a/crates/oxc_linter/src/config/config_builder.rs b/crates/oxc_linter/src/config/config_builder.rs index 7a3b92d4a7344..239101f1d4aaf 100644 --- a/crates/oxc_linter/src/config/config_builder.rs +++ b/crates/oxc_linter/src/config/config_builder.rs @@ -109,13 +109,13 @@ 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, }; @@ -123,9 +123,16 @@ impl ConfigStoreBuilder { // 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 { @@ -137,6 +144,8 @@ impl ConfigStoreBuilder { } } + let all_rules = builder.cache.borrow(); + oxlintrc_rules.override_rules(&mut builder.rules, all_rules.as_slice()); } @@ -185,6 +194,11 @@ impl ConfigStoreBuilder { self } + pub(crate) fn with_rules>(mut self, rules: R) -> Self { + self.rules.extend(rules); + self + } + pub fn with_filters>(mut self, filters: I) -> Self { for filter in filters { self = self.with_filter(filter);