Skip to content

Commit 287a9ea

Browse files
committed
fix(linter): delay merging of oxlintrc configs
1 parent a033c1e commit 287a9ea

16 files changed

Lines changed: 243 additions & 157 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"overrides": [
3+
{
4+
"files": ["*.test.ts"],
5+
"plugins": ["jest", "vitest"],
6+
"rules": {
7+
"jest/valid-title": "deny",
8+
"no-unused-vars": "off"
9+
}
10+
}
11+
]
12+
}
13+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
describe("", () => {
2+
//
3+
4+
it("", () => {});
5+
});
6+
7+
const foo = 123;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
const foo = 123;

apps/oxlint/src/lint.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,11 @@ impl Runner for LintRunner {
180180
FxHashMap::default()
181181
};
182182

183-
enable_plugins.apply_overrides(&mut oxlintrc.plugins);
183+
{
184+
let mut plugins = oxlintrc.plugins.unwrap_or_default();
185+
enable_plugins.apply_overrides(&mut plugins);
186+
oxlintrc.plugins = Some(plugins);
187+
}
184188

185189
let oxlintrc_for_print = if misc_options.print_config || basic_options.init {
186190
Some(oxlintrc.clone())
@@ -1153,4 +1157,10 @@ mod test {
11531157
let args = &["--import-plugin", "-D", "import/no-cycle"];
11541158
Tester::new().with_cwd("fixtures/import-cycle".into()).test_and_snapshot(args);
11551159
}
1160+
1161+
#[test]
1162+
fn test_plugins_in_overrides_enabled_correctly() {
1163+
let args = &["-c", ".oxlintrc.json"];
1164+
Tester::new().with_cwd("fixtures/overrides_with_plugin".into()).test_and_snapshot(args);
1165+
}
11561166
}

apps/oxlint/src/snapshots/fixtures__extends_config_--config relative_paths__extends_extends_config.json console.js@oxlint.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ working directory: fixtures/extends_config
1414
help: Delete this console statement.
1515
1616
Found 0 warnings and 1 error.
17-
Finished in <variable>ms on 1 file with 103 rules using 1 threads.
17+
Finished in <variable>ms on 1 file with 102 rules using 1 threads.
1818
----------
1919
CLI result: LintFoundErrors
2020
----------

