Skip to main content

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

1use ra_ap_syntax::{
2    AstNode,
3    ast::{self, HasArgList, HasName, HasVisibility, VisibilityKind},
4};
5
6use super::support::{doc_lines, has_heading, is_item_or_impl_fn};
7use crate::{AstCtx, Example, Violation};
8
9/// Pass/fail cases for `example_tests!`.
10#[rustfmt::skip]
11const EXAMPLES: &[Example] = &[
12    Example {
13        label: "documented fn with unwrap and no Panics section",
14        code: "/// Reads the value.\npub fn f(x: Option<u32>) -> u32 { x.unwrap() }",
15        pass: false,
16    },
17    Example {
18        label: "documented fn with unwrap and Panics section",
19        code: "/// Reads the value.\n///\n/// # Panics\n/// Panics when `x` is `None`.\npub fn f(x: Option<u32>) -> u32 { x.unwrap() }",
20        pass: true,
21    },
22    Example {
23        label: "undocumented fn with unwrap",
24        code: "pub fn f(x: Option<u32>) -> u32 { x.unwrap() }",
25        pass: true,
26    },
27    Example {
28        label: "documented fn without panic sources",
29        code: "/// Adds one.\npub fn f(x: u32) -> u32 { x.saturating_add(1) }",
30        pass: true,
31    },
32    Example {
33        label: "expect counts as a panic source",
34        code: "/// Reads the value.\npub fn f(x: Option<u32>) -> u32 { x.expect(\"present\") }",
35        pass: false,
36    },
37    Example {
38        label: "panic macro counts",
39        code: "/// Never returns.\npub fn f() { panic!(\"boom\"); }",
40        pass: false,
41    },
42    Example {
43        label: "assert macros count",
44        code: "/// Validates input.\npub fn f(x: u32, y: u32) { assert_eq!(x, y, \"mismatch\"); }",
45        pass: false,
46    },
47    Example {
48        label: "unreachable counts",
49        code: "/// Dispatches.\npub fn f(x: bool) { if x { unreachable!(); } }",
50        pass: false,
51    },
52    Example {
53        label: "debug_assert does not count",
54        code: "/// Validates input.\npub fn f(x: bool) { debug_assert!(x, \"must hold\"); }",
55        pass: true,
56    },
57    Example {
58        label: "private documented fn with unwrap",
59        code: "/// Reads the value.\nfn f(x: Option<u32>) -> u32 { x.unwrap() }",
60        pass: true,
61    },
62    Example {
63        label: "impl fn with unwrap and no Panics section",
64        code: "struct S;\nimpl S {\n    /// Reads the value.\n    pub fn f(&self, x: Option<u32>) -> u32 { x.unwrap() }\n}",
65        pass: false,
66    },
67    Example {
68        label: "unwrap inside nested item is not the fn's contract",
69        code: "/// Delegates.\npub fn f() { fn inner() { None::<u32>.unwrap(); } }",
70        pass: true,
71    },
72    Example {
73        label: "test module fn",
74        code: "#[cfg(test)]\nmod tests {\n    /// Reads the value.\n    pub fn f(x: Option<u32>) -> u32 { x.unwrap() }\n}",
75        pass: true,
76    },
77];
78
79crate::ast_rule!(
80    doc_panics_section,
81    "Require a `# Panics` section on documented pub fns that can panic.",
82    "A documented fn that may panic must state when under `# Panics` (M-CANONICAL-DOCS).",
83    Medium,
84);
85
86/// Macro names whose invocation may panic at runtime.
87const PANIC_MACROS: &[&str] = &["panic", "assert", "assert_eq", "assert_ne", "unreachable"];
88
89fn check_doc_panics_section(ctx: &AstCtx<'_>) -> Vec<Violation> {
90    ctx.nodes::<ast::Fn>()
91        .filter(is_item_or_impl_fn)
92        .filter(|function| !ctx.is_in_test(function) && is_fully_public(function))
93        .filter(can_panic)
94        .filter_map(|function| {
95            let docs = doc_lines(&function);
96
97            if docs.is_empty() || has_heading(&docs, "Panics") {
98                return None;
99            }
100
101            let name = function.name()?;
102
103            Some(ctx.violation(
104                &name,
105                format!(
106                    "documented `{}` can panic but its docs have no `# Panics` section",
107                    name.text()
108                ),
109            ))
110        })
111        .collect()
112}
113
114fn is_fully_public(function: &ast::Fn) -> bool {
115    function
116        .visibility()
117        .is_some_and(|visibility| matches!(visibility.kind(), VisibilityKind::Pub))
118}
119
120fn can_panic(function: &ast::Fn) -> bool {
121    let Some(body) = function.body() else {
122        return false;
123    };
124    let method_can_panic = body
125        .syntax()
126        .descendants()
127        .filter_map(ast::MethodCallExpr::cast)
128        .filter(|call| !inside_nested_item(call, function))
129        .any(|call| {
130            call.name_ref().is_some_and(|name| {
131                name.text() == "expect"
132                    || (name.text() == "unwrap"
133                        && call
134                            .arg_list()
135                            .is_none_or(|args| args.args().next().is_none()))
136            })
137        });
138
139    method_can_panic
140        || body
141            .syntax()
142            .descendants()
143            .filter_map(ast::MacroCall::cast)
144            .filter(|call| !inside_nested_item(call, function))
145            .any(|call| {
146                let name = call
147                    .path()
148                    .and_then(|path| path.segment())
149                    .and_then(|segment| segment.name_ref());
150
151                name.is_some_and(|name| PANIC_MACROS.contains(&name.text().as_str()))
152            })
153}
154
155fn inside_nested_item<N>(node: &N, function: &ast::Fn) -> bool
156where
157    N: AstNode,
158{
159    node.syntax()
160        .ancestors()
161        .skip(1)
162        .take_while(|ancestor| ancestor != function.syntax())
163        .any(|ancestor| ast::Item::can_cast(ancestor.kind()))
164}
165
166crate::tidy_ast_test!(check_doc_panics_section, {
167    crate::example_tests!(EXAMPLES, check_doc_panics_section);
168});