Skip to main content

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

1use ra_ap_syntax::{
2    AstNode,
3    ast::{self, HasAttrs, HasDocComments, HasName, HasVisibility, LiteralKind, VisibilityKind},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8/// Pass/fail cases for `example_tests!`.
9#[rustfmt::skip]
10const EXAMPLES: &[Example] = &[
11    Example {
12        label: "undocumented const with numeric literal",
13        code: "const TIMEOUT_SECS: u64 = 30;",
14        pass: false,
15    },
16    Example {
17        label: "undocumented static with string literal",
18        code: "static ENDPOINT: &str = \"primary\";",
19        pass: false,
20    },
21    Example {
22        label: "literal nested in a call",
23        code: "const RETRY: Duration = Duration::from_secs(30);",
24        pass: false,
25    },
26    Example {
27        label: "doc comment explains the value",
28        code: "/// Upstream aborts after thirty seconds.\nconst TIMEOUT_SECS: u64 = 30;",
29        pass: true,
30    },
31    Example {
32        label: "line comment above explains the value",
33        code: "// Matches the upstream timeout policy.\nconst TIMEOUT_SECS: u64 = 30;",
34        pass: true,
35    },
36    Example {
37        label: "comment above the attribute",
38        code: "// Fixture table kept verbatim.\n#[rustfmt::skip]\nconst NAMES: &[&str] = &[\"a\"];",
39        pass: true,
40    },
41    Example {
42        label: "pub const is pub_api_docs territory",
43        code: "pub const TIMEOUT_SECS: u64 = 30;",
44        pass: true,
45    },
46    Example {
47        label: "pub(crate) const still needs a doc",
48        code: "pub(crate) const TIMEOUT_SECS: u64 = 30;",
49        pass: false,
50    },
51    Example {
52        label: "no literal in initializer",
53        code: "const SIZE: usize = std::mem::size_of::<u64>();",
54        pass: true,
55    },
56    Example {
57        label: "const in test module",
58        code: "#[cfg(test)]\nmod tests {\n    const TIMEOUT_SECS: u64 = 30;\n}",
59        pass: true,
60    },
61    Example {
62        label: "function-local const is not module-level",
63        code: "fn f() -> u64 {\n    const LOCAL: u64 = 30;\n    LOCAL\n}",
64        pass: true,
65    },
66];
67
68crate::ast_rule!(
69    const_needs_doc,
70    "Require a doc or line comment on private consts and statics holding literal values.",
71    "Magic values need context: why the value was chosen and what depends on it (M-DOCUMENTED-MAGIC).",
72    Low,
73);
74
75fn check_const_needs_doc(ctx: &AstCtx<'_>) -> Vec<Violation> {
76    let mut violations = Vec::new();
77
78    for item in ctx.nodes::<ast::Const>() {
79        check_literal_item(ctx, &item, "const", item.body(), &mut violations);
80    }
81
82    for item in ctx.nodes::<ast::Static>() {
83        check_literal_item(ctx, &item, "static", item.body(), &mut violations);
84    }
85
86    violations
87}
88
89fn check_literal_item<N>(
90    ctx: &AstCtx<'_>,
91    item: &N,
92    kind: &str,
93    body: Option<ast::Expr>,
94    violations: &mut Vec<Violation>,
95) where
96    N: AstNode + HasAttrs + HasDocComments + HasName + HasVisibility,
97{
98    let Some(name) = item.name() else {
99        return;
100    };
101
102    if is_exempt_item(ctx, item)
103        || item
104            .visibility()
105            .is_some_and(|visibility| matches!(visibility.kind(), VisibilityKind::Pub))
106        || has_docs(item)
107        || body.is_none_or(|body| !contains_magic_literal(&body))
108        || has_comment_above(ctx, item, &name)
109    {
110        return;
111    }
112
113    violations.push(ctx.violation(
114        &name,
115        format!(
116            "{kind} `{}` holds a literal value without a doc or comment explaining it",
117            name.text()
118        ),
119    ));
120}
121
122fn is_exempt_item<N>(ctx: &AstCtx<'_>, item: &N) -> bool
123where
124    N: AstNode,
125{
126    ctx.is_in_test(item)
127        || item.syntax().ancestors().skip(1).any(|ancestor| {
128            ast::Fn::can_cast(ancestor.kind())
129                || ast::AssocItemList::can_cast(ancestor.kind())
130                || ast::ExternItemList::can_cast(ancestor.kind())
131        })
132}
133
134fn has_docs(item: &impl HasDocComments) -> bool {
135    item.doc_comments().next().is_some()
136        || item
137            .attrs()
138            .any(|attr| attr.simple_name().as_deref() == Some("doc"))
139}
140
141fn has_comment_above<N>(ctx: &AstCtx<'_>, item: &N, name: &ast::Name) -> bool
142where
143    N: AstNode + HasAttrs,
144{
145    let first_line = item
146        .attrs()
147        .next()
148        .map_or_else(|| ctx.line_of(name), |attr| ctx.line_of(&attr));
149
150    ctx.file
151        .line(first_line.saturating_sub(1))
152        .is_some_and(|line| line.trim().starts_with("//"))
153}
154
155fn contains_magic_literal(expr: &ast::Expr) -> bool {
156    expr.syntax()
157        .descendants()
158        .filter_map(ast::Literal::cast)
159        .any(|literal| {
160            matches!(
161                literal.kind(),
162                LiteralKind::IntNumber(_) | LiteralKind::FloatNumber(_) | LiteralKind::String(_)
163            )
164        })
165}
166
167crate::tidy_ast_test!(check_const_needs_doc, {
168    crate::example_tests!(EXAMPLES, check_const_needs_doc);
169});