Skip to main content

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

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