Update pest_generator to 2.7.15

Test: m
Change-Id: Ic2a86206b422690157987fa8102639e2abdf4d04
diff --git a/crates/pest_generator/.android-checksum.json b/crates/pest_generator/.android-checksum.json
index a31cbc9..9258c0b 100644
--- a/crates/pest_generator/.android-checksum.json
+++ b/crates/pest_generator/.android-checksum.json
@@ -1 +1 @@
-{"package":null,"files":{".cargo-checksum.json":"8711726461f3c9437e9eee9d36ed82d3974bc87116897dfed6a0cd2e83355291","Android.bp":"f7b74d3f7faa345bf489d5c4f04c0fe9f007794aa65863aa9c46b6506d86d8b9","Cargo.toml":"82a4dbe24b3fa489496018714d24c061558470c1a3caf5ac62d2b46c039c2cd1","LICENSE":"3c7cd2396b5b772507febd2615d3d5a55b80103845037df77c87ba6e64872f2c","LICENSE-APACHE":"3c7cd2396b5b772507febd2615d3d5a55b80103845037df77c87ba6e64872f2c","LICENSE-MIT":"38620a3cfaeec97a9197e8c39e436ea7f0bc86699b1f1c35f1aa41785b6d4eac","METADATA":"d1f5f3b6e746328d97d9b50ac450a8cd127f547a0719e79efd26a5998431fa93","MODULE_LICENSE_APACHE2":"0d6f8afa3940b7f06bebee651376d43bc8b0d5b437337be2696d30377451e93a","_README.md":"6b973bfc5a49b890c6c47d4705f7f6ae59ec30552d0c3fd0fccf77bb1c694157","cargo_embargo.json":"3ca1544e82177080d7388ff17069a5f1766ba92138a9e2c502c156a296f418d8","src/docs.rs":"fb624d5b414372276e70538ff7b9dfa79bdede5bbd9b4609a2b2456c428ae3e6","src/generator.rs":"f3f7e4a1a758d50d4d92062c754de222f3f16f7af0d49acc6e5847a25890004d","src/lib.rs":"8975d84fea527807a5ec8e9f88ce0790df86b1903175fb7cb51d1760d73a4727","src/macros.rs":"7bb24e482baae1b7f5a7845da000c2fb7a9417a802cdc27eb0de393efc73612c","tests/base.pest":"842b80623c38d4c5221105126135fc42fa7c8a9b631677d6f3013068370dc149","tests/test.pest":"84526a2b662665a3ed83baf588dbeaf8e92e1898b904d4c69f7c16d7b1c399ac"}}
\ No newline at end of file
+{"package":null,"files":{".cargo-checksum.json":"3ae08460988dbccb11757257a8b143f85c92286fdeda0c4d056fadeae4044337","Android.bp":"6649afc7911a5c155d85ee67c65c0a04cf71f59f87ca474a9c733d3d261b3656","Cargo.toml":"2cda83978ec6ff9cca0e36dd6b9ee88dfbf69589f73e219496e62fb585045f29","LICENSE":"3c7cd2396b5b772507febd2615d3d5a55b80103845037df77c87ba6e64872f2c","LICENSE-APACHE":"3c7cd2396b5b772507febd2615d3d5a55b80103845037df77c87ba6e64872f2c","LICENSE-MIT":"38620a3cfaeec97a9197e8c39e436ea7f0bc86699b1f1c35f1aa41785b6d4eac","METADATA":"aa4e506347229c5caedc047f7b8382fa4aa66a05847478973ac57187985e082d","MODULE_LICENSE_APACHE2":"0d6f8afa3940b7f06bebee651376d43bc8b0d5b437337be2696d30377451e93a","_README.md":"6b973bfc5a49b890c6c47d4705f7f6ae59ec30552d0c3fd0fccf77bb1c694157","cargo_embargo.json":"3ca1544e82177080d7388ff17069a5f1766ba92138a9e2c502c156a296f418d8","src/docs.rs":"bf0ca813afd16b6f3f4ae9bc6818f389bad0773249081895225b85bef2fea208","src/generator.rs":"c3df900c8448042d2011c08cf852480e8ff8a31c423c656f3df7500145be6d17","src/lib.rs":"7ff0e9dfe50cacb698a6b1c5e8e83d1b9aea42e9c367e4a515e1255a398245e1","src/macros.rs":"7bb24e482baae1b7f5a7845da000c2fb7a9417a802cdc27eb0de393efc73612c","src/parse_derive.rs":"b49de93f401c13928c662b0436ae7f4d08eb9af11139a6ee7b23fd400215c7eb","tests/base.pest":"842b80623c38d4c5221105126135fc42fa7c8a9b631677d6f3013068370dc149","tests/test.pest":"84526a2b662665a3ed83baf588dbeaf8e92e1898b904d4c69f7c16d7b1c399ac"}}
\ No newline at end of file
diff --git a/crates/pest_generator/.cargo-checksum.json b/crates/pest_generator/.cargo-checksum.json
index 141f902..37ccbe3 100644
--- a/crates/pest_generator/.cargo-checksum.json
+++ b/crates/pest_generator/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"Cargo.toml":"2936f4743aacfebd9824fb7fd69f6b03204312bd41153c2dcdfafa42ad0b05c1","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3","_README.md":"bde746653575153be4ae26ce950963ced5034449e352e60cfd8825260c666c16","src/docs.rs":"041b2c24377955dfdb6c29991b1f1dd7d7191431d8e5eaa245325253b250f702","src/generator.rs":"21dea1774cbca7c03bc7283157c449f1acad6ac387078bbaa3fd0d5134a5e4ab","src/lib.rs":"034624c6d8ad89b365f81ab04ad96a3d67909ba3485008355f21429a42b5e02c","src/macros.rs":"897d9004449b1c219f17c079630a790f3de1a27f61bc6a03cd777a163a6a1fba","tests/base.pest":"30f6965031bc52937114f60233a327e41ccc43429ae41a8e40c7b7c8006c466f","tests/test.pest":"f3fea8154a9a26c773ab8392685039d0d84bd845587bb2d42b970946f7967ee8"},"package":"2a31940305ffc96863a735bef7c7994a00b325a7138fdbc5bda0f1a0476d3275"}
\ No newline at end of file
+{"files":{"Cargo.toml":"7947cbefca45a6924315a1a3495beb7cb7bd6ff7602c7f7ebf7ce110656c3028","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3","_README.md":"bde746653575153be4ae26ce950963ced5034449e352e60cfd8825260c666c16","src/docs.rs":"47a5eaeeaa17efbf0537bd931eb388caa7a30cfd8504af0a90af8facee32e264","src/generator.rs":"bba65552cdf4816b7315cea607ea534d795e1ec239f1ab378cd85ef97a3ca335","src/lib.rs":"c515d9b83cd11ca3b7d9aecc3493adeb0e4eb370c0b274c4bcd1be20a48ff482","src/macros.rs":"897d9004449b1c219f17c079630a790f3de1a27f61bc6a03cd777a163a6a1fba","src/parse_derive.rs":"44fd33409a368c740d98e4eafba49db2da584dff9cda33b3de640b532a974f12","tests/base.pest":"30f6965031bc52937114f60233a327e41ccc43429ae41a8e40c7b7c8006c466f","tests/test.pest":"f3fea8154a9a26c773ab8392685039d0d84bd845587bb2d42b970946f7967ee8"},"package":"7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b"}
\ No newline at end of file
diff --git a/crates/pest_generator/Android.bp b/crates/pest_generator/Android.bp
index 5e5de3f..4d94340 100644
--- a/crates/pest_generator/Android.bp
+++ b/crates/pest_generator/Android.bp
@@ -18,7 +18,7 @@
     host_cross_supported: false,
     crate_name: "pest_generator",
     cargo_env_compat: true,
