diff --git a/crates/oxc_linter/src/config/mod.rs b/crates/oxc_linter/src/config/mod.rs index 8c818b1c9e8851..04fdf1fc9a168d 100644 --- a/crates/oxc_linter/src/config/mod.rs +++ b/crates/oxc_linter/src/config/mod.rs @@ -1,70 +1,15 @@ mod env; mod globals; +mod oxlintrc; mod rules; mod settings; -use std::path::Path; - -use oxc_diagnostics::OxcDiagnostic; -use rustc_hash::FxHashSet; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - pub use self::{ env::OxlintEnv, globals::OxlintGlobals, - rules::OxlintRules, + oxlintrc::Oxlintrc, settings::{jsdoc::JSDocPluginSettings, OxlintSettings}, }; -use crate::{ - rules::RuleEnum, - utils::{is_jest_rule_adapted_to_vitest, read_to_string}, - AllowWarnDeny, RuleWithSeverity, -}; - -/// Oxlint Configuration File -/// -/// This configuration is aligned with ESLint v8's configuration schema (`eslintrc.json`). -/// -/// Usage: `oxlint -c oxlintrc.json --import-plugin` -/// -/// ::: danger NOTE -/// -/// Only the `.json` format is supported. You can use comments in configuration files. -/// -/// ::: -/// -/// Example -/// -/// `.oxlintrc.json` -/// -/// ```json -/// { -/// "env": { -/// "browser": true -/// }, -/// "globals": { -/// "foo": "readonly" -/// }, -/// "settings": { -/// }, -/// "rules": { -/// "eqeqeq": "warn", -/// "import/no-cycle": "error" -/// } -/// } -/// ``` -#[derive(Debug, Default, Deserialize, Serialize, JsonSchema)] -#[serde(default)] -pub struct OxlintConfig { - /// See [Oxlint Rules](https://oxc.rs/docs/guide/usage/linter/rules.html). - pub rules: OxlintRules, - pub settings: OxlintSettings, - /// Environments enable and disable collections of global variables. - pub env: OxlintEnv, - /// Enabled or disabled specific global variables. - pub globals: OxlintGlobals, -} #[derive(Debug, Default)] pub(crate) struct LintConfig { @@ -75,134 +20,12 @@ pub(crate) struct LintConfig { pub(crate) globals: OxlintGlobals, } -impl From for LintConfig { - fn from(config: OxlintConfig) -> Self { +impl From for LintConfig { + fn from(config: Oxlintrc) -> Self { Self { settings: config.settings, env: config.env, globals: config.globals } } } -impl OxlintConfig { - /// # Errors - /// - /// * Parse Failure - pub fn from_file(path: &Path) -> Result { - let mut string = read_to_string(path).map_err(|e| { - OxcDiagnostic::error(format!("Failed to parse config {path:?} with error {e:?}")) - })?; - - // jsonc support - json_strip_comments::strip(&mut string).map_err(|err| { - OxcDiagnostic::error(format!("Failed to parse jsonc file {path:?}: {err:?}")) - })?; - - let json = serde_json::from_str::(&string).map_err(|err| { - let guess = mime_guess::from_path(path); - let err = match guess.first() { - // syntax error - Some(mime) if mime.subtype() == "json" => err.to_string(), - Some(_) => "Only json configuration is supported".to_string(), - None => { - format!( - "{err}, if the configuration is not a json file, please use json instead." - ) - } - }; - OxcDiagnostic::error(format!("Failed to parse eslint config {path:?}.\n{err}")) - })?; - - let config = Self::deserialize(&json).map_err(|err| { - OxcDiagnostic::error(format!("Failed to parse config with error {err:?}")) - })?; - - Ok(config) - } - - #[allow(clippy::option_if_let_else)] - pub fn override_rules( - &self, - rules_for_override: &mut FxHashSet, - all_rules: &[RuleEnum], - ) { - use itertools::Itertools; - let mut rules_to_replace: Vec = vec![]; - let mut rules_to_remove: Vec = vec![]; - - // Rules can have the same name but different plugin names - let lookup = self.rules.iter().into_group_map_by(|r| r.rule_name.as_str()); - - for (name, rule_configs) in &lookup { - match rule_configs.len() { - 0 => unreachable!(), - 1 => { - let rule_config = &rule_configs[0]; - let (rule_name, plugin_name) = transform_rule_and_plugin_name( - &rule_config.rule_name, - &rule_config.plugin_name, - ); - let severity = rule_config.severity; - match severity { - AllowWarnDeny::Warn | AllowWarnDeny::Deny => { - if let Some(rule) = all_rules - .iter() - .find(|r| r.name() == rule_name && r.plugin_name() == plugin_name) - { - let config = rule_config.config.clone().unwrap_or_default(); - let rule = rule.read_json(config); - rules_to_replace.push(RuleWithSeverity::new(rule, severity)); - } - } - AllowWarnDeny::Allow => { - if let Some(rule) = rules_for_override - .iter() - .find(|r| r.name() == rule_name && r.plugin_name() == plugin_name) - { - let rule = rule.clone(); - rules_to_remove.push(rule); - } - } - } - } - _ => { - // For overlapping rule names, use the "error" one - // "no-loss-of-precision": "off", - // "@typescript-eslint/no-loss-of-precision": "error" - if let Some(rule_config) = - rule_configs.iter().find(|r| r.severity.is_warn_deny()) - { - if let Some(rule) = rules_for_override.iter().find(|r| r.name() == *name) { - let config = rule_config.config.clone().unwrap_or_default(); - rules_to_replace - .push(RuleWithSeverity::new(rule.read_json(config), rule.severity)); - } - } else if rule_configs.iter().all(|r| r.severity.is_allow()) { - if let Some(rule) = rules_for_override.iter().find(|r| r.name() == *name) { - rules_to_remove.push(rule.clone()); - } - } - } - } - } - - for rule in rules_to_remove { - rules_for_override.remove(&rule); - } - for rule in rules_to_replace { - rules_for_override.replace(rule); - } - } -} - -fn transform_rule_and_plugin_name<'a>( - rule_name: &'a str, - plugin_name: &'a str, -) -> (&'a str, &'a str) { - if plugin_name == "vitest" && is_jest_rule_adapted_to_vitest(rule_name) { - return (rule_name, "jest"); - } - - (rule_name, plugin_name) -} - #[cfg(test)] mod test { use std::env; @@ -211,19 +34,19 @@ mod test { use rustc_hash::FxHashSet; use serde::Deserialize; - use super::OxlintConfig; + use super::Oxlintrc; use crate::rules::RULES; #[test] fn test_from_file() { let fixture_path = env::current_dir().unwrap().join("fixtures/eslint_config.json"); - let config = OxlintConfig::from_file(&fixture_path).unwrap(); + let config = Oxlintrc::from_file(&fixture_path).unwrap(); assert!(!config.rules.is_empty()); } #[test] fn test_deserialize() { - let config = OxlintConfig::deserialize(&serde_json::json!({ + let config = Oxlintrc::deserialize(&serde_json::json!({ "rules": { "no-console": "off", "no-debugger": 2, @@ -253,7 +76,7 @@ mod test { })); assert!(config.is_ok()); - let OxlintConfig { rules, settings, env, globals } = config.unwrap(); + let Oxlintrc { rules, settings, env, globals } = config.unwrap(); assert!(!rules.is_empty()); assert_eq!( settings.jsx_a11y.polymorphic_prop_name.as_ref().map(CompactStr::as_str), @@ -267,7 +90,7 @@ mod test { fn test_vitest_rule_replace() { let fixture_path: std::path::PathBuf = env::current_dir().unwrap().join("fixtures/eslint_config_vitest_replace.json"); - let config = OxlintConfig::from_file(&fixture_path).unwrap(); + let config = Oxlintrc::from_file(&fixture_path).unwrap(); let mut set = FxHashSet::default(); config.override_rules(&mut set, &RULES); diff --git a/crates/oxc_linter/src/config/oxlintrc.rs b/crates/oxc_linter/src/config/oxlintrc.rs new file mode 100644 index 00000000000000..a0007327bea9e7 --- /dev/null +++ b/crates/oxc_linter/src/config/oxlintrc.rs @@ -0,0 +1,180 @@ +use std::path::Path; + +use oxc_diagnostics::OxcDiagnostic; +use rustc_hash::FxHashSet; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::{env::OxlintEnv, globals::OxlintGlobals, rules::OxlintRules, settings::OxlintSettings}; + +use crate::{ + rules::RuleEnum, + utils::{is_jest_rule_adapted_to_vitest, read_to_string}, + AllowWarnDeny, RuleWithSeverity, +}; + +/// Oxlint Configuration File +/// +/// This configuration is aligned with ESLint v8's configuration schema (`eslintrc.json`). +/// +/// Usage: `oxlint -c oxlintrc.json --import-plugin` +/// +/// ::: danger NOTE +/// +/// Only the `.json` format is supported. You can use comments in configuration files. +/// +/// ::: +/// +/// Example +/// +/// `.oxlintrc.json` +/// +/// ```json +/// { +/// "env": { +/// "browser": true +/// }, +/// "globals": { +/// "foo": "readonly" +/// }, +/// "settings": { +/// }, +/// "rules": { +/// "eqeqeq": "warn", +/// "import/no-cycle": "error" +/// } +/// } +/// ``` +#[derive(Debug, Default, Deserialize, Serialize, JsonSchema)] +#[serde(default)] +pub struct Oxlintrc { + /// See [Oxlint Rules](https://oxc.rs/docs/guide/usage/linter/rules.html). + pub rules: OxlintRules, + pub settings: OxlintSettings, + /// Environments enable and disable collections of global variables. + pub env: OxlintEnv, + /// Enabled or disabled specific global variables. + pub globals: OxlintGlobals, +} + +impl Oxlintrc { + /// # Errors + /// + /// * Parse Failure + pub fn from_file(path: &Path) -> Result { + let mut string = read_to_string(path).map_err(|e| { + OxcDiagnostic::error(format!("Failed to parse config {path:?} with error {e:?}")) + })?; + + // jsonc support + json_strip_comments::strip(&mut string).map_err(|err| { + OxcDiagnostic::error(format!("Failed to parse jsonc file {path:?}: {err:?}")) + })?; + + let json = serde_json::from_str::(&string).map_err(|err| { + let guess = mime_guess::from_path(path); + let err = match guess.first() { + // syntax error + Some(mime) if mime.subtype() == "json" => err.to_string(), + Some(_) => "Only json configuration is supported".to_string(), + None => { + format!( + "{err}, if the configuration is not a json file, please use json instead." + ) + } + }; + OxcDiagnostic::error(format!("Failed to parse eslint config {path:?}.\n{err}")) + })?; + + let config = Self::deserialize(&json).map_err(|err| { + OxcDiagnostic::error(format!("Failed to parse config with error {err:?}")) + })?; + + Ok(config) + } + + #[allow(clippy::option_if_let_else)] + pub fn override_rules( + &self, + rules_for_override: &mut FxHashSet, + all_rules: &[RuleEnum], + ) { + use itertools::Itertools; + let mut rules_to_replace: Vec = vec![]; + let mut rules_to_remove: Vec = vec![]; + + // Rules can have the same name but different plugin names + let lookup = self.rules.iter().into_group_map_by(|r| r.rule_name.as_str()); + + for (name, rule_configs) in &lookup { + match rule_configs.len() { + 0 => unreachable!(), + 1 => { + let rule_config = &rule_configs[0]; + let (rule_name, plugin_name) = transform_rule_and_plugin_name( + &rule_config.rule_name, + &rule_config.plugin_name, + ); + let severity = rule_config.severity; + match severity { + AllowWarnDeny::Warn | AllowWarnDeny::Deny => { + if let Some(rule) = all_rules + .iter() + .find(|r| r.name() == rule_name && r.plugin_name() == plugin_name) + { + let config = rule_config.config.clone().unwrap_or_default(); + let rule = rule.read_json(config); + rules_to_replace.push(RuleWithSeverity::new(rule, severity)); + } + } + AllowWarnDeny::Allow => { + if let Some(rule) = rules_for_override + .iter() + .find(|r| r.name() == rule_name && r.plugin_name() == plugin_name) + { + let rule = rule.clone(); + rules_to_remove.push(rule); + } + } + } + } + _ => { + // For overlapping rule names, use the "error" one + // "no-loss-of-precision": "off", + // "@typescript-eslint/no-loss-of-precision": "error" + if let Some(rule_config) = + rule_configs.iter().find(|r| r.severity.is_warn_deny()) + { + if let Some(rule) = rules_for_override.iter().find(|r| r.name() == *name) { + let config = rule_config.config.clone().unwrap_or_default(); + rules_to_replace + .push(RuleWithSeverity::new(rule.read_json(config), rule.severity)); + } + } else if rule_configs.iter().all(|r| r.severity.is_allow()) { + if let Some(rule) = rules_for_override.iter().find(|r| r.name() == *name) { + rules_to_remove.push(rule.clone()); + } + } + } + } + } + + for rule in rules_to_remove { + rules_for_override.remove(&rule); + } + for rule in rules_to_replace { + rules_for_override.replace(rule); + } + } +} + +fn transform_rule_and_plugin_name<'a>( + rule_name: &'a str, + plugin_name: &'a str, +) -> (&'a str, &'a str) { + if plugin_name == "vitest" && is_jest_rule_adapted_to_vitest(rule_name) { + return (rule_name, "jest"); + } + + (rule_name, plugin_name) +} diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index e3272067e6682a..11688179362bca 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -28,7 +28,7 @@ use oxc_diagnostics::Error; use oxc_semantic::{AstNode, Semantic}; pub use crate::{ - config::OxlintConfig, + config::Oxlintrc, context::LintContext, fixer::FixKind, frameworks::FrameworkFlags, @@ -204,7 +204,7 @@ impl Linter { #[cfg(test)] mod test { - use super::{Linter, OxlintConfig}; + use super::{Linter, Oxlintrc}; #[test] fn print_rules() { @@ -219,7 +219,7 @@ mod test { use project_root::get_project_root; let path = get_project_root().unwrap().join("npm/oxlint/configuration_schema.json"); - let schema = schemars::schema_for!(OxlintConfig); + let schema = schemars::schema_for!(Oxlintrc); let json = serde_json::to_string_pretty(&schema).unwrap(); let existing_json = fs::read_to_string(&path).unwrap_or_default(); if existing_json.trim() != json.trim() { diff --git a/crates/oxc_linter/src/options/mod.rs b/crates/oxc_linter/src/options/mod.rs index 3461542c6bb46d..5a786cec13d9e1 100644 --- a/crates/oxc_linter/src/options/mod.rs +++ b/crates/oxc_linter/src/options/mod.rs @@ -14,7 +14,7 @@ pub use filter::{InvalidFilterKind, LintFilter}; pub use plugins::LintPluginOptions; use crate::{ - config::{LintConfig, OxlintConfig}, + config::{LintConfig, Oxlintrc}, fixer::FixKind, rules::RULES, utils::is_jest_rule_adapted_to_vitest, @@ -189,8 +189,7 @@ impl OxlintOptions { pub(crate) fn derive_rules_and_config( &self, ) -> Result<(Vec, LintConfig), Error> { - let config = - self.config_path.as_ref().map(|path| OxlintConfig::from_file(path)).transpose()?; + let config = self.config_path.as_ref().map(|path| Oxlintrc::from_file(path)).transpose()?; let mut rules: FxHashSet = FxHashSet::default(); let all_rules = self.get_filtered_rules(); diff --git a/crates/oxc_linter/src/snapshots/schema_json.snap b/crates/oxc_linter/src/snapshots/schema_json.snap index 6516dad2dcfee3..2ca0e388dc03ad 100644 --- a/crates/oxc_linter/src/snapshots/schema_json.snap +++ b/crates/oxc_linter/src/snapshots/schema_json.snap @@ -4,7 +4,7 @@ expression: json --- { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OxlintConfig", + "title": "Oxlintrc", "description": "Oxlint Configuration File\n\nThis configuration is aligned with ESLint v8's configuration schema (`eslintrc.json`).\n\nUsage: `oxlint -c oxlintrc.json --import-plugin`\n\n::: danger NOTE\n\nOnly the `.json` format is supported. You can use comments in configuration files.\n\n:::\n\nExample\n\n`.oxlintrc.json`\n\n```json { \"env\": { \"browser\": true }, \"globals\": { \"foo\": \"readonly\" }, \"settings\": { }, \"rules\": { \"eqeqeq\": \"warn\", \"import/no-cycle\": \"error\" } } ```", "type": "object", "properties": { diff --git a/crates/oxc_linter/src/tester.rs b/crates/oxc_linter/src/tester.rs index 2b3ed64daf7ed4..d0281289c968a3 100644 --- a/crates/oxc_linter/src/tester.rs +++ b/crates/oxc_linter/src/tester.rs @@ -11,7 +11,7 @@ use serde_json::Value; use crate::{ fixer::FixKind, options::LintPluginOptions, rules::RULES, AllowWarnDeny, Fixer, LintService, - LintServiceOptions, Linter, OxlintConfig, OxlintOptions, RuleEnum, RuleWithSeverity, + LintServiceOptions, Linter, OxlintOptions, Oxlintrc, RuleEnum, RuleWithSeverity, }; #[derive(Eq, PartialEq)] @@ -362,7 +362,7 @@ impl Tester { .with_node_plugin(self.plugins.node); let eslint_config = eslint_config .as_ref() - .map_or_else(OxlintConfig::default, |v| OxlintConfig::deserialize(v).unwrap()); + .map_or_else(Oxlintrc::default, |v| Oxlintrc::deserialize(v).unwrap()); let linter = Linter::from_options(options) .unwrap() .with_rules(vec![RuleWithSeverity::new(rule, AllowWarnDeny::Warn)]) diff --git a/npm/oxlint/configuration_schema.json b/npm/oxlint/configuration_schema.json index c27bbf4f7abf14..d5dadb9fe6337c 100644 --- a/npm/oxlint/configuration_schema.json +++ b/npm/oxlint/configuration_schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OxlintConfig", + "title": "Oxlintrc", "description": "Oxlint Configuration File\n\nThis configuration is aligned with ESLint v8's configuration schema (`eslintrc.json`).\n\nUsage: `oxlint -c oxlintrc.json --import-plugin`\n\n::: danger NOTE\n\nOnly the `.json` format is supported. You can use comments in configuration files.\n\n:::\n\nExample\n\n`.oxlintrc.json`\n\n```json { \"env\": { \"browser\": true }, \"globals\": { \"foo\": \"readonly\" }, \"settings\": { }, \"rules\": { \"eqeqeq\": \"warn\", \"import/no-cycle\": \"error\" } } ```", "type": "object", "properties": { diff --git a/tasks/website/src/linter/json_schema.rs b/tasks/website/src/linter/json_schema.rs index 850bb4de2461a2..53a40a0fe650c2 100644 --- a/tasks/website/src/linter/json_schema.rs +++ b/tasks/website/src/linter/json_schema.rs @@ -1,5 +1,5 @@ use handlebars::Handlebars; -use oxc_linter::OxlintConfig; +use oxc_linter::Oxlintrc; use schemars::{ schema::{RootSchema, Schema, SchemaObject, SingleOrVec, SubschemaValidation}, schema_for, @@ -11,7 +11,7 @@ pub fn print_schema_json() { } fn generate_schema_json() -> String { - let schema = schema_for!(OxlintConfig); + let schema = schema_for!(Oxlintrc); serde_json::to_string_pretty(&schema).unwrap() } @@ -28,7 +28,7 @@ pub fn print_schema_markdown() { } fn generate_schema_markdown() -> String { - let root_schema = schema_for!(OxlintConfig); + let root_schema = schema_for!(Oxlintrc); Renderer::new(root_schema).render() }