-
-
Notifications
You must be signed in to change notification settings - Fork 145
fix(attach): honor protection attachment exemptions (#4964) #5015
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
63b8efb
1b476f3
57d3f1e
cbff90b
a563c7a
37bec17
52b0e30
025ece3
751ecaf
659fd4b
f294011
d9f9c75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1400,13 +1400,27 @@ pub(crate) fn parse_continuous_modifications(text: &str) -> Vec<ContinuousModifi | |
| } | ||
| } | ||
| } else if let Some(keyword_text) = extract_keyword_clause(&unquoted_text) { | ||
| for part in split_keyword_list(keyword_text.trim().trim_end_matches('.')) { | ||
| let (keyword_only, trailing_exemption) = | ||
| match super::oracle_nom::bridge::split_once_on_lower( | ||
| keyword_text, | ||
| &keyword_text.to_lowercase(), | ||
| ". ", | ||
| ) { | ||
| Some((first, rest)) if trailing_mentions_protection_attachment_exemption(rest) => { | ||
| (first, Some(rest.trim())) | ||
| } | ||
| _ => (keyword_text, None), | ||
| }; | ||
|
Comment on lines
+1581
to
+1593
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] Verbatim string matching and Why it matters: Using Suggested fix: Use a dedicated nom parser to recognize the trailing exemption prose. let (keyword_only, trailing_exemption) =
match super::oracle_nom::bridge::split_once_on_lower(
keyword_text,
&keyword_text.to_lowercase(),
". ",
) {
Some((first, rest)) if parse_protection_exemption(rest.trim()).is_ok() => {
(first, Some(rest.trim()))
}
_ => (keyword_text, None),
};References
|
||
| for part in split_keyword_list(keyword_only.trim().trim_end_matches('.')) { | ||
| push_grant_clause_modifications( | ||
| &mut modifications, | ||
| part.as_ref(), | ||
| where_x_expression.as_deref(), | ||
| ); | ||
| } | ||
| if let Some(trailing) = trailing_exemption { | ||
| push_protection_attachment_exemption_modifications(&mut modifications, trailing); | ||
| } | ||
| } | ||
|
|
||
| // CR 613.1f: Pre-quote keyword recovery for compound lines like Swashbuckler's | ||
|
|
@@ -1461,11 +1475,12 @@ pub(crate) fn push_grant_clause_modifications( | |
| // parse_quoted_ability_modifications at :798) before extract_keyword_clause | ||
| // runs, so any ". " here can only introduce a trailing inert prose sentence | ||
| // (e.g. Benevolent Blessing's SBA-exemption "This effect doesn't remove ..."). | ||
| // Drop it so the keyword sentence reaches map_keyword clean. | ||
| let part = | ||
| // Strip it from the keyword token, but emit the matching protection-attachment | ||
| // exemption static when recognized. | ||
| let (part, trailing_exemption) = | ||
| match super::oracle_nom::bridge::split_once_on_lower(part, &part.to_lowercase(), ". ") { | ||
| Some((first, _)) => first, | ||
| None => part, | ||
| Some((first, rest)) => (first, Some(rest.trim())), | ||
| None => (part, None), | ||
| }; | ||
|
|
||
| let part_trimmed = part.trim().trim_end_matches('.'); | ||
|
|
@@ -1538,6 +1553,9 @@ pub(crate) fn push_grant_clause_modifications( | |
|
|
||
| if let Some(kw) = map_keyword(part_trimmed) { | ||
| modifications.push(ContinuousModification::AddKeyword { keyword: kw }); | ||
| if let Some(trailing) = trailing_exemption { | ||
| push_protection_attachment_exemption_modifications(modifications, trailing); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -1565,6 +1583,43 @@ pub(crate) fn push_grant_clause_modifications( | |
| } | ||
| } | ||
|
|
||
| fn trailing_mentions_protection_attachment_exemption(text: &str) -> bool { | ||
| nom_primitives::scan_contains(text, "doesn't remove") | ||
| || nom_primitives::scan_contains(text, "does not remove") | ||
| } | ||
|
|
||
| fn parse_protection_attachment_exemption_trailing(text: &str) -> Option<StaticMode> { | ||
| let lower = text.trim().to_ascii_lowercase(); | ||
| let mut this_aura = alt(( | ||
| tag::<_, _, OracleError<'_>>("this effect doesn't remove this aura"), | ||
| tag("this effect does not remove this aura"), | ||
| tag("doesn't remove this aura"), | ||
| tag("does not remove this aura"), | ||
| )); | ||
| if this_aura.parse(lower.as_str()).is_ok() { | ||
| return Some(StaticMode::ProtectionDoesntRemoveThisAura); | ||
| } | ||
| let mut controlled = alt(( | ||
| tag::<_, _, OracleError<'_>>("this effect doesn't remove auras and equipment you control"), | ||
| tag("this effect does not remove auras and equipment you control"), | ||
| tag("doesn't remove auras and equipment you control"), | ||
| tag("does not remove auras and equipment you control"), | ||
| )); | ||
| if controlled.parse(lower.as_str()).is_ok() { | ||
| return Some(StaticMode::ProtectionDoesntRemoveControlledAttachments); | ||
| } | ||
| None | ||
| } | ||
|
|
||
| fn push_protection_attachment_exemption_modifications( | ||
| modifications: &mut Vec<ContinuousModification>, | ||
| trailing: &str, | ||
| ) { | ||
| if let Some(mode) = parse_protection_attachment_exemption_trailing(trailing) { | ||
| modifications.push(ContinuousModification::AddStaticMode { mode }); | ||
| } | ||
| } | ||
|
Comment on lines
+1865
to
+1872
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] Verbatim string matching used for parsing Oracle phrases. Why it matters: Using verbatim string matching for compound phrases bypasses the robust nom-based parser and creates fragile matches, violating the repository rule on Oracle phrase parsing. Suggested fix: Decompose compound phrases into modular, reusable parsers for constituent parts (such as the subject, negation, and target) and compose them using idiomatic combinator aggregates (like nested fn parse_protection_exemption(input: &str) -> nom::IResult<&str, StaticMode> {
use nom::{
branch::alt,
bytes::complete::tag_no_case,
combinator::value,
sequence::tuple,
};
let effect_parser = tag_no_case("this effect ");
let negation_parser = alt((tag_no_case("doesn't remove "), tag_no_case("does not remove ")));
let this_aura = value(
StaticMode::ProtectionDoesntRemoveThisAura,
tag_no_case("this aura")
);
let controlled_attachments = value(
StaticMode::ProtectionDoesntRemoveControlledAttachments,
tag_no_case("auras and equipment you control that are already attached to it")
);
let (input, (_, _, mode)) = tuple((effect_parser, negation_parser, alt((this_aura, controlled_attachments))))(input)?;
Ok((input, mode))
}
fn push_protection_attachment_exemption_modifications(
modifications: &mut Vec<ContinuousModification>,
trailing: &str,
) {
if let Ok((_, mode)) = parse_protection_exemption(trailing) {
modifications.push(ContinuousModification::AddStaticMode { mode });
}
}References
|
||
|
|
||
| /// Extract quoted ability text from Oracle text and parse each into a typed AbilityDefinition. | ||
| /// | ||
| /// Quoted abilities like `"{T}: Add two mana of any one color."` are parsed by splitting | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.