1 // pest. The Elegant Parser
2 // Copyright (c) 2018 Dragoș Tiselice
3 //
4 // Licensed under the Apache License, Version 2.0
5 // <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
6 // license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7 // option. All files in the project carrying such notice may not be copied,
8 // modified, or distributed except according to those terms.
9
10 #![doc(
11 html_root_url = "https://docs.rs/pest_derive",
12 html_logo_url = "https://raw.githubusercontent.com/pest-parser/pest/master/pest-logo.svg",
13 html_favicon_url = "https://raw.githubusercontent.com/pest-parser/pest/master/pest-logo.svg"
14 )]
15 #![warn(missing_docs, rust_2018_idioms, unused_qualifications)]
16 #![recursion_limit = "256"]
17 //! # pest generator
18 //!
19 //! This crate generates code from ASTs (which is used in the `pest_derive` crate).
20
21 #[macro_use]
22 extern crate quote;
23
24 use std::env;
25 use std::fs::File;
26 use std::io::{self, Read};
27 use std::path::Path;
28
29 use proc_macro2::TokenStream;
30 use syn::{Attribute, DeriveInput, Expr, ExprLit, Generics, Ident, Lit, Meta};
31
32 #[macro_use]
33 mod macros;
34 mod docs;
35 mod generator;
36
37 use pest_meta::parser::{self, rename_meta_rule, Rule};
38 use pest_meta::{optimizer, unwrap_or_report, validator};
39
40 /// Processes the derive/proc macro input and generates the corresponding parser based
41 /// on the parsed grammar. If `include_grammar` is set to true, it'll generate an explicit
42 /// "include_str" statement (done in pest_derive, but turned off in the local bootstrap).
derive_parser(input: TokenStream, include_grammar: bool) -> TokenStream43 pub fn derive_parser(input: TokenStream, include_grammar: bool) -> TokenStream {
44 let ast: DeriveInput = syn::parse2(input).unwrap();
45 let (parsed_derive, contents) = parse_derive(ast);
46
47 let mut data = String::new();
48 let mut paths = vec![];
49
50 for content in contents {
51 let (_data, _path) = match content {
52 GrammarSource::File(ref path) => {
53 let root = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
54
55 // Check whether we can find a file at the path relative to the CARGO_MANIFEST_DIR
56 // first.
57 //
58 // If we cannot find the expected file over there, fallback to the
59 // `CARGO_MANIFEST_DIR/src`, which is the old default and kept for convenience
60 // reasons.
61 // TODO: This could be refactored once `std::path::absolute()` get's stabilized.
62 // https://doc.rust-lang.org/std/path/fn.absolute.html
63 let path = if Path::new(&root).join(path).exists() {
64 Path::new(&root).join(path)
65 } else {
66 Path::new(&root).join("src/").join(path)
67 };
68
69 let file_name = match path.file_name() {
70 Some(file_name) => file_name,
71 None => panic!("grammar attribute should point to a file"),
72 };
73
74 let data = match read_file(&path) {
75 Ok(data) => data,
76 Err(error) => panic!("error opening {:?}: {}", file_name, error),
77 };
78 (data, Some(path.clone()))
79 }
80 GrammarSource::Inline(content) => (content, None),
81 };
82
83 data.push_str(&_data);
84 if let Some(path) = _path {
85 paths.push(path);
86 }
87 }
88
89 let pairs = match parser::parse(Rule::grammar_rules, &data) {
90 Ok(pairs) => pairs,
91 Err(error) => panic!("error parsing \n{}", error.renamed_rules(rename_meta_rule)),
92 };
93
94 let defaults = unwrap_or_report(validator::validate_pairs(pairs.clone()));
95 let doc_comment = docs::consume(pairs.clone());
96 let ast = unwrap_or_report(parser::consume_rules(pairs));
97 let optimized = optimizer::optimize(ast);
98
99 generator::generate(
100 parsed_derive,
101 paths,
102 optimized,
103 defaults,
104 &doc_comment,
105 include_grammar,
106 )
107 }
108
read_file<P: AsRef<Path>>(path: P) -> io::Result<String>109 fn read_file<P: AsRef<Path>>(path: P) -> io::Result<String> {
110 let mut file = File::open(path.as_ref())?;
111 let mut string = String::new();
112 file.read_to_string(&mut string)?;
113 Ok(string)
114 }
115
116 #[derive(Debug, PartialEq)]
117 enum GrammarSource {
118 File(String),
119 Inline(String),
120 }
121
122 struct ParsedDerive {
123 pub(crate) name: Ident,
124 pub(crate) generics: Generics,
125 pub(crate) non_exhaustive: bool,
126 }
127
parse_derive(ast: DeriveInput) -> (ParsedDerive, Vec<GrammarSource>)128 fn parse_derive(ast: DeriveInput) -> (ParsedDerive, Vec<GrammarSource>) {
129 let name = ast.ident;
130 let generics = ast.generics;
131
132 let grammar: Vec<&Attribute> = ast
133 .attrs
134 .iter()
135 .filter(|attr| {
136 let path = attr.meta.path();
137 path.is_ident("grammar") || path.is_ident("grammar_inline")
138 })
139 .collect();
140
141 if grammar.is_empty() {
142 panic!("a grammar file needs to be provided with the #[grammar = \"PATH\"] or #[grammar_inline = \"GRAMMAR CONTENTS\"] attribute");
143 }
144
145 let mut grammar_sources = Vec::with_capacity(grammar.len());
146 for attr in grammar {
147 grammar_sources.push(get_attribute(attr))
148 }
149
150 let non_exhaustive = ast
151 .attrs
152 .iter()
153 .any(|attr| attr.meta.path().is_ident("non_exhaustive"));
154
155 (
156 ParsedDerive {
157 name,
158 generics,
159 non_exhaustive,
160 },
161 grammar_sources,
162 )
163 }
164
get_attribute(attr: &Attribute) -> GrammarSource165 fn get_attribute(attr: &Attribute) -> GrammarSource {
166 match &attr.meta {
167 Meta::NameValue(name_value) => match &name_value.value {
168 Expr::Lit(ExprLit {
169 lit: Lit::Str(string),
170 ..
171 }) => {
172 if name_value.path.is_ident("grammar") {
173 GrammarSource::File(string.value())
174 } else {
175 GrammarSource::Inline(string.value())
176 }
177 }
178 _ => panic!("grammar attribute must be a string"),
179 },
180 _ => panic!("grammar attribute must be of the form `grammar = \"...\"`"),
181 }
182 }
183
184 #[cfg(test)]
185 mod tests {
186 use super::parse_derive;
187 use super::GrammarSource;
188
189 #[test]
derive_inline_file()190 fn derive_inline_file() {
191 let definition = "
192 #[other_attr]
193 #[grammar_inline = \"GRAMMAR\"]
194 pub struct MyParser<'a, T>;
195 ";
196 let ast = syn::parse_str(definition).unwrap();
197 let (_, filenames) = parse_derive(ast);
198 assert_eq!(filenames, [GrammarSource::Inline("GRAMMAR".to_string())]);
199 }
200
201 #[test]
derive_ok()202 fn derive_ok() {
203 let definition = "
204 #[other_attr]
205 #[grammar = \"myfile.pest\"]
206 pub struct MyParser<'a, T>;
207 ";
208 let ast = syn::parse_str(definition).unwrap();
209 let (parsed_derive, filenames) = parse_derive(ast);
210 assert_eq!(filenames, [GrammarSource::File("myfile.pest".to_string())]);
211 assert!(!parsed_derive.non_exhaustive);
212 }
213
214 #[test]
derive_multiple_grammars()215 fn derive_multiple_grammars() {
216 let definition = "
217 #[other_attr]
218 #[grammar = \"myfile1.pest\"]
219 #[grammar = \"myfile2.pest\"]
220 pub struct MyParser<'a, T>;
221 ";
222 let ast = syn::parse_str(definition).unwrap();
223 let (_, filenames) = parse_derive(ast);
224 assert_eq!(
225 filenames,
226 [
227 GrammarSource::File("myfile1.pest".to_string()),
228 GrammarSource::File("myfile2.pest".to_string())
229 ]
230 );
231 }
232
233 #[test]
derive_nonexhaustive()234 fn derive_nonexhaustive() {
235 let definition = "
236 #[non_exhaustive]
237 #[grammar = \"myfile.pest\"]
238 pub struct MyParser<'a, T>;
239 ";
240 let ast = syn::parse_str(definition).unwrap();
241 let (parsed_derive, filenames) = parse_derive(ast);
242 assert_eq!(filenames, [GrammarSource::File("myfile.pest".to_string())]);
243 assert!(parsed_derive.non_exhaustive);
244 }
245
246 #[test]
247 #[should_panic(expected = "grammar attribute must be a string")]
derive_wrong_arg()248 fn derive_wrong_arg() {
249 let definition = "
250 #[other_attr]
251 #[grammar = 1]
252 pub struct MyParser<'a, T>;
253 ";
254 let ast = syn::parse_str(definition).unwrap();
255 parse_derive(ast);
256 }
257
258 #[test]
259 #[should_panic(
260 expected = "a grammar file needs to be provided with the #[grammar = \"PATH\"] or #[grammar_inline = \"GRAMMAR CONTENTS\"] attribute"
261 )]
derive_no_grammar()262 fn derive_no_grammar() {
263 let definition = "
264 #[other_attr]
265 pub struct MyParser<'a, T>;
266 ";
267 let ast = syn::parse_str(definition).unwrap();
268 parse_derive(ast);
269 }
270
271 #[doc = "Matches dar\n\nMatch dar description\n"]
272 #[test]
test_generate_doc()273 fn test_generate_doc() {
274 let input = quote! {
275 #[derive(Parser)]
276 #[non_exhaustive]
277 #[grammar = "../tests/test.pest"]
278 pub struct TestParser;
279 };
280
281 let token = super::derive_parser(input, true);
282
283 let expected = quote! {
284 #[doc = "A parser for JSON file.\nAnd this is a example for JSON parser.\n\n indent-4-space\n"]
285 #[allow(dead_code, non_camel_case_types, clippy::upper_case_acronyms)]
286 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
287 #[non_exhaustive]
288 pub enum Rule {
289 #[doc = "Matches foo str, e.g.: `foo`"]
290 r#foo,
291 #[doc = "Matches bar str\n\n Indent 2, e.g: `bar` or `foobar`"]
292 r#bar,
293 r#bar1,
294 #[doc = "Matches dar\n\nMatch dar description\n"]
295 r#dar
296 }
297 };
298
299 assert!(
300 token.to_string().contains(expected.to_string().as_str()),
301 "{}\n\nExpected to contains:\n{}",
302 token,
303 expected
304 );
305 }
306 }
307