From 964c7d7d54589529d4f47ee23421bbcdd9745db5 Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:57:35 +0800 Subject: [PATCH 1/2] fix(analyzer): replace serde_yaml with yaml_serde Fixes #358 --- Cargo.lock | 40 +++--- Cargo.toml | 2 +- crates/wright-analyzer/Cargo.toml | 2 +- crates/wright-analyzer/src/declarative.rs | 4 +- crates/wright-analyzer/src/registry.rs | 6 +- crates/wright-analyzer/tests/yaml_contract.rs | 126 ++++++++++++++++++ 6 files changed, 153 insertions(+), 27 deletions(-) create mode 100644 crates/wright-analyzer/tests/yaml_contract.rs diff --git a/Cargo.lock b/Cargo.lock index 3576302d..729f02a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -874,6 +874,12 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libyaml-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e126dda6f34391ab7b444f9922055facc83c07a910da3eb16f1e4d9c45dc777" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1505,19 +1511,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha2" version = "0.10.9" @@ -1848,12 +1841,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -2110,8 +2097,8 @@ version = "0.2.30" dependencies = [ "serde", "serde_json", - "serde_yaml", "workshop-rs", + "yaml_serde", ] [[package]] @@ -2224,6 +2211,19 @@ dependencies = [ "rustix", ] +[[package]] +name = "yaml_serde" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b729a08a9a6be689bbad3e2bf8015926db54b6622cc89c3a5f7dc174b9e918" +dependencies = [ + "indexmap", + "itoa", + "libyaml-rs", + "ryu", + "serde", +] + [[package]] name = "yoke" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index d1c397a3..5f9bb375 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ workshop-rs = "=0.3.11" libc = "0.2" serde = "1" serde_json = "1" -serde_yaml = "0.9" +yaml_serde = "0.10" sha2 = "0.10" flate2 = "1" tar = "0.4" diff --git a/crates/wright-analyzer/Cargo.toml b/crates/wright-analyzer/Cargo.toml index 23c153be..36ebf858 100644 --- a/crates/wright-analyzer/Cargo.toml +++ b/crates/wright-analyzer/Cargo.toml @@ -13,6 +13,6 @@ workspace = true [dependencies] serde = { workspace = true, features = ["derive"] } serde_json.workspace = true -serde_yaml.workspace = true +yaml_serde.workspace = true # Single pinned reference: `[workspace.dependencies]` in the root Cargo.toml. workshop-rs.workspace = true diff --git a/crates/wright-analyzer/src/declarative.rs b/crates/wright-analyzer/src/declarative.rs index 41b1e4ed..d0029643 100644 --- a/crates/wright-analyzer/src/declarative.rs +++ b/crates/wright-analyzer/src/declarative.rs @@ -205,7 +205,7 @@ enum CanonicalValuePattern { impl RuleDefinition { pub fn from_yaml_str(input: &str) -> Result { - serde_yaml::from_str(input).map_err(RuleError::Yaml) + yaml_serde::from_str(input).map_err(RuleError::Yaml) } } @@ -959,7 +959,7 @@ fn default_true() -> bool { #[derive(Debug)] pub enum RuleError { - Yaml(serde_yaml::Error), + Yaml(yaml_serde::Error), InvalidIdentity(String), UnsupportedLocale(String), UnknownSpelling { diff --git a/crates/wright-analyzer/src/registry.rs b/crates/wright-analyzer/src/registry.rs index 1b09a89b..acbac558 100644 --- a/crates/wright-analyzer/src/registry.rs +++ b/crates/wright-analyzer/src/registry.rs @@ -161,14 +161,14 @@ pub struct LintConfig { impl LintConfig { /// Parse a project lint configuration from the stable YAML surface. - pub fn from_yaml_str(input: &str) -> Result { - serde_yaml::from_str(input) + pub fn from_yaml_str(input: &str) -> Result { + yaml_serde::from_str(input) } /// Read and parse a project lint configuration from YAML. pub fn from_yaml_path(path: &Path) -> Result { let input = std::fs::read_to_string(path)?; - serde_yaml::from_str(&input).map_err(std::io::Error::other) + yaml_serde::from_str(&input).map_err(std::io::Error::other) } /// Disable a rule by its stable ID. diff --git a/crates/wright-analyzer/tests/yaml_contract.rs b/crates/wright-analyzer/tests/yaml_contract.rs new file mode 100644 index 00000000..c2d8a1b5 --- /dev/null +++ b/crates/wright-analyzer/tests/yaml_contract.rs @@ -0,0 +1,126 @@ +use std::path::{Path, PathBuf}; + +use workshop_rs::catalog::{Catalog, Locale}; +use workshop_rs::parser; +use wright_analyzer::analysis::Severity; +use wright_analyzer::declarative::{RuleDefinition, Scope}; +use wright_analyzer::registry::{LintConfig, LintRegistry}; + +const RULE_YAML: &str = r#" +id: community/minimum-wait +metadata: + summary: loop contains a minimum wait + rationale: minimum waits can create high-frequency loops + documentation: Finds a minimum wait inside a while scope. + known-limits: This is structural and does not measure runtime cost. + tags: [performance] +matcher: + scope: while + event: global + actions: + - kind: call + name: Wait + count: + min: 1 +"#; + +fn workshop_path(fixture_id: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../compatibility/fixtures") + .join(fixture_id) + .join("workshop.ws") +} + +fn workshop_program(fixture_id: &str) -> workshop_rs::wir::Program { + let text = std::fs::read_to_string(workshop_path(fixture_id)).unwrap(); + let catalog = Catalog::builtin().unwrap(); + parser::parse_wir_with_context(&text, &catalog, &Locale::new("en-US"), &catalog).unwrap() +} + +#[test] +fn rule_yaml_preserves_the_supported_definition_shape() { + let definition = RuleDefinition::from_yaml_str(RULE_YAML).expect("rule YAML parses"); + + assert_eq!(definition.id, "community/minimum-wait"); + assert_eq!(definition.locale, "en-US"); + assert!(matches!(definition.matcher.scope, Scope::While)); + assert_eq!(definition.matcher.event.as_deref(), Some("global")); + assert_eq!(definition.matcher.actions.len(), 1); + assert_eq!(definition.matcher.actions[0].name.as_deref(), Some("Wait")); + assert_eq!(definition.matcher.actions[0].count.min, Some(1)); +} + +#[test] +fn rule_and_project_yaml_preserve_registry_behavior() { + let mut registry = LintRegistry::default(); + registry + .load_yaml_str(RULE_YAML) + .expect("declarative rule loads"); + + let config = LintConfig::from_yaml_str( + r#" +rules: + community/minimum-wait: + enabled: true + severity: error + options: + min-matches: 1 +"#, + ) + .expect("project lint YAML parses"); + + assert!(config.is_enabled("community/minimum-wait")); + assert_eq!( + config.options("community/minimum-wait").min_matches, + Some(1) + ); + + let finding = registry + .run(&workshop_program("synthetic/control-flow"), &config) + .into_iter() + .find(|finding| finding.code == "community/minimum-wait") + .expect("the external rule finds the nested Wait action"); + assert_eq!(finding.severity, Severity::Error); + assert!(finding.span.is_some()); +} + +#[test] +fn lint_config_yaml_round_trips_through_yaml_serialization() { + let config = LintConfig::from_yaml_str( + r#" +rules: + community/minimum-wait: + enabled: false + severity: warn + options: + max-matches: 2 +"#, + ) + .expect("project lint YAML parses"); + + let serialized = yaml_serde::to_string(&config).expect("lint config serializes"); + let round_tripped = + LintConfig::from_yaml_str(&serialized).expect("serialized lint config parses"); + + assert!(!round_tripped.is_enabled("community/minimum-wait")); + assert_eq!( + round_tripped.options("community/minimum-wait").max_matches, + Some(2) + ); +} + +#[test] +fn invalid_yaml_remains_an_explicit_error() { + let rule_error = + RuleDefinition::from_yaml_str("id: community/broken\nmetadata: []\nmatcher: {}\n") + .expect_err("invalid rule YAML must be rejected"); + assert!(rule_error.to_string().starts_with("invalid rule YAML:")); + + let config_error = LintConfig::from_yaml_str( + "rules:\n community/minimum-wait:\n options:\n unsupported: true\n", + ); + assert!( + config_error.is_err(), + "unknown config options must be rejected" + ); +} From 36b73c2054a7b46d45a8e42b3e6ad71648d98940 Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:44:40 +0800 Subject: [PATCH 2/2] test(analyzer): narrow YAML compatibility coverage --- crates/wright-analyzer/tests/yaml_contract.rs | 105 ++---------------- 1 file changed, 7 insertions(+), 98 deletions(-) diff --git a/crates/wright-analyzer/tests/yaml_contract.rs b/crates/wright-analyzer/tests/yaml_contract.rs index c2d8a1b5..b918491e 100644 --- a/crates/wright-analyzer/tests/yaml_contract.rs +++ b/crates/wright-analyzer/tests/yaml_contract.rs @@ -1,88 +1,5 @@ -use std::path::{Path, PathBuf}; - -use workshop_rs::catalog::{Catalog, Locale}; -use workshop_rs::parser; -use wright_analyzer::analysis::Severity; -use wright_analyzer::declarative::{RuleDefinition, Scope}; -use wright_analyzer::registry::{LintConfig, LintRegistry}; - -const RULE_YAML: &str = r#" -id: community/minimum-wait -metadata: - summary: loop contains a minimum wait - rationale: minimum waits can create high-frequency loops - documentation: Finds a minimum wait inside a while scope. - known-limits: This is structural and does not measure runtime cost. - tags: [performance] -matcher: - scope: while - event: global - actions: - - kind: call - name: Wait - count: - min: 1 -"#; - -fn workshop_path(fixture_id: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../compatibility/fixtures") - .join(fixture_id) - .join("workshop.ws") -} - -fn workshop_program(fixture_id: &str) -> workshop_rs::wir::Program { - let text = std::fs::read_to_string(workshop_path(fixture_id)).unwrap(); - let catalog = Catalog::builtin().unwrap(); - parser::parse_wir_with_context(&text, &catalog, &Locale::new("en-US"), &catalog).unwrap() -} - -#[test] -fn rule_yaml_preserves_the_supported_definition_shape() { - let definition = RuleDefinition::from_yaml_str(RULE_YAML).expect("rule YAML parses"); - - assert_eq!(definition.id, "community/minimum-wait"); - assert_eq!(definition.locale, "en-US"); - assert!(matches!(definition.matcher.scope, Scope::While)); - assert_eq!(definition.matcher.event.as_deref(), Some("global")); - assert_eq!(definition.matcher.actions.len(), 1); - assert_eq!(definition.matcher.actions[0].name.as_deref(), Some("Wait")); - assert_eq!(definition.matcher.actions[0].count.min, Some(1)); -} - -#[test] -fn rule_and_project_yaml_preserve_registry_behavior() { - let mut registry = LintRegistry::default(); - registry - .load_yaml_str(RULE_YAML) - .expect("declarative rule loads"); - - let config = LintConfig::from_yaml_str( - r#" -rules: - community/minimum-wait: - enabled: true - severity: error - options: - min-matches: 1 -"#, - ) - .expect("project lint YAML parses"); - - assert!(config.is_enabled("community/minimum-wait")); - assert_eq!( - config.options("community/minimum-wait").min_matches, - Some(1) - ); - - let finding = registry - .run(&workshop_program("synthetic/control-flow"), &config) - .into_iter() - .find(|finding| finding.code == "community/minimum-wait") - .expect("the external rule finds the nested Wait action"); - assert_eq!(finding.severity, Severity::Error); - assert!(finding.span.is_some()); -} +use wright_analyzer::declarative::RuleDefinition; +use wright_analyzer::registry::LintConfig; #[test] fn lint_config_yaml_round_trips_through_yaml_serialization() { @@ -96,7 +13,7 @@ rules: max-matches: 2 "#, ) - .expect("project lint YAML parses"); + .expect("lint config YAML parses"); let serialized = yaml_serde::to_string(&config).expect("lint config serializes"); let round_tripped = @@ -110,17 +27,9 @@ rules: } #[test] -fn invalid_yaml_remains_an_explicit_error() { - let rule_error = - RuleDefinition::from_yaml_str("id: community/broken\nmetadata: []\nmatcher: {}\n") - .expect_err("invalid rule YAML must be rejected"); - assert!(rule_error.to_string().starts_with("invalid rule YAML:")); +fn invalid_rule_yaml_remains_an_explicit_error() { + let error = RuleDefinition::from_yaml_str("id: community/broken\nmetadata: []\nmatcher: {}\n") + .expect_err("invalid rule YAML must be rejected"); - let config_error = LintConfig::from_yaml_str( - "rules:\n community/minimum-wait:\n options:\n unsupported: true\n", - ); - assert!( - config_error.is_err(), - "unknown config options must be rejected" - ); + assert!(error.to_string().starts_with("invalid rule YAML:")); }