diff --git a/apps/oxlint/fixtures/overrides/.oxlintrc.json b/apps/oxlint/fixtures/overrides/.oxlintrc.json index 132ee0cae91c4..db0cc82707d4e 100644 --- a/apps/oxlint/fixtures/overrides/.oxlintrc.json +++ b/apps/oxlint/fixtures/overrides/.oxlintrc.json @@ -5,19 +5,25 @@ }, "overrides": [ { - "files": ["*.js"], + "files": [ + "*.js" + ], "rules": { "no-console": "warn" } }, { - "files": ["*.{js,jsx}"], + "files": [ + "*.{js,jsx}" + ], "rules": { "no-console": "off" } }, { - "files": ["*.ts"], + "files": [ + "*.ts" + ], "rules": { "no-console": "warn" } diff --git a/apps/oxlint/fixtures/overrides/directories-config.json b/apps/oxlint/fixtures/overrides/directories-config.json index 02c53eda8fcea..3294b8dca4067 100644 --- a/apps/oxlint/fixtures/overrides/directories-config.json +++ b/apps/oxlint/fixtures/overrides/directories-config.json @@ -4,13 +4,18 @@ }, "overrides": [ { - "files": ["lib/*.{js,ts}", "src/*"], + "files": [ + "lib/*.{js,ts}", + "src/*" + ], "rules": { "no-debugger": "error" } }, { - "files": ["**/tests/*"], + "files": [ + "**/tests/*" + ], "rules": { "no-debugger": "warn" } diff --git a/apps/oxlint/fixtures/overrides_env_globals/.oxlintrc.json b/apps/oxlint/fixtures/overrides_env_globals/.oxlintrc.json new file mode 100644 index 0000000000000..2e10a114b917d --- /dev/null +++ b/apps/oxlint/fixtures/overrides_env_globals/.oxlintrc.json @@ -0,0 +1,32 @@ +{ + "env": { + "jquery": true + }, + "globals": { + "Foo": "readonly" + }, + "overrides": [ + { + "files": [ + "*.ts" + ], + "env": { + "jquery": false + }, + "globals": { + "Foo": "writeable" + } + }, + { + "files": [ + "src/*" + ], + "env": { + "jquery": false + }, + "globals": { + "Foo": "writeable" + } + } + ] +} \ No newline at end of file diff --git a/apps/oxlint/fixtures/overrides_env_globals/src/test.js b/apps/oxlint/fixtures/overrides_env_globals/src/test.js new file mode 100644 index 0000000000000..00af8cf5616f0 --- /dev/null +++ b/apps/oxlint/fixtures/overrides_env_globals/src/test.js @@ -0,0 +1,6 @@ +// for env detection +globalThis = 'abc'; +$ = 'abc'; + +// for globals detection +Foo = 'readable'; \ No newline at end of file diff --git a/apps/oxlint/fixtures/overrides_env_globals/test.js b/apps/oxlint/fixtures/overrides_env_globals/test.js new file mode 100644 index 0000000000000..00af8cf5616f0 --- /dev/null +++ b/apps/oxlint/fixtures/overrides_env_globals/test.js @@ -0,0 +1,6 @@ +// for env detection +globalThis = 'abc'; +$ = 'abc'; + +// for globals detection +Foo = 'readable'; \ No newline at end of file diff --git a/apps/oxlint/fixtures/overrides_env_globals/test.ts b/apps/oxlint/fixtures/overrides_env_globals/test.ts new file mode 100644 index 0000000000000..00af8cf5616f0 --- /dev/null +++ b/apps/oxlint/fixtures/overrides_env_globals/test.ts @@ -0,0 +1,6 @@ +// for env detection +globalThis = 'abc'; +$ = 'abc'; + +// for globals detection +Foo = 'readable'; \ No newline at end of file diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index 3c1f2f1ebdbc0..7cad78de57b12 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -755,6 +755,12 @@ mod test { Tester::new().test_and_snapshot(args); } + #[test] + fn test_overrides_envs_and_global() { + let args = &["-c", ".oxlintrc.json", "."]; + Tester::new().with_cwd("fixtures/overrides_env_globals".into()).test_and_snapshot(args); + } + #[test] fn test_ignore_patterns() { let args = &["-c", "./test/eslintrc.json", "--ignore-pattern", "*.ts", "."]; diff --git a/apps/oxlint/src/snapshots/fixtures__overrides_env_globals_-c .oxlintrc.json .@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__overrides_env_globals_-c .oxlintrc.json .@oxlint.snap new file mode 100644 index 0000000000000..34a647377f1fa --- /dev/null +++ b/apps/oxlint/src/snapshots/fixtures__overrides_env_globals_-c .oxlintrc.json .@oxlint.snap @@ -0,0 +1,57 @@ +--- +source: apps/oxlint/src/tester.rs +--- +########## +arguments: -c .oxlintrc.json . +working directory: fixtures/overrides_env_globals +---------- + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-global-assign.html\eslint(no-global-assign)]8;;\: Read-only global 'globalThis' should not be modified. + ,-[src/test.js:2:1] + 1 | // for env detection + 2 | globalThis = 'abc'; + : ^^^^^|^^^^ + : `-- Read-only global 'globalThis' should not be modified. + 3 | $ = 'abc'; + `---- + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-global-assign.html\eslint(no-global-assign)]8;;\: Read-only global 'globalThis' should not be modified. + ,-[test.js:2:1] + 1 | // for env detection + 2 | globalThis = 'abc'; + : ^^^^^|^^^^ + : `-- Read-only global 'globalThis' should not be modified. + 3 | $ = 'abc'; + `---- + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-global-assign.html\eslint(no-global-assign)]8;;\: Read-only global 'globalThis' should not be modified. + ,-[test.ts:2:1] + 1 | // for env detection + 2 | globalThis = 'abc'; + : ^^^^^|^^^^ + : `-- Read-only global 'globalThis' should not be modified. + 3 | $ = 'abc'; + `---- + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-global-assign.html\eslint(no-global-assign)]8;;\: Read-only global '$' should not be modified. + ,-[test.js:3:1] + 2 | globalThis = 'abc'; + 3 | $ = 'abc'; + : | + : `-- Read-only global '$' should not be modified. + 4 | + `---- + + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-global-assign.html\eslint(no-global-assign)]8;;\: Read-only global 'Foo' should not be modified. + ,-[test.js:6:1] + 5 | // for globals detection + 6 | Foo = 'readable'; + : ^|^ + : `-- Read-only global 'Foo' should not be modified. + `---- + +Found 5 warnings and 0 errors. +Finished in ms on 3 files with 97 rules using 1 threads. +---------- +CLI result: LintSucceeded +---------- diff --git a/crates/oxc_linter/src/config/config_store.rs b/crates/oxc_linter/src/config/config_store.rs index dd20f85d7c534..3fe2e2fcdead3 100644 --- a/crates/oxc_linter/src/config/config_store.rs +++ b/crates/oxc_linter/src/config/config_store.rs @@ -85,12 +85,9 @@ impl ConfigStore { return config.base.clone(); } + let mut env = config.base.config.env.clone(); + let mut globals = config.base.config.globals.clone(); let mut plugins = config.base.config.plugins; - let all_rules = RULES - .iter() - .filter(|rule| plugins.contains(LintPlugins::from(rule.plugin_name()))) - .cloned() - .collect::>(); let mut rules = config .base .rules @@ -99,6 +96,12 @@ impl ConfigStore { .cloned() .collect::>(); + let all_rules = RULES + .iter() + .filter(|rule| plugins.contains(LintPlugins::from(rule.plugin_name()))) + .cloned() + .collect::>(); + for override_config in overrides_to_apply { if !override_config.rules.is_empty() { override_config.rules.override_rules(&mut rules, &all_rules); @@ -107,18 +110,31 @@ impl ConfigStore { if let Some(override_plugins) = override_config.plugins { plugins |= override_plugins; } + + if let Some(override_env) = &override_config.env { + override_env.override_envs(&mut env); + } + + if let Some(override_globals) = &override_config.globals { + override_globals.override_globals(&mut globals); + } } - let rules = rules.into_iter().collect::>(); - let config = if plugins == config.base.config.plugins { + let config = if plugins == config.base.config.plugins + && env == config.base.config.env + && globals == config.base.config.globals + { Arc::clone(&config.base.config) } else { let mut config = (*config.base.config).clone(); config.plugins = plugins; + config.env = env; + config.globals = globals; Arc::new(config) }; + let rules = rules.into_iter().collect::>(); ResolvedLinterState { rules: Arc::from(rules.into_boxed_slice()), config } } } @@ -282,4 +298,109 @@ mod test { let app = store.resolve("App.tsx".as_ref()).config; assert_eq!(app.plugins, LintPlugins::IMPORT | LintPlugins::REACT | LintPlugins::TYPESCRIPT); } + + #[test] + fn test_add_env() { + let base_config = LintConfig { + env: OxlintEnv::default(), + plugins: LintPlugins::ESLINT, + settings: OxlintSettings::default(), + globals: OxlintGlobals::default(), + path: None, + }; + + let overrides = from_json!([{ + "files": ["*.tsx"], + "env": { + "es2024": true + }, + }]); + + let store = ConfigStore::new(vec![], base_config, overrides); + assert!(!store.base.base.config.env.contains("React")); + + let app = store.resolve("App.tsx".as_ref()).config; + assert!(app.env.contains("es2024")); + } + + #[test] + fn test_replace_env() { + let base_config = LintConfig { + env: OxlintEnv::from_iter(["es2024".into()]), + plugins: LintPlugins::ESLINT, + settings: OxlintSettings::default(), + globals: OxlintGlobals::default(), + path: None, + }; + + let overrides = from_json!([{ + "files": ["*.tsx"], + "env": { + "es2024": false + }, + }]); + + let store = ConfigStore::new(vec![], base_config, overrides); + assert!(store.base.base.config.env.contains("es2024")); + + let app = store.resolve("App.tsx".as_ref()).config; + assert!(!app.env.contains("es2024")); + } + + #[test] + fn test_add_globals() { + let base_config = LintConfig { + env: OxlintEnv::default(), + plugins: LintPlugins::ESLINT, + settings: OxlintSettings::default(), + globals: OxlintGlobals::default(), + path: None, + }; + + let overrides = from_json!([{ + "files": ["*.tsx"], + "globals": { + "React": "readonly", + "Secret": "writeable" + }, + }]); + + let store = ConfigStore::new(vec![], base_config, overrides); + assert!(!store.base.base.config.globals.is_enabled("React")); + assert!(!store.base.base.config.globals.is_enabled("Secret")); + + let app = store.resolve("App.tsx".as_ref()).config; + assert!(app.globals.is_enabled("React")); + assert!(app.globals.is_enabled("Secret")); + } + + #[test] + fn test_replace_globals() { + let base_config = LintConfig { + env: OxlintEnv::default(), + plugins: LintPlugins::ESLINT, + settings: OxlintSettings::default(), + globals: from_json!({ + "React": "readonly", + "Secret": "writeable" + }), + path: None, + }; + + let overrides = from_json!([{ + "files": ["*.tsx"], + "globals": { + "React": "off", + "Secret": "off" + }, + }]); + + let store = ConfigStore::new(vec![], base_config, overrides); + assert!(store.base.base.config.globals.is_enabled("React")); + assert!(store.base.base.config.globals.is_enabled("Secret")); + + let app = store.resolve("App.tsx".as_ref()).config; + assert!(!app.globals.is_enabled("React")); + assert!(!app.globals.is_enabled("Secret")); + } } diff --git a/crates/oxc_linter/src/config/env.rs b/crates/oxc_linter/src/config/env.rs index 9f11447315421..cab011440a433 100644 --- a/crates/oxc_linter/src/config/env.rs +++ b/crates/oxc_linter/src/config/env.rs @@ -10,25 +10,9 @@ use serde::{Deserialize, Serialize}; /// list of /// environments](https://eslint.org/docs/v8.x/use/configure/language-options#specifying-environments) /// for what environments are available and what each one provides. -#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] -#[cfg_attr(test, derive(PartialEq))] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub struct OxlintEnv(FxHashMap); -impl OxlintEnv { - pub fn contains(&self, key: &Q) -> bool - where - String: Borrow, - Q: ?Sized + Hash + Eq, - { - self.0.get(key).is_some_and(|v| *v) - } - - pub fn iter(&self) -> impl Iterator + '_ { - // Filter out false values - self.0.iter().filter_map(|(k, v)| (*v).then_some(k.as_str())) - } -} - impl FromIterator for OxlintEnv { fn from_iter>(iter: T) -> Self { let map = iter.into_iter().map(|key| (key, true)).collect(); @@ -46,6 +30,27 @@ impl Default for OxlintEnv { } } +impl OxlintEnv { + pub fn contains(&self, key: &Q) -> bool + where + String: Borrow, + Q: ?Sized + Hash + Eq, + { + self.0.get(key).is_some_and(|v| *v) + } + + pub fn iter(&self) -> impl Iterator + '_ { + // Filter out false values + self.0.iter().filter_map(|(k, v)| (*v).then_some(k.as_str())) + } + + pub(crate) fn override_envs(&self, envs_to_override: &mut OxlintEnv) { + for (env, supported) in self.0.clone() { + envs_to_override.0.insert(env, supported); + } + } +} + #[cfg(test)] mod test { use serde::Deserialize; @@ -70,4 +75,17 @@ mod test { assert_eq!(env.iter().count(), 1); assert!(env.contains("builtin")); } + + #[test] + fn test_override_envs() { + let mut env = OxlintEnv::default(); + let override_env = OxlintEnv::deserialize(&serde_json::json!({ + "browser": true, + })) + .unwrap(); + + override_env.override_envs(&mut env); + + assert!(env.contains("browser")); + } } diff --git a/crates/oxc_linter/src/config/globals.rs b/crates/oxc_linter/src/config/globals.rs index a3556f6c2f4ab..0aaf0ed2dea87 100644 --- a/crates/oxc_linter/src/config/globals.rs +++ b/crates/oxc_linter/src/config/globals.rs @@ -30,7 +30,7 @@ use serde::{de::Visitor, Deserialize, Serialize}; /// You may also use `"readable"` or `false` to represent `"readonly"`, and /// `"writeable"` or `true` to represent `"writable"`. // -#[derive(Debug, Default, Deserialize, Serialize, JsonSchema, Clone)] +#[derive(Debug, Default, PartialEq, Deserialize, Serialize, JsonSchema, Clone)] pub struct OxlintGlobals(FxHashMap); impl Deref for OxlintGlobals { @@ -49,6 +49,12 @@ impl OxlintGlobals { { self.0.get(name).is_some_and(|value| *value != GlobalValue::Off) } + + pub(crate) fn override_globals(&self, globals_to_override: &mut OxlintGlobals) { + for (env, supported) in self.0.clone() { + globals_to_override.0.insert(env, supported); + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)] @@ -174,4 +180,20 @@ mod test { assert!(globals.is_enabled("foo")); assert!(globals.is_enabled("bar")); } + + #[test] + fn test_override_globals() { + let mut globals = OxlintGlobals::deserialize(&serde_json::json!({ + "Foo": "writeable", + })) + .unwrap(); + let override_globals = OxlintGlobals::deserialize(&serde_json::json!({ + "Foo": "off", + })) + .unwrap(); + + override_globals.override_globals(&mut globals); + + assert!(!globals.is_enabled("Foo")); + } } diff --git a/crates/oxc_linter/src/config/overrides.rs b/crates/oxc_linter/src/config/overrides.rs index 93be198c22302..86b8919efa345 100644 --- a/crates/oxc_linter/src/config/overrides.rs +++ b/crates/oxc_linter/src/config/overrides.rs @@ -6,7 +6,7 @@ use serde::{de, ser, Deserialize, Serialize}; use oxc_index::{Idx, IndexVec}; -use crate::{config::OxlintRules, LintPlugins}; +use crate::{config::OxlintRules, LintPlugins, OxlintEnv, OxlintGlobals}; #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct OverrideId(NonMaxU32); @@ -73,6 +73,12 @@ pub struct OxlintOverride { /// `[ "*.test.ts", "*.spec.ts" ]` pub files: GlobSet, + /// Environments enable and disable collections of global variables. + pub env: Option, + + /// Enabled or disabled specific global variables. + pub globals: Option, + /// Optionally change what plugins are enabled for this override. When /// omitted, the base config's plugins are used. #[serde(default)] @@ -153,13 +159,15 @@ impl JsonSchema for GlobSet { } } +#[cfg(test)] mod test { - #[test] - fn test_globset() { - use serde_json::{from_value, json}; + use crate::config::globals::GlobalValue; - use super::*; + use super::*; + use serde_json::{from_value, json}; + #[test] + fn test_globset() { let config: OxlintOverride = from_value(json!({ "files": ["*.tsx",], })) @@ -177,10 +185,6 @@ mod test { #[test] fn test_parsing_plugins() { - use serde_json::{from_value, json}; - - use super::*; - let config: OxlintOverride = from_value(json!({ "files": ["*.tsx"], })) @@ -201,4 +205,45 @@ mod test { .unwrap(); assert_eq!(config.plugins, Some(LintPlugins::REACT | LintPlugins::TYPESCRIPT)); } + + #[test] + fn test_parsing_globals() { + let config: OxlintOverride = from_value(json!({ + "files": ["*.tsx"], + })) + .unwrap(); + assert!(config.globals.is_none()); + + let config: OxlintOverride = from_value(json!({ + "files": ["*.tsx"], + "globals": { + "Foo": "readable" + }, + })) + .unwrap(); + + assert_eq!(*config.globals.unwrap().get("Foo").unwrap(), GlobalValue::Readonly); + } + + #[test] + fn test_parsing_env() { + let config: OxlintOverride = from_value(json!({ + "files": ["*.tsx"], + })) + .unwrap(); + assert!(config.env.is_none()); + + let config: OxlintOverride = from_value(json!({ + "files": ["*.tsx"], + "env": { + "es2022": true, + "es2023": false, + }, + })) + .unwrap(); + + let env = &config.env.unwrap(); + assert!(env.contains("es2022")); + assert!(!env.contains("es2023")); + } } diff --git a/crates/oxc_linter/src/snapshots/schema_json.snap b/crates/oxc_linter/src/snapshots/schema_json.snap index 25a54c4948416..8b2923340d0c7 100644 --- a/crates/oxc_linter/src/snapshots/schema_json.snap +++ b/crates/oxc_linter/src/snapshots/schema_json.snap @@ -355,6 +355,17 @@ expression: json "files" ], "properties": { + "env": { + "description": "Environments enable and disable collections of global variables.", + "anyOf": [ + { + "$ref": "#/definitions/OxlintEnv" + }, + { + "type": "null" + } + ] + }, "files": { "description": "A list of glob patterns to override.\n\n## Example `[ \"*.test.ts\", \"*.spec.ts\" ]`", "allOf": [ @@ -363,6 +374,17 @@ expression: json } ] }, + "globals": { + "description": "Enabled or disabled specific global variables.", + "anyOf": [ + { + "$ref": "#/definitions/OxlintGlobals" + }, + { + "type": "null" + } + ] + }, "plugins": { "description": "Optionally change what plugins are enabled for this override. When omitted, the base config's plugins are used.", "default": null, diff --git a/npm/oxlint/configuration_schema.json b/npm/oxlint/configuration_schema.json index 641d05f79b336..7c3ee3f5015fe 100644 --- a/npm/oxlint/configuration_schema.json +++ b/npm/oxlint/configuration_schema.json @@ -351,6 +351,17 @@ "files" ], "properties": { + "env": { + "description": "Environments enable and disable collections of global variables.", + "anyOf": [ + { + "$ref": "#/definitions/OxlintEnv" + }, + { + "type": "null" + } + ] + }, "files": { "description": "A list of glob patterns to override.\n\n## Example `[ \"*.test.ts\", \"*.spec.ts\" ]`", "allOf": [ @@ -359,6 +370,17 @@ } ] }, + "globals": { + "description": "Enabled or disabled specific global variables.", + "anyOf": [ + { + "$ref": "#/definitions/OxlintGlobals" + }, + { + "type": "null" + } + ] + }, "plugins": { "description": "Optionally change what plugins are enabled for this override. When omitted, the base config's plugins are used.", "default": null,