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]
22extern crate quote;
23
24use std::env;
25use std::fs::File;
26use std::io::{self, Read};
27use std::path::Path;
28
29use proc_macro2::TokenStream;
30use syn::{Attribute, DeriveInput, Expr, ExprLit, Generics, Ident, Lit, Meta};
31
32#[macro_use]
33mod macros;
34mod docs;
35mod generator;
36
37use pest_meta::parser::{self, rename_meta_rule, Rule};
38use 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).
43pub 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
110fn 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)]
118enum GrammarSource {
119 File(String),
120 Inline(String),
121}
122
123fn 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
148fn 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)]
168mod 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