Skip to main content

wowlab_tidy/languages/rust/rules/safety/
unsafe_fn_safety_doc.rs

1use 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});