From 1015a657be0f1ecc30ad0aae7e3f5994e897c295 Mon Sep 17 00:00:00 2001 From: glorydavid03023 Date: Fri, 3 Jul 2026 18:23:57 +0900 Subject: [PATCH 1/2] fix(parser): accept U+2019 apostrophe in "unless they're mana abilities" exemption MTGJSON oracle text uses the typographic apostrophe (U+2019). The "can't be activated" predicate already accepts both apostrophe forms (shared.rs / evasion.rs), but the trailing "unless they're mana abilities" exemption suffix was straight-apostrophe only, so a U+2019 printing (Pithing Needle, Bound by Moonsilver, and every aura templated "...its activated abilities can't be activated unless they're mana abilities") silently dropped the exemption. The static was built with ActivationExemption::None and the runtime then wrongly blocked the permanent's mana abilities (CR 605.1a violation). Add the U+2019 branch to both exemption-suffix parse sites (parse_activation_exemption_suffix, parse_activation_compound_tail), mirroring the dual-apostrophe pattern already used for the adjacent predicate. Building-block test drives the suffix combinator with both apostrophe forms. --- .../src/parser/oracle_static/restriction.rs | 45 ++++++++++++++++++- .../engine/src/parser/oracle_static/shared.rs | 7 ++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/parser/oracle_static/restriction.rs b/crates/engine/src/parser/oracle_static/restriction.rs index b079be4d64..56bc59f533 100644 --- a/crates/engine/src/parser/oracle_static/restriction.rs +++ b/crates/engine/src/parser/oracle_static/restriction.rs @@ -288,8 +288,12 @@ pub(crate) fn parse_attach_only_restriction( pub(crate) fn parse_activation_exemption_suffix( input: &str, ) -> OracleResult<'_, ActivationExemption> { + // CR 605.1a: MTGJSON oracle text uses the typographic apostrophe (U+2019), + // so accept both forms — the adjacent "can't be activated" predicate already + // does (see shared.rs / evasion.rs). Straight-apostrophe-only here silently + // dropped the exemption and wrongly blocked mana abilities (Pithing Needle). let mut parser = opt(preceded( - tag(" unless they're "), + alt((tag(" unless they're "), tag(" unless they\u{2019}re "))), value(ActivationExemption::ManaAbilities, tag("mana abilities")), )); let (rest, exemption) = parser.parse(input)?; @@ -2838,6 +2842,45 @@ mod spend_any_color_to_activate_abilities_tests { } } +#[cfg(test)] +mod activation_exemption_apostrophe_tests { + use super::*; + + /// CR 605.1a: MTGJSON ships oracle text with the U+2019 apostrophe, so the + /// "unless they're mana abilities" exemption must parse with BOTH apostrophe + /// forms — matching the adjacent dual-apostrophe "can't be activated" + /// predicate. With only the straight apostrophe, a U+2019 printing (Pithing + /// Needle, Bound by Moonsilver, ...) silently dropped the exemption and the + /// runtime wrongly blocked the permanent's mana abilities. This is the + /// building-block test for the whole class, driving the suffix combinator + /// directly rather than one card. + #[test] + fn exemption_suffix_accepts_both_apostrophes() { + for suffix in [ + " unless they're mana abilities", + " unless they\u{2019}re mana abilities", + ] { + let (rest, exemption) = + parse_activation_exemption_suffix(suffix).expect("exemption suffix must parse"); + assert_eq!( + exemption, + ActivationExemption::ManaAbilities, + "suffix `{suffix}` must yield the mana-abilities exemption" + ); + assert!(rest.is_empty(), "suffix `{suffix}` left `{rest}` unconsumed"); + } + } + + /// Absent suffix stays `None` (the exemption is genuinely optional). + #[test] + fn exemption_suffix_absent_is_none() { + let (rest, exemption) = + parse_activation_exemption_suffix(" and other text").expect("must parse (opt)"); + assert_eq!(exemption, ActivationExemption::None); + assert_eq!(rest, " and other text"); + } +} + #[cfg(test)] mod filtered_spend_any_type_tests { use super::*; diff --git a/crates/engine/src/parser/oracle_static/shared.rs b/crates/engine/src/parser/oracle_static/shared.rs index 19d047b278..d0cf03fe7d 100644 --- a/crates/engine/src/parser/oracle_static/shared.rs +++ b/crates/engine/src/parser/oracle_static/shared.rs @@ -693,7 +693,12 @@ fn parse_activation_compound_tail(input: &str) -> OracleResult<'_, ()> { tag("activated abilities can't be activated"), tag("activated abilities can\u{2019}t be activated"), )), - opt((tag(" unless they're "), tag("mana abilities"))), + // CR 605.1a: accept the U+2019 apostrophe too (MTGJSON's form), + // mirroring the dual-apostrophe "can't be activated" tag above. + opt(( + alt((tag(" unless they're "), tag(" unless they\u{2019}re "))), + tag("mana abilities"), + )), opt(tag(".")), ), ) From abfe62f0b6208acdcd758ead7421f045678151f4 Mon Sep 17 00:00:00 2001 From: glorydavid03023 Date: Fri, 3 Jul 2026 18:49:34 +0900 Subject: [PATCH 2/2] style: rustfmt the exemption-suffix test assert --- crates/engine/src/parser/oracle_static/restriction.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/parser/oracle_static/restriction.rs b/crates/engine/src/parser/oracle_static/restriction.rs index 56bc59f533..5583638baf 100644 --- a/crates/engine/src/parser/oracle_static/restriction.rs +++ b/crates/engine/src/parser/oracle_static/restriction.rs @@ -2867,7 +2867,10 @@ mod activation_exemption_apostrophe_tests { ActivationExemption::ManaAbilities, "suffix `{suffix}` must yield the mana-abilities exemption" ); - assert!(rest.is_empty(), "suffix `{suffix}` left `{rest}` unconsumed"); + assert!( + rest.is_empty(), + "suffix `{suffix}` left `{rest}` unconsumed" + ); } }