wowlab_tidy/languages/rust/rules/api/
manual_error_impl.rs1use ra_ap_syntax::{AstNode, ast};
2
3use crate::{AstCtx, Example, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7 Example { label: "manual display", code: "struct ParseError; impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { todo!() } }", pass: false },
8 Example { label: "manual error", code: "struct ParseError; impl std::error::Error for ParseError {}", pass: false },
9 Example { label: "thiserror", code: "#[derive(Debug, thiserror::Error)] #[error(\"bad\")] struct ParseError;", pass: true },
10 Example { label: "non-error display", code: "struct Label; impl std::fmt::Display for Label { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { todo!() } }", pass: true },
11 Example { label: "unrelated trait", code: "struct ParseError; impl Clone for ParseError { fn clone(&self) -> Self { Self } }", pass: true },
12 Example { label: "test error", code: "#[cfg(test)] mod tests { struct StubError; impl std::error::Error for StubError {} }", pass: true },
13];
14
15crate::ast_rule!(
16 manual_error_impl,
17 "Reject hand-written `Display` and `Error` implementations for `*Error` types.",
18 "Canonical errors derive `thiserror::Error` so formatting and source propagation remain declarative and consistent.",
19 Low,
20);
21
22fn check_manual_error_impl(ctx: &AstCtx<'_>) -> Vec<Violation> {
23 ctx.nodes::<ast::Impl>()
24 .filter(|item_impl| !ctx.is_in_test(item_impl))
25 .filter_map(|item_impl| {
26 let self_path = item_impl
27 .self_ty()
28 .and_then(|ty| match ty {
29 ast::Type::PathType(path_type) => path_type.path(),
30 _ => None,
31 })
32 .and_then(|path| path.segment());
33 let self_name = self_path
34 .and_then(|segment| segment.name_ref())?;
35
36 if !self_name.text().ends_with("Error") {
37 return None;
38 }
39
40 let trait_type = item_impl.trait_()?;
41 let normalized: String = trait_type
42 .syntax()
43 .text()
44 .to_string()
45 .chars()
46 .filter(|ch| !ch.is_whitespace())
47 .collect();
48 let trait_name = match normalized.as_str() {
49 "Display" | "fmt::Display" | "std::fmt::Display" => "Display",
50 "Error" | "error::Error" | "std::error::Error" => "std::error::Error",
51 _ => return None,
52 };
53
54 Some(ctx.violation(
55 &item_impl,
56 format!(
57 "manual `{trait_name}` implementation for `{self_name}` — derive `thiserror::Error`"
58 ),
59 ))
60 })
61 .collect()
62}
63
64crate::tidy_ast_test!(check_manual_error_impl, {
65 crate::example_tests!(EXAMPLES, check_manual_error_impl);
66});