]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/ide-db/src/tests/sourcegen_lints.rs
Auto merge of #102622 - camsteffen:move-layout, r=fee1-dead
[rust.git] / src / tools / rust-analyzer / crates / ide-db / src / tests / sourcegen_lints.rs
1 //! Generates descriptors structure for unstable feature from Unstable Book
2 use std::{borrow::Cow, fs, path::Path};
3
4 use itertools::Itertools;
5 use stdx::format_to;
6 use test_utils::project_root;
7 use xshell::{cmd, Shell};
8
9 /// This clones rustc repo, and so is not worth to keep up-to-date. We update
10 /// manually by un-ignoring the test from time to time.
11 #[test]
12 #[ignore]
13 fn sourcegen_lint_completions() {
14     let sh = &Shell::new().unwrap();
15
16     let rust_repo = project_root().join("./target/rust");
17     if !rust_repo.exists() {
18         cmd!(sh, "git clone --depth=1 https://github.com/rust-lang/rust {rust_repo}")
19             .run()
20             .unwrap();
21     }
22
23     let mut contents = String::from(
24         r"
25 #[derive(Clone)]
26 pub struct Lint {
27     pub label: &'static str,
28     pub description: &'static str,
29 }
30 pub struct LintGroup {
31     pub lint: Lint,
32     pub children: &'static [&'static str],
33 }
34 ",
35     );
36
37     generate_lint_descriptor(sh, &mut contents);
38     contents.push('\n');
39
40     generate_feature_descriptor(&mut contents, &rust_repo.join("src/doc/unstable-book/src"));
41     contents.push('\n');
42
43     let lints_json = project_root().join("./target/clippy_lints.json");
44     cmd!(
45         sh,
46         "curl https://rust-lang.github.io/rust-clippy/master/lints.json --output {lints_json}"
47     )
48     .run()
49     .unwrap();
50     generate_descriptor_clippy(&mut contents, &lints_json);
51
52     let contents = sourcegen::add_preamble("sourcegen_lints", sourcegen::reformat(contents));
53
54     let destination = project_root().join("crates/ide_db/src/generated/lints.rs");
55     sourcegen::ensure_file_contents(destination.as_path(), &contents);
56 }
57
58 fn generate_lint_descriptor(sh: &Shell, buf: &mut String) {
59     // FIXME: rustdoc currently requires an input file for -Whelp cc https://github.com/rust-lang/rust/pull/88831
60     let file = project_root().join(file!());
61     let stdout = cmd!(sh, "rustdoc -W help {file}").read().unwrap();
62     let start_lints = stdout.find("----  -------  -------").unwrap();
63     let start_lint_groups = stdout.find("----  ---------").unwrap();
64     let start_lints_rustdoc =
65         stdout.find("Lint checks provided by plugins loaded by this crate:").unwrap();
66     let start_lint_groups_rustdoc =
67         stdout.find("Lint groups provided by plugins loaded by this crate:").unwrap();
68
69     buf.push_str(r#"pub const DEFAULT_LINTS: &[Lint] = &["#);
70     buf.push('\n');
71
72     let lints = stdout[start_lints..].lines().skip(1).take_while(|l| !l.is_empty()).map(|line| {
73         let (name, rest) = line.trim().split_once(char::is_whitespace).unwrap();
74         let (_default_level, description) = rest.trim().split_once(char::is_whitespace).unwrap();
75         (name.trim(), Cow::Borrowed(description.trim()), vec![])
76     });
77     let lint_groups =
78         stdout[start_lint_groups..].lines().skip(1).take_while(|l| !l.is_empty()).map(|line| {
79             let (name, lints) = line.trim().split_once(char::is_whitespace).unwrap();
80             (
81                 name.trim(),
82                 format!("lint group for: {}", lints.trim()).into(),
83                 lints
84                     .split_ascii_whitespace()
85                     .map(|s| s.trim().trim_matches(',').replace('-', "_"))
86                     .collect(),
87             )
88         });
89
90     let lints = lints
91         .chain(lint_groups)
92         .sorted_by(|(ident, ..), (ident2, ..)| ident.cmp(ident2))
93         .collect::<Vec<_>>();
94     for (name, description, ..) in &lints {
95         push_lint_completion(buf, &name.replace('-', "_"), description);
96     }
97     buf.push_str("];\n");
98     buf.push_str(r#"pub const DEFAULT_LINT_GROUPS: &[LintGroup] = &["#);
99     for (name, description, children) in &lints {
100         if !children.is_empty() {
101             // HACK: warnings is emitted with a general description, not with its members
102             if name == &"warnings" {
103                 push_lint_group(buf, name, description, &Vec::new());
104                 continue;
105             }
106             push_lint_group(buf, &name.replace('-', "_"), description, children);
107         }
108     }
109     buf.push('\n');
110     buf.push_str("];\n");
111
112     // rustdoc
113
114     buf.push('\n');
115     buf.push_str(r#"pub const RUSTDOC_LINTS: &[Lint] = &["#);
116     buf.push('\n');
117
118     let lints_rustdoc =
119         stdout[start_lints_rustdoc..].lines().skip(2).take_while(|l| !l.is_empty()).map(|line| {
120             let (name, rest) = line.trim().split_once(char::is_whitespace).unwrap();
121             let (_default_level, description) =
122                 rest.trim().split_once(char::is_whitespace).unwrap();
123             (name.trim(), Cow::Borrowed(description.trim()), vec![])
124         });
125     let lint_groups_rustdoc =
126         stdout[start_lint_groups_rustdoc..].lines().skip(2).take_while(|l| !l.is_empty()).map(
127             |line| {
128                 let (name, lints) = line.trim().split_once(char::is_whitespace).unwrap();
129                 (
130                     name.trim(),
131                     format!("lint group for: {}", lints.trim()).into(),
132                     lints
133                         .split_ascii_whitespace()
134                         .map(|s| s.trim().trim_matches(',').replace('-', "_"))
135                         .collect(),
136                 )
137             },
138         );
139
140     let lints_rustdoc = lints_rustdoc
141         .chain(lint_groups_rustdoc)
142         .sorted_by(|(ident, ..), (ident2, ..)| ident.cmp(ident2))
143         .collect::<Vec<_>>();
144
145     for (name, description, ..) in &lints_rustdoc {
146         push_lint_completion(buf, &name.replace('-', "_"), description)
147     }
148     buf.push_str("];\n");
149
150     buf.push_str(r#"pub const RUSTDOC_LINT_GROUPS: &[LintGroup] = &["#);
151     for (name, description, children) in &lints_rustdoc {
152         if !children.is_empty() {
153             push_lint_group(buf, &name.replace('-', "_"), description, children);
154         }
155     }
156     buf.push('\n');
157     buf.push_str("];\n");
158 }
159
160 fn generate_feature_descriptor(buf: &mut String, src_dir: &Path) {
161     let mut features = ["language-features", "library-features"]
162         .into_iter()
163         .flat_map(|it| sourcegen::list_files(&src_dir.join(it)))
164         .filter(|path| {
165             // Get all `.md ` files
166             path.extension().unwrap_or_default().to_str().unwrap_or_default() == "md"
167         })
168         .map(|path| {
169             let feature_ident = path.file_stem().unwrap().to_str().unwrap().replace('-', "_");
170             let doc = fs::read_to_string(path).unwrap();
171             (feature_ident, doc)
172         })
173         .collect::<Vec<_>>();
174     features.sort_by(|(feature_ident, _), (feature_ident2, _)| feature_ident.cmp(feature_ident2));
175
176     buf.push_str(r#"pub const FEATURES: &[Lint] = &["#);
177     for (feature_ident, doc) in features.into_iter() {
178         push_lint_completion(buf, &feature_ident, &doc)
179     }
180     buf.push('\n');
181     buf.push_str("];\n");
182 }
183
184 #[derive(Default)]
185 struct ClippyLint {
186     help: String,
187     id: String,
188 }
189
190 fn unescape(s: &str) -> String {
191     s.replace(r#"\""#, "").replace(r#"\n"#, "\n").replace(r#"\r"#, "")
192 }
193
194 fn generate_descriptor_clippy(buf: &mut String, path: &Path) {
195     let file_content = std::fs::read_to_string(path).unwrap();
196     let mut clippy_lints: Vec<ClippyLint> = Vec::new();
197     let mut clippy_groups: std::collections::BTreeMap<String, Vec<String>> = Default::default();
198
199     for line in file_content.lines().map(|line| line.trim()) {
200         if let Some(line) = line.strip_prefix(r#""id": ""#) {
201             let clippy_lint = ClippyLint {
202                 id: line.strip_suffix(r#"","#).expect("should be suffixed by comma").into(),
203                 help: String::new(),
204             };
205             clippy_lints.push(clippy_lint)
206         } else if let Some(line) = line.strip_prefix(r#""group": ""#) {
207             if let Some(group) = line.strip_suffix("\",") {
208                 clippy_groups
209                     .entry(group.to_owned())
210                     .or_default()
211                     .push(clippy_lints.last().unwrap().id.clone());
212             }
213         } else if let Some(line) = line.strip_prefix(r#""docs": ""#) {
214             let prefix_to_strip = r#" ### What it does"#;
215             let line = match line.strip_prefix(prefix_to_strip) {
216                 Some(line) => line,
217                 None => {
218                     eprintln!("unexpected clippy prefix for {}", clippy_lints.last().unwrap().id);
219                     continue;
220                 }
221             };
222             // Only take the description, any more than this is a lot of additional data we would embed into the exe
223             // which seems unnecessary
224             let up_to = line.find(r#"###"#).expect("no second section found?");
225             let line = &line[..up_to];
226
227             let clippy_lint = clippy_lints.last_mut().expect("clippy lint must already exist");
228             clippy_lint.help = unescape(line).trim().to_string();
229         }
230     }
231     clippy_lints.sort_by(|lint, lint2| lint.id.cmp(&lint2.id));
232
233     buf.push_str(r#"pub const CLIPPY_LINTS: &[Lint] = &["#);
234     buf.push('\n');
235     for clippy_lint in clippy_lints.into_iter() {
236         let lint_ident = format!("clippy::{}", clippy_lint.id);
237         let doc = clippy_lint.help;
238         push_lint_completion(buf, &lint_ident, &doc);
239     }
240     buf.push_str("];\n");
241
242     buf.push_str(r#"pub const CLIPPY_LINT_GROUPS: &[LintGroup] = &["#);
243     for (id, children) in clippy_groups {
244         let children = children.iter().map(|id| format!("clippy::{}", id)).collect::<Vec<_>>();
245         if !children.is_empty() {
246             let lint_ident = format!("clippy::{}", id);
247             let description = format!("lint group for: {}", children.iter().join(", "));
248             push_lint_group(buf, &lint_ident, &description, &children);
249         }
250     }
251     buf.push('\n');
252     buf.push_str("];\n");
253 }
254
255 fn push_lint_completion(buf: &mut String, label: &str, description: &str) {
256     format_to!(
257         buf,
258         r###"    Lint {{
259         label: "{}",
260         description: r##"{}"##,
261     }},"###,
262         label,
263         description,
264     );
265 }
266
267 fn push_lint_group(buf: &mut String, label: &str, description: &str, children: &[String]) {
268     buf.push_str(
269         r###"    LintGroup {
270         lint:
271         "###,
272     );
273
274     push_lint_completion(buf, label, description);
275
276     let children = format!("&[{}]", children.iter().map(|it| format!("\"{}\"", it)).join(", "));
277     format_to!(
278         buf,
279         r###"
280         children: {},
281         }},"###,
282         children,
283     );
284 }