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). |
43 | pub fn derive_parser(input: TokenStream, include_grammar: bool) -> TokenStream { |
44 | let ast: DeriveInput = syn::parse2(input).unwrap(); |
45 | let (name, generics, 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 | name, |
101 | &generics, |
102 | paths, |
103 | optimized, |
104 | defaults, |
105 | &doc_comment, |
106 | include_grammar, |
107 | ) |
108 | } |
109 | |
110 | fn read_file<P: AsRef<Path>>(path: P) -> io::Result<String> { |
111 | let mut file: File = File::open(path.as_ref())?; |
112 | let mut string: String = String::new(); |
113 | file.read_to_string(&mut string)?; |
114 | Ok(string) |
115 | } |
116 | |
117 | #[derive (Debug, PartialEq)] |
118 | enum GrammarSource { |
119 | File(String), |
120 | Inline(String), |
121 | } |
122 | |
123 | fn parse_derive(ast: DeriveInput) -> (Ident, Generics, Vec<GrammarSource>) { |
124 | let name: Ident = ast.ident; |
125 | let generics: Generics = ast.generics; |
126 | |
127 | let grammar: Vec<&Attribute> = astimpl Iterator |
128 | .attrs |
129 | .iter() |
130 | .filter(|attr: &&Attribute| { |
131 | let path: &Path = attr.meta.path(); |
132 | path.is_ident("grammar" ) || path.is_ident("grammar_inline" ) |
133 | }) |
134 | .collect(); |
135 | |
136 | if grammar.is_empty() { |
137 | panic!("a grammar file needs to be provided with the #[grammar = \"PATH \"] or #[grammar_inline = \"GRAMMAR CONTENTS \"] attribute" ); |
138 | } |
139 | |
140 | let mut grammar_sources: Vec = Vec::with_capacity(grammar.len()); |
141 | for attr: &Attribute in grammar { |
142 | grammar_sources.push(get_attribute(attr)) |
143 | } |
144 | |
145 | (name, generics, grammar_sources) |
146 | } |
147 | |
148 | fn get_attribute(attr: &Attribute) -> GrammarSource { |
149 | match &attr.meta { |
150 | Meta::NameValue(name_value: &MetaNameValue) => match &name_value.value { |
151 | Expr::Lit(ExprLit { |
152 | lit: Lit::Str(string: &LitStr), |
153 | .. |
154 | }) => { |
155 | if name_value.path.is_ident("grammar" ) { |
156 | GrammarSource::File(string.value()) |
157 | } else { |
158 | GrammarSource::Inline(string.value()) |
159 | } |
160 | } |
161 | _ => panic!("grammar attribute must be a string" ), |
162 | }, |
163 | _ => panic!("grammar attribute must be of the form `grammar = \"... \"`" ), |
164 | } |
165 | } |
166 | |
167 | #[cfg (test)] |
168 | mod tests { |
169 | use super::parse_derive; |
170 | use super::GrammarSource; |
171 | |
172 | #[test ] |
173 | fn derive_inline_file() { |
174 | let definition = " |
175 | #[other_attr] |
176 | #[grammar_inline = \"GRAMMAR \"] |
177 | pub struct MyParser<'a, T>; |
178 | " ; |
179 | let ast = syn::parse_str(definition).unwrap(); |
180 | let (_, _, filenames) = parse_derive(ast); |
181 | assert_eq!(filenames, [GrammarSource::Inline("GRAMMAR" .to_string())]); |
182 | } |
183 | |
184 | #[test ] |
185 | fn derive_ok() { |
186 | let definition = " |
187 | #[other_attr] |
188 | #[grammar = \"myfile.pest \"] |
189 | pub struct MyParser<'a, T>; |
190 | " ; |
191 | let ast = syn::parse_str(definition).unwrap(); |
192 | let (_, _, filenames) = parse_derive(ast); |
193 | assert_eq!(filenames, [GrammarSource::File("myfile.pest" .to_string())]); |
194 | } |
195 | |
196 | #[test ] |
197 | fn derive_multiple_grammars() { |
198 | let definition = " |
199 | #[other_attr] |
200 | #[grammar = \"myfile1.pest \"] |
201 | #[grammar = \"myfile2.pest \"] |
202 | pub struct MyParser<'a, T>; |
203 | " ; |
204 | let ast = syn::parse_str(definition).unwrap(); |
205 | let (_, _, filenames) = parse_derive(ast); |
206 | assert_eq!( |
207 | filenames, |
208 | [ |
209 | GrammarSource::File("myfile1.pest" .to_string()), |
210 | GrammarSource::File("myfile2.pest" .to_string()) |
211 | ] |
212 | ); |
213 | } |
214 | |
215 | #[test ] |
216 | #[should_panic (expected = "grammar attribute must be a string" )] |
217 | fn derive_wrong_arg() { |
218 | let definition = " |
219 | #[other_attr] |
220 | #[grammar = 1] |
221 | pub struct MyParser<'a, T>; |
222 | " ; |
223 | let ast = syn::parse_str(definition).unwrap(); |
224 | parse_derive(ast); |
225 | } |
226 | |
227 | #[test ] |
228 | #[should_panic ( |
229 | expected = "a grammar file needs to be provided with the #[grammar = \"PATH \"] or #[grammar_inline = \"GRAMMAR CONTENTS \"] attribute" |
230 | )] |
231 | fn derive_no_grammar() { |
232 | let definition = " |
233 | #[other_attr] |
234 | pub struct MyParser<'a, T>; |
235 | " ; |
236 | let ast = syn::parse_str(definition).unwrap(); |
237 | parse_derive(ast); |
238 | } |
239 | |
240 | #[doc = "Matches dar \n\nMatch dar description \n" ] |
241 | #[test ] |
242 | fn test_generate_doc() { |
243 | let input = quote! { |
244 | #[derive(Parser)] |
245 | #[grammar = "../tests/test.pest" ] |
246 | pub struct TestParser; |
247 | }; |
248 | |
249 | let token = super::derive_parser(input, true); |
250 | |
251 | let expected = quote! { |
252 | #[doc = "A parser for JSON file. \nAnd this is a example for JSON parser. \n\n indent-4-space \n" ] |
253 | #[allow(dead_code, non_camel_case_types, clippy::upper_case_acronyms)] |
254 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
255 | |
256 | pub enum Rule { |
257 | #[doc = "Matches foo str, e.g.: `foo`" ] |
258 | r#foo, |
259 | #[doc = "Matches bar str \n\n Indent 2, e.g: `bar` or `foobar`" ] |
260 | r#bar, |
261 | r#bar1, |
262 | #[doc = "Matches dar \n\nMatch dar description \n" ] |
263 | r#dar |
264 | } |
265 | }; |
266 | |
267 | assert!( |
268 | token.to_string().contains(expected.to_string().as_str()), |
269 | " {}\n\nExpected to contains: \n{}" , |
270 | token, |
271 | expected |
272 | ); |
273 | } |
274 | } |
275 | |