Skip to main content

wowlab_tidy/languages/rust/rules/docs/
doc_errors_section.rs

1use 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/// Pass/fail cases for `example_tests!`.
7#[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});