wowlab_tidy/languages/rust/rules/api/
error_missing_traits.rs1use ra_ap_syntax::{
2 AstNode,
3 ast::{self, HasAttrs, HasName, HasVisibility, VisibilityKind},
4};
5use wowlab_types::sim::FastSet;
6
7use super::support::type_name;
8use crate::{AstCtx, Example, Violation};
9
10#[rustfmt::skip]
11const EXAMPLES: &[Example] = &[
12 Example {
13 label: "error struct without traits",
14 code: "pub struct ParseError { line: usize }",
15 pass: false,
16 },
17 Example {
18 label: "error struct with Display only",
19 code: "pub struct ParseError;\nimpl std::fmt::Display for ParseError {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n f.write_str(\"parse error\")\n }\n}",
20 pass: false,
21 },
22 Example {
23 label: "error struct with Display and Error",
24 code: "pub struct ParseError;\nimpl std::fmt::Display for ParseError {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n f.write_str(\"parse error\")\n }\n}\nimpl std::error::Error for ParseError {}",
25 pass: true,
26 },
27 Example {
28 label: "thiserror derive on struct",
29 code: "#[derive(Debug, thiserror::Error)]\n#[error(\"parse failed\")]\npub struct ParseError;",
30 pass: true,
31 },
32 Example {
33 label: "thiserror derive on enum",
34 code: "#[derive(Debug, Error)]\npub enum FetchError {\n #[error(\"io failed\")]\n Io,\n}",
35 pass: true,
36 },
37 Example {
38 label: "private error struct",
39 code: "struct ParseError;",
40 pass: true,
41 },
42 Example {
43 label: "pub struct without error suffix",
44 code: "pub struct Parser { pos: usize }",
45 pass: true,
46 },
47 Example {
48 label: "error struct in test module",
49 code: "#[cfg(test)]\nmod tests {\n pub struct ParseError;\n}",
50 pass: true,
51 },
52];
53
54crate::ast_rule!(
55 error_missing_traits,
56 "Require `Display` and `std::error::Error` on public `*Error` types.",
57 "std::error::Error mandates Display, and error types without both cannot participate in ?-chains, error reporting, or dyn Error composition.",
58 Medium,
59);
60
61fn check_error_missing_traits(ctx: &AstCtx<'_>) -> Vec<Violation> {
62 let (display_impls, error_impls) = collect_trait_impls(ctx);
63 let structs = ctx
64 .nodes::<ast::Struct>()
65 .flat_map(|item| check_error_item(ctx, &item, false, &display_impls, &error_impls));
66 let enums = ctx.nodes::<ast::Enum>().flat_map(|item| {
67 let variant_error_attrs = item
68 .variant_list()
69 .is_some_and(|variants| variants.variants().any(|variant| has_error_attr(&variant)));
70
71 check_error_item(
72 ctx,
73 &item,
74 variant_error_attrs,
75 &display_impls,
76 &error_impls,
77 )
78 });
79
80 structs.chain(enums).collect()
81}
82
83fn collect_trait_impls(ctx: &AstCtx<'_>) -> (FastSet<String>, FastSet<String>) {
84 let mut display_impls = FastSet::default();
85 let mut error_impls = FastSet::default();
86
87 for item in ctx.nodes::<ast::Impl>() {
88 let Some(trait_name) = item.trait_().and_then(|ty| type_name(&ty)) else {
89 continue;
90 };
91 let Some(type_name) = item.self_ty().and_then(|ty| type_name(&ty)) else {
92 continue;
93 };
94
95 if trait_name == "Display" {
96 display_impls.insert(type_name);
97 } else if trait_name == "Error" {
98 error_impls.insert(type_name);
99 }
100 }
101
102 (display_impls, error_impls)
103}
104
105fn has_error_derive(item: &impl HasAttrs) -> bool {
106 item.attrs().any(|attr| {
107 attr.as_simple_call().is_some_and(|(name, tokens)| {
108 name == "derive"
109 && tokens.syntax().text().to_string().split(',').any(|entry| {
110 entry
111 .trim_matches(|ch: char| ch.is_whitespace() || matches!(ch, '(' | ')'))
112 .rsplit("::")
113 .next()
114 == Some("Error")
115 })
116 })
117 })
118}
119
120fn has_error_attr(item: &impl HasAttrs) -> bool {
121 item.attrs()
122 .any(|attr| attr.simple_name().as_deref() == Some("error"))
123}
124
125fn check_error_item<T>(
126 ctx: &AstCtx<'_>,
127 item: &T,
128 variant_error_attrs: bool,
129 display_impls: &FastSet<String>,
130 error_impls: &FastSet<String>,
131) -> Vec<Violation>
132where
133 T: AstNode + HasAttrs + HasName + HasVisibility,
134{
135 let Some(name_node) = item.name() else {
136 return Vec::new();
137 };
138 let name = name_node.text().to_string();
139
140 if ctx.is_in_test(item)
141 || !item
142 .visibility()
143 .is_some_and(|vis| matches!(vis.kind(), VisibilityKind::Pub))
144 || !name.ends_with("Error")
145 {
146 return Vec::new();
147 }
148
149 let derive_error = has_error_derive(item);
150 let display_ok = derive_error
151 || variant_error_attrs
152 || has_error_attr(item)
153 || display_impls.contains(&name);
154 let error_ok = derive_error || error_impls.contains(&name);
155 let mut out = Vec::new();
156
157 if !display_ok {
158 out.push(ctx.violation(
159 &name_node,
160 format!("public error type `{name}` must implement `Display`"),
161 ));
162 }
163
164 if !error_ok {
165 out.push(ctx.violation(
166 &name_node,
167 format!("public error type `{name}` must implement `std::error::Error`"),
168 ));
169 }
170
171 out
172}
173
174crate::tidy_ast_test!(check_error_missing_traits, {
175 crate::example_tests!(EXAMPLES, check_error_missing_traits);
176});