Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 9 additions & 186 deletions crates/oxc_linter/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -75,134 +20,12 @@ pub(crate) struct LintConfig {
pub(crate) globals: OxlintGlobals,
}

impl From<OxlintConfig> for LintConfig {
fn from(config: OxlintConfig) -> Self {
impl From<Oxlintrc> 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<Self, OxcDiagnostic> {
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::<serde_json::Value>(&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<RuleWithSeverity>,
all_rules: &[RuleEnum],
) {
use itertools::Itertools;
let mut rules_to_replace: Vec<RuleWithSeverity> = vec![];
let mut rules_to_remove: Vec<RuleWithSeverity> = 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;
Expand All @@ -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,
Expand Down Expand Up @@ -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),
Expand All @@ -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);

Expand Down
Loading