apps/oxlint/src/snapshots/fixtures__extends_config_overrides@oxlint.snap

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
22
source: apps/oxlint/src/tester.rs
3-
snapshot_kind: text
43
---
54
##########
65
arguments: overrides
@@ -31,7 +30,16 @@ working directory: fixtures/extends_config
3130
`----
3231
help: Avoid using ambiguous text like "click here", replace it with more descriptive text that provides context.
3332
34-
Found 0 warnings and 3 errors.
33+
! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/anchor-is-valid.html\eslint-plugin-jsx-a11y(anchor-is-valid)]8;;\: Missing `href` attribute for the `a` element.
34+
,-[overrides/test.tsx:2:11]
35+
1 | function component(): any {
36+
2 | return <a>click here</a>;
37+
: ^
38+
3 | }
39+
`----
40+
help: Provide an `href` for the `a` element.
41+
42+
Found 1 warning and 3 errors.
3543
Finished in <variable>ms on 2 files using 1 threads.
3644
----------
3745
CLI result: LintFoundErrors
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
source: apps/oxlint/src/tester.rs
3+
---
4+
##########
5+
arguments: -c .oxlintrc.json
6+
working directory: fixtures/overrides_with_plugin
7+
----------
8+
9+
x ]8;;https://oxc.rs/docs/guide/usage/linter/rules/jest/valid-title.html\eslint-plugin-jest(valid-title)]8;;\: "Should not have an empty title"
10+
,-[index.test.ts:1:10]
11+
1 | describe("", () => {
12+
: ^^
13+
2 | //
14+
`----
15+
help: "Write a meaningful title for your test"
16+
17+
! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unused-vars.html\eslint(no-unused-vars)]8;;\: Variable 'foo' is declared but never used. Unused variables should start with a '_'.
18+
,-[index.ts:1:7]
19+
1 | const foo = 123;
20+
: ^|^
21+
: `-- 'foo' is declared here
22+
`----
23+
help: Consider removing this declaration.
24+
25+
x ]8;;https://oxc.rs/docs/guide/usage/linter/rules/jest/valid-title.html\eslint-plugin-jest(valid-title)]8;;\: "Should not have an empty title"
26+
,-[index.test.ts:4:6]
27+
3 |
28+
4 | it("", () => {});
29+
: ^^
30+
5 | });
31+
`----
32+
help: "Write a meaningful title for your test"
33+
34+
Found 1 warning and 2 errors.
35+
Finished in <variable>ms on 2 files with 101 rules using 1 threads.
36+
----------
37+
CLI result: LintFoundErrors
38+
----------

crates/oxc_linter/src/config/categories.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::{borrow::Cow, ops::Deref};
1+
use std::{
2+
borrow::Cow,
3+
ops::{Deref, DerefMut},
4+
};
25

36
use rustc_hash::FxHashMap;
47
use schemars::JsonSchema;
@@ -18,6 +21,12 @@ impl Deref for OxlintCategories {
1821
}
1922
}
2023

24+
impl DerefMut for OxlintCategories {
25+
fn deref_mut(&mut self) -> &mut Self::Target {
26+
&mut self.0
27+
}
28+
}
29+
2130
impl OxlintCategories {
2231
pub fn filters(&self) -> impl Iterator<Item = LintFilter> + '_ {
2332
self.iter().map(|(category, severity)| LintFilter::new(*severity, *category).unwrap())

crates/oxc_linter/src/config/config_builder.rs

Lines changed: 56 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -87,99 +87,70 @@ impl ConfigStoreBuilder {
8787
start_empty: bool,
8888
oxlintrc: Oxlintrc,
8989
) -> Result<Self, ConfigBuilderError> {
90-
// TODO(refactor); can we make this function infallible, and move all the error handling to
91-
// the `build` method?
92-
let Oxlintrc {
93-
plugins,
94-
settings,
95-
env,
96-
globals,
97-
categories,
98-
rules: oxlintrc_rules,
99-
overrides,
100-
path,
101-
ignore_patterns: _,
102-
extends,
103-
} = oxlintrc;
104-
105-
let config = LintConfig { plugins, settings, env, globals, path: Some(path) };
106-
let rules =
107-
if start_empty { FxHashSet::default() } else { Self::warn_correctness(plugins) };
108-
let cache = RulesCache::new(config.plugins);
109-
let mut builder = Self { rules, config, overrides, cache };
90+
// TODO: this can be cached to avoid re-conputing the same oxlintrc
91+
fn resolve_oxlintrc_config(config: Oxlintrc) -> Result<Oxlintrc, ConfigBuilderError> {
92+
let path = config.path.clone();
93+
let root_path = path.parent();
94+
let extends = config.extends.clone();
95+
96+
let mut oxlintrc = config;
97+
98+
for path in extends.iter().rev() {
99+
if path.starts_with("eslint:") || path.starts_with("plugin:") {
100+
// `eslint:` and `plugin:` named configs are not supported
101+
continue;
102+
}
103+
// if path does not include a ".", then we will heuristically skip it since it
104+
// kind of looks like it might be a named config
105+
if !path.to_string_lossy().contains('.') {
106+
continue;
107+
}
110108

111-
for filter in categories.filters() {
112-
builder = builder.with_filter(&filter);
113-
}
109+
let path = match root_path {
110+
Some(p) => &p.join(path),
111+
None => path,
112+
};
114113

115-
{
116-
if !extends.is_empty() {
117-
let config_path = builder.config.path.clone();
118-
let config_path_parent = config_path.as_ref().and_then(|p| p.parent());
119-
120-
for path in &extends {
121-
if path.starts_with("eslint:") || path.starts_with("plugin:") {
122-
// eslint: and plugin: named configs are not supported
123-
continue;
124-
}
125-
// if path does not include a ".", then we will heuristically skip it since it
126-
// kind of looks like it might be a named config
127-
if !path.to_string_lossy().contains('.') {
128-
continue;
114+
let extends_oxlintrc = Oxlintrc::from_file(path).map_err(|e| {
115+
ConfigBuilderError::InvalidConfigFile {
116+
file: path.display().to_string(),
117+
reason: e.to_string(),
129118
}
119+
})?;
130120

131-
// resolve path relative to config path
132-
let path = match config_path_parent {
133-
Some(config_file_path) => &config_file_path.join(path),
134-
None => path,
135-
};
136-
// TODO: throw an error if this is a self-referential extend
137-
// TODO(perf): use a global config cache to avoid re-parsing the same file multiple times
138-
match Oxlintrc::from_file(path) {
139-
Ok(extended_config) => {
140-
// TODO(refactor): can we merge this together? seems redundant to use `override_rules` and then
141-
// use `ConfigStoreBuilder`, but we don't have a better way of loading rules from config files other than that.
142-
// Use `override_rules` to apply rule configurations and add/remove rules as needed
143-
extended_config
144-
.rules
145-
.override_rules(&mut builder.rules, &builder.cache.borrow());
146-
// Use `ConfigStoreBuilder` to load extended config files and then apply rules from those
147-
let mut extended_config_store =
148-
ConfigStoreBuilder::from_oxlintrc(true, extended_config)?;
149-
let rules = std::mem::take(&mut extended_config_store.rules);
150-
builder = builder.with_rules(rules);
151-
152-
// Handle plugin inheritance
153-
let parent_plugins = extended_config_store.plugins();
154-
let child_plugins = builder.plugins();
155-
156-
if child_plugins == LintPlugins::default() {
157-
// If child has default plugins, inherit from parent
158-
builder = builder.with_plugins(parent_plugins);
159-
} else if child_plugins != LintPlugins::empty() {
160-
// If child specifies plugins, combine with parent's plugins
161-
builder = builder.with_plugins(child_plugins.union(parent_plugins));
162-
}
163-
164-
if !extended_config_store.overrides.is_empty() {
165-
let overrides =
166-
std::mem::take(&mut extended_config_store.overrides);
167-
builder = builder.with_overrides(overrides);
168-
}
169-
}
170-
Err(err) => {
171-
return Err(ConfigBuilderError::InvalidConfigFile {
172-
file: path.display().to_string(),
173-
reason: err.to_string(),
174-
});
175-
}
176-
}
177-
}
121+
let extends = resolve_oxlintrc_config(extends_oxlintrc)?;
122+
123+
oxlintrc = oxlintrc.merge(extends);
178124
}
125+
Ok(oxlintrc)
126+
}
127+
128+
let oxlintrc = resolve_oxlintrc_config(oxlintrc)?;
179129

130+
let rules = if start_empty {
131+
FxHashSet::default()
132+
} else {
133+
Self::warn_correctness(oxlintrc.plugins.unwrap_or_default())
134+
};
135+
136+
let config = LintConfig {
137+
plugins: oxlintrc.plugins.unwrap_or_default(),
138+
settings: oxlintrc.settings,
139+
env: oxlintrc.env,
140+
globals: oxlintrc.globals,
141+
path: Some(oxlintrc.path),
142+
};
143+
let cache = RulesCache::new(config.plugins);
144+
let mut builder = Self { rules, config, overrides: oxlintrc.overrides, cache };
145+
146+
for filter in oxlintrc.categories.filters() {
147+
builder = builder.with_filter(&filter);
148+
}
149+
150+
{
180151
let all_rules = builder.cache.borrow();
181152

182-
oxlintrc_rules.override_rules(&mut builder.rules, all_rules.as_slice());
153+
oxlintrc.rules.override_rules(&mut builder.rules, all_rules.as_slice());
183154
}
184155

185156
Ok(builder)
@@ -227,11 +198,6 @@ impl ConfigStoreBuilder {
227198
self
228199
}
229200

230-
pub(crate) fn with_rules<R: IntoIterator<Item = RuleWithSeverity>>(mut self, rules: R) -> Self {
231-
self.rules.extend(rules);
232-
self
233-
}
234-
235201
/// Appends an override to the end of the current list of overrides.
236202
pub fn with_overrides<O: IntoIterator<Item = OxlintOverride>>(mut self, overrides: O) -> Self {
237203
self.overrides.extend(overrides);

crates/oxc_linter/src/config/config_store.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ impl Config {
8585
let mut env = self.base.config.env.clone();
8686
let mut globals = self.base.config.globals.clone();
8787
let mut plugins = self.base.config.plugins;
88+
89+
for override_config in overrides_to_apply.clone() {
90+
if let Some(override_plugins) = override_config.plugins {
91+
plugins |= override_plugins;
92+
}
93+
}
94+
8895
let mut rules = self
8996
.base
9097
.rules
@@ -104,10 +111,6 @@ impl Config {
104111
override_config.rules.override_rules(&mut rules, &all_rules);
105112
}
106113

107-
if let Some(override_plugins) = override_config.plugins {
108-
plugins |= override_plugins;
109-
}
110-
111114
if let Some(override_env) = &override_config.env {
112115
override_env.override_envs(&mut env);
113116
}
@@ -136,11 +139,14 @@ impl Config {
136139
}
137140
}
138141

139-
/// Resolves a lint configuration for a given file, by applying overrides based on the file's path.
142+
/// Stores the configuration state for the linter including:
143+
/// 1. the root configuration (base)
144+
/// 2. any nested configurations (`nested_configs`)
145+
///
146+
/// If an explicit config has been provided `-c config.json`, then `nested_configs` will be empty
140147
#[derive(Debug, Clone)]
141148
pub struct ConfigStore {
142149
base: Config,
143-
144150
nested_configs: FxHashMap<PathBuf, Config>,
145151
}
146152

0 commit comments

Comments
 (0)