Skip to content

Commit fbb8f22

Browse files
committed
feat(linter): support ignores in overrides (#22148)
Adds override-local `ignores` support so matching files can be excluded from a specific oxlint override without being globally ignored. Example: ```json { "overrides": [ { "files": ["**/*.js"], "ignores": ["**/*.generated.js"], "rules": { "no-var": "error" } } ] } ``` closes #15997 fixes #15932
1 parent f31fc82 commit fbb8f22

8 files changed

Lines changed: 137 additions & 4 deletions

File tree

apps/oxlint/src-js/package/config.generated.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,16 @@ export interface OxlintOverride {
489489
* Enabled or disabled specific global variables.
490490
*/
491491
globals?: OxlintGlobals;
492+
/**
493+
* A list of glob patterns to exclude from this override.
494+
*
495+
* Files matching these patterns are not globally ignored; this override
496+
* simply does not apply to them.
497+
*
498+
* ## Example
499+
* `[ "*.generated.ts", "fixtures/**" ]`
500+
*/
501+
ignores?: GlobSet;
492502
/**
493503
* JS plugins for this override, allows usage of ESLint plugins with Oxlint.
494504
*

crates/oxc_linter/src/config/config_builder.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,7 @@ impl ConfigStoreBuilder {
530530

531531
Ok::<_, Vec<OverrideRulesError>>(ResolvedOxlintOverride {
532532
files: override_config.files,
533+
ignores: override_config.ignores,
533534
env: override_config.env,
534535
globals: override_config.globals,
535536
plugins: override_config.plugins,

crates/oxc_linter/src/config/config_store.rs

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ impl ResolvedOxlintOverrides {
4545
#[derive(Debug, Clone)]
4646
pub struct ResolvedOxlintOverride {
4747
pub files: GlobSet,
48+
pub ignores: GlobSet,
4849
pub env: Option<OxlintEnv>,
4950
pub globals: Option<OxlintGlobals>,
5051
pub plugins: Option<LintPlugins>,
@@ -138,8 +139,11 @@ impl Config {
138139
.unwrap_or(path);
139140

140141
let path = relative_path.to_string_lossy();
141-
let overrides_to_apply =
142-
self.overrides.iter().filter(|config| config.files.is_match(path.as_ref()));
142+
let path = path.as_ref();
143+
let overrides_to_apply = self
144+
.overrides
145+
.iter()
146+
.filter(|config| config.files.is_match(path) && !config.ignores.is_match(path));
143147

144148
let mut overrides_to_apply = overrides_to_apply.peekable();
145149

@@ -396,7 +400,10 @@ impl ConfigStore {
396400

397401
#[cfg(test)]
398402
mod test {
399-
use std::{path::PathBuf, str::FromStr};
403+
use std::{
404+
path::{Path, PathBuf},
405+
str::FromStr,
406+
};
400407

401408
use rustc_hash::FxHashMap;
402409
use serde_json::Value;
@@ -436,6 +443,7 @@ mod test {
436443
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
437444
env: None,
438445
files: GlobSet::new(vec!["*.test.{ts,tsx}"]),
446+
ignores: GlobSet::default(),
439447
plugins: None,
440448
globals: None,
441449
rules: ResolvedOxlintOverrideRules { builtin_rules: vec![], external_rules: vec![] },
@@ -467,6 +475,7 @@ mod test {
467475
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
468476
env: None,
469477
files: GlobSet::new(vec!["*.test.{ts,tsx}"]),
478+
ignores: GlobSet::default(),
470479
plugins: Some(
471480
LintPlugins::REACT
472481
| LintPlugins::TYPESCRIPT
@@ -503,6 +512,7 @@ mod test {
503512
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
504513
env: None,
505514
files: GlobSet::new(vec!["*.test.{ts,tsx}"]),
515+
ignores: GlobSet::default(),
506516
plugins: None,
507517
globals: None,
508518
rules: ResolvedOxlintOverrideRules {
@@ -540,6 +550,7 @@ mod test {
540550
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
541551
env: None,
542552
files: GlobSet::new(vec!["src/**/*.{ts,tsx}"]),
553+
ignores: GlobSet::default(),
543554
plugins: None,
544555
globals: None,
545556
rules: ResolvedOxlintOverrideRules {
@@ -577,6 +588,7 @@ mod test {
577588
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
578589
env: None,
579590
files: GlobSet::new(vec!["src/**/*.{ts,tsx}"]),
591+
ignores: GlobSet::default(),
580592
plugins: None,
581593
globals: None,
582594
rules: ResolvedOxlintOverrideRules {
@@ -617,6 +629,7 @@ mod test {
617629
ResolvedOxlintOverride {
618630
env: None,
619631
files: GlobSet::new(vec!["*.jsx", "*.tsx"]),
632+
ignores: GlobSet::default(),
620633
plugins: Some(LintPlugins::REACT),
621634
globals: None,
622635
rules: ResolvedOxlintOverrideRules {
@@ -627,6 +640,7 @@ mod test {
627640
ResolvedOxlintOverride {
628641
env: None,
629642
files: GlobSet::new(vec!["*.ts", "*.tsx"]),
643+
ignores: GlobSet::default(),
630644
plugins: Some(LintPlugins::TYPESCRIPT),
631645
globals: None,
632646
rules: ResolvedOxlintOverrideRules {
@@ -663,6 +677,7 @@ mod test {
663677
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
664678
env: Some(OxlintEnv::from_iter(["es2024".to_string()])),
665679
files: GlobSet::new(vec!["*.tsx"]),
680+
ignores: GlobSet::default(),
666681
plugins: None,
667682
globals: None,
668683
rules: ResolvedOxlintOverrideRules { builtin_rules: vec![], external_rules: vec![] },
@@ -685,6 +700,7 @@ mod test {
685700
LintConfig { env: OxlintEnv::from_iter(["es2024".into()]), ..Default::default() };
686701
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
687702
files: GlobSet::new(vec!["*.tsx"]),
703+
ignores: GlobSet::default(),
688704
env: Some(from_json!({ "es2024": false })),
689705
plugins: None,
690706
globals: None,
@@ -708,6 +724,7 @@ mod test {
708724

709725
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
710726
files: GlobSet::new(vec!["*.tsx"]),
727+
ignores: GlobSet::default(),
711728
env: None,
712729
plugins: None,
713730
globals: Some(from_json!({ "React": "readonly", "Secret": "writable" })),
@@ -746,6 +763,7 @@ mod test {
746763

747764
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
748765
files: GlobSet::new(vec!["*.ts"]),
766+
ignores: GlobSet::default(),
749767
env: None,
750768
plugins: None,
751769
globals: None,
@@ -783,6 +801,7 @@ mod test {
783801

784802
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
785803
files: GlobSet::new(vec!["*.tsx"]),
804+
ignores: GlobSet::default(),
786805
env: None,
787806
plugins: None,
788807
globals: Some(from_json!({ "React": "off", "Secret": "off" })),
@@ -828,6 +847,7 @@ mod test {
828847
ResolvedOxlintOverride {
829848
env: None,
830849
files: GlobSet::new(vec!["*.{ts,tsx,mts}"]),
850+
ignores: GlobSet::default(),
831851
plugins: Some(LintPlugins::TYPESCRIPT),
832852
globals: None,
833853
rules: ResolvedOxlintOverrideRules {
@@ -839,6 +859,7 @@ mod test {
839859
ResolvedOxlintOverride {
840860
env: None,
841861
files: GlobSet::new(vec!["*.{ts,tsx}"]),
862+
ignores: GlobSet::default(),
842863
plugins: Some(LintPlugins::REACT),
843864
globals: None,
844865
rules: ResolvedOxlintOverrideRules {
@@ -853,6 +874,7 @@ mod test {
853874
ResolvedOxlintOverride {
854875
env: None,
855876
files: GlobSet::new(vec!["*.{ts,tsx,mts}"]),
877+
ignores: GlobSet::default(),
856878
plugins: Some(LintPlugins::UNICORN),
857879
globals: None,
858880
rules: ResolvedOxlintOverrideRules {
@@ -914,6 +936,7 @@ mod test {
914936
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
915937
env: None,
916938
files: GlobSet::new(vec!["*.tsx"]),
939+
ignores: GlobSet::default(),
917940
plugins: Some(LintPlugins::REACT),
918941
globals: None,
919942
rules: ResolvedOxlintOverrideRules { builtin_rules: vec![], external_rules: vec![] },
@@ -947,6 +970,7 @@ mod test {
947970
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
948971
env: None,
949972
files: GlobSet::new(vec!["*.tsx"]),
973+
ignores: GlobSet::default(),
950974
plugins: None,
951975
globals: None,
952976
rules: ResolvedOxlintOverrideRules {
@@ -1026,6 +1050,7 @@ mod test {
10261050
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
10271051
env: None,
10281052
files: GlobSet::new(vec!["*.tsx"]),
1053+
ignores: GlobSet::default(),
10291054
plugins: Some(LintPlugins::TYPESCRIPT),
10301055
globals: None,
10311056
rules: ResolvedOxlintOverrideRules { builtin_rules: vec![], external_rules: vec![] },
@@ -1141,6 +1166,7 @@ mod test {
11411166
LintConfig::default(),
11421167
ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
11431168
files: GlobSet::new(vec!["*.js"]),
1169+
ignores: GlobSet::default(),
11441170
env: None,
11451171
globals: None,
11461172
plugins: None,
@@ -1333,6 +1359,47 @@ mod test {
13331359
.unwrap()
13341360
}
13351361

1362+
#[test]
1363+
fn test_override_ignores_exclude_only_that_override() {
1364+
let config = config_from_str_with_defaults(
1365+
r#"
1366+
{
1367+
"overrides": [
1368+
{
1369+
"files": ["**/*.js"],
1370+
"ignores": ["**/*.generated.js"],
1371+
"rules": { "no-var": "error" }
1372+
},
1373+
{
1374+
"files": ["**/*.js"],
1375+
"rules": { "no-console": "error" }
1376+
}
1377+
]
1378+
}
1379+
"#,
1380+
);
1381+
1382+
let regular = config.apply_overrides(Path::new("src/app.js"));
1383+
assert!(
1384+
regular.rules.iter().any(|(rule, _)| rule.name() == "no-var"),
1385+
"first override should apply to regular JS files"
1386+
);
1387+
assert!(
1388+
regular.rules.iter().any(|(rule, _)| rule.name() == "no-console"),
1389+
"second override should apply to regular JS files"
1390+
);
1391+
1392+
let generated = config.apply_overrides(Path::new("src/app.generated.js"));
1393+
assert!(
1394+
!generated.rules.iter().any(|(rule, _)| rule.name() == "no-var"),
1395+
"ignores should exclude only the override where it is declared"
1396+
);
1397+
assert!(
1398+
generated.rules.iter().any(|(rule, _)| rule.name() == "no-console"),
1399+
"ignores should not globally ignore the file"
1400+
);
1401+
}
1402+
13361403
#[test]
13371404
fn test_override_new_plugin_does_not_reapply_categories_to_eslint_rules() {
13381405
// ESLINT = 0 (bitflag value 0) causes `unconfigured_plugins.contains(ESLINT)`
@@ -1351,6 +1418,7 @@ mod test {
13511418
let overrides = ResolvedOxlintOverrides::new(vec![ResolvedOxlintOverride {
13521419
env: None,
13531420
files: GlobSet::new(vec!["**/*.ts"]),
1421+
ignores: GlobSet::default(),
13541422
plugins: Some(LintPlugins::IMPORT),
13551423
globals: None,
13561424
rules: ResolvedOxlintOverrideRules { builtin_rules: vec![], external_rules: vec![] },

crates/oxc_linter/src/config/glob_set.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ impl<'de> Deserialize<'de> for GlobSet {
1212
}
1313

1414
impl GlobSet {
15+
/// Returns `true` when the glob set has no patterns.
16+
#[inline]
17+
pub fn is_empty(&self) -> bool {
18+
self.0.is_empty()
19+
}
20+
1521
pub fn new<S: AsRef<str>, I: IntoIterator<Item = S>>(patterns: I) -> Self {
1622
Self(
1723
patterns

crates/oxc_linter/src/config/overrides.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ pub struct OxlintOverride {
8686
/// `[ "*.test.ts", "*.spec.ts" ]`
8787
pub files: GlobSet,
8888

89+
/// A list of glob patterns to exclude from this override.
90+
///
91+
/// Files matching these patterns are not globally ignored; this override
92+
/// simply does not apply to them.
93+
///
94+
/// ## Example
95+
/// `[ "*.generated.ts", "fixtures/**" ]`
96+
#[serde(default, skip_serializing_if = "GlobSet::is_empty")]
97+
pub ignores: GlobSet,
98+
8999
/// Environments enable and disable collections of global variables.
90100
pub env: Option<OxlintEnv>,
91101

@@ -141,6 +151,18 @@ mod test {
141151
assert_eq!(config.plugins, Some(LintPlugins::REACT | LintPlugins::TYPESCRIPT));
142152
}
143153

154+
#[test]
155+
fn test_parsing_ignores() {
156+
let config: OxlintOverride = from_value(json!({
157+
"files": ["*.tsx"],
158+
"ignores": ["*.generated.tsx"],
159+
}))
160+
.unwrap();
161+
162+
assert!(config.ignores.is_match("App.generated.tsx"));
163+
assert!(!config.ignores.is_match("App.tsx"));
164+
}
165+
144166
#[test]
145167
fn test_parsing_globals() {
146168
let config: OxlintOverride = from_value(json!({

npm/oxlint/configuration_schema.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,15 @@
596596
],
597597
"markdownDescription": "Enabled or disabled specific global variables."
598598
},
599+
"ignores": {
600+
"description": "A list of glob patterns to exclude from this override.\n\nFiles matching these patterns are not globally ignored; this override\nsimply does not apply to them.\n\n## Example\n`[ \"*.generated.ts\", \"fixtures/**\" ]`",
601+
"allOf": [
602+
{
603+
"$ref": "#/definitions/GlobSet"
604+
}
605+
],
606+
"markdownDescription": "A list of glob patterns to exclude from this override.\n\nFiles matching these patterns are not globally ignored; this override\nsimply does not apply to them.\n\n## Example\n`[ \"*.generated.ts\", \"fixtures/**\" ]`"
607+
},
599608
"jsPlugins": {
600609
"description": "JS plugins for this override, allows usage of ESLint plugins with Oxlint.\n\nRead more about JS plugins in\n[the docs](https://oxc.rs/docs/guide/usage/linter/js-plugins.html).\n\nNote: JS plugins are in alpha and not subject to semver.",
601610
"anyOf": [
@@ -814,4 +823,4 @@
814823
}
815824
},
816825
"markdownDescription": "Oxlint Configuration File\n\nThis configuration is aligned with ESLint v8's configuration schema (`eslintrc.json`).\n\nUsage: `oxlint -c oxlintrc.json`\n\nExample\n\n`.oxlintrc.json`\n\n```json\n{\n\"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n\"plugins\": [\"import\", \"typescript\", \"unicorn\"],\n\"env\": {\n\"browser\": true\n},\n\"globals\": {\n\"foo\": \"readonly\"\n},\n\"settings\": {\n\"react\": {\n\"version\": \"18.2.0\"\n},\n\"custom\": { \"option\": true }\n},\n\"rules\": {\n\"eqeqeq\": \"warn\",\n\"import/no-cycle\": \"error\",\n\"react/self-closing-comp\": [\"error\", { \"html\": false }]\n},\n\"overrides\": [\n{\n\"files\": [\"*.test.ts\", \"*.spec.ts\"],\n\"rules\": {\n\"@typescript-eslint/no-explicit-any\": \"off\"\n}\n}\n]\n}\n```\n\n`oxlint.config.ts`\n\n```ts\nimport { defineConfig } from \"oxlint\";\n\nexport default defineConfig({\nplugins: [\"import\", \"typescript\", \"unicorn\"],\nenv: {\n\"browser\": true\n},\nglobals: {\n\"foo\": \"readonly\"\n},\nsettings: {\nreact: {\nversion: \"18.2.0\"\n},\ncustom: { option: true }\n},\nrules: {\n\"eqeqeq\": \"warn\",\n\"import/no-cycle\": \"error\",\n\"react/self-closing-comp\": [\"error\", { \"html\": false }]\n},\noverrides: [\n{\nfiles: [\"*.test.ts\", \"*.spec.ts\"],\nrules: {\n\"@typescript-eslint/no-explicit-any\": \"off\"\n}\n}\n]\n});\n```"
817-
}
826+
}

tasks/website_linter/src/snapshots/schema_json.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,15 @@ expression: json
600600
],
601601
"markdownDescription": "Enabled or disabled specific global variables."
602602
},
603+
"ignores": {
604+
"description": "A list of glob patterns to exclude from this override.\n\nFiles matching these patterns are not globally ignored; this override\nsimply does not apply to them.\n\n## Example\n`[ \"*.generated.ts\", \"fixtures/**\" ]`",
605+
"allOf": [
606+
{
607+
"$ref": "#/definitions/GlobSet"
608+
}
609+
],
610+
"markdownDescription": "A list of glob patterns to exclude from this override.\n\nFiles matching these patterns are not globally ignored; this override\nsimply does not apply to them.\n\n## Example\n`[ \"*.generated.ts\", \"fixtures/**\" ]`"
611+
},
603612
"jsPlugins": {
604613
"description": "JS plugins for this override, allows usage of ESLint plugins with Oxlint.\n\nRead more about JS plugins in\n[the docs](https://oxc.rs/docs/guide/usage/linter/js-plugins.html).\n\nNote: JS plugins are in alpha and not subject to semver.",
605614
"anyOf": [

tasks/website_linter/src/snapshots/schema_markdown.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,14 @@ type: `object`
510510
Enabled or disabled specific global variables.
511511

512512

513+
#### overrides[n].ignores
514+
515+
type: `string[]`
516+
517+
518+
A set of glob patterns.
519+
520+
513521
#### overrides[n].jsPlugins
514522

515523
type: `array`

0 commit comments

Comments
 (0)