aboutsummaryrefslogtreecommitdiff
path: root/xtask
diff options
context:
space:
mode:
Diffstat (limited to 'xtask')
-rw-r--r--xtask/Cargo.toml15
-rw-r--r--xtask/src/bin/pre-commit.rs31
-rw-r--r--xtask/src/boilerplate_gen.rs348
-rw-r--r--xtask/src/help.rs47
-rw-r--r--xtask/src/lib.rs332
-rw-r--r--xtask/src/main.rs256
-rw-r--r--xtask/tests/tidy-tests/cli.rs45
-rw-r--r--xtask/tests/tidy-tests/docs.rs63
-rw-r--r--xtask/tests/tidy-tests/main.rs2
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]
2edition = "2018"
3name = "xtask"
4version = "0.1.0"
5authors = ["rust-analyzer developers"]
6publish = false
7
8[dependencies]
9walkdir = "2.1.3"
10itertools = "0.8.0"
11pico-args = "0.3.0"
12quote = "1.0.2"
13proc-macro2 = "1.0.1"
14ron = "0.5.1"
15serde = { 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
3use std::process::Command;
4
5use xtask::{project_root, run, run_rustfmt, Overwrite, Result};
6
7fn main() -> Result<()> {
8 run_rustfmt(Overwrite)?;
9 update_staged()
10}
11
12fn 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
3use std::{
4 collections::BTreeMap,
5 fs,
6 io::Write,
7 process::{Command, Stdio},
8};
9
10use proc_macro2::{Punct, Spacing};
11use quote::{format_ident, quote};
12use ron;
13use serde::Deserialize;
14
15use crate::{project_root, update, Mode, Result, AST, GRAMMAR, SYNTAX_KINDS};
16
17pub 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
35fn 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
164fn 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
276fn 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)]
291struct 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)]
302struct 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)]
317enum Attr {
318 Type(String),
319 NameType(String, String),
320}
321
322fn 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
336fn 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
3pub const GLOBAL_HELP: &str = "tasks
4
5USAGE:
6 ra_tools <SUBCOMMAND>
7
8FLAGS:
9 -h, --help Prints help information
10
11SUBCOMMANDS:
12 format
13 format-hook
14 fuzz-tests
15 codegen
16 gen-tests
17 install
18 lint";
19
20pub const INSTALL_HELP: &str = "ra_tools-install
21
22USAGE:
23 ra_tools.exe install [FLAGS]
24
25FLAGS:
26 --client-code
27 -h, --help Prints help information
28 --jemalloc
29 --server";
30
31pub fn print_no_param_subcommand_help(subcommand: &str) {
32 eprintln!(
33 "ra_tools-{}
34
35USAGE:
36 ra_tools {}
37
38FLAGS:
39 -h, --help Prints help information",
40 subcommand, subcommand
41 );
42}
43
44pub const INSTALL_RA_CONFLICT: &str =
45 "error: The argument `--server` cannot be used with `--client-code`
46
47For 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
3mod boilerplate_gen;
4
5use 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
14use itertools::Itertools;
15
16pub use self::boilerplate_gen::generate_boilerplate;
17
18pub type Result<T> = std::result::Result<T, Box<dyn Error>>;
19
20pub const GRAMMAR: &str = "crates/ra_syntax/src/grammar.ron";
21const GRAMMAR_DIR: &str = "crates/ra_parser/src/grammar";
22const OK_INLINE_TESTS_DIR: &str = "crates/ra_syntax/test_data/parser/inline/ok";
23const ERR_INLINE_TESTS_DIR: &str = "crates/ra_syntax/test_data/parser/inline/err";
24
25pub const SYNTAX_KINDS: &str = "crates/ra_parser/src/syntax_kind/generated.rs";
26pub const AST: &str = "crates/ra_syntax/src/ast/generated.rs";
27const TOOLCHAIN: &str = "stable";
28
29#[derive(Debug, PartialEq, Eq, Clone, Copy)]
30pub enum Mode {
31 Overwrite,
32 Verify,
33}
34pub use Mode::*;
35
36#[derive(Debug)]
37pub struct Test {
38 pub name: String,
39 pub text: String,
40 pub ok: bool,
41}
42
43pub 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
80pub fn project_root() -> PathBuf {
81 Path::new(&env!("CARGO_MANIFEST_DIR")).ancestors().nth(1).unwrap().to_path_buf()
82}
83
84pub struct Cmd<'a> {
85 pub unix: &'a str,
86 pub windows: &'a str,
87 pub work_dir: &'a str,
88}
89
90impl 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
107pub fn run(cmdline: &str, dir: &str) -> Result<()> {
108 do_run(cmdline, dir, |c| {
109 c.stdout(Stdio::inherit());
110 })
111 .map(|_| ())
112}
113
114pub fn run_with_output(cmdline: &str, dir: &str) -> Result<Output> {
115 do_run(cmdline, dir, |_| {})
116}
117
118pub 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
137pub fn install_rustfmt() -> Result<()> {
138 run(&format!("rustup install {}", TOOLCHAIN), ".")?;
139 run(&format!("rustup component add rustfmt --toolchain {}", TOOLCHAIN), ".")
140}
141
142pub 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
161pub 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
190pub fn install_clippy() -> Result<()> {
191 run(&format!("rustup install {}", TOOLCHAIN), ".")?;
192 run(&format!("rustup component add clippy --toolchain {}", TOOLCHAIN), ".")
193}
194
195pub 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
209pub 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
240fn do_run<F>(cmdline: &str, dir: &str, mut f: F) -> Result<Output>
241where
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)]
258struct Tests {
259 pub ok: HashMap<String, Test>,
260 pub err: HashMap<String, Test>,
261}
262
263fn 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
296fn 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,
319pub 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
3mod help;
4
5use core::fmt::Write;
6use core::str;
7use pico_args::Arguments;
8use std::{env, path::PathBuf};
9use 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.
15const REQUIRED_RUST_VERSION: u32 = 38;
16
17struct InstallOpt {
18 client: Option<ClientOpt>,
19 server: Option<ServerOpt>,
20}
21
22enum ClientOpt {
23 VsCode,
24}
25
26struct ServerOpt {
27 jemalloc: bool,
28}
29
30fn 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
107fn 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
120fn 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
133fn 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
166fn 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
215fn 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
248fn 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 @@
1use walkdir::WalkDir;
2use xtask::{gen_tests, generate_boilerplate, project_root, run_rustfmt, Verify};
3
4#[test]
5fn 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]
12fn 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]
19fn 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]
26fn 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 @@
1use std::fs;
2use std::io::prelude::*;
3use std::io::BufReader;
4use std::path::Path;
5
6use walkdir::{DirEntry, WalkDir};
7
8use xtask::project_root;
9
10fn 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
23fn 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
29fn is_hidden(entry: &DirEntry) -> bool {
30 entry.file_name().to_str().map(|s| s.starts_with(".")).unwrap_or(false)
31}
32
33#[test]
34fn 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 @@
1mod cli;
2mod docs;