diff options
-rw-r--r-- | .cargo/config | 4 | ||||
-rw-r--r-- | .travis.yml | 2 | ||||
-rw-r--r-- | src/bin/cli/parse.rs (renamed from tools/src/bin/parse.rs) | 5 | ||||
-rw-r--r-- | src/syntax_kinds/generated.rs | 69 | ||||
-rw-r--r-- | src/syntax_kinds/generated.rs.tera | 59 | ||||
-rw-r--r-- | tools/Cargo.toml | 13 | ||||
-rw-r--r-- | tools/src/bin/collect-tests.rs | 133 | ||||
-rw-r--r-- | tools/src/bin/gen.rs | 121 | ||||
-rw-r--r-- | tools/src/bin/main.rs | 184 |
9 files changed, 290 insertions, 300 deletions
diff --git a/.cargo/config b/.cargo/config index 7d89cf490..1898d28d3 100644 --- a/.cargo/config +++ b/.cargo/config | |||
@@ -1,4 +1,4 @@ | |||
1 | [alias] | 1 | [alias] |
2 | parse = "run --package tools --bin parse" | 2 | parse = "run --package tools --bin parse" |
3 | gen = "run --package tools --bin gen" | 3 | gen-kinds = "run --package tools -- gen-kinds" |
4 | collect-tests = "run --package tools --bin collect-tests --" | 4 | gen-tests = "run --package tools -- gen-tests" |
diff --git a/.travis.yml b/.travis.yml index 425494e15..f4ee048f4 100644 --- a/.travis.yml +++ b/.travis.yml | |||
@@ -8,6 +8,8 @@ matrix: | |||
8 | script: | 8 | script: |
9 | - cargo fmt --all -- --write-mode=diff | 9 | - cargo fmt --all -- --write-mode=diff |
10 | - cargo test | 10 | - cargo test |
11 | - cargo gen-kinds --verify | ||
12 | - cargo gen-tests --verify | ||
11 | - rust: nightly | 13 | - rust: nightly |
12 | before_script: | 14 | before_script: |
13 | - rustup component add clippy-preview | 15 | - rustup component add clippy-preview |
diff --git a/tools/src/bin/parse.rs b/src/bin/cli/parse.rs index cb3414711..563ea92f6 100644 --- a/tools/src/bin/parse.rs +++ b/src/bin/cli/parse.rs | |||
@@ -2,8 +2,9 @@ extern crate libsyntax2; | |||
2 | 2 | ||
3 | use std::io::Read; | 3 | use std::io::Read; |
4 | 4 | ||
5 | use libsyntax2::{parse}; | 5 | use libsyntax2::{ |
6 | use libsyntax2::utils::dump_tree_green; | 6 | parse, utils::dump_tree_green |
7 | }; | ||
7 | 8 | ||
8 | fn main() { | 9 | fn main() { |
9 | let text = read_input(); | 10 | let text = read_input(); |
diff --git a/src/syntax_kinds/generated.rs b/src/syntax_kinds/generated.rs index d332fd02e..029972bb3 100644 --- a/src/syntax_kinds/generated.rs +++ b/src/syntax_kinds/generated.rs | |||
@@ -1,6 +1,5 @@ | |||
1 | #![allow(bad_style, missing_docs, unreachable_pub)] | 1 | #![allow(bad_style, missing_docs, unreachable_pub)] |
2 | #![cfg_attr(rustfmt, rustfmt_skip)] | 2 | #![cfg_attr(rustfmt, rustfmt_skip)] |
3 | //! Generated from grammar.ron | ||
4 | use super::SyntaxInfo; | 3 | use super::SyntaxInfo; |
5 | 4 | ||
6 | /// The kind of syntax node, e.g. `IDENT`, `USE_KW`, or `STRUCT_DEF`. | 5 | /// The kind of syntax node, e.g. `IDENT`, `USE_KW`, or `STRUCT_DEF`. |
@@ -138,7 +137,6 @@ pub enum SyntaxKind { | |||
138 | VALUE_PARAMETER, | 137 | VALUE_PARAMETER, |
139 | BLOCK, | 138 | BLOCK, |
140 | LET_STMT, | 139 | LET_STMT, |
141 | |||
142 | // Technical SyntaxKinds: they appear temporally during parsing, | 140 | // Technical SyntaxKinds: they appear temporally during parsing, |
143 | // but never end up in the final tree | 141 | // but never end up in the final tree |
144 | #[doc(hidden)] | 142 | #[doc(hidden)] |
@@ -146,7 +144,7 @@ pub enum SyntaxKind { | |||
146 | #[doc(hidden)] | 144 | #[doc(hidden)] |
147 | EOF, | 145 | EOF, |
148 | } | 146 | } |
149 | pub(crate) use self::SyntaxKind::*; | 147 | use self::SyntaxKind::*; |
150 | 148 | ||
151 | impl SyntaxKind { | 149 | impl SyntaxKind { |
152 | pub(crate) fn info(self) -> &'static SyntaxInfo { | 150 | pub(crate) fn info(self) -> &'static SyntaxInfo { |
@@ -289,38 +287,39 @@ impl SyntaxKind { | |||
289 | } | 287 | } |
290 | } | 288 | } |
291 | pub(crate) fn from_keyword(ident: &str) -> Option<SyntaxKind> { | 289 | pub(crate) fn from_keyword(ident: &str) -> Option<SyntaxKind> { |
292 | match ident { | 290 | let kw = match ident { |
293 | "use" => Some(USE_KW), | 291 | "use" => USE_KW, |
294 | "fn" => Some(FN_KW), | 292 | "fn" => FN_KW, |
295 | "struct" => Some(STRUCT_KW), | 293 | "struct" => STRUCT_KW, |
296 | "enum" => Some(ENUM_KW), | 294 | "enum" => ENUM_KW, |
297 | "trait" => Some(TRAIT_KW), | 295 | "trait" => TRAIT_KW, |
298 | "impl" => Some(IMPL_KW), | 296 | "impl" => IMPL_KW, |
299 | "true" => Some(TRUE_KW), | 297 | "true" => TRUE_KW, |
300 | "false" => Some(FALSE_KW), | 298 | "false" => FALSE_KW, |
301 | "as" => Some(AS_KW), | 299 | "as" => AS_KW, |
302 | "extern" => Some(EXTERN_KW), | 300 | "extern" => EXTERN_KW, |
303 | "crate" => Some(CRATE_KW), | 301 | "crate" => CRATE_KW, |
304 | "mod" => Some(MOD_KW), | 302 | "mod" => MOD_KW, |
305 | "pub" => Some(PUB_KW), | 303 | "pub" => PUB_KW, |
306 | "self" => Some(SELF_KW), | 304 | "self" => SELF_KW, |
307 | "super" => Some(SUPER_KW), | 305 | "super" => SUPER_KW, |
308 | "in" => Some(IN_KW), | 306 | "in" => IN_KW, |
309 | "where" => Some(WHERE_KW), | 307 | "where" => WHERE_KW, |
310 | "for" => Some(FOR_KW), | 308 | "for" => FOR_KW, |
311 | "loop" => Some(LOOP_KW), | 309 | "loop" => LOOP_KW, |
312 | "while" => Some(WHILE_KW), | 310 | "while" => WHILE_KW, |
313 | "if" => Some(IF_KW), | 311 | "if" => IF_KW, |
314 | "match" => Some(MATCH_KW), | 312 | "match" => MATCH_KW, |
315 | "const" => Some(CONST_KW), | 313 | "const" => CONST_KW, |
316 | "static" => Some(STATIC_KW), | 314 | "static" => STATIC_KW, |
317 | "mut" => Some(MUT_KW), | 315 | "mut" => MUT_KW, |
318 | "unsafe" => Some(UNSAFE_KW), | 316 | "unsafe" => UNSAFE_KW, |
319 | "type" => Some(TYPE_KW), | 317 | "type" => TYPE_KW, |
320 | "ref" => Some(REF_KW), | 318 | "ref" => REF_KW, |
321 | "let" => Some(LET_KW), | 319 | "let" => LET_KW, |
322 | _ => None, | 320 | _ => return None, |
323 | } | 321 | }; |
322 | Some(kw) | ||
324 | } | 323 | } |
325 | } | 324 | } |
326 | 325 | ||
diff --git a/src/syntax_kinds/generated.rs.tera b/src/syntax_kinds/generated.rs.tera new file mode 100644 index 000000000..aa672d89a --- /dev/null +++ b/src/syntax_kinds/generated.rs.tera | |||
@@ -0,0 +1,59 @@ | |||
1 | #![allow(bad_style, missing_docs, unreachable_pub)] | ||
2 | #![cfg_attr(rustfmt, rustfmt_skip)] | ||
3 | use super::SyntaxInfo; | ||
4 | |||
5 | /// The kind of syntax node, e.g. `IDENT`, `USE_KW`, or `STRUCT_DEF`. | ||
6 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] | ||
7 | pub enum SyntaxKind { | ||
8 | {%- for t in tokens %} | ||
9 | {{t}}, | ||
10 | {%- endfor -%} | ||
11 | {% for kw in keywords %} | ||
12 | {{kw | upper}}_KW, | ||
13 | {%- endfor -%} | ||
14 | {% for kw in contextual_keywords %} | ||
15 | {{kw | upper}}_KW, | ||
16 | {%- endfor -%} | ||
17 | {% for node in nodes %} | ||
18 | {{node}}, | ||
19 | {%- endfor %} | ||
20 | // Technical SyntaxKinds: they appear temporally during parsing, | ||
21 | // but never end up in the final tree | ||
22 | #[doc(hidden)] | ||
23 | TOMBSTONE, | ||
24 | #[doc(hidden)] | ||
25 | EOF, | ||
26 | } | ||
27 | use self::SyntaxKind::*; | ||
28 | |||
29 | impl SyntaxKind { | ||
30 | pub(crate) fn info(self) -> &'static SyntaxInfo { | ||
31 | match self { | ||
32 | {%- for t in tokens %} | ||
33 | {{t}} => &SyntaxInfo { name: "{{t}}" }, | ||
34 | {%- endfor -%} | ||
35 | {% for kw in keywords %} | ||
36 | {{kw | upper}}_KW => &SyntaxInfo { name: "{{kw | upper}}_KW" }, | ||
37 | {%- endfor -%} | ||
38 | {% for kw in contextual_keywords %} | ||
39 | {{kw | upper}}_KW => &SyntaxInfo { name: "{{kw | upper}}_KW" }, | ||
40 | {%- endfor -%} | ||
41 | {% for node in nodes %} | ||
42 | {{node}} => &SyntaxInfo { name: "{{node}}" }, | ||
43 | {%- endfor %} | ||
44 | |||
45 | TOMBSTONE => &SyntaxInfo { name: "TOMBSTONE" }, | ||
46 | EOF => &SyntaxInfo { name: "EOF" }, | ||
47 | } | ||
48 | } | ||
49 | pub(crate) fn from_keyword(ident: &str) -> Option<SyntaxKind> { | ||
50 | let kw = match ident { | ||
51 | {%- for kw in keywords %} | ||
52 | "{{kw}}" => {{kw | upper}}_KW, | ||
53 | {%- endfor %} | ||
54 | _ => return None, | ||
55 | }; | ||
56 | Some(kw) | ||
57 | } | ||
58 | } | ||
59 | |||
diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 8cbc2fc93..4fcddebf0 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml | |||
@@ -5,10 +5,9 @@ authors = ["Aleksey Kladov <[email protected]>"] | |||
5 | publish = false | 5 | publish = false |
6 | 6 | ||
7 | [dependencies] | 7 | [dependencies] |
8 | serde = "1.0.26" | 8 | ron = "0.1.7" |
9 | serde_derive = "1.0.26" | 9 | walkdir = "2.1.3" |
10 | file = "1.1.1" | 10 | itertools = "0.7.8" |
11 | ron = "0.1.5" | 11 | tera = "0.11" |
12 | walkdir = "2" | 12 | clap = "2.32.0" |
13 | itertools = "0.7" | 13 | failure = "0.1.1" |
14 | libsyntax2 = { path = "../" } | ||
diff --git a/tools/src/bin/collect-tests.rs b/tools/src/bin/collect-tests.rs deleted file mode 100644 index a52e7b119..000000000 --- a/tools/src/bin/collect-tests.rs +++ /dev/null | |||
@@ -1,133 +0,0 @@ | |||
1 | extern crate file; | ||
2 | extern crate itertools; | ||
3 | extern crate walkdir; | ||
4 | |||
5 | use walkdir::WalkDir; | ||
6 | use itertools::Itertools; | ||
7 | |||
8 | use std::path::{Path, PathBuf}; | ||
9 | use std::collections::HashSet; | ||
10 | use std::fs; | ||
11 | |||
12 | fn main() { | ||
13 | let verify = ::std::env::args().any(|arg| arg == "--verify"); | ||
14 | |||
15 | let d = grammar_dir(); | ||
16 | let tests = tests_from_dir(&d); | ||
17 | let existing = existing_tests(); | ||
18 | |||
19 | for t in existing.difference(&tests) { | ||
20 | panic!("Test is deleted: {}\n{}", t.name, t.text); | ||
21 | } | ||
22 | |||
23 | let new_tests = tests.difference(&existing); | ||
24 | for (i, t) in new_tests.enumerate() { | ||
25 | if verify { | ||
26 | panic!("Inline test is not recorded: {}", t.name); | ||
27 | } | ||
28 | |||
29 | let name = format!("{:04}_{}.rs", existing.len() + i + 1, t.name); | ||
30 | println!("Creating {}", name); | ||
31 | let path = inline_tests_dir().join(name); | ||
32 | file::put_text(&path, &t.text).unwrap(); | ||
33 | } | ||
34 | } | ||
35 | |||
36 | #[derive(Debug, Eq)] | ||
37 | struct Test { | ||
38 | name: String, | ||
39 | text: String, | ||
40 | } | ||
41 | |||
42 | impl PartialEq for Test { | ||
43 | fn eq(&self, other: &Test) -> bool { | ||
44 | self.name.eq(&other.name) | ||
45 | } | ||
46 | } | ||
47 | |||
48 | impl ::std::hash::Hash for Test { | ||
49 | fn hash<H: ::std::hash::Hasher>(&self, state: &mut H) { | ||
50 | self.name.hash(state) | ||
51 | } | ||
52 | } | ||
53 | |||
54 | fn tests_from_dir(dir: &Path) -> HashSet<Test> { | ||
55 | let mut res = HashSet::new(); | ||
56 | for entry in WalkDir::new(dir) { | ||
57 | let entry = entry.unwrap(); | ||
58 | if !entry.file_type().is_file() { | ||
59 | continue; | ||
60 | } | ||
61 | if entry.path().extension().unwrap_or_default() != "rs" { | ||
62 | continue; | ||
63 | } | ||
64 | let text = file::get_text(entry.path()).unwrap(); | ||
65 | |||
66 | for test in collect_tests(&text) { | ||
67 | if let Some(old_test) = res.replace(test) { | ||
68 | panic!("Duplicate test: {}", old_test.name) | ||
69 | } | ||
70 | } | ||
71 | } | ||
72 | res | ||
73 | } | ||
74 | |||
75 | fn collect_tests(s: &str) -> Vec<Test> { | ||
76 | let mut res = vec![]; | ||
77 | let prefix = "// "; | ||
78 | let comment_blocks = s.lines() | ||
79 | .map(str::trim_left) | ||
80 | .group_by(|line| line.starts_with(prefix)); | ||
81 | |||
82 | 'outer: for (is_comment, block) in comment_blocks.into_iter() { | ||
83 | if !is_comment { | ||
84 | continue; | ||
85 | } | ||
86 | let mut block = block.map(|line| &line[prefix.len()..]); | ||
87 | |||
88 | let name = loop { | ||
89 | match block.next() { | ||
90 | Some(line) if line.starts_with("test ") => break line["test ".len()..].to_string(), | ||
91 | Some(_) => (), | ||
92 | None => continue 'outer, | ||
93 | } | ||
94 | }; | ||
95 | let text: String = itertools::join(block.chain(::std::iter::once("")), "\n"); | ||
96 | assert!(!text.trim().is_empty() && text.ends_with("\n")); | ||
97 | res.push(Test { name, text }) | ||
98 | } | ||
99 | res | ||
100 | } | ||
101 | |||
102 | fn existing_tests() -> HashSet<Test> { | ||
103 | let mut res = HashSet::new(); | ||
104 | for file in fs::read_dir(&inline_tests_dir()).unwrap() { | ||
105 | let file = file.unwrap(); | ||
106 | let path = file.path(); | ||
107 | if path.extension().unwrap_or_default() != "rs" { | ||
108 | continue; | ||
109 | } | ||
110 | let name = path.file_name().unwrap().to_str().unwrap(); | ||
111 | let name = name["0000_".len()..name.len() - 3].to_string(); | ||
112 | let text = file::get_text(&path).unwrap(); | ||
113 | res.insert(Test { name, text }); | ||
114 | } | ||
115 | res | ||
116 | } | ||
117 | |||
118 | fn inline_tests_dir() -> PathBuf { | ||
119 | let res = base_dir().join("tests/data/parser/inline"); | ||
120 | if !res.is_dir() { | ||
121 | fs::create_dir_all(&res).unwrap(); | ||
122 | } | ||
123 | res | ||
124 | } | ||
125 | |||
126 | fn grammar_dir() -> PathBuf { | ||
127 | base_dir().join("src/parser/grammar") | ||
128 | } | ||
129 | |||
130 | fn base_dir() -> PathBuf { | ||
131 | let dir = env!("CARGO_MANIFEST_DIR"); | ||
132 | PathBuf::from(dir).parent().unwrap().to_owned() | ||
133 | } | ||
diff --git a/tools/src/bin/gen.rs b/tools/src/bin/gen.rs deleted file mode 100644 index 2d3cd422d..000000000 --- a/tools/src/bin/gen.rs +++ /dev/null | |||
@@ -1,121 +0,0 @@ | |||
1 | extern crate serde; | ||
2 | #[macro_use] | ||
3 | extern crate serde_derive; | ||
4 | |||
5 | extern crate file; | ||
6 | extern crate ron; | ||
7 | |||
8 | use std::path::PathBuf; | ||
9 | use std::fmt::Write; | ||
10 | |||
11 | fn main() { | ||
12 | let grammar = Grammar::read(); | ||
13 | let text = grammar.to_syntax_kinds(); | ||
14 | let target = generated_file(); | ||
15 | if text != file::get_text(&target).unwrap_or_default() { | ||
16 | file::put_text(&target, &text).unwrap(); | ||
17 | } | ||
18 | } | ||
19 | |||
20 | #[derive(Deserialize)] | ||
21 | struct Grammar { | ||
22 | keywords: Vec<String>, | ||
23 | contextual_keywords: Vec<String>, | ||
24 | tokens: Vec<String>, | ||
25 | nodes: Vec<String>, | ||
26 | } | ||
27 | |||
28 | impl Grammar { | ||
29 | fn read() -> Grammar { | ||
30 | let text = file::get_text(&grammar_file()).unwrap(); | ||
31 | ron::de::from_str(&text).unwrap() | ||
32 | } | ||
33 | |||
34 | fn to_syntax_kinds(&self) -> String { | ||
35 | let mut acc = String::new(); | ||
36 | acc.push_str("#![allow(bad_style, missing_docs, unreachable_pub)]\n"); | ||
37 | acc.push_str("#![cfg_attr(rustfmt, rustfmt_skip)]\n"); | ||
38 | acc.push_str("//! Generated from grammar.ron\n"); | ||
39 | acc.push_str("use super::SyntaxInfo;\n"); | ||
40 | acc.push_str("\n"); | ||
41 | |||
42 | let syntax_kinds: Vec<String> = self.tokens | ||
43 | .iter() | ||
44 | .cloned() | ||
45 | .chain(self.keywords.iter().map(|kw| kw_token(kw))) | ||
46 | .chain(self.contextual_keywords.iter().map(|kw| kw_token(kw))) | ||
47 | .chain(self.nodes.iter().cloned()) | ||
48 | .collect(); | ||
49 | |||
50 | // enum SyntaxKind | ||
51 | acc.push_str("/// The kind of syntax node, e.g. `IDENT`, `USE_KW`, or `STRUCT_DEF`.\n"); | ||
52 | acc.push_str("#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]\n"); | ||
53 | acc.push_str("pub enum SyntaxKind {\n"); | ||
54 | for kind in syntax_kinds.iter() { | ||
55 | write!(acc, " {},\n", scream(kind)).unwrap(); | ||
56 | } | ||
57 | acc.push_str("\n"); | ||
58 | acc.push_str(" // Technical SyntaxKinds: they appear temporally during parsing,\n"); | ||
59 | acc.push_str(" // but never end up in the final tree\n"); | ||
60 | acc.push_str(" #[doc(hidden)]\n"); | ||
61 | acc.push_str(" TOMBSTONE,\n"); | ||
62 | acc.push_str(" #[doc(hidden)]\n"); | ||
63 | acc.push_str(" EOF,\n"); | ||
64 | acc.push_str("}\n"); | ||
65 | acc.push_str("pub(crate) use self::SyntaxKind::*;\n"); | ||
66 | acc.push_str("\n"); | ||
67 | |||
68 | // fn info | ||
69 | acc.push_str("impl SyntaxKind {\n"); | ||
70 | acc.push_str(" pub(crate) fn info(self) -> &'static SyntaxInfo {\n"); | ||
71 | acc.push_str(" match self {\n"); | ||
72 | for kind in syntax_kinds.iter() { | ||
73 | let sname = scream(kind); | ||
74 | write!( | ||
75 | acc, | ||
76 | " {sname} => &SyntaxInfo {{ name: \"{sname}\" }},\n", | ||
77 | sname = sname | ||
78 | ).unwrap(); | ||
79 | } | ||
80 | acc.push_str("\n"); | ||
81 | acc.push_str(" TOMBSTONE => &SyntaxInfo { name: \"TOMBSTONE\" },\n"); | ||
82 | acc.push_str(" EOF => &SyntaxInfo { name: \"EOF\" },\n"); | ||
83 | acc.push_str(" }\n"); | ||
84 | acc.push_str(" }\n"); | ||
85 | |||
86 | // fn from_keyword | ||
87 | acc.push_str(" pub(crate) fn from_keyword(ident: &str) -> Option<SyntaxKind> {\n"); | ||
88 | acc.push_str(" match ident {\n"); | ||
89 | // NB: no contextual_keywords here! | ||
90 | for kw in self.keywords.iter() { | ||
91 | write!(acc, " {:?} => Some({}),\n", kw, kw_token(kw)).unwrap(); | ||
92 | } | ||
93 | acc.push_str(" _ => None,\n"); | ||
94 | acc.push_str(" }\n"); | ||
95 | acc.push_str(" }\n"); | ||
96 | acc.push_str("}\n"); | ||
97 | acc.push_str("\n"); | ||
98 | acc | ||
99 | } | ||
100 | } | ||
101 | |||
102 | fn grammar_file() -> PathBuf { | ||
103 | base_dir().join("src/grammar.ron") | ||
104 | } | ||
105 | |||
106 | fn generated_file() -> PathBuf { | ||
107 | base_dir().join("src/syntax_kinds/generated.rs") | ||
108 | } | ||
109 | |||
110 | fn scream(word: &str) -> String { | ||
111 | word.chars().map(|c| c.to_ascii_uppercase()).collect() | ||
112 | } | ||
113 | |||
114 | fn kw_token(keyword: &str) -> String { | ||
115 | format!("{}_KW", scream(keyword)) | ||
116 | } | ||
117 | |||
118 | fn base_dir() -> PathBuf { | ||
119 | let dir = env!("CARGO_MANIFEST_DIR"); | ||
120 | PathBuf::from(dir).parent().unwrap().to_owned() | ||
121 | } | ||
diff --git a/tools/src/bin/main.rs b/tools/src/bin/main.rs new file mode 100644 index 000000000..6a9793fff --- /dev/null +++ b/tools/src/bin/main.rs | |||
@@ -0,0 +1,184 @@ | |||
1 | extern crate clap; | ||
2 | #[macro_use] | ||
3 | extern crate failure; | ||
4 | extern crate tera; | ||
5 | extern crate ron; | ||
6 | extern crate walkdir; | ||
7 | extern crate itertools; | ||
8 | |||
9 | use std::{ | ||
10 | fs, | ||
11 | path::{Path}, | ||
12 | collections::HashSet, | ||
13 | }; | ||
14 | use clap::{App, Arg, SubCommand}; | ||
15 | use itertools::Itertools; | ||
16 | |||
17 | type Result<T> = ::std::result::Result<T, failure::Error>; | ||
18 | |||
19 | const GRAMMAR_DIR: &str = "./src/parser/grammar"; | ||
20 | const INLINE_TESTS_DIR: &str = "tests/data/parser/inline"; | ||
21 | const GRAMMAR: &str = "./src/grammar.ron"; | ||
22 | const SYNTAX_KINDS: &str = "./src/syntax_kinds/generated.rs"; | ||
23 | const SYNTAX_KINDS_TEMPLATE: &str = "./src/syntax_kinds/generated.rs.tera"; | ||
24 | |||
25 | fn main() -> Result<()> { | ||
26 | let matches = App::new("tasks") | ||
27 | .setting(clap::AppSettings::SubcommandRequiredElseHelp) | ||
28 | .arg( | ||
29 | Arg::with_name("verify") | ||
30 | .long("--verify") | ||
31 | .help("Verify that generated code is up-to-date") | ||
32 | .global(true) | ||
33 | ) | ||
34 | .subcommand(SubCommand::with_name("gen-kinds")) | ||
35 | .subcommand(SubCommand::with_name("gen-tests")) | ||
36 | .get_matches(); | ||
37 | match matches.subcommand() { | ||
38 | (name, Some(matches)) => run_gen_command(name, matches.is_present("verify"))?, | ||
39 | _ => unreachable!(), | ||
40 | } | ||
41 | Ok(()) | ||
42 | } | ||
43 | |||
44 | fn run_gen_command(name: &str, verify: bool) -> Result<()> { | ||
45 | match name { | ||
46 | "gen-kinds" => update(Path::new(SYNTAX_KINDS), &get_kinds()?, verify), | ||
47 | "gen-tests" => gen_tests(verify), | ||
48 | _ => unreachable!(), | ||
49 | } | ||
50 | } | ||
51 | |||
52 | fn update(path: &Path, contents: &str, verify: bool) -> Result<()> { | ||
53 | match fs::read_to_string(path) { | ||
54 | Ok(ref old_contents) if old_contents == contents => { | ||
55 | return Ok(()); | ||
56 | } | ||
57 | _ => (), | ||
58 | } | ||
59 | if verify { | ||
60 | bail!("`{}` is not up-to-date", path.display()); | ||
61 | } | ||
62 | fs::write(path, contents)?; | ||
63 | Ok(()) | ||
64 | } | ||
65 | |||
66 | fn get_kinds() -> Result<String> { | ||
67 | let grammar = grammar()?; | ||
68 | let template = fs::read_to_string(SYNTAX_KINDS_TEMPLATE)?; | ||
69 | let ret = tera::Tera::one_off(&template, &grammar, false).map_err(|e| { | ||
70 | format_err!("template error: {}", e) | ||
71 | })?; | ||
72 | Ok(ret) | ||
73 | } | ||
74 | |||
75 | fn grammar() -> Result<ron::value::Value> { | ||
76 | let text = fs::read_to_string(GRAMMAR)?; | ||
77 | let ret = ron::de::from_str(&text)?; | ||
78 | Ok(ret) | ||
79 | } | ||
80 | |||
81 | fn gen_tests(verify: bool) -> Result<()> { | ||
82 | let tests = tests_from_dir(Path::new(GRAMMAR_DIR))?; | ||
83 | |||
84 | let inline_tests_dir = Path::new(INLINE_TESTS_DIR); | ||
85 | if !inline_tests_dir.is_dir() { | ||
86 | fs::create_dir_all(inline_tests_dir)?; | ||
87 | } | ||
88 | let existing = existing_tests(inline_tests_dir)?; | ||
89 | |||
90 | for t in existing.difference(&tests) { | ||
91 | panic!("Test is deleted: {}\n{}", t.name, t.text); | ||
92 | } | ||
93 | |||
94 | let new_tests = tests.difference(&existing); | ||
95 | for (i, t) in new_tests.enumerate() { | ||
96 | let name = format!("{:04}_{}.rs", existing.len() + i + 1, t.name); | ||
97 | let path = inline_tests_dir.join(name); | ||
98 | update(&path, &t.text, verify)?; | ||
99 | } | ||
100 | Ok(()) | ||
101 | } | ||
102 | |||
103 | #[derive(Debug, Eq)] | ||
104 | struct Test { | ||
105 | name: String, | ||
106 | text: String, | ||
107 | } | ||
108 | |||
109 | impl PartialEq for Test { | ||
110 | fn eq(&self, other: &Test) -> bool { | ||
111 | self.name.eq(&other.name) | ||
112 | } | ||
113 | } | ||
114 | |||
115 | impl ::std::hash::Hash for Test { | ||
116 | fn hash<H: ::std::hash::Hasher>(&self, state: &mut H) { | ||
117 | self.name.hash(state) | ||
118 | } | ||
119 | } | ||
120 | |||
121 | fn tests_from_dir(dir: &Path) -> Result<HashSet<Test>> { | ||
122 | let mut res = HashSet::new(); | ||
123 | for entry in ::walkdir::WalkDir::new(dir) { | ||
124 | let entry = entry.unwrap(); | ||
125 | if !entry.file_type().is_file() { | ||
126 | continue; | ||
127 | } | ||
128 | if entry.path().extension().unwrap_or_default() != "rs" { | ||
129 | continue; | ||
130 | } | ||
131 | let text = fs::read_to_string(entry.path())?; | ||
132 | |||
133 | for test in collect_tests(&text) { | ||
134 | if let Some(old_test) = res.replace(test) { | ||
135 | bail!("Duplicate test: {}", old_test.name) | ||
136 | } | ||
137 | } | ||
138 | } | ||
139 | Ok(res) | ||
140 | } | ||
141 | |||
142 | fn collect_tests(s: &str) -> Vec<Test> { | ||
143 | let mut res = vec![]; | ||
144 | let prefix = "// "; | ||
145 | let comment_blocks = s.lines() | ||
146 | .map(str::trim_left) | ||
147 | .group_by(|line| line.starts_with(prefix)); | ||
148 | |||
149 | 'outer: for (is_comment, block) in comment_blocks.into_iter() { | ||
150 | if !is_comment { | ||
151 | continue; | ||
152 | } | ||
153 | let mut block = block.map(|line| &line[prefix.len()..]); | ||
154 | |||
155 | let name = loop { | ||
156 | match block.next() { | ||
157 | Some(line) if line.starts_with("test ") => break line["test ".len()..].to_string(), | ||
158 | Some(_) => (), | ||
159 | None => continue 'outer, | ||
160 | } | ||
161 | }; | ||
162 | let text: String = itertools::join(block.chain(::std::iter::once("")), "\n"); | ||
163 | assert!(!text.trim().is_empty() && text.ends_with("\n")); | ||
164 | res.push(Test { name, text }) | ||
165 | } | ||
166 | res | ||
167 | } | ||
168 | |||
169 | fn existing_tests(dir: &Path) -> Result<HashSet<Test>> { | ||
170 | let mut res = HashSet::new(); | ||
171 | for file in fs::read_dir(dir)? { | ||
172 | let file = file?; | ||
173 | let path = file.path(); | ||
174 | if path.extension().unwrap_or_default() != "rs" { | ||
175 | continue; | ||
176 | } | ||
177 | let name = path.file_name().unwrap().to_str().unwrap(); | ||
178 | let name = name["0000_".len()..name.len() - 3].to_string(); | ||
179 | let text = fs::read_to_string(&path)?; | ||
180 | res.insert(Test { name, text }); | ||
181 | } | ||
182 | Ok(res) | ||
183 | } | ||
184 | |||