wowlab_tidy/languages/rust/rules/docs/
doc_errors_section.rs1use ra_ap_syntax::ast::{self, HasName, HasVisibility, VisibilityKind};
2
3use super::support::{doc_lines, has_heading, is_item_or_impl_fn};
4use crate::{AstCtx, Example, Violation};
5
6#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9 Example {
10 label: "documented Result fn without Errors section",
11 code: "/// Parses input.\npub fn f() -> Result<(), Error> { Ok(()) }",
12 pass: false,
13 },
14 Example {
15 label: "documented Result fn with Errors section",
16 code: "/// Parses input.\n///\n/// # Errors\n/// Fails on malformed input.\npub fn f() -> Result<(), Error> { Ok(()) }",
17 pass: true,
18 },
19 Example {
20 label: "undocumented Result fn is pub_api_docs territory",
21 code: "pub fn f() -> Result<(), Error> { Ok(()) }",
22 pass: true,
23 },
24 Example {
25 label: "documented fn without Result",
26 code: "/// Adds one.\npub fn f(x: u32) -> u32 { x }",
27 pass: true,
28 },
29 Example {
30 label: "private documented Result fn",
31 code: "/// Parses input.\nfn f() -> Result<(), Error> { Ok(()) }",
32 pass: true,
33 },
34 Example {
35 label: "Result type alias counts",
36 code: "/// Reads bytes.\npub fn f() -> io::Result<()> { Ok(()) }",
37 pass: false,
38 },
39 Example {
40 label: "impl fn without Errors section",
41 code: "struct S;\nimpl S {\n /// Parses input.\n pub fn f(&self) -> Result<(), Error> { Ok(()) }\n}",
42 pass: false,
43 },
44 Example {
45 label: "impl fn with Errors section",
46 code: "struct S;\nimpl S {\n /// Parses input.\n ///\n /// # Errors\n /// Fails on malformed input.\n pub fn f(&self) -> Result<(), Error> { Ok(()) }\n}",
47 pass: true,
48 },
49 Example {
50 label: "doc(hidden) has no doc text",
51 code: "#[doc(hidden)]\npub fn f() -> Result<(), Error> { Ok(()) }",
52 pass: true,
53 },
54 Example {
55 label: "test module fn",
56 code: "#[cfg(test)]\nmod tests {\n /// Parses input.\n pub fn f() -> Result<(), Error> { Ok(()) }\n}",
57 pass: true,
58 },
59];
60
61crate::ast_rule!(
62 doc_errors_section,
63 "Require a `# Errors` section on documented pub fns returning `Result`.",
64 "Callers need failure conditions listed; canonical docs put them under `# Errors` (M-CANONICAL-DOCS).",
65 Medium,
66);
67
68fn check_doc_errors_section(ctx: &AstCtx<'_>) -> Vec<Violation> {
69 ctx.nodes::<ast::Fn>()
70 .filter(is_item_or_impl_fn)
71 .filter(|function| !ctx.is_in_test(function) && is_fully_public(function))
72 .filter(returns_result)
73 .filter_map(|function| {
74 let docs = doc_lines(&function);
75
76 if docs.is_empty() || has_heading(&docs, "Errors") {
77 return None;
78 }
79
80 let name = function.name()?;
81
82 Some(ctx.violation(
83 &name,
84 format!(
85 "documented `{}` returns `Result` but its docs have no `# Errors` section",
86 name.text()
87 ),
88 ))
89 })
90 .collect()
91}
92
93fn is_fully_public(function: &ast::Fn) -> bool {
94 function
95 .visibility()
96 .is_some_and(|visibility| matches!(visibility.kind(), VisibilityKind::Pub))
97}
98
99fn returns_result(function: &ast::Fn) -> bool {
100 let Some(ast::Type::PathType(path_type)) = function.ret_type().and_then(|ret| ret.ty()) else {
101 return false;
102 };
103
104 let name = path_type
105 .path()
106 .and_then(|path| path.segment())
107 .and_then(|segment| segment.name_ref());
108
109 name.is_some_and(|name| name.text().ends_with("Result"))
110}
111
112crate::tidy_ast_test!(check_doc_errors_section, {
113 crate::example_tests!(EXAMPLES, check_doc_errors_section);
114});