xtask/codegen/
lints.rs

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