diff options
Diffstat (limited to 'xtask')
-rw-r--r-- | xtask/Cargo.toml | 15 | ||||
-rw-r--r-- | xtask/src/bin/pre-commit.rs | 31 | ||||
-rw-r--r-- | xtask/src/boilerplate_gen.rs | 348 | ||||
-rw-r--r-- | xtask/src/help.rs | 47 | ||||
-rw-r--r-- | xtask/src/lib.rs | 332 | ||||
-rw-r--r-- | xtask/src/main.rs | 256 | ||||
-rw-r--r-- | xtask/tests/tidy-tests/cli.rs | 45 | ||||
-rw-r--r-- | xtask/tests/tidy-tests/docs.rs | 63 | ||||
-rw-r--r-- | xtask/tests/tidy-tests/main.rs | 2 |
9 files changed, 1139 insertions, 0 deletions
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 000000000..4fc1c744b --- /dev/null +++ b/xtask/Cargo.toml | |||
@@ -0,0 +1,15 @@ | |||
1 | [package] | ||
2 | edition = "2018" | ||
3 | name = "xtask" | ||
4 | version = "0.1.0" | ||
5 | authors = ["rust-analyzer developers"] | ||
6 | publish = false | ||
7 | |||
8 | [dependencies] | ||
9 | walkdir = "2.1.3" | ||
10 | itertools = "0.8.0" | ||
11 | pico-args = "0.3.0" | ||
12 | quote = "1.0.2" | ||
13 | proc-macro2 = "1.0.1" | ||
14 | ron = "0.5.1" | ||
15 | serde = { version = "1.0.0", features = ["derive"] } | ||
diff --git a/xtask/src/bin/pre-commit.rs b/xtask/src/bin/pre-commit.rs new file mode 100644 index 000000000..4ee864756 --- /dev/null +++ b/xtask/src/bin/pre-commit.rs | |||
@@ -0,0 +1,31 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::process::Command; | ||
4 | |||
5 | use xtask::{project_root, run, run_rustfmt, Overwrite, Result}; | ||
6 | |||
7 | fn main() -> Result<()> { | ||
8 | run_rustfmt(Overwrite)?; | ||
9 | update_staged() | ||
10 | } | ||
11 | |||
12 | fn update_staged() -> Result<()> { | ||
13 | let root = project_root(); | ||
14 | let output = Command::new("git") | ||
15 | .arg("diff") | ||
16 | .arg("--diff-filter=MAR") | ||
17 | .arg("--name-only") | ||
18 | .arg("--cached") | ||
19 | .current_dir(&root) | ||
20 | .output()?; | ||
21 | if !output.status.success() { | ||
22 | Err(format!( | ||
23 | "`git diff --diff-filter=MAR --name-only --cached` exited with {}", | ||
24 | output.status | ||
25 | ))?; | ||
26 | } | ||
27 | for line in String::from_utf8(output.stdout)?.lines() { | ||
28 | run(&format!("git update-index --add {}", root.join(line).to_string_lossy()), ".")?; | ||
29 | } | ||
30 | Ok(()) | ||
31 | } | ||
diff --git a/xtask/src/boilerplate_gen.rs b/xtask/src/boilerplate_gen.rs new file mode 100644 index 000000000..39f1cae66 --- /dev/null +++ b/xtask/src/boilerplate_gen.rs | |||
@@ -0,0 +1,348 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::{ | ||
4 | collections::BTreeMap, | ||
5 | fs, | ||
6 | io::Write, | ||
7 | process::{Command, Stdio}, | ||
8 | }; | ||
9 | |||
10 | use proc_macro2::{Punct, Spacing}; | ||
11 | use quote::{format_ident, quote}; | ||
12 | use ron; | ||
13 | use serde::Deserialize; | ||
14 | |||
15 | use crate::{project_root, update, Mode, Result, AST, GRAMMAR, SYNTAX_KINDS}; | ||
16 | |||
17 | pub fn generate_boilerplate(mode: Mode) -> Result<()> { | ||
18 | let grammar = project_root().join(GRAMMAR); | ||
19 | let grammar: Grammar = { | ||
20 | let text = fs::read_to_string(grammar)?; | ||
21 | ron::de::from_str(&text)? | ||
22 | }; | ||
23 | |||
24 | let syntax_kinds_file = project_root().join(SYNTAX_KINDS); | ||
25 | let syntax_kinds = generate_syntax_kinds(&grammar)?; | ||
26 | update(syntax_kinds_file.as_path(), &syntax_kinds, mode)?; | ||
27 | |||
28 | let ast_file = project_root().join(AST); | ||
29 | let ast = generate_ast(&grammar)?; | ||
30 | update(ast_file.as_path(), &ast, mode)?; | ||
31 | |||
32 | Ok(()) | ||
33 | } | ||
34 | |||
35 | fn generate_ast(grammar: &Grammar) -> Result<String> { | ||
36 | let nodes = grammar.ast.iter().map(|(name, ast_node)| { | ||
37 | let variants = | ||
38 | ast_node.variants.iter().map(|var| format_ident!("{}", var)).collect::<Vec<_>>(); | ||
39 | let name = format_ident!("{}", name); | ||
40 | |||
41 | let adt = if variants.is_empty() { | ||
42 | let kind = format_ident!("{}", to_upper_snake_case(&name.to_string())); | ||
43 | quote! { | ||
44 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
45 | pub struct #name { | ||
46 | pub(crate) syntax: SyntaxNode, | ||
47 | } | ||
48 | |||
49 | impl AstNode for #name { | ||
50 | fn can_cast(kind: SyntaxKind) -> bool { | ||
51 | match kind { | ||
52 | #kind => true, | ||
53 | _ => false, | ||
54 | } | ||
55 | } | ||
56 | fn cast(syntax: SyntaxNode) -> Option<Self> { | ||
57 | if Self::can_cast(syntax.kind()) { Some(Self { syntax }) } else { None } | ||
58 | } | ||
59 | fn syntax(&self) -> &SyntaxNode { &self.syntax } | ||
60 | } | ||
61 | } | ||
62 | } else { | ||
63 | let kinds = variants | ||
64 | .iter() | ||
65 | .map(|name| format_ident!("{}", to_upper_snake_case(&name.to_string()))) | ||
66 | .collect::<Vec<_>>(); | ||
67 | |||
68 | quote! { | ||
69 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
70 | pub enum #name { | ||
71 | #(#variants(#variants),)* | ||
72 | } | ||
73 | |||
74 | #( | ||
75 | impl From<#variants> for #name { | ||
76 | fn from(node: #variants) -> #name { | ||
77 | #name::#variants(node) | ||
78 | } | ||
79 | } | ||
80 | )* | ||
81 | |||
82 | impl AstNode for #name { | ||
83 | fn can_cast(kind: SyntaxKind) -> bool { | ||
84 | match kind { | ||
85 | #(#kinds)|* => true, | ||
86 | _ => false, | ||
87 | } | ||
88 | } | ||
89 | fn cast(syntax: SyntaxNode) -> Option<Self> { | ||
90 | let res = match syntax.kind() { | ||
91 | #( | ||
92 | #kinds => #name::#variants(#variants { syntax }), | ||
93 | )* | ||
94 | _ => return None, | ||
95 | }; | ||
96 | Some(res) | ||
97 | } | ||
98 | fn syntax(&self) -> &SyntaxNode { | ||
99 | match self { | ||
100 | #( | ||
101 | #name::#variants(it) => &it.syntax, | ||
102 | )* | ||
103 | } | ||
104 | } | ||
105 | } | ||
106 | } | ||
107 | }; | ||
108 | |||
109 | let traits = ast_node.traits.iter().map(|trait_name| { | ||
110 | let trait_name = format_ident!("{}", trait_name); | ||
111 | quote!(impl ast::#trait_name for #name {}) | ||
112 | }); | ||
113 | |||
114 | let collections = ast_node.collections.iter().map(|(name, kind)| { | ||
115 | let method_name = format_ident!("{}", name); | ||
116 | let kind = format_ident!("{}", kind); | ||
117 | quote! { | ||
118 | pub fn #method_name(&self) -> AstChildren<#kind> { | ||
119 | AstChildren::new(&self.syntax) | ||
120 | } | ||
121 | } | ||
122 | }); | ||
123 | |||
124 | let options = ast_node.options.iter().map(|attr| { | ||
125 | let method_name = match attr { | ||
126 | Attr::Type(t) => format_ident!("{}", to_lower_snake_case(&t)), | ||
127 | Attr::NameType(n, _) => format_ident!("{}", n), | ||
128 | }; | ||
129 | let ty = match attr { | ||
130 | Attr::Type(t) | Attr::NameType(_, t) => format_ident!("{}", t), | ||
131 | }; | ||
132 | quote! { | ||
133 | pub fn #method_name(&self) -> Option<#ty> { | ||
134 | AstChildren::new(&self.syntax).next() | ||
135 | } | ||
136 | } | ||
137 | }); | ||
138 | |||
139 | quote! { | ||
140 | #adt | ||
141 | |||
142 | #(#traits)* | ||
143 | |||
144 | impl #name { | ||
145 | #(#collections)* | ||
146 | #(#options)* | ||
147 | } | ||
148 | } | ||
149 | }); | ||
150 | |||
151 | let ast = quote! { | ||
152 | use crate::{ | ||
153 | SyntaxNode, SyntaxKind::{self, *}, | ||
154 | ast::{self, AstNode, AstChildren}, | ||
155 | }; | ||
156 | |||
157 | #(#nodes)* | ||
158 | }; | ||
159 | |||
160 | let pretty = reformat(ast)?; | ||
161 | Ok(pretty) | ||
162 | } | ||
163 | |||
164 | fn generate_syntax_kinds(grammar: &Grammar) -> Result<String> { | ||
165 | let (single_byte_tokens_values, single_byte_tokens): (Vec<_>, Vec<_>) = grammar | ||
166 | .punct | ||
167 | .iter() | ||
168 | .filter(|(token, _name)| token.len() == 1) | ||
169 | .map(|(token, name)| (token.chars().next().unwrap(), format_ident!("{}", name))) | ||
170 | .unzip(); | ||
171 | |||
172 | let punctuation_values = grammar.punct.iter().map(|(token, _name)| { | ||
173 | if "{}[]()".contains(token) { | ||
174 | let c = token.chars().next().unwrap(); | ||
175 | quote! { #c } | ||
176 | } else { | ||
177 | let cs = token.chars().map(|c| Punct::new(c, Spacing::Joint)); | ||
178 | quote! { #(#cs)* } | ||
179 | } | ||
180 | }); | ||
181 | let punctuation = | ||
182 | grammar.punct.iter().map(|(_token, name)| format_ident!("{}", name)).collect::<Vec<_>>(); | ||
183 | |||
184 | let full_keywords_values = &grammar.keywords; | ||
185 | let full_keywords = | ||
186 | full_keywords_values.iter().map(|kw| format_ident!("{}_KW", to_upper_snake_case(&kw))); | ||
187 | |||
188 | let all_keywords_values = | ||
189 | grammar.keywords.iter().chain(grammar.contextual_keywords.iter()).collect::<Vec<_>>(); | ||
190 | let all_keywords_idents = all_keywords_values.iter().map(|kw| format_ident!("{}", kw)); | ||
191 | let all_keywords = all_keywords_values | ||
192 | .iter() | ||
193 | .map(|name| format_ident!("{}_KW", to_upper_snake_case(&name))) | ||
194 | .collect::<Vec<_>>(); | ||
195 | |||
196 | let literals = | ||
197 | grammar.literals.iter().map(|name| format_ident!("{}", name)).collect::<Vec<_>>(); | ||
198 | |||
199 | let tokens = grammar.tokens.iter().map(|name| format_ident!("{}", name)).collect::<Vec<_>>(); | ||
200 | |||
201 | let nodes = grammar.nodes.iter().map(|name| format_ident!("{}", name)).collect::<Vec<_>>(); | ||
202 | |||
203 | let ast = quote! { | ||
204 | #![allow(bad_style, missing_docs, unreachable_pub)] | ||
205 | /// The kind of syntax node, e.g. `IDENT`, `USE_KW`, or `STRUCT_DEF`. | ||
206 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] | ||
207 | #[repr(u16)] | ||
208 | pub enum SyntaxKind { | ||
209 | // Technical SyntaxKinds: they appear temporally during parsing, | ||
210 | // but never end up in the final tree | ||
211 | #[doc(hidden)] | ||
212 | TOMBSTONE, | ||
213 | #[doc(hidden)] | ||
214 | EOF, | ||
215 | #(#punctuation,)* | ||
216 | #(#all_keywords,)* | ||
217 | #(#literals,)* | ||
218 | #(#tokens,)* | ||
219 | #(#nodes,)* | ||
220 | |||
221 | // Technical kind so that we can cast from u16 safely | ||
222 | #[doc(hidden)] | ||
223 | __LAST, | ||
224 | } | ||
225 | use self::SyntaxKind::*; | ||
226 | |||
227 | impl SyntaxKind { | ||
228 | pub fn is_keyword(self) -> bool { | ||
229 | match self { | ||
230 | #(#all_keywords)|* => true, | ||
231 | _ => false, | ||
232 | } | ||
233 | } | ||
234 | |||
235 | pub fn is_punct(self) -> bool { | ||
236 | match self { | ||
237 | #(#punctuation)|* => true, | ||
238 | _ => false, | ||
239 | } | ||
240 | } | ||
241 | |||
242 | pub fn is_literal(self) -> bool { | ||
243 | match self { | ||
244 | #(#literals)|* => true, | ||
245 | _ => false, | ||
246 | } | ||
247 | } | ||
248 | |||
249 | pub fn from_keyword(ident: &str) -> Option<SyntaxKind> { | ||
250 | let kw = match ident { | ||
251 | #(#full_keywords_values => #full_keywords,)* | ||
252 | _ => return None, | ||
253 | }; | ||
254 | Some(kw) | ||
255 | } | ||
256 | |||
257 | pub fn from_char(c: char) -> Option<SyntaxKind> { | ||
258 | let tok = match c { | ||
259 | #(#single_byte_tokens_values => #single_byte_tokens,)* | ||
260 | _ => return None, | ||
261 | }; | ||
262 | Some(tok) | ||
263 | } | ||
264 | } | ||
265 | |||
266 | #[macro_export] | ||
267 | macro_rules! T { | ||
268 | #((#punctuation_values) => { $crate::SyntaxKind::#punctuation };)* | ||
269 | #((#all_keywords_idents) => { $crate::SyntaxKind::#all_keywords };)* | ||
270 | } | ||
271 | }; | ||
272 | |||
273 | reformat(ast) | ||
274 | } | ||
275 | |||
276 | fn reformat(text: impl std::fmt::Display) -> Result<String> { | ||
277 | let mut rustfmt = Command::new("rustfmt") | ||
278 | .arg("--config-path") | ||
279 | .arg(project_root().join("rustfmt.toml")) | ||
280 | .stdin(Stdio::piped()) | ||
281 | .stdout(Stdio::piped()) | ||
282 | .spawn()?; | ||
283 | write!(rustfmt.stdin.take().unwrap(), "{}", text)?; | ||
284 | let output = rustfmt.wait_with_output()?; | ||
285 | let stdout = String::from_utf8(output.stdout)?; | ||
286 | let preamble = "Generated file, do not edit by hand, see `crate/ra_tools/src/codegen`"; | ||
287 | Ok(format!("//! {}\n\n{}", preamble, stdout)) | ||
288 | } | ||
289 | |||
290 | #[derive(Deserialize, Debug)] | ||
291 | struct Grammar { | ||
292 | punct: Vec<(String, String)>, | ||
293 | keywords: Vec<String>, | ||
294 | contextual_keywords: Vec<String>, | ||
295 | literals: Vec<String>, | ||
296 | tokens: Vec<String>, | ||
297 | nodes: Vec<String>, | ||
298 | ast: BTreeMap<String, AstNode>, | ||
299 | } | ||
300 | |||
301 | #[derive(Deserialize, Debug)] | ||
302 | struct AstNode { | ||
303 | #[serde(default)] | ||
304 | #[serde(rename = "enum")] | ||
305 | variants: Vec<String>, | ||
306 | |||
307 | #[serde(default)] | ||
308 | traits: Vec<String>, | ||
309 | #[serde(default)] | ||
310 | collections: Vec<(String, String)>, | ||
311 | #[serde(default)] | ||
312 | options: Vec<Attr>, | ||
313 | } | ||
314 | |||
315 | #[derive(Deserialize, Debug)] | ||
316 | #[serde(untagged)] | ||
317 | enum Attr { | ||
318 | Type(String), | ||
319 | NameType(String, String), | ||
320 | } | ||
321 | |||
322 | fn to_upper_snake_case(s: &str) -> String { | ||
323 | let mut buf = String::with_capacity(s.len()); | ||
324 | let mut prev_is_upper = None; | ||
325 | for c in s.chars() { | ||
326 | if c.is_ascii_uppercase() && prev_is_upper == Some(false) { | ||
327 | buf.push('_') | ||
328 | } | ||
329 | prev_is_upper = Some(c.is_ascii_uppercase()); | ||
330 | |||
331 | buf.push(c.to_ascii_uppercase()); | ||
332 | } | ||
333 | buf | ||
334 | } | ||
335 | |||
336 | fn to_lower_snake_case(s: &str) -> String { | ||
337 | let mut buf = String::with_capacity(s.len()); | ||
338 | let mut prev_is_upper = None; | ||
339 | for c in s.chars() { | ||
340 | if c.is_ascii_uppercase() && prev_is_upper == Some(false) { | ||
341 | buf.push('_') | ||
342 | } | ||
343 | prev_is_upper = Some(c.is_ascii_uppercase()); | ||
344 | |||
345 | buf.push(c.to_ascii_lowercase()); | ||
346 | } | ||
347 | buf | ||
348 | } | ||
diff --git a/xtask/src/help.rs b/xtask/src/help.rs new file mode 100644 index 000000000..4c6bf6b53 --- /dev/null +++ b/xtask/src/help.rs | |||
@@ -0,0 +1,47 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | pub const GLOBAL_HELP: &str = "tasks | ||
4 | |||
5 | USAGE: | ||
6 | ra_tools <SUBCOMMAND> | ||
7 | |||
8 | FLAGS: | ||
9 | -h, --help Prints help information | ||
10 | |||
11 | SUBCOMMANDS: | ||
12 | format | ||
13 | format-hook | ||
14 | fuzz-tests | ||
15 | codegen | ||
16 | gen-tests | ||
17 | install | ||
18 | lint"; | ||
19 | |||
20 | pub const INSTALL_HELP: &str = "ra_tools-install | ||
21 | |||
22 | USAGE: | ||
23 | ra_tools.exe install [FLAGS] | ||
24 | |||
25 | FLAGS: | ||
26 | --client-code | ||
27 | -h, --help Prints help information | ||
28 | --jemalloc | ||
29 | --server"; | ||
30 | |||
31 | pub fn print_no_param_subcommand_help(subcommand: &str) { | ||
32 | eprintln!( | ||
33 | "ra_tools-{} | ||
34 | |||
35 | USAGE: | ||
36 | ra_tools {} | ||
37 | |||
38 | FLAGS: | ||
39 | -h, --help Prints help information", | ||
40 | subcommand, subcommand | ||
41 | ); | ||
42 | } | ||
43 | |||
44 | pub const INSTALL_RA_CONFLICT: &str = | ||
45 | "error: The argument `--server` cannot be used with `--client-code` | ||
46 | |||
47 | For more information try --help"; | ||
diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs new file mode 100644 index 000000000..a8685f567 --- /dev/null +++ b/xtask/src/lib.rs | |||
@@ -0,0 +1,332 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | mod boilerplate_gen; | ||
4 | |||
5 | use std::{ | ||
6 | collections::HashMap, | ||
7 | error::Error, | ||
8 | fs, | ||
9 | io::{Error as IoError, ErrorKind}, | ||
10 | path::{Path, PathBuf}, | ||
11 | process::{Command, Output, Stdio}, | ||
12 | }; | ||
13 | |||
14 | use itertools::Itertools; | ||
15 | |||
16 | pub use self::boilerplate_gen::generate_boilerplate; | ||
17 | |||
18 | pub type Result<T> = std::result::Result<T, Box<dyn Error>>; | ||
19 | |||
20 | pub const GRAMMAR: &str = "crates/ra_syntax/src/grammar.ron"; | ||
21 | const GRAMMAR_DIR: &str = "crates/ra_parser/src/grammar"; | ||
22 | const OK_INLINE_TESTS_DIR: &str = "crates/ra_syntax/test_data/parser/inline/ok"; | ||
23 | const ERR_INLINE_TESTS_DIR: &str = "crates/ra_syntax/test_data/parser/inline/err"; | ||
24 | |||
25 | pub const SYNTAX_KINDS: &str = "crates/ra_parser/src/syntax_kind/generated.rs"; | ||
26 | pub const AST: &str = "crates/ra_syntax/src/ast/generated.rs"; | ||
27 | const TOOLCHAIN: &str = "stable"; | ||
28 | |||
29 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] | ||
30 | pub enum Mode { | ||
31 | Overwrite, | ||
32 | Verify, | ||
33 | } | ||
34 | pub use Mode::*; | ||
35 | |||
36 | #[derive(Debug)] | ||
37 | pub struct Test { | ||
38 | pub name: String, | ||
39 | pub text: String, | ||
40 | pub ok: bool, | ||
41 | } | ||
42 | |||
43 | pub fn collect_tests(s: &str) -> Vec<(usize, Test)> { | ||
44 | let mut res = vec![]; | ||
45 | let prefix = "// "; | ||
46 | let comment_blocks = s | ||
47 | .lines() | ||
48 | .map(str::trim_start) | ||
49 | .enumerate() | ||
50 | .group_by(|(_idx, line)| line.starts_with(prefix)); | ||
51 | |||
52 | 'outer: for (is_comment, block) in comment_blocks.into_iter() { | ||
53 | if !is_comment { | ||
54 | continue; | ||
55 | } | ||
56 | let mut block = block.map(|(idx, line)| (idx, &line[prefix.len()..])); | ||
57 | |||
58 | let mut ok = true; | ||
59 | let (start_line, name) = loop { | ||
60 | match block.next() { | ||
61 | Some((idx, line)) if line.starts_with("test ") => { | ||
62 | break (idx, line["test ".len()..].to_string()); | ||
63 | } | ||
64 | Some((idx, line)) if line.starts_with("test_err ") => { | ||
65 | ok = false; | ||
66 | break (idx, line["test_err ".len()..].to_string()); | ||
67 | } | ||
68 | Some(_) => (), | ||
69 | None => continue 'outer, | ||
70 | } | ||
71 | }; | ||
72 | let text: String = | ||
73 | itertools::join(block.map(|(_, line)| line).chain(::std::iter::once("")), "\n"); | ||
74 | assert!(!text.trim().is_empty() && text.ends_with('\n')); | ||
75 | res.push((start_line, Test { name, text, ok })) | ||
76 | } | ||
77 | res | ||
78 | } | ||
79 | |||
80 | pub fn project_root() -> PathBuf { | ||
81 | Path::new(&env!("CARGO_MANIFEST_DIR")).ancestors().nth(1).unwrap().to_path_buf() | ||
82 | } | ||
83 | |||
84 | pub struct Cmd<'a> { | ||
85 | pub unix: &'a str, | ||
86 | pub windows: &'a str, | ||
87 | pub work_dir: &'a str, | ||
88 | } | ||
89 | |||
90 | impl Cmd<'_> { | ||
91 | pub fn run(self) -> Result<()> { | ||
92 | if cfg!(windows) { | ||
93 | run(self.windows, self.work_dir) | ||
94 | } else { | ||
95 | run(self.unix, self.work_dir) | ||
96 | } | ||
97 | } | ||
98 | pub fn run_with_output(self) -> Result<Output> { | ||
99 | if cfg!(windows) { | ||
100 | run_with_output(self.windows, self.work_dir) | ||
101 | } else { | ||
102 | run_with_output(self.unix, self.work_dir) | ||
103 | } | ||
104 | } | ||
105 | } | ||
106 | |||
107 | pub fn run(cmdline: &str, dir: &str) -> Result<()> { | ||
108 | do_run(cmdline, dir, |c| { | ||
109 | c.stdout(Stdio::inherit()); | ||
110 | }) | ||
111 | .map(|_| ()) | ||
112 | } | ||
113 | |||
114 | pub fn run_with_output(cmdline: &str, dir: &str) -> Result<Output> { | ||
115 | do_run(cmdline, dir, |_| {}) | ||
116 | } | ||
117 | |||
118 | pub fn run_rustfmt(mode: Mode) -> Result<()> { | ||
119 | match Command::new("rustup") | ||
120 | .args(&["run", TOOLCHAIN, "--", "cargo", "fmt", "--version"]) | ||
121 | .stderr(Stdio::null()) | ||
122 | .stdout(Stdio::null()) | ||
123 | .status() | ||
124 | { | ||
125 | Ok(status) if status.success() => (), | ||
126 | _ => install_rustfmt()?, | ||
127 | }; | ||
128 | |||
129 | if mode == Verify { | ||
130 | run(&format!("rustup run {} -- cargo fmt -- --check", TOOLCHAIN), ".")?; | ||
131 | } else { | ||
132 | run(&format!("rustup run {} -- cargo fmt", TOOLCHAIN), ".")?; | ||
133 | } | ||
134 | Ok(()) | ||
135 | } | ||
136 | |||
137 | pub fn install_rustfmt() -> Result<()> { | ||
138 | run(&format!("rustup install {}", TOOLCHAIN), ".")?; | ||
139 | run(&format!("rustup component add rustfmt --toolchain {}", TOOLCHAIN), ".") | ||
140 | } | ||
141 | |||
142 | pub fn install_format_hook() -> Result<()> { | ||
143 | let result_path = Path::new(if cfg!(windows) { | ||
144 | "./.git/hooks/pre-commit.exe" | ||
145 | } else { | ||
146 | "./.git/hooks/pre-commit" | ||
147 | }); | ||
148 | if !result_path.exists() { | ||
149 | run("cargo build --package xtask --bin pre-commit", ".")?; | ||
150 | if cfg!(windows) { | ||
151 | fs::copy("./target/debug/pre-commit.exe", result_path)?; | ||
152 | } else { | ||
153 | fs::copy("./target/debug/pre-commit", result_path)?; | ||
154 | } | ||
155 | } else { | ||
156 | Err(IoError::new(ErrorKind::AlreadyExists, "Git hook already created"))?; | ||
157 | } | ||
158 | Ok(()) | ||
159 | } | ||
160 | |||
161 | pub fn run_clippy() -> Result<()> { | ||
162 | match Command::new("rustup") | ||
163 | .args(&["run", TOOLCHAIN, "--", "cargo", "clippy", "--version"]) | ||
164 | .stderr(Stdio::null()) | ||
165 | .stdout(Stdio::null()) | ||
166 | .status() | ||
167 | { | ||
168 | Ok(status) if status.success() => (), | ||
169 | _ => install_clippy()?, | ||
170 | }; | ||
171 | |||
172 | let allowed_lints = [ | ||
173 | "clippy::collapsible_if", | ||
174 | "clippy::map_clone", // FIXME: remove when Iterator::copied stabilizes (1.36.0) | ||
175 | "clippy::needless_pass_by_value", | ||
176 | "clippy::nonminimal_bool", | ||
177 | "clippy::redundant_pattern_matching", | ||
178 | ]; | ||
179 | run( | ||
180 | &format!( | ||
181 | "rustup run {} -- cargo clippy --all-features --all-targets -- -A {}", | ||
182 | TOOLCHAIN, | ||
183 | allowed_lints.join(" -A ") | ||
184 | ), | ||
185 | ".", | ||
186 | )?; | ||
187 | Ok(()) | ||
188 | } | ||
189 | |||
190 | pub fn install_clippy() -> Result<()> { | ||
191 | run(&format!("rustup install {}", TOOLCHAIN), ".")?; | ||
192 | run(&format!("rustup component add clippy --toolchain {}", TOOLCHAIN), ".") | ||
193 | } | ||
194 | |||
195 | pub fn run_fuzzer() -> Result<()> { | ||
196 | match Command::new("cargo") | ||
197 | .args(&["fuzz", "--help"]) | ||
198 | .stderr(Stdio::null()) | ||
199 | .stdout(Stdio::null()) | ||
200 | .status() | ||
201 | { | ||
202 | Ok(status) if status.success() => (), | ||
203 | _ => run("cargo install cargo-fuzz", ".")?, | ||
204 | }; | ||
205 | |||
206 | run("rustup run nightly -- cargo fuzz run parser", "./crates/ra_syntax") | ||
207 | } | ||
208 | |||
209 | pub fn gen_tests(mode: Mode) -> Result<()> { | ||
210 | let tests = tests_from_dir(&project_root().join(Path::new(GRAMMAR_DIR)))?; | ||
211 | fn install_tests(tests: &HashMap<String, Test>, into: &str, mode: Mode) -> Result<()> { | ||
212 | let tests_dir = project_root().join(into); | ||
213 | if !tests_dir.is_dir() { | ||
214 | fs::create_dir_all(&tests_dir)?; | ||
215 | } | ||
216 | // ok is never actually read, but it needs to be specified to create a Test in existing_tests | ||
217 | let existing = existing_tests(&tests_dir, true)?; | ||
218 | for t in existing.keys().filter(|&t| !tests.contains_key(t)) { | ||
219 | panic!("Test is deleted: {}", t); | ||
220 | } | ||
221 | |||
222 | let mut new_idx = existing.len() + 1; | ||
223 | for (name, test) in tests { | ||
224 | let path = match existing.get(name) { | ||
225 | Some((path, _test)) => path.clone(), | ||
226 | None => { | ||
227 | let file_name = format!("{:04}_{}.rs", new_idx, name); | ||
228 | new_idx += 1; | ||
229 | tests_dir.join(file_name) | ||
230 | } | ||
231 | }; | ||
232 | update(&path, &test.text, mode)?; | ||
233 | } | ||
234 | Ok(()) | ||
235 | } | ||
236 | install_tests(&tests.ok, OK_INLINE_TESTS_DIR, mode)?; | ||
237 | install_tests(&tests.err, ERR_INLINE_TESTS_DIR, mode) | ||
238 | } | ||
239 | |||
240 | fn do_run<F>(cmdline: &str, dir: &str, mut f: F) -> Result<Output> | ||
241 | where | ||
242 | F: FnMut(&mut Command), | ||
243 | { | ||
244 | eprintln!("\nwill run: {}", cmdline); | ||
245 | let proj_dir = project_root().join(dir); | ||
246 | let mut args = cmdline.split_whitespace(); | ||
247 | let exec = args.next().unwrap(); | ||
248 | let mut cmd = Command::new(exec); | ||
249 | f(cmd.args(args).current_dir(proj_dir).stderr(Stdio::inherit())); | ||
250 | let output = cmd.output()?; | ||
251 | if !output.status.success() { | ||
252 | Err(format!("`{}` exited with {}", cmdline, output.status))?; | ||
253 | } | ||
254 | Ok(output) | ||
255 | } | ||
256 | |||
257 | #[derive(Default, Debug)] | ||
258 | struct Tests { | ||
259 | pub ok: HashMap<String, Test>, | ||
260 | pub err: HashMap<String, Test>, | ||
261 | } | ||
262 | |||
263 | fn tests_from_dir(dir: &Path) -> Result<Tests> { | ||
264 | let mut res = Tests::default(); | ||
265 | for entry in ::walkdir::WalkDir::new(dir) { | ||
266 | let entry = entry.unwrap(); | ||
267 | if !entry.file_type().is_file() { | ||
268 | continue; | ||
269 | } | ||
270 | if entry.path().extension().unwrap_or_default() != "rs" { | ||
271 | continue; | ||
272 | } | ||
273 | process_file(&mut res, entry.path())?; | ||
274 | } | ||
275 | let grammar_rs = dir.parent().unwrap().join("grammar.rs"); | ||
276 | process_file(&mut res, &grammar_rs)?; | ||
277 | return Ok(res); | ||
278 | fn process_file(res: &mut Tests, path: &Path) -> Result<()> { | ||
279 | let text = fs::read_to_string(path)?; | ||
280 | |||
281 | for (_, test) in collect_tests(&text) { | ||
282 | if test.ok { | ||
283 | if let Some(old_test) = res.ok.insert(test.name.clone(), test) { | ||
284 | Err(format!("Duplicate test: {}", old_test.name))? | ||
285 | } | ||
286 | } else { | ||
287 | if let Some(old_test) = res.err.insert(test.name.clone(), test) { | ||
288 | Err(format!("Duplicate test: {}", old_test.name))? | ||
289 | } | ||
290 | } | ||
291 | } | ||
292 | Ok(()) | ||
293 | } | ||
294 | } | ||
295 | |||
296 | fn existing_tests(dir: &Path, ok: bool) -> Result<HashMap<String, (PathBuf, Test)>> { | ||
297 | let mut res = HashMap::new(); | ||
298 | for file in fs::read_dir(dir)? { | ||
299 | let file = file?; | ||
300 | let path = file.path(); | ||
301 | if path.extension().unwrap_or_default() != "rs" { | ||
302 | continue; | ||
303 | } | ||
304 | let name = { | ||
305 | let file_name = path.file_name().unwrap().to_str().unwrap(); | ||
306 | file_name[5..file_name.len() - 3].to_string() | ||
307 | }; | ||
308 | let text = fs::read_to_string(&path)?; | ||
309 | let test = Test { name: name.clone(), text, ok }; | ||
310 | if let Some(old) = res.insert(name, (path, test)) { | ||
311 | println!("Duplicate test: {:?}", old); | ||
312 | } | ||
313 | } | ||
314 | Ok(res) | ||
315 | } | ||
316 | |||
317 | /// A helper to update file on disk if it has changed. | ||
318 | /// With verify = false, | ||
319 | pub fn update(path: &Path, contents: &str, mode: Mode) -> Result<()> { | ||
320 | match fs::read_to_string(path) { | ||
321 | Ok(ref old_contents) if old_contents == contents => { | ||
322 | return Ok(()); | ||
323 | } | ||
324 | _ => (), | ||
325 | } | ||
326 | if mode == Verify { | ||
327 | Err(format!("`{}` is not up-to-date", path.display()))?; | ||
328 | } | ||
329 | eprintln!("updating {}", path.display()); | ||
330 | fs::write(path, contents)?; | ||
331 | Ok(()) | ||
332 | } | ||
diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 000000000..c08915aac --- /dev/null +++ b/xtask/src/main.rs | |||
@@ -0,0 +1,256 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | mod help; | ||
4 | |||
5 | use core::fmt::Write; | ||
6 | use core::str; | ||
7 | use pico_args::Arguments; | ||
8 | use std::{env, path::PathBuf}; | ||
9 | use xtask::{ | ||
10 | gen_tests, generate_boilerplate, install_format_hook, run, run_clippy, run_fuzzer, run_rustfmt, | ||
11 | run_with_output, Cmd, Overwrite, Result, | ||
12 | }; | ||
13 | |||
14 | // Latest stable, feel free to send a PR if this lags behind. | ||
15 | const REQUIRED_RUST_VERSION: u32 = 38; | ||
16 | |||
17 | struct InstallOpt { | ||
18 | client: Option<ClientOpt>, | ||
19 | server: Option<ServerOpt>, | ||
20 | } | ||
21 | |||
22 | enum ClientOpt { | ||
23 | VsCode, | ||
24 | } | ||
25 | |||
26 | struct ServerOpt { | ||
27 | jemalloc: bool, | ||
28 | } | ||
29 | |||
30 | fn main() -> Result<()> { | ||
31 | let subcommand = match std::env::args_os().nth(1) { | ||
32 | None => { | ||
33 | eprintln!("{}", help::GLOBAL_HELP); | ||
34 | return Ok(()); | ||
35 | } | ||
36 | Some(s) => s, | ||
37 | }; | ||
38 | let mut matches = Arguments::from_vec(std::env::args_os().skip(2).collect()); | ||
39 | let subcommand = &*subcommand.to_string_lossy(); | ||
40 | match subcommand { | ||
41 | "install" => { | ||
42 | if matches.contains(["-h", "--help"]) { | ||
43 | eprintln!("{}", help::INSTALL_HELP); | ||
44 | return Ok(()); | ||
45 | } | ||
46 | let server = matches.contains("--server"); | ||
47 | let client_code = matches.contains("--client-code"); | ||
48 | if server && client_code { | ||
49 | eprintln!("{}", help::INSTALL_RA_CONFLICT); | ||
50 | return Ok(()); | ||
51 | } | ||
52 | let jemalloc = matches.contains("--jemalloc"); | ||
53 | matches.finish().or_else(handle_extra_flags)?; | ||
54 | let opts = InstallOpt { | ||
55 | client: if server { None } else { Some(ClientOpt::VsCode) }, | ||
56 | server: if client_code { None } else { Some(ServerOpt { jemalloc: jemalloc }) }, | ||
57 | }; | ||
58 | install(opts)? | ||
59 | } | ||
60 | "gen-tests" => { | ||
61 | if matches.contains(["-h", "--help"]) { | ||
62 | help::print_no_param_subcommand_help(&subcommand); | ||
63 | return Ok(()); | ||
64 | } | ||
65 | gen_tests(Overwrite)? | ||
66 | } | ||
67 | "codegen" => { | ||
68 | if matches.contains(["-h", "--help"]) { | ||
69 | help::print_no_param_subcommand_help(&subcommand); | ||
70 | return Ok(()); | ||
71 | } | ||
72 | generate_boilerplate(Overwrite)? | ||
73 | } | ||
74 | "format" => { | ||
75 | if matches.contains(["-h", "--help"]) { | ||
76 | help::print_no_param_subcommand_help(&subcommand); | ||
77 | return Ok(()); | ||
78 | } | ||
79 | run_rustfmt(Overwrite)? | ||
80 | } | ||
81 | "format-hook" => { | ||
82 | if matches.contains(["-h", "--help"]) { | ||
83 | help::print_no_param_subcommand_help(&subcommand); | ||
84 | return Ok(()); | ||
85 | } | ||
86 | install_format_hook()? | ||
87 | } | ||
88 | "lint" => { | ||
89 | if matches.contains(["-h", "--help"]) { | ||
90 | help::print_no_param_subcommand_help(&subcommand); | ||
91 | return Ok(()); | ||
92 | } | ||
93 | run_clippy()? | ||
94 | } | ||
95 | "fuzz-tests" => { | ||
96 | if matches.contains(["-h", "--help"]) { | ||
97 | help::print_no_param_subcommand_help(&subcommand); | ||
98 | return Ok(()); | ||
99 | } | ||
100 | run_fuzzer()? | ||
101 | } | ||
102 | _ => eprintln!("{}", help::GLOBAL_HELP), | ||
103 | } | ||
104 | Ok(()) | ||
105 | } | ||
106 | |||
107 | fn handle_extra_flags(e: pico_args::Error) -> Result<()> { | ||
108 | if let pico_args::Error::UnusedArgsLeft(flags) = e { | ||
109 | let mut invalid_flags = String::new(); | ||
110 | for flag in flags { | ||
111 | write!(&mut invalid_flags, "{}, ", flag)?; | ||
112 | } | ||
113 | let (invalid_flags, _) = invalid_flags.split_at(invalid_flags.len() - 2); | ||
114 | Err(format!("Invalid flags: {}", invalid_flags).into()) | ||
115 | } else { | ||
116 | Err(e.to_string().into()) | ||
117 | } | ||
118 | } | ||
119 | |||
120 | fn install(opts: InstallOpt) -> Result<()> { | ||
121 | if cfg!(target_os = "macos") { | ||
122 | fix_path_for_mac()? | ||
123 | } | ||
124 | if let Some(server) = opts.server { | ||
125 | install_server(server)?; | ||
126 | } | ||
127 | if let Some(client) = opts.client { | ||
128 | install_client(client)?; | ||
129 | } | ||
130 | Ok(()) | ||
131 | } | ||
132 | |||
133 | fn fix_path_for_mac() -> Result<()> { | ||
134 | let mut vscode_path: Vec<PathBuf> = { | ||
135 | const COMMON_APP_PATH: &str = | ||
136 | r"/Applications/Visual Studio Code.app/Contents/Resources/app/bin"; | ||
137 | const ROOT_DIR: &str = ""; | ||
138 | let home_dir = match env::var("HOME") { | ||
139 | Ok(home) => home, | ||
140 | Err(e) => Err(format!("Failed getting HOME from environment with error: {}.", e))?, | ||
141 | }; | ||
142 | |||
143 | [ROOT_DIR, &home_dir] | ||
144 | .iter() | ||
145 | .map(|dir| String::from(*dir) + COMMON_APP_PATH) | ||
146 | .map(PathBuf::from) | ||
147 | .filter(|path| path.exists()) | ||
148 | .collect() | ||
149 | }; | ||
150 | |||
151 | if !vscode_path.is_empty() { | ||
152 | let vars = match env::var_os("PATH") { | ||
153 | Some(path) => path, | ||
154 | None => Err("Could not get PATH variable from env.")?, | ||
155 | }; | ||
156 | |||
157 | let mut paths = env::split_paths(&vars).collect::<Vec<_>>(); | ||
158 | paths.append(&mut vscode_path); | ||
159 | let new_paths = env::join_paths(paths)?; | ||
160 | env::set_var("PATH", &new_paths); | ||
161 | } | ||
162 | |||
163 | Ok(()) | ||
164 | } | ||
165 | |||
166 | fn install_client(ClientOpt::VsCode: ClientOpt) -> Result<()> { | ||
167 | Cmd { unix: r"npm ci", windows: r"cmd.exe /c npm.cmd ci", work_dir: "./editors/code" }.run()?; | ||
168 | Cmd { | ||
169 | unix: r"npm run package", | ||
170 | windows: r"cmd.exe /c npm.cmd run package", | ||
171 | work_dir: "./editors/code", | ||
172 | } | ||
173 | .run()?; | ||
174 | |||
175 | let code_binary = ["code", "code-insiders", "codium"].iter().find(|bin| { | ||
176 | Cmd { | ||
177 | unix: &format!("{} --version", bin), | ||
178 | windows: &format!("cmd.exe /c {}.cmd --version", bin), | ||
179 | work_dir: "./editors/code", | ||
180 | } | ||
181 | .run() | ||
182 | .is_ok() | ||
183 | }); | ||
184 | |||
185 | let code_binary = match code_binary { | ||
186 | Some(it) => it, | ||
187 | None => Err("Can't execute `code --version`. Perhaps it is not in $PATH?")?, | ||
188 | }; | ||
189 | |||
190 | Cmd { | ||
191 | unix: &format!(r"{} --install-extension ./ra-lsp-0.0.1.vsix --force", code_binary), | ||
192 | windows: &format!( | ||
193 | r"cmd.exe /c {}.cmd --install-extension ./ra-lsp-0.0.1.vsix --force", | ||
194 | code_binary | ||
195 | ), | ||
196 | work_dir: "./editors/code", | ||
197 | } | ||
198 | .run()?; | ||
199 | |||
200 | let output = Cmd { | ||
201 | unix: &format!(r"{} --list-extensions", code_binary), | ||
202 | windows: &format!(r"cmd.exe /c {}.cmd --list-extensions", code_binary), | ||
203 | work_dir: ".", | ||
204 | } | ||
205 | .run_with_output()?; | ||
206 | |||
207 | if !str::from_utf8(&output.stdout)?.contains("ra-lsp") { | ||
208 | Err("Could not install the Visual Studio Code extension. \ | ||
209 | Please make sure you have at least NodeJS 10.x installed and try again.")?; | ||
210 | } | ||
211 | |||
212 | Ok(()) | ||
213 | } | ||
214 | |||
215 | fn install_server(opts: ServerOpt) -> Result<()> { | ||
216 | let mut old_rust = false; | ||
217 | if let Ok(output) = run_with_output("cargo --version", ".") { | ||
218 | if let Ok(stdout) = String::from_utf8(output.stdout) { | ||
219 | if !check_version(&stdout, REQUIRED_RUST_VERSION) { | ||
220 | old_rust = true; | ||
221 | } | ||
222 | } | ||
223 | } | ||
224 | |||
225 | if old_rust { | ||
226 | eprintln!( | ||
227 | "\nWARNING: at least rust 1.{}.0 is required to compile rust-analyzer\n", | ||
228 | REQUIRED_RUST_VERSION | ||
229 | ) | ||
230 | } | ||
231 | |||
232 | let res = if opts.jemalloc { | ||
233 | run("cargo install --path crates/ra_lsp_server --locked --force --features jemalloc", ".") | ||
234 | } else { | ||
235 | run("cargo install --path crates/ra_lsp_server --locked --force", ".") | ||
236 | }; | ||
237 | |||
238 | if res.is_err() && old_rust { | ||
239 | eprintln!( | ||
240 | "\nWARNING: at least rust 1.{}.0 is required to compile rust-analyzer\n", | ||
241 | REQUIRED_RUST_VERSION | ||
242 | ) | ||
243 | } | ||
244 | |||
245 | res | ||
246 | } | ||
247 | |||
248 | fn check_version(version_output: &str, min_minor_version: u32) -> bool { | ||
249 | // Parse second the number out of | ||
250 | // cargo 1.39.0-beta (1c6ec66d5 2019-09-30) | ||
251 | let minor: Option<u32> = version_output.split('.').nth(1).and_then(|it| it.parse().ok()); | ||
252 | match minor { | ||
253 | None => true, | ||
254 | Some(minor) => minor >= min_minor_version, | ||
255 | } | ||
256 | } | ||
diff --git a/xtask/tests/tidy-tests/cli.rs b/xtask/tests/tidy-tests/cli.rs new file mode 100644 index 000000000..5d8ddea83 --- /dev/null +++ b/xtask/tests/tidy-tests/cli.rs | |||
@@ -0,0 +1,45 @@ | |||
1 | use walkdir::WalkDir; | ||
2 | use xtask::{gen_tests, generate_boilerplate, project_root, run_rustfmt, Verify}; | ||
3 | |||
4 | #[test] | ||
5 | fn generated_grammar_is_fresh() { | ||
6 | if let Err(error) = generate_boilerplate(Verify) { | ||
7 | panic!("{}. Please update it by running `cargo xtask codegen`", error); | ||
8 | } | ||
9 | } | ||
10 | |||
11 | #[test] | ||
12 | fn generated_tests_are_fresh() { | ||
13 | if let Err(error) = gen_tests(Verify) { | ||
14 | panic!("{}. Please update tests by running `cargo xtask gen-tests`", error); | ||
15 | } | ||
16 | } | ||
17 | |||
18 | #[test] | ||
19 | fn check_code_formatting() { | ||
20 | if let Err(error) = run_rustfmt(Verify) { | ||
21 | panic!("{}. Please format the code by running `cargo format`", error); | ||
22 | } | ||
23 | } | ||
24 | |||
25 | #[test] | ||
26 | fn no_todo() { | ||
27 | WalkDir::new(project_root().join("crates")).into_iter().for_each(|e| { | ||
28 | let e = e.unwrap(); | ||
29 | if e.path().extension().map(|it| it != "rs").unwrap_or(true) { | ||
30 | return; | ||
31 | } | ||
32 | if e.path().ends_with("tests/cli.rs") { | ||
33 | return; | ||
34 | } | ||
35 | let text = std::fs::read_to_string(e.path()).unwrap(); | ||
36 | if text.contains("TODO") || text.contains("TOOD") { | ||
37 | panic!( | ||
38 | "\nTODO markers should not be committed to the master branch,\n\ | ||
39 | use FIXME instead\n\ | ||
40 | {}\n", | ||
41 | e.path().display(), | ||
42 | ) | ||
43 | } | ||
44 | }) | ||
45 | } | ||
diff --git a/xtask/tests/tidy-tests/docs.rs b/xtask/tests/tidy-tests/docs.rs new file mode 100644 index 000000000..fe5852bc6 --- /dev/null +++ b/xtask/tests/tidy-tests/docs.rs | |||
@@ -0,0 +1,63 @@ | |||
1 | use std::fs; | ||
2 | use std::io::prelude::*; | ||
3 | use std::io::BufReader; | ||
4 | use std::path::Path; | ||
5 | |||
6 | use walkdir::{DirEntry, WalkDir}; | ||
7 | |||
8 | use xtask::project_root; | ||
9 | |||
10 | fn is_exclude_dir(p: &Path) -> bool { | ||
11 | let exclude_dirs = ["tests", "test_data"]; | ||
12 | let mut cur_path = p; | ||
13 | while let Some(path) = cur_path.parent() { | ||
14 | if exclude_dirs.iter().any(|dir| path.ends_with(dir)) { | ||
15 | return true; | ||
16 | } | ||
17 | cur_path = path; | ||
18 | } | ||
19 | |||
20 | false | ||
21 | } | ||
22 | |||
23 | fn is_exclude_file(d: &DirEntry) -> bool { | ||
24 | let file_names = ["tests.rs"]; | ||
25 | |||
26 | d.file_name().to_str().map(|f_n| file_names.iter().any(|name| *name == f_n)).unwrap_or(false) | ||
27 | } | ||
28 | |||
29 | fn is_hidden(entry: &DirEntry) -> bool { | ||
30 | entry.file_name().to_str().map(|s| s.starts_with(".")).unwrap_or(false) | ||
31 | } | ||
32 | |||
33 | #[test] | ||
34 | fn no_docs_comments() { | ||
35 | let crates = project_root().join("crates"); | ||
36 | let iter = WalkDir::new(crates); | ||
37 | for f in iter.into_iter().filter_entry(|e| !is_hidden(e)) { | ||
38 | let f = f.unwrap(); | ||
39 | if f.file_type().is_dir() { | ||
40 | continue; | ||
41 | } | ||
42 | if f.path().extension().map(|it| it != "rs").unwrap_or(false) { | ||
43 | continue; | ||
44 | } | ||
45 | if is_exclude_dir(f.path()) { | ||
46 | continue; | ||
47 | } | ||
48 | if is_exclude_file(&f) { | ||
49 | continue; | ||
50 | } | ||
51 | let mut reader = BufReader::new(fs::File::open(f.path()).unwrap()); | ||
52 | let mut line = String::new(); | ||
53 | reader.read_line(&mut line).unwrap(); | ||
54 | if !line.starts_with("//!") { | ||
55 | panic!( | ||
56 | "\nMissing docs strings\n\ | ||
57 | module: {}\n\ | ||
58 | Need add doc for module\n", | ||
59 | f.path().display() | ||
60 | ) | ||
61 | } | ||
62 | } | ||
63 | } | ||
diff --git a/xtask/tests/tidy-tests/main.rs b/xtask/tests/tidy-tests/main.rs new file mode 100644 index 000000000..56d1318d6 --- /dev/null +++ b/xtask/tests/tidy-tests/main.rs | |||
@@ -0,0 +1,2 @@ | |||
1 | mod cli; | ||
2 | mod docs; | ||