wowlab_tidy/languages/rust/rules/docs/
doc_panics_section.rs1use 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#[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
86const 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});