]> git.lizzy.rs Git - rust.git/blob - xtask/tests/tidy.rs
6abad189adfadb1d4df6013606aaedfa9bf41c25
[rust.git] / xtask / tests / tidy.rs
1 use std::{
2     collections::HashMap,
3     path::{Path, PathBuf},
4 };
5
6 use xshell::{cmd, read_file};
7 use xtask::{
8     codegen::{self, Mode},
9     project_root, run_rustfmt, rust_files,
10 };
11
12 #[test]
13 fn generated_grammar_is_fresh() {
14     if let Err(error) = codegen::generate_syntax(Mode::Verify) {
15         panic!("{}. Please update it by running `cargo xtask codegen`", error);
16     }
17 }
18
19 #[test]
20 fn generated_tests_are_fresh() {
21     if let Err(error) = codegen::generate_parser_tests(Mode::Verify) {
22         panic!("{}. Please update tests by running `cargo xtask codegen`", error);
23     }
24 }
25
26 #[test]
27 fn generated_assists_are_fresh() {
28     if let Err(error) = codegen::generate_assists_tests(Mode::Verify) {
29         panic!("{}. Please update assists by running `cargo xtask codegen`", error);
30     }
31 }
32
33 #[test]
34 fn check_code_formatting() {
35     if let Err(error) = run_rustfmt(Mode::Verify) {
36         panic!("{}. Please format the code by running `cargo format`", error);
37     }
38 }
39
40 #[test]
41 fn smoke_test_docs_generation() {
42     // We don't commit docs to the repo, so we can just overwrite in tests.
43     codegen::generate_assists_docs(Mode::Overwrite).unwrap();
44     codegen::generate_feature_docs(Mode::Overwrite).unwrap();
45     codegen::generate_diagnostic_docs(Mode::Overwrite).unwrap();
46 }
47
48 #[test]
49 fn check_lsp_extensions_docs() {
50     let expected_hash = {
51         let lsp_ext_rs =
52             read_file(project_root().join("crates/rust-analyzer/src/lsp_ext.rs")).unwrap();
53         stable_hash(lsp_ext_rs.as_str())
54     };
55
56     let actual_hash = {
57         let lsp_extensions_md =
58             read_file(project_root().join("docs/dev/lsp-extensions.md")).unwrap();
59         let text = lsp_extensions_md
60             .lines()
61             .find_map(|line| line.strip_prefix("lsp_ext.rs hash:"))
62             .unwrap()
63             .trim();
64         u64::from_str_radix(text, 16).unwrap()
65     };
66
67     if actual_hash != expected_hash {
68         panic!(
69             "
70 lsp_ext.rs was changed without touching lsp-extensions.md.
71
72 Expected hash: {:x}
73 Actual hash:   {:x}
74
75 Please adjust docs/dev/lsp-extensions.md.
76 ",
77             expected_hash, actual_hash
78         )
79     }
80 }
81
82 #[test]
83 fn rust_files_are_tidy() {
84     let mut tidy_docs = TidyDocs::default();
85     for path in rust_files(&project_root().join("crates")) {
86         let text = read_file(&path).unwrap();
87         check_todo(&path, &text);
88         check_dbg(&path, &text);
89         check_trailing_ws(&path, &text);
90         deny_clippy(&path, &text);
91         tidy_docs.visit(&path, &text);
92     }
93     tidy_docs.finish();
94 }
95
96 #[test]
97 fn check_merge_commits() {
98     let stdout = cmd!("git rev-list --merges --invert-grep --author 'bors\\[bot\\]' HEAD~19..")
99         .read()
100         .unwrap();
101     if !stdout.is_empty() {
102         panic!(
103             "
104 Merge commits are not allowed in the history.
105
106 When updating a pull-request, please rebase your feature branch
107 on top of master by running `git rebase master`. If rebase fails,
108 you can re-apply your changes like this:
109
110   # Just look around to see the current state.
111   $ git status
112   $ git log
113
114   # Abort in-progress rebase and merges, if any.
115   $ git rebase --abort
116   $ git merge --abort
117
118   # Make the branch point to the latest commit from master,
119   # while maintaining your local changes uncommited.
120   $ git reset --soft origin/master
121
122   # Commit all changes in a single batch.
123   $ git commit -am'My changes'
124
125   # Verify that everything looks alright.
126   $ git status
127   $ git log
128
129   # Push the changes. We did a rebase, so we need `--force` option.
130   # `--force-with-lease` is a more safe (Rusty) version of `--force`.
131   $ git push --force-with-lease
132
133   # Verify that both local and remote branch point to the same commit.
134   $ git log
135
136 And don't fear to mess something up during a rebase -- you can
137 always restore the previous state using `git ref-log`:
138
139 https://github.blog/2015-06-08-how-to-undo-almost-anything-with-git/#redo-after-undo-local
140 "
141         );
142     }
143 }
144
145 fn deny_clippy(path: &PathBuf, text: &String) {
146     let ignore = &[
147         // The documentation in string literals may contain anything for its own purposes
148         "completion/src/generated_lint_completions.rs",
149     ];
150     if ignore.iter().any(|p| path.ends_with(p)) {
151         return;
152     }
153
154     if text.contains("\u{61}llow(clippy") {
155         panic!(
156             "\n\nallowing lints is forbidden: {}.
157 rust-analyzer intentionally doesn't check clippy on CI.
158 You can allow lint globally via `xtask clippy`.
159 See https://github.com/rust-lang/rust-clippy/issues/5537 for discussion.
160
161 ",
162             path.display()
163         )
164     }
165 }
166
167 #[test]
168 fn check_licenses() {
169     let expected = "
170 0BSD OR MIT OR Apache-2.0
171 Apache-2.0
172 Apache-2.0 OR BSL-1.0
173 Apache-2.0 OR MIT
174 Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
175 Apache-2.0/MIT
176 BSD-3-Clause
177 CC0-1.0
178 ISC
179 MIT
180 MIT / Apache-2.0
181 MIT OR Apache-2.0
182 MIT OR Apache-2.0 OR Zlib
183 MIT OR Zlib OR Apache-2.0
184 MIT/Apache-2.0
185 Unlicense OR MIT
186 Unlicense/MIT
187 Zlib OR Apache-2.0 OR MIT
188 "
189     .lines()
190     .filter(|it| !it.is_empty())
191     .collect::<Vec<_>>();
192
193     let meta = cmd!("cargo metadata --format-version 1").read().unwrap();
194     let mut licenses = meta
195         .split(|c| c == ',' || c == '{' || c == '}')
196         .filter(|it| it.contains(r#""license""#))
197         .map(|it| it.trim())
198         .map(|it| it[r#""license":"#.len()..].trim_matches('"'))
199         .collect::<Vec<_>>();
200     licenses.sort();
201     licenses.dedup();
202     if licenses != expected {
203         let mut diff = String::new();
204
205         diff += &format!("New Licenses:\n");
206         for &l in licenses.iter() {
207             if !expected.contains(&l) {
208                 diff += &format!("  {}\n", l)
209             }
210         }
211
212         diff += &format!("\nMissing Licenses:\n");
213         for &l in expected.iter() {
214             if !licenses.contains(&l) {
215                 diff += &format!("  {}\n", l)
216             }
217         }
218
219         panic!("different set of licenses!\n{}", diff);
220     }
221     assert_eq!(licenses, expected);
222 }
223
224 fn check_todo(path: &Path, text: &str) {
225     let need_todo = &[
226         // This file itself obviously needs to use todo (<- like this!).
227         "tests/tidy.rs",
228         // Some of our assists generate `todo!()`.
229         "handlers/add_turbo_fish.rs",
230         "handlers/generate_function.rs",
231         // To support generating `todo!()` in assists, we have `expr_todo()` in
232         // `ast::make`.
233         "ast/make.rs",
234         // The documentation in string literals may contain anything for its own purposes
235         "completion/src/generated_lint_completions.rs",
236     ];
237     if need_todo.iter().any(|p| path.ends_with(p)) {
238         return;
239     }
240     if text.contains("TODO") || text.contains("TOOD") || text.contains("todo!") {
241         // Generated by an assist
242         if text.contains("${0:todo!()}") {
243             return;
244         }
245
246         panic!(
247             "\nTODO markers or todo! macros should not be committed to the master branch,\n\
248              use FIXME instead\n\
249              {}\n",
250             path.display(),
251         )
252     }
253 }
254
255 fn check_dbg(path: &Path, text: &str) {
256     let need_dbg = &[
257         // This file itself obviously needs to use dbg.
258         "tests/tidy.rs",
259         // Assists to remove `dbg!()`
260         "handlers/remove_dbg.rs",
261         // We have .dbg postfix
262         "completion/src/completions/postfix.rs",
263         // The documentation in string literals may contain anything for its own purposes
264         "completion/src/lib.rs",
265         "completion/src/generated_lint_completions.rs",
266         // test for doc test for remove_dbg
267         "src/tests/generated.rs",
268     ];
269     if need_dbg.iter().any(|p| path.ends_with(p)) {
270         return;
271     }
272     if text.contains("dbg!") {
273         panic!(
274             "\ndbg! macros should not be committed to the master branch,\n\
275              {}\n",
276             path.display(),
277         )
278     }
279 }
280
281 fn check_trailing_ws(path: &Path, text: &str) {
282     if is_exclude_dir(path, &["test_data"]) {
283         return;
284     }
285     for (line_number, line) in text.lines().enumerate() {
286         if line.chars().last().map(char::is_whitespace) == Some(true) {
287             panic!("Trailing whitespace in {} at line {}", path.display(), line_number)
288         }
289     }
290 }
291
292 #[derive(Default)]
293 struct TidyDocs {
294     missing_docs: Vec<String>,
295     contains_fixme: Vec<PathBuf>,
296 }
297
298 impl TidyDocs {
299     fn visit(&mut self, path: &Path, text: &str) {
300         // Test hopefully don't really need comments, and for assists we already
301         // have special comments which are source of doc tests and user docs.
302         if is_exclude_dir(path, &["tests", "test_data"]) {
303             return;
304         }
305
306         if is_exclude_file(path) {
307             return;
308         }
309
310         let first_line = match text.lines().next() {
311             Some(it) => it,
312             None => return,
313         };
314
315         if first_line.starts_with("//!") {
316             if first_line.contains("FIXME") {
317                 self.contains_fixme.push(path.to_path_buf());
318             }
319         } else {
320             if text.contains("// Feature:") || text.contains("// Assist:") {
321                 return;
322             }
323             self.missing_docs.push(path.display().to_string());
324         }
325
326         fn is_exclude_file(d: &Path) -> bool {
327             let file_names = ["tests.rs", "famous_defs_fixture.rs"];
328
329             d.file_name()
330                 .unwrap_or_default()
331                 .to_str()
332                 .map(|f_n| file_names.iter().any(|name| *name == f_n))
333                 .unwrap_or(false)
334         }
335     }
336
337     fn finish(self) {
338         if !self.missing_docs.is_empty() {
339             panic!(
340                 "\nMissing docs strings\n\n\
341                  modules:\n{}\n\n",
342                 self.missing_docs.join("\n")
343             )
344         }
345
346         let poorly_documented = [
347             "hir",
348             "hir_expand",
349             "ide",
350             "mbe",
351             "parser",
352             "profile",
353             "project_model",
354             "syntax",
355             "tt",
356             "hir_ty",
357         ];
358
359         let mut has_fixmes =
360             poorly_documented.iter().map(|it| (*it, false)).collect::<HashMap<&str, bool>>();
361         'outer: for path in self.contains_fixme {
362             for krate in poorly_documented.iter() {
363                 if path.components().any(|it| it.as_os_str() == *krate) {
364                     has_fixmes.insert(krate, true);
365                     continue 'outer;
366                 }
367             }
368             panic!("FIXME doc in a fully-documented crate: {}", path.display())
369         }
370
371         for (krate, has_fixme) in has_fixmes.iter() {
372             if !has_fixme {
373                 panic!("crate {} is fully documented :tada:, remove it from the list of poorly documented crates", krate)
374             }
375         }
376     }
377 }
378
379 fn is_exclude_dir(p: &Path, dirs_to_exclude: &[&str]) -> bool {
380     p.strip_prefix(project_root())
381         .unwrap()
382         .components()
383         .rev()
384         .skip(1)
385         .filter_map(|it| it.as_os_str().to_str())
386         .any(|it| dirs_to_exclude.contains(&it))
387 }
388
389 #[allow(deprecated)]
390 fn stable_hash(text: &str) -> u64 {
391     use std::hash::{Hash, Hasher, SipHasher};
392
393     let text = text.replace('\r', "");
394     let mut hasher = SipHasher::default();
395     text.hash(&mut hasher);
396     hasher.finish()
397 }