Skip to main content

wowlab_tidy/languages/rust/rules/api/
pub_api_docs.rs

1use ra_ap_syntax::{
2    AstNode,
3    ast::{self, HasAttrs, HasDocComments, HasName, HasVisibility, VisibilityKind},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10    Example {
11        label: "undocumented pub fn",
12        code: "pub fn foo() {}",
13        pass: false,
14    },
15    Example {
16        label: "documented pub fn",
17        code: "/// Does something.\npub fn foo() {}",
18        pass: true,
19    },
20    Example {
21        label: "private fn",
22        code: "fn foo() {}",
23        pass: true,
24    },
25    Example {
26        label: "pub(crate) fn",
27        code: "pub(crate) fn foo() {}",
28        pass: true,
29    },
30    Example {
31        label: "doc hidden pub fn",
32        code: "#[doc(hidden)]\npub fn foo() {}",
33        pass: true,
34    },
35    Example {
36        label: "undocumented pub struct",
37        code: "pub struct S;",
38        pass: false,
39    },
40    Example {
41        label: "undocumented pub enum",
42        code: "pub enum E { A }",
43        pass: false,
44    },
45    Example {
46        label: "undocumented pub trait",
47        code: "pub trait T {}",
48        pass: false,
49    },
50    Example {
51        label: "undocumented pub type alias",
52        code: "pub type A = u8;",
53        pass: false,
54    },
55    Example {
56        label: "undocumented pub const",
57        code: "pub const C: u8 = 1;",
58        pass: false,
59    },
60    Example {
61        label: "undocumented pub static",
62        code: "pub static S: u8 = 1;",
63        pass: false,
64    },
65    Example {
66        label: "documented pub struct",
67        code: "/// A struct.\npub struct S;",
68        pass: true,
69    },
70    Example {
71        label: "pub(crate) struct not fully public",
72        code: "pub(crate) struct S;",
73        pass: true,
74    },
75    Example {
76        label: "doc hidden pub struct",
77        code: "#[doc(hidden)]\npub struct S;",
78        pass: true,
79    },
80    Example {
81        label: "doc name-value attr pub struct",
82        code: "#[doc = \"x\"]\npub struct S;",
83        pass: true,
84    },
85];
86
87crate::ast_rule!(
88    pub_api_docs,
89    "Require doc comments on public items.",
90    "Undocumented public items force users to read source code. Doc comments generate searchable API documentation.",
91);
92
93fn check_pub_api_docs(ctx: &AstCtx<'_>) -> Vec<Violation> {
94    let functions = ctx
95        .nodes::<ast::Fn>()
96        .filter_map(|item| missing_docs(ctx, &item, "function"));
97    let structs = ctx
98        .nodes::<ast::Struct>()
99        .filter_map(|item| missing_docs(ctx, &item, "struct"));
100    let enums = ctx
101        .nodes::<ast::Enum>()
102        .filter_map(|item| missing_docs(ctx, &item, "enum"));
103    let traits = ctx
104        .nodes::<ast::Trait>()
105        .filter_map(|item| missing_docs(ctx, &item, "trait"));
106    let aliases = ctx
107        .nodes::<ast::TypeAlias>()
108        .filter_map(|item| missing_docs(ctx, &item, "type alias"));
109    let constants = ctx
110        .nodes::<ast::Const>()
111        .filter_map(|item| missing_docs(ctx, &item, "const"));
112    let statics = ctx
113        .nodes::<ast::Static>()
114        .filter_map(|item| missing_docs(ctx, &item, "static"));
115
116    functions
117        .chain(structs)
118        .chain(enums)
119        .chain(traits)
120        .chain(aliases)
121        .chain(constants)
122        .chain(statics)
123        .collect()
124}
125
126fn missing_docs<T>(ctx: &AstCtx<'_>, item: &T, kind: &str) -> Option<Violation>
127where
128    T: AstNode + HasAttrs + HasDocComments + HasName + HasVisibility,
129{
130    if ctx.is_in_test(item)
131        || !item
132            .visibility()
133            .is_some_and(|vis| matches!(vis.kind(), VisibilityKind::Pub))
134        || item.doc_comments().next().is_some()
135        || item
136            .attrs()
137            .any(|attr| attr.simple_name().as_deref() == Some("doc"))
138    {
139        return None;
140    }
141
142    let name = item.name()?;
143
144    Some(ctx.violation(
145        &name,
146        format!("public {kind} `{name}` is missing documentation"),
147    ))
148}
149
150crate::tidy_ast_test!(check_pub_api_docs, {
151    crate::example_tests!(EXAMPLES, check_pub_api_docs);
152});