wowlab_tidy/languages/rust/rules/api/
missing_debug.rs1use 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});