wowlab_tidy/languages/rust/rules/safety/
unsafe_fn_safety_doc.rs1use ra_ap_syntax::ast::{self, HasAttrs, HasDocComments, HasName, LiteralKind};
2
3use super::super::support::is_item_or_impl_fn;
4use crate::{AstCtx, Example, Violation, infra::helpers};
5
6#[rustfmt::skip]
7const EXAMPLES: &[Example] = &[
8 Example {
9 label: "unsafe fn without safety docs",
10 code: "unsafe fn f(x: u32) -> u32 { x }",
11 pass: false,
12 },
13 Example {
14 label: "private unsafe fn without safety docs",
15 code: "pub(crate) unsafe fn f() {}",
16 pass: false,
17 },
18 Example {
19 label: "unsafe method without safety docs",
20 code: "struct S;\nimpl S {\n unsafe fn m(&self) {}\n}",
21 pass: false,
22 },
23 Example {
24 label: "unsafe fn with Safety doc section",
25 code: "/// Reads the value behind `p`.\n///\n/// # Safety\n/// `p` must be valid and aligned.\nunsafe fn f(p: *const u8) -> u8 { unsafe { *p } }",
26 pass: true,
27 },
28 Example {
29 label: "unsafe fn with SAFETY comment",
30 code: "// SAFETY: callers uphold the invariants documented on the module\nunsafe fn f() {}",
31 pass: true,
32 },
33 Example {
34 label: "safe fn needs nothing",
35 code: "fn f(x: u32) -> u32 { x }",
36 pass: true,
37 },
38 Example {
39 label: "extern block fns are exempt",
40 code: "extern \"C\" {\n fn ffi(x: u32) -> u32;\n}",
41 pass: true,
42 },
43 Example {
44 label: "unsafe fn in test module",
45 code: "#[cfg(test)]\nmod tests {\n unsafe fn t() {}\n}",
46 pass: true,
47 },
48];
49
50crate::ast_rule!(
51 unsafe_fn_safety_doc,
52 "Require a `# Safety` doc section or `// SAFETY:` comment on every `unsafe fn`.",
53 "Callers cannot uphold contracts that are not written down, and clippy's missing_safety_doc only covers public functions (M-UNSAFE, M-CANONICAL-DOCS).",
54 High,
55);
56
57fn check_unsafe_fn_safety_doc(ctx: &AstCtx<'_>) -> Vec<Violation> {
58 let unsafe_functions = ctx
59 .nodes::<ast::Fn>()
60 .filter(is_item_or_impl_fn)
61 .filter(|function| function.unsafe_token().is_some() && !ctx.is_in_test(function))
62 .filter(|function| !has_safety_heading(function));
63
64 unsafe_functions
65 .filter_map(|function| {
66 let line = function.attrs().next().map_or_else(
67 || {
68 function.fn_token().map_or_else(
69 || ctx.line_of(&function),
70 |token| {
71 ctx.line_index.line_col(token.text_range().start()).line as usize + 1
72 },
73 )
74 },
75 |attr| ctx.line_of(&attr),
76 );
77
78 if helpers::has_preceding_comment(ctx.file.lines, line, &["SAFETY:"]) {
79 return None;
80 }
81
82 let name = function.name()?;
83
84 Some(ctx.violation(
85 &name,
86 format!(
87 "unsafe fn `{}` lacks both a `# Safety` doc section and a `// SAFETY:` comment",
88 name.text()
89 ),
90 ))
91 })
92 .collect()
93}
94
95fn has_safety_heading(function: &ast::Fn) -> bool {
96 function.doc_comments().any(|comment| {
97 comment
98 .doc_comment()
99 .is_some_and(|(text, _)| text.contains("# Safety"))
100 }) || function.attrs().any(|attr| {
101 let Some(ast::Meta::KeyValueMeta(meta)) = attr.meta() else {
102 return false;
103 };
104 let name = meta.path()
105 .and_then(|path| path.segment())
106 .and_then(|segment| segment.name_ref());
107 let is_doc = name.is_some_and(|name| name.text() == "doc");
108 let Some(ast::Expr::Literal(literal)) = meta.expr() else {
109 return false;
110 };
111
112 is_doc
113 && matches!(literal.kind(), LiteralKind::String(text) if text.value().is_ok_and(|value| value.contains("# Safety")))
114 })
115}
116
117crate::tidy_ast_test!(check_unsafe_fn_safety_doc, {
118 crate::example_tests!(EXAMPLES, check_unsafe_fn_safety_doc);
119});