1use std::{
5 collections::{HashMap, HashSet, hash_map},
6 fs,
7 path::Path,
8 str::FromStr,
9};
10
11use edition::Edition;
12use stdx::format_to;
13use xshell::{Shell, cmd};
14
15use crate::{
16 codegen::{add_preamble, ensure_file_contents, reformat},
17 project_root,
18 util::list_files,
19};
20
21const DESTINATION: &str = "crates/ide-db/src/generated/lints.rs";
22
23pub(crate) fn generate(check: bool) {
25 let sh = &Shell::new().unwrap();
26
27 let rust_repo = project_root().join("./target/rust");
28 if rust_repo.exists() {
29 cmd!(sh, "git -C {rust_repo} pull --rebase").run().unwrap();
30 } else {
31 cmd!(sh, "git clone --depth=1 https://github.com/rust-lang/rust {rust_repo}")
32 .run()
33 .unwrap();
34 }
35 cmd!(
37 sh,
38 "git -C {rust_repo} submodule update --init --recursive --depth=1 --
39 compiler library src/tools src/doc/book"
40 )
41 .run()
42 .unwrap();
43
44 let mut contents = String::from(
45 r"
46use span::Edition;
47
48use crate::Severity;
49
50#[derive(Clone)]
51pub struct Lint {
52 pub label: &'static str,
53 pub description: &'static str,
54 pub default_severity: Severity,
55 pub warn_since: Option<Edition>,
56 pub deny_since: Option<Edition>,
57}
58
59pub struct LintGroup {
60 pub lint: Lint,
61 pub children: &'static [&'static str],
62}
63
64",
65 );
66
67 generate_lint_descriptor(sh, &mut contents);
68 contents.push('\n');
69
70 let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned());
71 let unstable_book = project_root().join("./target/unstable-book-gen");
72 cmd!(
73 sh,
74 "{cargo} run --manifest-path {rust_repo}/src/tools/unstable-book-gen/Cargo.toml --
75 {rust_repo}/library {rust_repo}/compiler {rust_repo}/src {unstable_book}"
76 )
77 .run()
78 .unwrap();
79 generate_feature_descriptor(&mut contents, &unstable_book.join("src"));
80 contents.push('\n');
81
82 let lints_json = project_root().join("./target/clippy_lints.json");
83 cmd!(
84 sh,
85 "curl -f https://raw.githubusercontent.com/rust-lang/rust-clippy/21fd71e3fe6eb063cfb619ecc37b1023f5283894/beta/lints.json --output {lints_json}"
86 )
87 .run()
88 .unwrap();
89 generate_descriptor_clippy(&mut contents, &lints_json);
90
91 let contents = add_preamble(crate::flags::CodegenType::LintDefinitions, reformat(contents));
92
93 let destination = project_root().join(DESTINATION);
94 ensure_file_contents(
95 crate::flags::CodegenType::LintDefinitions,
96 destination.as_path(),
97 &contents,
98 check,
99 );
100}
101
102#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
103enum Severity {
104 Allow,
105 Warn,
106 Deny,
107}
108
109impl std::fmt::Display for Severity {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 write!(
112 f,
113 "Severity::{}",
114 match self {
115 Severity::Allow => "Allow",
116 Severity::Warn => "Warning",
117 Severity::Deny => "Error",
118 }
119 )
120 }
121}
122
123impl FromStr for Severity {
124 type Err = &'static str;
125
126 fn from_str(s: &str) -> Result<Self, Self::Err> {
127 match s {
128 "allow" => Ok(Self::Allow),
129 "warn" => Ok(Self::Warn),
130 "deny" => Ok(Self::Deny),
131 _ => Err("invalid severity"),
132 }
133 }
134}
135
136#[derive(Debug)]
137struct Lint {
138 description: String,
139 default_severity: Severity,
140 warn_since: Option<Edition>,
141 deny_since: Option<Edition>,
142}
143
144fn generate_lint_descriptor(sh: &Shell, buf: &mut String) {
167 fn get_lints_as_text(
168 stdout: &str,
169 ) -> (
170 impl Iterator<Item = (String, &str, Severity)> + '_,
171 impl Iterator<Item = (String, Lint, impl Iterator<Item = String> + '_)> + '_,
172 impl Iterator<Item = (String, &str, Severity)> + '_,
173 impl Iterator<Item = (String, Lint, impl Iterator<Item = String> + '_)> + '_,
174 ) {
175 let lints_pat = "---- ------- -------\n";
176 let lint_groups_pat = "---- ---------\n";
177 let lints = find_and_slice(stdout, lints_pat);
178 let lint_groups = find_and_slice(lints, lint_groups_pat);
179 let lints_rustdoc = find_and_slice(lint_groups, lints_pat);
180 let lint_groups_rustdoc = find_and_slice(lints_rustdoc, lint_groups_pat);
181
182 let lints = lints.lines().take_while(|l| !l.is_empty()).map(|line| {
183 let (name, rest) = line.trim().split_once(char::is_whitespace).unwrap();
184 let (severity, description) = rest.trim().split_once(char::is_whitespace).unwrap();
185 (name.trim().replace('-', "_"), description.trim(), severity.parse().unwrap())
186 });
187 let lint_groups = lint_groups.lines().take_while(|l| !l.is_empty()).map(|line| {
188 let (name, lints) = line.trim().split_once(char::is_whitespace).unwrap();
189 let label = name.trim().replace('-', "_");
190 let lint = Lint {
191 description: format!("lint group for: {}", lints.trim()),
192 default_severity: Severity::Allow,
193 warn_since: None,
194 deny_since: None,
195 };
196 let children = lints
197 .split_ascii_whitespace()
198 .map(|s| s.trim().trim_matches(',').replace('-', "_"));
199 (label, lint, children)
200 });
201
202 let lints_rustdoc = lints_rustdoc.lines().take_while(|l| !l.is_empty()).map(|line| {
203 let (name, rest) = line.trim().split_once(char::is_whitespace).unwrap();
204 let (severity, description) = rest.trim().split_once(char::is_whitespace).unwrap();
205 (name.trim().replace('-', "_"), description.trim(), severity.parse().unwrap())
206 });
207 let lint_groups_rustdoc =
208 lint_groups_rustdoc.lines().take_while(|l| !l.is_empty()).map(|line| {
209 let (name, lints) = line.trim().split_once(char::is_whitespace).unwrap();
210 let label = name.trim().replace('-', "_");
211 let lint = Lint {
212 description: format!("lint group for: {}", lints.trim()),
213 default_severity: Severity::Allow,
214 warn_since: None,
215 deny_since: None,
216 };
217 let children = lints
218 .split_ascii_whitespace()
219 .map(|s| s.trim().trim_matches(',').replace('-', "_"));
220 (label, lint, children)
221 });
222
223 (lints, lint_groups, lints_rustdoc, lint_groups_rustdoc)
224 }
225
226 fn insert_lints<'a>(
227 edition: Edition,
228 lints_map: &mut HashMap<String, Lint>,
229 lint_groups_map: &mut HashMap<String, (Lint, Vec<String>)>,
230 lints: impl Iterator<Item = (String, &'a str, Severity)>,
231 lint_groups: impl Iterator<Item = (String, Lint, impl Iterator<Item = String>)>,
232 ) {
233 for (lint_name, lint_description, lint_severity) in lints {
234 let lint = lints_map.entry(lint_name).or_insert_with(|| Lint {
235 description: lint_description.to_owned(),
236 default_severity: Severity::Allow,
237 warn_since: None,
238 deny_since: None,
239 });
240 if lint_severity == Severity::Warn
241 && lint.warn_since.is_none()
242 && lint.default_severity < Severity::Warn
243 {
244 lint.warn_since = Some(edition);
245 }
246 if lint_severity == Severity::Deny
247 && lint.deny_since.is_none()
248 && lint.default_severity < Severity::Deny
249 {
250 lint.deny_since = Some(edition);
251 }
252 }
253
254 for (group_name, lint, children) in lint_groups {
255 match lint_groups_map.entry(group_name) {
256 hash_map::Entry::Vacant(entry) => {
257 entry.insert((lint, Vec::from_iter(children)));
258 }
259 hash_map::Entry::Occupied(mut entry) => {
260 *entry.get_mut() = (lint, Vec::from_iter(children));
262 }
263 }
264 }
265 }
266
267 fn get_lints(
268 sh: &Shell,
269 edition: Edition,
270 lints_map: &mut HashMap<String, Lint>,
271 lint_groups_map: &mut HashMap<String, (Lint, Vec<String>)>,
272 lints_rustdoc_map: &mut HashMap<String, Lint>,
273 lint_groups_rustdoc_map: &mut HashMap<String, (Lint, Vec<String>)>,
274 ) {
275 let edition_str = edition.to_string();
276 let stdout = cmd!(sh, "rustdoc +nightly -Whelp -Zunstable-options --edition={edition_str}")
277 .read()
278 .unwrap();
279 let (lints, lint_groups, lints_rustdoc, lint_groups_rustdoc) = get_lints_as_text(&stdout);
280
281 insert_lints(edition, lints_map, lint_groups_map, lints, lint_groups);
282 insert_lints(
283 edition,
284 lints_rustdoc_map,
285 lint_groups_rustdoc_map,
286 lints_rustdoc,
287 lint_groups_rustdoc,
288 );
289 }
290
291 let basic_lints = cmd!(sh, "rustdoc +nightly -Whelp --edition=2015").read().unwrap();
292 let (lints, lint_groups, lints_rustdoc, lint_groups_rustdoc) = get_lints_as_text(&basic_lints);
293
294 let mut lints = lints
295 .map(|(label, description, severity)| {
296 (
297 label,
298 Lint {
299 description: description.to_owned(),
300 default_severity: severity,
301 warn_since: None,
302 deny_since: None,
303 },
304 )
305 })
306 .collect::<HashMap<_, _>>();
307 let mut lint_groups = lint_groups
308 .map(|(label, lint, children)| (label, (lint, Vec::from_iter(children))))
309 .collect::<HashMap<_, _>>();
310 let mut lints_rustdoc = lints_rustdoc
311 .map(|(label, description, severity)| {
312 (
313 label,
314 Lint {
315 description: description.to_owned(),
316 default_severity: severity,
317 warn_since: None,
318 deny_since: None,
319 },
320 )
321 })
322 .collect::<HashMap<_, _>>();
323 let mut lint_groups_rustdoc = lint_groups_rustdoc
324 .map(|(label, lint, children)| (label, (lint, Vec::from_iter(children))))
325 .collect::<HashMap<_, _>>();
326
327 for edition in Edition::iter().skip(1) {
328 get_lints(
329 sh,
330 edition,
331 &mut lints,
332 &mut lint_groups,
333 &mut lints_rustdoc,
334 &mut lint_groups_rustdoc,
335 );
336 }
337
338 let mut lints = Vec::from_iter(lints);
339 lints.sort_unstable_by(|a, b| a.0.cmp(&b.0));
340 let mut lint_groups = Vec::from_iter(lint_groups);
341 lint_groups.sort_unstable_by(|a, b| a.0.cmp(&b.0));
342 let mut lints_rustdoc = Vec::from_iter(lints_rustdoc);
343 lints_rustdoc.sort_unstable_by(|a, b| a.0.cmp(&b.0));
344 let mut lint_groups_rustdoc = Vec::from_iter(lint_groups_rustdoc);
345 lint_groups_rustdoc.sort_unstable_by(|a, b| a.0.cmp(&b.0));
346
347 buf.push_str(r#"pub const DEFAULT_LINTS: &[Lint] = &["#);
348 buf.push('\n');
349
350 let mut known_lints: HashSet<&String> = HashSet::with_capacity(lints.len() + lint_groups.len());
351 for (name, lint) in &lints {
352 push_lint_completion(buf, name, lint);
353 known_lints.insert(name);
354 }
355 for (name, (group, _)) in lint_groups.iter().filter(|(name, _)| !known_lints.contains(name)) {
356 push_lint_completion(buf, name, group);
357 }
358 buf.push_str("];\n\n");
359
360 buf.push_str(r#"pub const DEFAULT_LINT_GROUPS: &[LintGroup] = &["#);
361 for (name, (lint, children)) in &lint_groups {
362 if name == "warnings" {
363 continue;
364 }
365 push_lint_group(buf, name, lint, children);
366 }
367 buf.push('\n');
368 buf.push_str("];\n");
369
370 buf.push('\n');
373 buf.push_str(r#"pub const RUSTDOC_LINTS: &[Lint] = &["#);
374 buf.push('\n');
375
376 let mut known_rustdoc_lints: HashSet<&String> =
377 HashSet::with_capacity(lints_rustdoc.len() + lint_groups_rustdoc.len());
378 for (name, lint) in &lints_rustdoc {
379 push_lint_completion(buf, name, lint);
380 known_rustdoc_lints.insert(name);
381 }
382 for (name, (group, _)) in
383 lint_groups_rustdoc.iter().filter(|(name, _)| !known_rustdoc_lints.contains(name))
384 {
385 push_lint_completion(buf, name, group);
386 }
387 buf.push_str("];\n\n");
388
389 buf.push_str(r#"pub const RUSTDOC_LINT_GROUPS: &[LintGroup] = &["#);
390 for (name, (lint, children)) in &lint_groups_rustdoc {
391 push_lint_group(buf, name, lint, children);
392 }
393 buf.push('\n');
394 buf.push_str("];\n");
395}
396
397#[track_caller]
398fn find_and_slice<'a>(i: &'a str, p: &str) -> &'a str {
399 let idx = i.find(p).unwrap();
400 &i[idx + p.len()..]
401}
402
403fn generate_feature_descriptor(buf: &mut String, src_dir: &Path) {
410 let mut features = ["language-features", "library-features"]
411 .into_iter()
412 .flat_map(|it| list_files(&src_dir.join(it)))
413 .filter(|path| path.extension() == Some("md".as_ref()))
415 .map(|path| {
416 let feature_ident = path.file_stem().unwrap().to_str().unwrap().replace('-', "_");
417 let doc = fs::read_to_string(path).unwrap();
418 (feature_ident, doc)
419 })
420 .collect::<Vec<_>>();
421 features.sort_by(|(feature_ident, _), (feature_ident2, _)| feature_ident.cmp(feature_ident2));
422
423 buf.push_str(r#"pub const FEATURES: &[Lint] = &["#);
424 for (feature_ident, doc) in features.into_iter() {
425 let lint = Lint {
426 description: doc,
427 default_severity: Severity::Allow,
428 warn_since: None,
429 deny_since: None,
430 };
431 push_lint_completion(buf, &feature_ident, &lint);
432 }
433 buf.push('\n');
434 buf.push_str("];\n");
435}
436
437#[derive(Debug, Default)]
438struct ClippyLint {
439 help: String,
440 id: String,
441}
442
443fn unescape(s: &str) -> String {
444 s.replace(r#"\""#, "").replace(r#"\n"#, "\n").replace(r#"\r"#, "")
445}
446
447#[allow(clippy::print_stderr)]
448fn generate_descriptor_clippy(buf: &mut String, path: &Path) {
449 let file_content = std::fs::read_to_string(path).unwrap();
450 let mut clippy_lints: Vec<ClippyLint> = Vec::new();
451 let mut clippy_groups: std::collections::BTreeMap<String, Vec<String>> = Default::default();
452
453 for line in file_content.lines().map(str::trim) {
454 if let Some(line) = line.strip_prefix(r#""id": ""#) {
455 let clippy_lint = ClippyLint {
456 id: line.strip_suffix(r#"","#).expect("should be suffixed by comma").into(),
457 help: String::new(),
458 };
459 clippy_lints.push(clippy_lint)
460 } else if let Some(line) = line.strip_prefix(r#""group": ""#) {
461 if let Some(group) = line.strip_suffix("\",") {
462 clippy_groups
463 .entry(group.to_owned())
464 .or_default()
465 .push(clippy_lints.last().unwrap().id.clone());
466 }
467 } else if let Some(line) = line.strip_prefix(r#""docs": ""#) {
468 let header = "### What it does";
469 let line = match line.find(header) {
470 Some(idx) => &line[idx + header.len()..],
471 None => {
472 let id = &clippy_lints.last().unwrap().id;
473 let allowed = ["allow_attributes", "read_line_without_trim"];
475 if allowed.contains(&id.as_str()) {
476 line
477 } else {
478 eprintln!("\nunexpected clippy prefix for {id}, line={line:?}\n",);
479 continue;
480 }
481 }
482 };
483 let up_to = line.find(r#"###"#).expect("no second section found?");
486 let line = &line[..up_to];
487
488 let clippy_lint = clippy_lints.last_mut().expect("clippy lint must already exist");
489 unescape(line).trim().clone_into(&mut clippy_lint.help);
490 }
491 }
492 clippy_lints.sort_by(|lint, lint2| lint.id.cmp(&lint2.id));
493
494 buf.push_str(r#"pub const CLIPPY_LINTS: &[Lint] = &["#);
495 buf.push('\n');
496 for clippy_lint in clippy_lints.into_iter() {
497 let lint_ident = format!("clippy::{}", clippy_lint.id);
498 let lint = Lint {
499 description: clippy_lint.help,
500 default_severity: Severity::Allow,
502 warn_since: None,
503 deny_since: None,
504 };
505 push_lint_completion(buf, &lint_ident, &lint);
506 }
507 buf.push_str("];\n");
508
509 buf.push_str(r#"pub const CLIPPY_LINT_GROUPS: &[LintGroup] = &["#);
510 for (id, children) in clippy_groups {
511 let children = children.iter().map(|id| format!("clippy::{id}")).collect::<Vec<_>>();
512 if !children.is_empty() {
513 let lint_ident = format!("clippy::{id}");
514 let description = format!("lint group for: {}", children.join(", "));
515 let lint = Lint {
516 description,
517 default_severity: Severity::Allow,
518 warn_since: None,
519 deny_since: None,
520 };
521 push_lint_group(buf, &lint_ident, &lint, &children);
522 }
523 }
524 buf.push('\n');
525 buf.push_str("];\n");
526}
527
528fn push_lint_completion(buf: &mut String, name: &str, lint: &Lint) {
529 format_to!(
530 buf,
531 r###" Lint {{
532 label: "{}",
533 description: r##"{}"##,
534 default_severity: {},
535 warn_since: "###,
536 name,
537 lint.description,
538 lint.default_severity,
539 );
540 match lint.warn_since {
541 Some(edition) => format_to!(buf, "Some(Edition::Edition{edition})"),
542 None => buf.push_str("None"),
543 }
544 format_to!(
545 buf,
546 r###",
547 deny_since: "###
548 );
549 match lint.deny_since {
550 Some(edition) => format_to!(buf, "Some(Edition::Edition{edition})"),
551 None => buf.push_str("None"),
552 }
553 format_to!(
554 buf,
555 r###",
556 }},"###
557 );
558}
559
560fn push_lint_group(buf: &mut String, name: &str, lint: &Lint, children: &[String]) {
561 buf.push_str(
562 r###" LintGroup {
563 lint:
564 "###,
565 );
566
567 push_lint_completion(buf, name, lint);
568
569 let children = format!(
570 "&[{}]",
571 children.iter().map(|it| format!("\"{it}\"")).collect::<Vec<_>>().join(", ")
572 );
573 format_to!(
574 buf,
575 r###"
576 children: {},
577 }},"###,
578 children,
579 );
580}