-    cargo_pkg_version: "2.7.6",
+    cargo_pkg_version: "2.7.15",
     crate_root: "src/lib.rs",
     edition: "2021",
     features: [
diff --git a/crates/pest_generator/Cargo.toml b/crates/pest_generator/Cargo.toml
index 85dabac..2c3305f 100644
--- a/crates/pest_generator/Cargo.toml
+++ b/crates/pest_generator/Cargo.toml
@@ -13,7 +13,7 @@
 edition = "2021"
 rust-version = "1.61"
 name = "pest_generator"
-version = "2.7.6"
+version = "2.7.15"
 authors = ["DragoČ™ Tiselice <dragostiselice@gmail.com>"]
 description = "pest code generator"
 homepage = "https://pest.rs/"
@@ -28,11 +28,11 @@
 repository = "https://github.com/pest-parser/pest"
 
 [dependencies.pest]
-version = "2.7.6"
+version = "2.7.15"
 default-features = false
 
 [dependencies.pest_meta]
-version = "2.7.6"
+version = "2.7.15"
 
 [dependencies.proc-macro2]
 version = "1.0"
@@ -45,6 +45,7 @@
 
 [features]
 default = ["std"]
+export-internal = []
 grammar-extras = ["pest_meta/grammar-extras"]
 not-bootstrap-in-src = ["pest_meta/not-bootstrap-in-src"]
 std = ["pest/std"]
diff --git a/crates/pest_generator/METADATA b/crates/pest_generator/METADATA
index 331a5f5..b64d063 100644
--- a/crates/pest_generator/METADATA
+++ b/crates/pest_generator/METADATA
@@ -1,17 +1,17 @@
 name: "pest_generator"
 description: "pest code generator"
 third_party {
-  version: "2.7.6"
+  version: "2.7.15"
   license_type: NOTICE
   last_upgrade_date {
-    year: 2024
-    month: 2
-    day: 5
+    year: 2025
+    month: 1
+    day: 15
   }
   homepage: "https://crates.io/crates/pest_generator"
   identifier {
     type: "Archive"
-    value: "https://static.crates.io/crates/pest_generator/pest_generator-2.7.6.crate"
-    version: "2.7.6"
+    value: "https://static.crates.io/crates/pest_generator/pest_generator-2.7.15.crate"
+    version: "2.7.15"
   }
 }
diff --git a/crates/pest_generator/src/docs.rs b/crates/pest_generator/src/docs.rs
index ccc82e7..22f4f31 100644
--- a/crates/pest_generator/src/docs.rs
+++ b/crates/pest_generator/src/docs.rs
@@ -1,9 +1,13 @@
+//! Type and helper to collect the gramamr and rule documentation.
+
 use pest::iterators::Pairs;
 use pest_meta::parser::Rule;
 use std::collections::HashMap;
 
+/// Abstraction for the grammer and rule doc.
 #[derive(Debug)]
-pub(crate) struct DocComment {
+pub struct DocComment {
+    /// The grammar documentation is defined at the beginning of a file with //!.
     pub grammar_doc: String,
 
     /// HashMap for store all doc_comments for rules.
@@ -33,7 +37,7 @@
 /// grammar_doc = "This is a grammar doc"
 /// line_docs = { "foo": "line doc 1\nline doc 2", "bar": "line doc 3" }
 /// ```
-pub(crate) fn consume(pairs: Pairs<'_, Rule>) -> DocComment {
+pub fn consume(pairs: Pairs<'_, Rule>) -> DocComment {
     let mut grammar_doc = String::new();
 
     let mut line_docs: HashMap<String, String> = HashMap::new();
diff --git a/crates/pest_generator/src/generator.rs b/crates/pest_generator/src/generator.rs
index 7a527c5..fd48ec9 100644
--- a/crates/pest_generator/src/generator.rs
+++ b/crates/pest_generator/src/generator.rs
@@ -7,6 +7,8 @@
 // option. All files in the project carrying such notice may not be copied,
 // modified, or distributed except according to those terms.
 
+//! Helpers to generate the code for the Parser `derive``.
+
 use std::path::PathBuf;
 
 use proc_macro2::TokenStream;
@@ -18,9 +20,12 @@
 use pest_meta::optimizer::*;
 
 use crate::docs::DocComment;
-use crate::ParsedDerive;
+use crate::parse_derive::ParsedDerive;
 
-pub(crate) fn generate(
+/// Generates the corresponding parser based based on the processed macro input. If `include_grammar`
+/// is set to true, it'll generate an explicit "include_str" statement (done in pest_derive, but
+/// turned off in the local bootstrap).
+pub fn generate(
     parsed_derive: ParsedDerive,
     paths: Vec<PathBuf>,
     rules: Vec<OptimizedRule>,
@@ -218,10 +223,17 @@
     });
 
     let grammar_doc = &doc_comment.grammar_doc;
-    let mut result = quote! {
-        #[doc = #grammar_doc]
-        #[allow(dead_code, non_camel_case_types, clippy::upper_case_acronyms)]
-        #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+    let mut result = if grammar_doc.is_empty() {
+        quote! {
+            #[allow(dead_code, non_camel_case_types, clippy::upper_case_acronyms)]
+            #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+        }
+    } else {
+        quote! {
+            #[doc = #grammar_doc]
+            #[allow(dead_code, non_camel_case_types, clippy::upper_case_acronyms)]
+            #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+        }
     };
     if non_exhaustive {
         result.append_all(quote! {
@@ -566,12 +578,42 @@
             }
         }
         #[cfg(feature = "grammar-extras")]
-        OptimizedExpr::NodeTag(expr, tag) => {
-            let expr = generate_expr(*expr);
-            quote! {
-                #expr.and_then(|state| state.tag_node(#tag))
+        OptimizedExpr::NodeTag(expr, tag) => match *expr {
+            OptimizedExpr::Opt(expr) => {
+                let expr = generate_expr(*expr);
+                quote! {
+                    state.optional(|state| {
+                        #expr.and_then(|state| state.tag_node(#tag))
+                    })
+                }
             }
-        }
+            OptimizedExpr::Rep(expr) => {
+                let expr = generate_expr(*expr);
+                quote! {
+                    state.sequence(|state| {
+                        state.optional(|state| {
+                            #expr.and_then(|state| {
+                                state.repeat(|state| {
+                                    state.sequence(|state| {
+                                        super::hidden::skip(
+                                            state
+                                        ).and_then(|state| {
+                                            #expr.and_then(|state| state.tag_node(#tag))
+                                        })
+                                    })
+                                })
+                            }).and_then(|state| state.tag_node(#tag))
+                        })
+                    })
+                }
+            }
+            expr => {
+                let expr = generate_expr(expr);
+                quote! {
+                    #expr.and_then(|state| state.tag_node(#tag))
+                }
+            }
+        },
     }
 }
 
@@ -721,12 +763,32 @@
             }
         }
         #[cfg(feature = "grammar-extras")]
-        OptimizedExpr::NodeTag(expr, tag) => {
-            let expr = generate_expr_atomic(*expr);
-            quote! {
-                #expr.and_then(|state| state.tag_node(#tag))
+        OptimizedExpr::NodeTag(expr, tag) => match *expr {
+            OptimizedExpr::Opt(expr) => {
+                let expr = generate_expr_atomic(*expr);
+
+                quote! {
+                    state.optional(|state| {
+                        #expr.and_then(|state| state.tag_node(#tag))
+                    })
+                }
             }
-        }
+            OptimizedExpr::Rep(expr) => {
+                let expr = generate_expr_atomic(*expr);
+
+                quote! {
+                    state.repeat(|state| {
+                        #expr.and_then(|state| state.tag_node(#tag))
+                    })
+                }
+            }
+            expr => {
+                let expr = generate_expr_atomic(expr);
+                quote! {
+                    #expr.and_then(|state| state.tag_node(#tag))
+                }
+            }
+        },
     }
 }
 
@@ -811,6 +873,41 @@
     }
 
     #[test]
+    fn rule_empty_doc() {
+        let rules = vec![OptimizedRule {
+            name: "f".to_owned(),
+            ty: RuleType::Normal,
+            expr: OptimizedExpr::Ident("g".to_owned()),
+        }];
+
+        let mut line_docs = HashMap::new();
+        line_docs.insert("f".to_owned(), "This is rule comment".to_owned());
+
+        let doc_comment = &DocComment {
+            grammar_doc: "".to_owned(),
+            line_docs,
+        };
+
+        assert_eq!(
+            generate_enum(&rules, doc_comment, false, false).to_string(),
+            quote! {
+                #[allow(dead_code, non_camel_case_types, clippy::upper_case_acronyms)]
+                #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+                pub enum Rule {
+                    #[doc = "This is rule comment"]
+                    r#f
+                }
+                impl Rule {
+                    pub fn all_rules() -> &'static [Rule] {
+                        &[Rule::r#f]
+                    }
+                }
+            }
+            .to_string()
+        );
+    }
+
+    #[test]
     fn sequence() {
         let expr = OptimizedExpr::Seq(
             Box::new(OptimizedExpr::Str("a".to_owned())),
diff --git a/crates/pest_generator/src/lib.rs b/crates/pest_generator/src/lib.rs
index cbd13ea..7277c75 100644
--- a/crates/pest_generator/src/lib.rs
+++ b/crates/pest_generator/src/lib.rs
@@ -26,14 +26,29 @@
 use std::io::{self, Read};
 use std::path::Path;
 
+use generator::generate;
 use proc_macro2::TokenStream;
-use syn::{Attribute, DeriveInput, Expr, ExprLit, Generics, Ident, Lit, Meta};
+use syn::DeriveInput;
 
 #[macro_use]
 mod macros;
+
+#[cfg(feature = "export-internal")]
+pub mod docs;
+#[cfg(not(feature = "export-internal"))]
 mod docs;
+
+#[cfg(feature = "export-internal")]
+pub mod generator;
+#[cfg(not(feature = "export-internal"))]
 mod generator;
 
+#[cfg(feature = "export-internal")]
+pub mod parse_derive;
+#[cfg(not(feature = "export-internal"))]
+mod parse_derive;
+
+use crate::parse_derive::{parse_derive, GrammarSource};
 use pest_meta::parser::{self, rename_meta_rule, Rule};
 use pest_meta::{optimizer, unwrap_or_report, validator};
 
@@ -44,6 +59,7 @@
     let ast: DeriveInput = syn::parse2(input).unwrap();
     let (parsed_derive, contents) = parse_derive(ast);
 
+    // Grammar presented in a view of a string.
     let mut data = String::new();
     let mut paths = vec![];
 
@@ -86,6 +102,7 @@
         }
     }
 
+    // `Rule::grammar_rules` is taken from meta/srd/parser.rs.
     let pairs = match parser::parse(Rule::grammar_rules, &data) {
         Ok(pairs) => pairs,
         Err(error) => panic!("error parsing \n{}", error.renamed_rules(rename_meta_rule)),
@@ -96,7 +113,7 @@
     let ast = unwrap_or_report(parser::consume_rules(pairs));
     let optimized = optimizer::optimize(ast);
 
-    generator::generate(
+    generate(
         parsed_derive,
         paths,
         optimized,
@@ -113,160 +130,8 @@
     Ok(string)
 }
 
-#[derive(Debug, PartialEq)]
-enum GrammarSource {
-    File(String),
-    Inline(String),
-}
-
-struct ParsedDerive {
-    pub(crate) name: Ident,
-    pub(crate) generics: Generics,
-    pub(crate) non_exhaustive: bool,
-}
-
-fn parse_derive(ast: DeriveInput) -> (ParsedDerive, Vec<GrammarSource>) {
-    let name = ast.ident;
-    let generics = ast.generics;
-
-    let grammar: Vec<&Attribute> = ast
-        .attrs
-        .iter()
-        .filter(|attr| {
-            let path = attr.meta.path();
-            path.is_ident("grammar") || path.is_ident("grammar_inline")
-        })
-        .collect();
-
-    if grammar.is_empty() {
-        panic!("a grammar file needs to be provided with the #[grammar = \"PATH\"] or #[grammar_inline = \"GRAMMAR CONTENTS\"] attribute");
-    }
-
-    let mut grammar_sources = Vec::with_capacity(grammar.len());
-    for attr in grammar {
-        grammar_sources.push(get_attribute(attr))
-    }
-
-    let non_exhaustive = ast
-        .attrs
-        .iter()
-        .any(|attr| attr.meta.path().is_ident("non_exhaustive"));
-
-    (
-        ParsedDerive {
-            name,
-            generics,
-            non_exhaustive,
-        },
-        grammar_sources,
-    )
-}
-
-fn get_attribute(attr: &Attribute) -> GrammarSource {
-    match &attr.meta {
-        Meta::NameValue(name_value) => match &name_value.value {
-            Expr::Lit(ExprLit {
-                lit: Lit::Str(string),
-                ..
-            }) => {
-                if name_value.path.is_ident("grammar") {
-                    GrammarSource::File(string.value())
-                } else {
-                    GrammarSource::Inline(string.value())
-                }
-            }
-            _ => panic!("grammar attribute must be a string"),
-        },
-        _ => panic!("grammar attribute must be of the form `grammar = \"...\"`"),
-    }
-}
-
 #[cfg(test)]
 mod tests {
-    use super::parse_derive;
-    use super::GrammarSource;
-
-    #[test]
-    fn derive_inline_file() {
-        let definition = "
-            #[other_attr]
-            #[grammar_inline = \"GRAMMAR\"]
-            pub struct MyParser<'a, T>;
-        ";
-        let ast = syn::parse_str(definition).unwrap();
-        let (_, filenames) = parse_derive(ast);
-        assert_eq!(filenames, [GrammarSource::Inline("GRAMMAR".to_string())]);
-    }
-
-    #[test]
-    fn derive_ok() {
-        let definition = "
-            #[other_attr]
-            #[grammar = \"myfile.pest\"]
-            pub struct MyParser<'a, T>;
-        ";
-        let ast = syn::parse_str(definition).unwrap();
-        let (parsed_derive, filenames) = parse_derive(ast);
-        assert_eq!(filenames, [GrammarSource::File("myfile.pest".to_string())]);
-        assert!(!parsed_derive.non_exhaustive);
-    }
-
-    #[test]
-    fn derive_multiple_grammars() {
-        let definition = "
-            #[other_attr]
-            #[grammar = \"myfile1.pest\"]
-            #[grammar = \"myfile2.pest\"]
-            pub struct MyParser<'a, T>;
-        ";
-        let ast = syn::parse_str(definition).unwrap();
-        let (_, filenames) = parse_derive(ast);
-        assert_eq!(
-            filenames,
-            [
-                GrammarSource::File("myfile1.pest".to_string()),
-                GrammarSource::File("myfile2.pest".to_string())
-            ]
-        );
-    }
-
-    #[test]
-    fn derive_nonexhaustive() {
-        let definition = "
-            #[non_exhaustive]
-            #[grammar = \"myfile.pest\"]
-            pub struct MyParser<'a, T>;
-        ";
-        let ast = syn::parse_str(definition).unwrap();
-        let (parsed_derive, filenames) = parse_derive(ast);
-        assert_eq!(filenames, [GrammarSource::File("myfile.pest".to_string())]);
-        assert!(parsed_derive.non_exhaustive);
-    }
-
-    #[test]
-    #[should_panic(expected = "grammar attribute must be a string")]
-    fn derive_wrong_arg() {
-        let definition = "
-            #[other_attr]
-            #[grammar = 1]
-            pub struct MyParser<'a, T>;
-        ";
-        let ast = syn::parse_str(definition).unwrap();
-        parse_derive(ast);
-    }
-
-    #[test]
-    #[should_panic(
-        expected = "a grammar file needs to be provided with the #[grammar = \"PATH\"] or #[grammar_inline = \"GRAMMAR CONTENTS\"] attribute"
-    )]
-    fn derive_no_grammar() {
-        let definition = "
-            #[other_attr]
-            pub struct MyParser<'a, T>;
-        ";
-        let ast = syn::parse_str(definition).unwrap();
-        parse_derive(ast);
-    }
 
     #[doc = "Matches dar\n\nMatch dar description\n"]
     #[test]
diff --git a/crates/pest_generator/src/parse_derive.rs b/crates/pest_generator/src/parse_derive.rs
new file mode 100644
index 0000000..52374c2
--- /dev/null
+++ b/crates/pest_generator/src/parse_derive.rs
@@ -0,0 +1,172 @@
+// pest. The Elegant Parser
+// Copyright (c) 2018 DragoČ™ Tiselice
+//
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
+// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. All files in the project carrying such notice may not be copied,
+// modified, or distributed except according to those terms.
+
+//! Types and helpers to parse the input of the derive macro.
+
+use syn::{Attribute, DeriveInput, Expr, ExprLit, Generics, Ident, Lit, Meta};
+
+#[derive(Debug, PartialEq)]
+pub(crate) enum GrammarSource {
+    File(String),
+    Inline(String),
+}
+
+/// Parsed information of the derive and the attributes.
+pub struct ParsedDerive {
+    /// The identifier of the deriving struct, union, or enum.
+    pub name: Ident,
+    /// The generics of the deriving struct, union, or enum.
+    pub generics: Generics,
+    /// Indicates whether the 'non_exhaustive' attribute is added to the 'Rule' enum.
+    pub non_exhaustive: bool,
+}
+
+pub(crate) fn parse_derive(ast: DeriveInput) -> (ParsedDerive, Vec<GrammarSource>) {
+    let name = ast.ident;
+    let generics = ast.generics;
+
+    let grammar: Vec<&Attribute> = ast
+        .attrs
+        .iter()
+        .filter(|attr| {
+            let path = attr.meta.path();
+            path.is_ident("grammar") || path.is_ident("grammar_inline")
+        })
+        .collect();
+
+    if grammar.is_empty() {
+        panic!("a grammar file needs to be provided with the #[grammar = \"PATH\"] or #[grammar_inline = \"GRAMMAR CONTENTS\"] attribute");
+    }
+
+    let mut grammar_sources = Vec::with_capacity(grammar.len());
+    for attr in grammar {
+        grammar_sources.push(get_attribute(attr))
+    }
+
+    let non_exhaustive = ast
+        .attrs
+        .iter()
+        .any(|attr| attr.meta.path().is_ident("non_exhaustive"));
+
+    (
+        ParsedDerive {
+            name,
+            generics,
+            non_exhaustive,
+        },
+        grammar_sources,
+    )
+}
+
+fn get_attribute(attr: &Attribute) -> GrammarSource {
+    match &attr.meta {
+        Meta::NameValue(name_value) => match &name_value.value {
+            Expr::Lit(ExprLit {
+                lit: Lit::Str(string),
+                ..
+            }) => {
+                if name_value.path.is_ident("grammar") {
+                    GrammarSource::File(string.value())
+                } else {
+                    GrammarSource::Inline(string.value())
+                }
+            }
+            _ => panic!("grammar attribute must be a string"),
+        },
+        _ => panic!("grammar attribute must be of the form `grammar = \"...\"`"),
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::parse_derive;
+    use super::GrammarSource;
+
+    #[test]
+    fn derive_inline_file() {
+        let definition = "
+            #[other_attr]
+            #[grammar_inline = \"GRAMMAR\"]
+            pub struct MyParser<'a, T>;
+        ";
+        let ast = syn::parse_str(definition).unwrap();
+        let (_, filenames) = parse_derive(ast);
+        assert_eq!(filenames, [GrammarSource::Inline("GRAMMAR".to_string())]);
+    }
+
+    #[test]
+    fn derive_ok() {
+        let definition = "
+            #[other_attr]
+            #[grammar = \"myfile.pest\"]
+            pub struct MyParser<'a, T>;
+        ";
+        let ast = syn::parse_str(definition).unwrap();
+        let (parsed_derive, filenames) = parse_derive(ast);
+        assert_eq!(filenames, [GrammarSource::File("myfile.pest".to_string())]);
+        assert!(!parsed_derive.non_exhaustive);
+    }
+
+    #[test]
+    fn derive_multiple_grammars() {
+        let definition = "
+            #[other_attr]
+            #[grammar = \"myfile1.pest\"]
+            #[grammar = \"myfile2.pest\"]
+            pub struct MyParser<'a, T>;
+        ";
+        let ast = syn::parse_str(definition).unwrap();
+        let (_, filenames) = parse_derive(ast);
+        assert_eq!(
+            filenames,
+            [
+                GrammarSource::File("myfile1.pest".to_string()),
+                GrammarSource::File("myfile2.pest".to_string())
+            ]
+        );
+    }
+
+    #[test]
+    fn derive_nonexhaustive() {
+        let definition = "
+            #[non_exhaustive]
+            #[grammar = \"myfile.pest\"]
+            pub struct MyParser<'a, T>;
+        ";
+        let ast = syn::parse_str(definition).unwrap();
+        let (parsed_derive, filenames) = parse_derive(ast);
+        assert_eq!(filenames, [GrammarSource::File("myfile.pest".to_string())]);
+        assert!(parsed_derive.non_exhaustive);
+    }
+
+    #[test]
+    #[should_panic(expected = "grammar attribute must be a string")]
+    fn derive_wrong_arg() {
+        let definition = "
+            #[other_attr]
+            #[grammar = 1]
+            pub struct MyParser<'a, T>;
+        ";
+        let ast = syn::parse_str(definition).unwrap();
+        parse_derive(ast);
+    }
+
+    #[test]
+    #[should_panic(
+        expected = "a grammar file needs to be provided with the #[grammar = \"PATH\"] or #[grammar_inline = \"GRAMMAR CONTENTS\"] attribute"
+    )]
+    fn derive_no_grammar() {
+        let definition = "
+            #[other_attr]
+            pub struct MyParser<'a, T>;
+        ";
+        let ast = syn::parse_str(definition).unwrap();
+        parse_derive(ast);
+    }
+}
diff --git a/pseudo_crate/Cargo.lock b/pseudo_crate/Cargo.lock
index 984e54a..6abba92 100644
--- a/pseudo_crate/Cargo.lock
+++ b/pseudo_crate/Cargo.lock
@@ -3001,20 +3001,11 @@
 
 [[package]]
 name = "litrs"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b487d13a3f4b465df87895a37b24e364907019afa12d943528df5b7abe0836f1"
-dependencies = [
- "proc-macro2 1.0.93",
-]
-
-[[package]]
-name = "litrs"
 version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
 dependencies = [
- "proc-macro2 1.0.92",
+ "proc-macro2 1.0.93",
 ]
 
 [[package]]
@@ -3338,18 +3329,6 @@
 
 [[package]]
 name = "mockall_derive"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2"
-dependencies = [
- "cfg-if",
- "proc-macro2 1.0.93",
- "quote 1.0.38",
- "syn 2.0.96",
-]
-
-[[package]]
-name = "mockall_derive"
 version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898"
@@ -3634,7 +3613,7 @@
 dependencies = [
  "proc-macro2 1.0.93",
  "quote 1.0.38",
- "syn 2.0.90",
+ "syn 2.0.96",
 ]
 
 [[package]]
@@ -3852,9 +3831,9 @@
 
 [[package]]
 name = "pest_generator"
-version = "2.7.6"
+version = "2.7.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a31940305ffc96863a735bef7c7994a00b325a7138fdbc5bda0f1a0476d3275"
+checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b"
 dependencies = [
  "pest",
  "pest_meta",
@@ -4371,7 +4350,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.7.0",
 ]
 
 [[package]]
@@ -5229,9 +5208,9 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c"
 dependencies = [
- "proc-macro2 1.0.92",
+ "proc-macro2 1.0.93",
  "quote 1.0.38",
- "syn 2.0.90",
+ "syn 2.0.96",
 ]
 
 [[package]]
diff --git a/pseudo_crate/Cargo.toml b/pseudo_crate/Cargo.toml
index e24e8f0..7c4b956 100644
--- a/pseudo_crate/Cargo.toml
+++ b/pseudo_crate/Cargo.toml
@@ -236,7 +236,7 @@
 percore = "=0.1.0"
 pest = "=2.7.15"
 pest_derive = "=2.7.6"
-pest_generator = "=2.7.6"
+pest_generator = "=2.7.15"
 pest_meta = "=2.7.15"
 petgraph = "=0.6.5"
 pin-project = "=1.1.3"