xtask/codegen/
diagnostics_docs.rs

1//! Generates `diagnostics_generated.md` documentation.
2
3use std::{fmt, fs, io, path::PathBuf};
4
5use crate::{
6    codegen::{CommentBlock, Location, add_preamble},
7    project_root,
8    util::list_rust_files,
9};
10
11pub(crate) fn generate(check: bool) {
12    let diagnostics = Diagnostic::collect().unwrap();
13    // Do not generate docs when run with `--check`
14    if check {
15        return;
16    }
17    let contents =
18        diagnostics.into_iter().map(|it| it.to_string()).collect::<Vec<_>>().join("\n\n");
19    let contents = add_preamble(crate::flags::CodegenType::DiagnosticsDocs, contents);
20    let dst = project_root().join("docs/book/src/diagnostics_generated.md");
21    fs::write(dst, contents).unwrap();
22}
23
24#[derive(Debug)]
25struct Diagnostic {
26    id: String,
27    location: Location,
28    doc: String,
29}
30
31impl Diagnostic {
32    fn collect() -> io::Result<Vec<Diagnostic>> {
33        let handlers_dir = project_root().join("crates/ide-diagnostics/src/handlers");
34
35        let mut res = Vec::new();
36        for path in list_rust_files(&handlers_dir) {
37            collect_file(&mut res, path)?;
38        }
39        res.sort_by(|lhs, rhs| lhs.id.cmp(&rhs.id));
40        return Ok(res);
41
42        fn collect_file(acc: &mut Vec<Diagnostic>, path: PathBuf) -> io::Result<()> {
43            let text = fs::read_to_string(&path)?;
44            let comment_blocks = CommentBlock::extract("Diagnostic", &text);
45
46            for block in comment_blocks {
47                let id = block.id;
48                if let Err(msg) = is_valid_diagnostic_name(&id) {
49                    panic!("invalid diagnostic name: {id:?}:\n  {msg}")
50                }
51                let doc = block.contents.join("\n");
52                let location = Location { file: path.clone(), line: block.line };
53                acc.push(Diagnostic { id, location, doc })
54            }
55
56            Ok(())
57        }
58    }
59}
60
61fn is_valid_diagnostic_name(diagnostic: &str) -> Result<(), String> {
62    let diagnostic = diagnostic.trim();
63    if diagnostic.find(char::is_whitespace).is_some() {
64        return Err("Diagnostic names can't contain whitespace symbols".into());
65    }
66    if diagnostic.chars().any(|c| c.is_ascii_uppercase()) {
67        return Err("Diagnostic names can't contain uppercase symbols".into());
68    }
69    if !diagnostic.is_ascii() {
70        return Err("Diagnostic can't contain non-ASCII symbols".into());
71    }
72
73    Ok(())
74}
75
76impl fmt::Display for Diagnostic {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        writeln!(f, "#### {}\n\nSource: {}\n\n{}\n\n", self.id, self.location, self.doc)
79    }
80}