Repository: rust-analyzer/rust-analyzer (rust)

Issue:
Document rust-analyzer diagnostics
We should document all our diagnostics in the manual (probably don't need a lot of text, just the diagnostic code and maybe an example message). We could probably generate this similarly to the assist documentation.

Pull request:
Generate diagnostics docs
Resolves #6215


Patch:
diff --git a/.gitignore b/.gitignore
index 472fe1a137..b205bf3fb6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@ crates/*/target
 .vscode/settings.json
 generated_assists.adoc
 generated_features.adoc
+generated_diagnostic.adoc
diff --git a/crates/hir_def/src/diagnostics.rs b/crates/hir_def/src/diagnostics.rs
index 001b3c5db6..fcfbbbad38 100644
--- a/crates/hir_def/src/diagnostics.rs
+++ b/crates/hir_def/src/diagnostics.rs
@@ -7,6 +7,9 @@ use syntax::{ast, AstPtr, SyntaxNodePtr};
 
 use hir_expand::{HirFileId, InFile};
 
+// Diagnostic: unresolved-module
+//
+// This diagnostic is triggered if rust-analyzer is unable to discover referred module.
 #[derive(Debug)]
 pub struct UnresolvedModule {
     pub file: HirFileId,
@@ -29,6 +32,9 @@ impl Diagnostic for UnresolvedModule {
     }
 }
 
+// Diagnostic: unresolved-extern-crate
+//
+// This diagnostic is triggered if rust-analyzer is unable to discover referred extern crate.
 #[derive(Debug)]
 pub struct UnresolvedExternCrate {
     pub file: HirFileId,
@@ -50,6 +56,9 @@ impl Diagnostic for UnresolvedExternCrate {
     }
 }
 
+// Diagnostic: unresolved-import
+//
+// This diagnostic is triggered if rust-analyzer is unable to discover imported module.
 #[derive(Debug)]
 pub struct UnresolvedImport {
     pub file: HirFileId,
diff --git a/crates/hir_ty/src/diagnostics.rs b/crates/hir_ty/src/diagnostics.rs
index dfe98571e7..b58fe0ed77 100644
--- a/crates/hir_ty/src/diagnostics.rs
+++ b/crates/hir_ty/src/diagnostics.rs
@@ -36,6 +36,9 @@ pub fn validate_body(db: &dyn HirDatabase, owner: DefWithBodyId, sink: &mut Diag
     validator.validate_body(db);
 }
 
+// Diagnostic: no-such-field
+//
+// This diagnostic is triggered if created structure does not have field provided in record.
 #[derive(Debug)]
 pub struct NoSuchField {
     pub file: HirFileId,
@@ -60,6 +63,17 @@ impl Diagnostic for NoSuchField {
     }
 }
 
+// Diagnostic: missing-structure-fields
+//
+// This diagnostic is triggered if record lacks some fields that exist in the corresponding structure.
+//
+// Example:
+//
+// ```rust
+// struct A { a: u8, b: u8 }
+//
+// let a = A { a: 10 };
+// ```
 #[derive(Debug)]
 pub struct MissingFields {
     pub file: HirFileId,
@@ -96,6 +110,21 @@ impl Diagnostic for MissingFields {
     }
 }
 
+// Diagnostic: missing-pat-fields
+//
+// This diagnostic is triggered if pattern lacks some fields that exist in the corresponding structure.
+//
+// Example:
+//
+// ```rust
+// struct A { a: u8, b: u8 }
+//
+// let a = A { a: 10, b: 20 };
+//
+// if let A { a } = a {
+//     // ...
+// }
+// ```
 #[derive(Debug)]
 pub struct MissingPatFields {
     pub file: HirFileId,
@@ -130,6 +159,9 @@ impl Diagnostic for MissingPatFields {
     }
 }
 
+// Diagnostic: missing-match-arm
+//
+// This diagnostic is triggered if `match` block is missing one or more match arms.
 #[derive(Debug)]
 pub struct MissingMatchArms {
     pub file: HirFileId,
@@ -152,6 +184,17 @@ impl Diagnostic for MissingMatchArms {
     }
 }
 
+// Diagnostic: missing-ok-in-tail-expr
+//
+// This diagnostic is triggered if block that should return `Result` returns a value not wrapped in `Ok`.
+//
+// Example:
+//
+// ```rust
+// fn foo() -> Result<u8, ()> {
+//     10
+// }
+// ```
 #[derive(Debug)]
 pub struct MissingOkInTailExpr {
     pub file: HirFileId,
@@ -173,6 +216,9 @@ impl Diagnostic for MissingOkInTailExpr {
     }
 }
 
+// Diagnostic: break-outside-of-loop
+//
+// This diagnostic is triggered if `break` keyword is used outside of a loop.
 #[derive(Debug)]
 pub struct BreakOutsideOfLoop {
     pub file: HirFileId,
@@ -194,6 +240,9 @@ impl Diagnostic for BreakOutsideOfLoop {
     }
 }
 
+// Diagnostic: missing-unsafe
+//
+// This diagnostic is triggered if operation marked as `unsafe` is used outside of `unsafe` function or block.
 #[derive(Debug)]
 pub struct MissingUnsafe {
     pub file: HirFileId,
@@ -215,6 +264,9 @@ impl Diagnostic for MissingUnsafe {
     }
 }
 
+// Diagnostic: mismatched-arg-count
+//
+// This diagnostic is triggered if function is invoked with an incorrect amount of arguments.
 #[derive(Debug)]
 pub struct MismatchedArgCount {
     pub file: HirFileId,
@@ -264,6 +316,9 @@ impl fmt::Display for CaseType {
     }
 }
 
+// Diagnostic: incorrect-ident-case
+//
+// This diagnostic is triggered if item name doesn't follow https://doc.rust-lang.org/1.0.0/style/style/naming/README.html[Rust naming convention].
 #[derive(Debug)]
 pub struct IncorrectCase {
     pub file: HirFileId,
diff --git a/xtask/src/codegen.rs b/xtask/src/codegen.rs
index 3ee4c1adf4..afa7034719 100644
--- a/xtask/src/codegen.rs
+++ b/xtask/src/codegen.rs
@@ -10,6 +10,7 @@ mod gen_parser_tests;
 mod gen_assists_docs;
 mod gen_feature_docs;
 mod gen_features;
+mod gen_diagnostic_docs;
 
 use std::{
     fmt, mem,
@@ -21,6 +22,7 @@ use crate::{ensure_rustfmt, project_root, Result};
 
 pub use self::{
     gen_assists_docs::{generate_assists_docs, generate_assists_tests},
+    gen_diagnostic_docs::generate_diagnostic_docs,
     gen_feature_docs::generate_feature_docs,
     gen_features::generate_features,
     gen_parser_tests::generate_parser_tests,
@@ -47,6 +49,7 @@ impl CodegenCmd {
         generate_assists_tests(Mode::Overwrite)?;
         generate_assists_docs(Mode::Overwrite)?;
         generate_feature_docs(Mode::Overwrite)?;
+        generate_diagnostic_docs(Mode::Overwrite)?;
         Ok(())
     }
 }
diff --git a/xtask/src/codegen/gen_assists_docs.rs b/xtask/src/codegen/gen_assists_docs.rs
index ff307e2aa5..d7c85ebe9f 100644
--- a/xtask/src/codegen/gen_assists_docs.rs
+++ b/xtask/src/codegen/gen_assists_docs.rs
@@ -1,6 +1,6 @@
 //! Generates `assists.md` documentation.
 
-use std::{fmt, fs, path::Path};
+use std::{fmt, path::Path};
 
 use crate::{
     codegen::{self, extract_comment_blocks_with_empty_lines, reformat, Location, Mode, PREAMBLE},
@@ -39,7 +39,7 @@ impl Assist {
         return Ok(res);
 
         fn collect_file(acc: &mut Vec<Assist>, path: &Path) -> Result<()> {
-            let text = fs::read_to_string(path)?;
+            let text = xshell::read_file(path)?;
             let comment_blocks = extract_comment_blocks_with_empty_lines("Assist", &text);
 
             for block in comment_blocks {
diff --git a/xtask/src/codegen/gen_diagnostic_docs.rs b/xtask/src/codegen/gen_diagnostic_docs.rs
new file mode 100644
index 0000000000..00aaea5b77
--- /dev/null
+++ b/xtask/src/codegen/gen_diagnostic_docs.rs
@@ -0,0 +1,74 @@
+//! Generates `assists.md` documentation.
+
+use std::{fmt, path::PathBuf};
+
+use crate::{
+    codegen::{self, extract_comment_blocks_with_empty_lines, Location, Mode, PREAMBLE},
+    project_root, rust_files, Result,
+};
+
+pub fn generate_diagnostic_docs(mode: Mode) -> Result<()> {
+    let diagnostics = Diagnostic::collect()?;
+    let contents =
+        diagnostics.into_iter().map(|it| it.to_string()).collect::<Vec<_>>().join("\n\n");
+    let contents = format!("//{}\n{}\n", PREAMBLE, contents.trim());
+    let dst = project_root().join("docs/user/generated_diagnostic.adoc");
+    codegen::update(&dst, &contents, mode)?;
+    Ok(())
+}
+
+#[derive(Debug)]
+struct Diagnostic {
+    id: String,
+    location: Location,
+    doc: String,
+}
+
+impl Diagnostic {
+    fn collect() -> Result<Vec<Diagnostic>> {
+        let mut res = Vec::new();
+        for path in rust_files(&project_root()) {
+            collect_file(&mut res, path)?;
+        }
+        res.sort_by(|lhs, rhs| lhs.id.cmp(&rhs.id));
+        return Ok(res);
+
+        fn collect_file(acc: &mut Vec<Diagnostic>, path: PathBuf) -> Result<()> {
+            let text = xshell::read_file(&path)?;
+            let comment_blocks = extract_comment_blocks_with_empty_lines("Diagnostic", &text);
+
+            for block in comment_blocks {
+                let id = block.id;
+                if let Err(msg) = is_valid_diagnostic_name(&id) {
+                    panic!("invalid diagnostic name: {:?}:\n  {}", id, msg)
+                }
+                let doc = block.contents.join("\n");
+                let location = Location::new(path.clone(), block.line);
+                acc.push(Diagnostic { id, location, doc })
+            }
+
+            Ok(())
+        }
+    }
+}
+
+fn is_valid_diagnostic_name(diagnostic: &str) -> Result<(), String> {
+    let diagnostic = diagnostic.trim();
+    if diagnostic.find(char::is_whitespace).is_some() {
+        return Err("Diagnostic names can't contain whitespace symbols".into());
+    }
+    if diagnostic.chars().any(|c| c.is_ascii_uppercase()) {
+        return Err("Diagnostic names can't contain uppercase symbols".into());
+    }
+    if diagnostic.chars().any(|c| !c.is_ascii()) {
+        return Err("Diagnostic can't contain non-ASCII symbols".into());
+    }
+
+    Ok(())
+}
+
+impl fmt::Display for Diagnostic {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        writeln!(f, "=== {}\n**Source:** {}\n{}", self.id, self.location, self.doc)
+    }
+}
diff --git a/xtask/src/codegen/gen_feature_docs.rs b/xtask/src/codegen/gen_feature_docs.rs
index 341e67c733..065dd33f15 100644
--- a/xtask/src/codegen/gen_feature_docs.rs
+++ b/xtask/src/codegen/gen_feature_docs.rs
@@ -1,6 +1,6 @@
 //! Generates `assists.md` documentation.
 
-use std::{fmt, fs, path::PathBuf};
+use std::{fmt, path::PathBuf};
 
 use crate::{
     codegen::{self, extract_comment_blocks_with_empty_lines, Location, Mode, PREAMBLE},
@@ -33,7 +33,7 @@ impl Feature {
         return Ok(res);
 
         fn collect_file(acc: &mut Vec<Feature>, path: PathBuf) -> Result<()> {
-            let text = fs::read_to_string(&path)?;
+            let text = xshell::read_file(&path)?;
             let comment_blocks = extract_comment_blocks_with_empty_lines("Feature", &text);
 
             for block in comment_blocks {
diff --git a/xtask/src/release.rs b/xtask/src/release.rs
index 14fc1f0ddf..3cf0d849f4 100644
--- a/xtask/src/release.rs
+++ b/xtask/src/release.rs
@@ -52,7 +52,14 @@ https://github.com/sponsors/rust-analyzer[GitHub Sponsors].
         let path = changelog_dir.join(format!("{}-changelog-{}.adoc", today, changelog_n));
         write_file(&path, &contents)?;
 
-        for &adoc in ["manual.adoc", "generated_features.adoc", "generated_assists.adoc"].iter() {
+        for &adoc in [
+            "manual.adoc",
+            "generated_features.adoc",
+            "generated_assists.adoc",
+            "generated_diagnostic.adoc",
+        ]
+        .iter()
+        {
             let src = project_root().join("./docs/user/").join(adoc);
             let dst = website_root.join(adoc);
             cp(src, dst)?;


Test patch:
diff --git a/xtask/src/codegen/gen_parser_tests.rs b/xtask/src/codegen/gen_parser_tests.rs
index 96fdd92169..19ae949d4a 100644
--- a/xtask/src/codegen/gen_parser_tests.rs
+++ b/xtask/src/codegen/gen_parser_tests.rs
@@ -124,7 +124,7 @@ fn existing_tests(dir: &Path, ok: bool) -> Result<HashMap<String, (PathBuf, Test
             let file_name = path.file_name().unwrap().to_str().unwrap();
             file_name[5..file_name.len() - 3].to_string()
         };
-        let text = fs::read_to_string(&path)?;
+        let text = xshell::read_file(&path)?;
         let test = Test { name: name.clone(), text, ok };
         if let Some(old) = res.insert(name, (path, test)) {
             println!("Duplicate test: {:?}", old);
diff --git a/xtask/tests/tidy.rs b/xtask/tests/tidy.rs
index 4600694074..faaef2fd45 100644
--- a/xtask/tests/tidy.rs
+++ b/xtask/tests/tidy.rs
@@ -42,6 +42,7 @@ fn smoke_test_docs_generation() {
     // We don't commit docs to the repo, so we can just overwrite in tests.
     codegen::generate_assists_docs(Mode::Overwrite).unwrap();
     codegen::generate_feature_docs(Mode::Overwrite).unwrap();
+    codegen::generate_diagnostic_docs(Mode::Overwrite).unwrap();
 }
 
 #[test]