diff --git a/cfgrammar/src/lib/header.rs b/cfgrammar/src/lib/header.rs index 645277f43..05a9ab19f 100644 --- a/cfgrammar/src/lib/header.rs +++ b/cfgrammar/src/lib/header.rs @@ -48,7 +48,7 @@ impl Spanned for HeaderError { // This is essentially a tuple that needs a newtype so we can implement `From` for it. // Thus we aren't worried about it being `pub`. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] #[doc(hidden)] pub struct HeaderValue(pub T, pub Value); diff --git a/cfgrammar/src/lib/markmap.rs b/cfgrammar/src/lib/markmap.rs index b2f910290..bf367751f 100644 --- a/cfgrammar/src/lib/markmap.rs +++ b/cfgrammar/src/lib/markmap.rs @@ -21,7 +21,7 @@ use std::fmt; /// /// Merge behaviors configure how the merge operator handles cases where both `MarkMaps` being merged /// contain a particular key. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] #[doc(hidden)] pub struct MarkMap { default_merge_behavior: MergeBehavior, diff --git a/cfgrammar/src/lib/yacc/ast.rs b/cfgrammar/src/lib/yacc/ast.rs index fb1ef5cb2..8bc806c20 100644 --- a/cfgrammar/src/lib/yacc/ast.rs +++ b/cfgrammar/src/lib/yacc/ast.rs @@ -14,7 +14,7 @@ use super::{ use crate::{ Span, - header::{GrmtoolsSectionParser, HeaderError, HeaderErrorKind, HeaderValue}, + header::{GrmtoolsSectionParser, Header, HeaderError, HeaderErrorKind, HeaderValue, Value}, yacc::YaccOriginalActionKind, }; @@ -39,6 +39,7 @@ impl fmt::Display for ASTModificationError { pub struct ASTWithValidityInfo { yacc_kind: YaccKind, ast: GrammarAST, + grmtools_section: Header, errs: Vec, } @@ -52,18 +53,19 @@ impl ASTWithValidityInfo { /// already extracted the `YaccKind` if any. pub fn new(yacc_kind: YaccKind, s: &str) -> Self { let mut errs = Vec::new(); - let ast = { + let (ast, grmtools_section) = { let mut yp = YaccParser::new(yacc_kind, s); yp.parse().map_err(|e| errs.extend(e)).ok(); - let mut ast = yp.build(); + let (mut ast, grmtools_section) = yp.build(); ast.complete_and_validate(Some(yacc_kind)) .map_err(|e| errs.push(e)) .ok(); - ast + (ast, grmtools_section) }; ASTWithValidityInfo { ast, errs, + grmtools_section, yacc_kind, } } @@ -108,6 +110,34 @@ impl ASTWithValidityInfo { }) } } + + /// Performs a lookup in the grmtools section for an entry with the key `crate_name.key_name` and returns it. + /// If the entry is found it marks the key as `used`, for the purposes of `unused_grmtools_section_keys_for_crate`. + pub fn grmtools_section_value_for_crate( + &mut self, + crate_name: &str, + key_name: &str, + ) -> Option<(Span, &Value)> { + let key = format!("{crate_name}.{key_name}"); + self.grmtools_section.mark_used(&key); + if let Some(HeaderValue(span, value)) = self.grmtools_section.get(&key) { + Some((*span, value)) + } else { + None + } + } + + pub fn unused_grmtools_section_keys_for_crate(&self, crate_name: &str) -> Vec { + self.grmtools_section + .unused() + .iter() + .filter(|key_name| { + let crate_prefix = format!("{crate_name}."); + key_name.starts_with(&crate_prefix) + }) + .cloned() + .collect::>() + } } impl FromStr for ASTWithValidityInfo { @@ -120,19 +150,20 @@ impl FromStr for ASTWithValidityInfo { .map_err(|mut errs| errs.drain(..).map(|e| e.into()).collect::>())?; if let Some(HeaderValue(_, yk_val)) = header.get("cfgrammar.yacckind") { let yacc_kind = YaccKind::try_from(yk_val).map_err(|e| vec![e.into()])?; - let ast = { + let (ast, grmtools_section) = { // We don't want to strip off the header so that span's will be correct. let mut yp = YaccParser::new(yacc_kind, src); yp.parse().map_err(|e| errs.extend(e)).ok(); - let mut ast = yp.build(); + let (mut ast, grmtools_section) = yp.build(); ast.complete_and_validate(Some(yacc_kind)) .map_err(|e| errs.push(e)) .ok(); - ast + (ast, grmtools_section) }; Ok(ASTWithValidityInfo { ast, errs, + grmtools_section, yacc_kind, }) } else { @@ -984,4 +1015,205 @@ start -> () : "a" {$;;;; }; }] ); } + + #[test] + fn test_grmtools_section_values() { + use super::*; + use crate::header::Value; + let src = r#" +%grmtools { + yacckind: Grmtools, + lrpar.recoverer: CPCTPlus, + test.Flag, + !test.Negative, + test.string: "Foo", + test.vec: ["Aaaa", "Bbbb"], + test.num: 1234, + test.unused: 5678 +} +%token a +%% +start -> () : "a" { () }; +"#; + let mut ast_validity = ASTWithValidityInfo::from_str(src).unwrap(); + for (key, (expected_span, expected_value)) in [ + ( + "Flag", + ( + src.find_span("test.Flag"), + Value::Bool(true, src.find_span("test.Flag")), + ), + ), + ( + "Negative", + ( + src.find_span("test.Negative"), + Value::Bool(false, src.find_span("!test.Negative")), + ), + ), + ( + "string", + ( + src.find_span("test.string"), + Value::String("Foo".to_string(), src.find_span("Foo")), + ), + ), + ( + "vec", + ( + src.find_span("test.vec"), + Value::Array( + vec![ + Value::String("Aaaa".to_string(), src.find_span("Aaaa")), + Value::String("Bbbb".to_string(), src.find_span("Bbbb")), + ], + src.find_span("[\"Aaaa\", \"Bbbb\"]"), + ), + ), + ), + ( + "num", + ( + src.find_span("test.num"), + Value::Num(1234, src.find_span("1234")), + ), + ), + ] { + let value = ast_validity.grmtools_section_value_for_crate("test", key); + assert_eq!(value, Some((expected_span, &expected_value))); + } + assert_eq!( + ast_validity.unused_grmtools_section_keys_for_crate("test"), + vec!["test.unused"] + ); + assert_eq!( + ast_validity.grmtools_section_value_for_crate("cfgrammar", "yacckind"), + Some(( + src.find_span("yacckind"), + &Value::Namespaced("Grmtools".to_string(), src.find_span("Grmtools")) + )) + ); + + assert!( + ast_validity + .unused_grmtools_section_keys_for_crate("cfgrammar") + .is_empty() + ); + + assert_eq!( + ast_validity.grmtools_section_value_for_crate("lrpar", "recoverer"), + Some(( + src.find_span("lrpar.recoverer"), + &Value::Namespaced("CPCTPlus".to_string(), src.find_span("CPCTPlus")) + )) + ); + + assert!( + ast_validity + .unused_grmtools_section_keys_for_crate("lrpar") + .is_empty() + ); + } + + #[test] + fn test_grmtools_section_values2() { + use super::*; + let src = r#" +%grmtools { + yacckind: Original(YaccOriginalActionKind::UserAction), +} +%token a +%actiontype () +%% +start: "a" { () }; +"#; + let mut ast_validity = ASTWithValidityInfo::from_str(src).unwrap(); + assert_eq!( + ast_validity.grmtools_section_value_for_crate("cfgrammar", "yacckind"), + Some(( + src.find_span("yacckind"), + &Value::Namespaced( + "Original(YaccOriginalActionKind::UserAction)".to_string(), + src.find_span("Original(YaccOriginalActionKind::UserAction)"), + ), + )) + ); + assert!( + ast_validity + .unused_grmtools_section_keys_for_crate("cfgrammar") + .is_empty() + ); + } + + #[test] + fn test_grmtools_section_values3() { + use super::*; + let src = r#" +%grmtools { + yacckind: YaccKind::Original(YaccOriginalActionKind::UserAction), +} +%token a +%actiontype () +%% +start: "a" { () }; +"#; + let mut ast_validity = ASTWithValidityInfo::from_str(src).unwrap(); + assert_eq!( + ast_validity.grmtools_section_value_for_crate("cfgrammar", "yacckind"), + Some(( + src.find_span("yacckind"), + &Value::Namespaced( + "YaccKind::Original(YaccOriginalActionKind::UserAction)".to_string(), + src.find_span("YaccKind::Original(YaccOriginalActionKind::UserAction)"), + ), + )) + ); + assert!( + ast_validity + .unused_grmtools_section_keys_for_crate("cfgrammar") + .is_empty() + ); + } + + #[test] + fn test_grmtools_section_values4() { + use super::*; + let src = r#" +%grmtools { + yacckind: YaccKind::Original(UserAction), +} +%token a +%actiontype () +%% +start: "a" { () }; +"#; + let mut ast_validity = ASTWithValidityInfo::from_str(src).unwrap(); + assert_eq!( + ast_validity.grmtools_section_value_for_crate("cfgrammar", "yacckind"), + Some(( + src.find_span("yacckind"), + &Value::Namespaced( + "YaccKind::Original(UserAction)".to_string(), + src.find_span("YaccKind::Original(UserAction)"), + ), + )) + ); + assert!( + ast_validity + .unused_grmtools_section_keys_for_crate("cfgrammar") + .is_empty() + ); + } + + trait FindSpan { + fn find_span(&self, s: &str) -> Span; + } + + impl FindSpan for &'_ str { + #[track_caller] + fn find_span(&self, s: &str) -> Span { + let start_pos = self.find(s).unwrap(); + Span::new(start_pos, start_pos + s.len()) + } + } } diff --git a/cfgrammar/src/lib/yacc/parser.rs b/cfgrammar/src/lib/yacc/parser.rs index 4ee8eec99..6604a03ae 100644 --- a/cfgrammar/src/lib/yacc/parser.rs +++ b/cfgrammar/src/lib/yacc/parser.rs @@ -16,7 +16,7 @@ use wincode::{SchemaRead, SchemaWrite}; use crate::{ Span, Spanned, - header::{GrmtoolsSectionParser, HeaderErrorKind}, + header::{GrmtoolsSectionParser, Header, HeaderErrorKind}, }; pub type YaccGrammarResult = Result>; @@ -294,6 +294,7 @@ pub(crate) struct YaccParser<'a> { src: &'a str, num_newlines: usize, ast: GrammarAST, + header: Option>, global_actiontype: Option<(String, Span)>, } @@ -331,15 +332,17 @@ impl YaccParser<'_> { src, num_newlines: 0, ast: GrammarAST::new(), + header: None, global_actiontype: None, } } pub(crate) fn parse(&mut self) -> YaccGrammarResult { let mut errs = Vec::new(); - let (_, pos) = GrmtoolsSectionParser::new(self.src, false) + let (header, pos) = GrmtoolsSectionParser::new(self.src, false) .parse() .map_err(|mut errs| errs.drain(..).map(|e| e.into()).collect::>())?; + self.header = Some(header); // We pass around an index into the *bytes* of self.src. We guarantee that at all times // this points to the beginning of a UTF-8 character (since multibyte characters exist, not // every byte within the string is also a valid character). @@ -371,8 +374,8 @@ impl YaccParser<'_> { } } - pub(crate) fn build(self) -> GrammarAST { - self.ast + pub(crate) fn build(self) -> (GrammarAST, Header) { + (self.ast, self.header.expect("set by parse()")) } fn parse_declarations( @@ -1083,7 +1086,8 @@ mod test { fn parse(yacc_kind: YaccKind, s: &str) -> Result> { let mut yp = YaccParser::new(yacc_kind, s); yp.parse()?; - Ok(yp.build()) + let (ast, _) = yp.build(); + Ok(ast) } fn rule(n: &str) -> Symbol {