Skip to main content

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

1// #t(file: rust_default_hasher) small per-file scan set on a cold path; capacity and hasher tuning buy nothing
2
3use std::collections::HashSet;
4
5use ra_ap_syntax::{
6    AstNode,
7    ast::{self, HasAttrs, HasName, HasVisibility, VisibilityKind},
8};
9
10use super::support::{has_derive, type_name};
11use crate::{AstCtx, Example, Fix, Violation};
12
13#[rustfmt::skip]
14const EXAMPLES: &[Example] = &[
15    Example {
16        label: "pub struct without Debug",
17        code: "pub struct Foo {}",
18        pass: false,
19    },
20    Example {
21        label: "pub struct with Debug",
22        code: "#[derive(Debug)]\npub struct Foo {}",
23        pass: true,
24    },
25    Example {
26        label: "private struct",
27        code: "struct Foo {}",
28        pass: true,
29    },
30    Example {
31        label: "pub(crate) struct",
32        code: "pub(crate) struct Foo {}",
33        pass: true,
34    },
35    Example {
36        label: "pub enum without Debug",
37        code: "pub enum E { A, B }",
38        pass: false,
39    },
40    Example {
41        label: "pub enum with Debug",
42        code: "#[derive(Debug)]\npub enum E { A, B }",
43        pass: true,
44    },
45    Example {
46        label: "pub struct in test module",
47        code: "#[cfg(test)]\nmod tests {\n  pub struct Foo {}\n}",
48        pass: true,
49    },
50    Example {
51        label: "derive with other traits",
52        code: "#[derive(Clone, Debug, PartialEq)]\npub struct Foo {}",
53        pass: true,
54    },
55    Example {
56        label: "manual Debug impl",
57        code: "pub struct Bar {}\nimpl std::fmt::Debug for Bar {\n  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n    f.debug_struct(\"Bar\").finish()\n  }\n}",
58        pass: true,
59    },
60];
61
62crate::ast_rule!(
63    missing_debug,
64    "Require `#[derive(Debug)]` on public structs and enums.",
65    "Public types without Debug are hard to inspect during development and cannot be used in assert messages.",
66    Low,
67    fix_missing_debug,
68);
69
70fn check_missing_debug(ctx: &AstCtx<'_>) -> Vec<Violation> {
71    let manual_impls = collect_manual_debug_impls(ctx);
72    let structs = ctx
73        .nodes::<ast::Struct>()
74        .filter_map(|item| missing_debug(ctx, &item, "struct", &manual_impls));
75    let enums = ctx
76        .nodes::<ast::Enum>()
77        .filter_map(|item| missing_debug(ctx, &item, "enum", &manual_impls));
78
79    structs.chain(enums).collect()
80}
81
82fn collect_manual_debug_impls(ctx: &AstCtx<'_>) -> HashSet<String> {
83    ctx.nodes::<ast::Impl>()
84        .filter(|item| {
85            item.trait_()
86                .and_then(|ty| type_name(&ty))
87                .is_some_and(|name| name == "Debug")
88        })
89        .filter_map(|item| item.self_ty().and_then(|ty| type_name(&ty)))
90        .collect()
91}
92
93fn missing_debug<T>(
94    ctx: &AstCtx<'_>,
95    item: &T,
96    kind: &str,
97    manual_impls: &HashSet<String>,
98) -> Option<Violation>
99where
100    T: AstNode + HasAttrs + HasName + HasVisibility,
101{
102    let name = item.name()?;
103
104    if ctx.is_in_test(item)
105        || !item
106            .visibility()
107            .is_some_and(|vis| matches!(vis.kind(), VisibilityKind::Pub))
108        || has_derive(item, "Debug")
109        || manual_impls.contains(name.text().as_str())
110    {
111        return None;
112    }
113
114    Some(ctx.violation(
115        &name,
116        format!("public {kind} `{name}` is missing #[derive(Debug)]"),
117    ))
118}
119
120fn fix_missing_debug(ctx: &AstCtx<'_>, v: &Violation) -> Option<Fix> {
121    let line = ctx.file.line(v.line)?;
122    let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect();
123    let derive = format!("{indent}#[derive(Debug)]");
124
125    Some(Fix::replace_line(v.line, format!("{derive}\n{line}")))
126}
127
128crate::tidy_ast_test!(check_missing_debug, {
129    crate::example_tests!(EXAMPLES, check_missing_debug);
130    crate::fix_tests!(ast, check_missing_debug, fix_missing_debug);
131});