From 04aab0ca8e652783dbf4e42439eb72f1add507c5 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Fri, 27 Sep 2024 12:49:45 -0400 Subject: [PATCH] refactor(linter): add schemars and serde traits to AllowWarnDeny and RuleCategories --- .../oxc_linter/src/options/allow_warn_deny.rs | 136 ++++++++++++++++-- crates/oxc_linter/src/rule.rs | 27 +++- 2 files changed, 154 insertions(+), 9 deletions(-) diff --git a/crates/oxc_linter/src/options/allow_warn_deny.rs b/crates/oxc_linter/src/options/allow_warn_deny.rs index 4de811b7c1c88b..4d53bbfaae611f 100644 --- a/crates/oxc_linter/src/options/allow_warn_deny.rs +++ b/crates/oxc_linter/src/options/allow_warn_deny.rs @@ -1,10 +1,15 @@ -use std::{convert::From, fmt}; +use std::{ + convert::From, + fmt::{self, Display}, +}; use oxc_diagnostics::{OxcDiagnostic, Severity}; use schemars::{schema::SchemaObject, JsonSchema}; +use serde::{de, Deserialize, Serialize}; use serde_json::{Number, Value}; -#[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] pub enum AllowWarnDeny { Allow, // Off Warn, // Warn @@ -65,18 +70,93 @@ impl TryFrom<&Value> for AllowWarnDeny { } } +fn invalid_int_severity(value: D) -> OxcDiagnostic { + OxcDiagnostic::error(format!( + r#"Failed to parse rule severity, expected one of `0`, `1` or `2`, but got {value}"# + )) +} + +impl TryFrom for AllowWarnDeny { + type Error = OxcDiagnostic; + fn try_from(value: u64) -> Result { + match value { + 0 => Ok(Self::Allow), + 1 => Ok(Self::Warn), + 2 => Ok(Self::Deny), + x => Err(invalid_int_severity(x)), + } + } +} + +impl TryFrom for AllowWarnDeny { + type Error = OxcDiagnostic; + + fn try_from(value: i64) -> Result { + if value < 0 { + return Err(invalid_int_severity("a negative number")); + } + #[allow(clippy::cast_sign_loss)] + Self::try_from(value as u64) + } +} + impl TryFrom<&Number> for AllowWarnDeny { type Error = OxcDiagnostic; fn try_from(value: &Number) -> Result { - match value.as_i64() { - Some(0) => Ok(Self::Allow), - Some(1) => Ok(Self::Warn), - Some(2) => Ok(Self::Deny), - _ => Err(OxcDiagnostic::error(format!( + let value = value.as_i64().ok_or_else(|| { + OxcDiagnostic::error(format!( r#"Failed to parse rule severity, expected one of `0`, `1` or `2`, but got {value:?}"# - ))), + )) + })?; + Self::try_from(value) + } +} + +impl<'de> Deserialize<'de> for AllowWarnDeny { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + struct AllowWarnDenyVisitor; + + impl<'de> de::Visitor<'de> for AllowWarnDenyVisitor { + type Value = AllowWarnDeny; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + "an int between 0 and 2 or a string".fmt(f) + } + + fn visit_i64(self, v: i64) -> Result + where + E: de::Error, + { + Self::Value::try_from(v).map_err(de::Error::custom) + } + + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + Self::Value::try_from(v).map_err(de::Error::custom) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Self::Value::try_from(v).map_err(de::Error::custom) + } + + fn visit_string(self, v: String) -> Result + where + E: de::Error, + { + Self::Value::try_from(v.as_str()).map_err(de::Error::custom) + } } + + deserializer.deserialize_any(AllowWarnDenyVisitor) } } @@ -128,3 +208,43 @@ impl From for Severity { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_serialize() { + let tests = [ + (AllowWarnDeny::Allow, r#""allow""#), + (AllowWarnDeny::Warn, r#""warn""#), + (AllowWarnDeny::Deny, r#""deny""#), + ]; + for (input, expected) in tests { + assert_eq!(serde_json::to_string(&input).unwrap(), expected); + } + } + + #[test] + fn test_deserialize() { + let tests = [ + // allow + (r#""allow""#, AllowWarnDeny::Allow), + (r#""off""#, AllowWarnDeny::Allow), + ("0", AllowWarnDeny::Allow), + // warn + (r#""warn""#, AllowWarnDeny::Warn), + ("1", AllowWarnDeny::Warn), + // deny + (r#""error""#, AllowWarnDeny::Deny), + (r#""deny""#, AllowWarnDeny::Deny), + ("2", AllowWarnDeny::Deny), + ]; + + for (input, expected) in tests { + let msg = format!("input: {input}"); + let actual: AllowWarnDeny = serde_json::from_str(input).expect(&msg); + assert_eq!(actual, expected); + } + } +} diff --git a/crates/oxc_linter/src/rule.rs b/crates/oxc_linter/src/rule.rs index 348f9b6dae1724..abd2b0b23a9c27 100644 --- a/crates/oxc_linter/src/rule.rs +++ b/crates/oxc_linter/src/rule.rs @@ -6,6 +6,8 @@ use std::{ }; use oxc_semantic::SymbolId; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use crate::{ context::{ContextHost, LintContext}, @@ -61,7 +63,8 @@ pub trait RuleMeta { } /// Rule categories defined by rust-clippy -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] pub enum RuleCategory { /// Code that is outright wrong or useless Correctness, @@ -277,6 +280,7 @@ impl RuleWithSeverity { mod test { use markdown::{to_html_with_options, Options}; + use super::RuleCategory; use crate::rules::RULES; #[test] @@ -295,4 +299,25 @@ mod test { assert!(!html.is_empty()); } } + + #[test] + fn test_deserialize_rule_category() { + let tests = [ + ("correctness", RuleCategory::Correctness), + ("suspicious", RuleCategory::Suspicious), + ("restriction", RuleCategory::Restriction), + ("perf", RuleCategory::Perf), + ("pedantic", RuleCategory::Pedantic), + ("style", RuleCategory::Style), + ("nursery", RuleCategory::Nursery), + ]; + + for (input, expected) in tests { + let de: RuleCategory = serde_json::from_str(&format!("{input:?}")).unwrap(); + // deserializes to expected value + assert_eq!(de, expected, "{input}"); + // try_from on a str produces the same value as deserializing + assert_eq!(de, RuleCategory::try_from(input).unwrap(), "{input}"); + } + } }