Skip to main content

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

1use 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});