Skip to main content

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

1use ra_ap_syntax::{
2    ast,
3    ast::{HasGenericArgs, HasName},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10    Example { label: "owned string error", code: "fn parse() -> Result<u32, String> { Err(String::new()) }", pass: false },
11    Example { label: "borrowed string error", code: "fn parse() -> Result<u32, &'static str> { Err(\"bad\") }", pass: false },
12    Example { label: "qualified result", code: "fn parse() -> std::result::Result<u32, String> { Err(String::new()) }", pass: false },
13    Example { label: "typed error", code: "fn parse() -> Result<u32, ParseError> { todo!() }", pass: true },
14    Example { label: "string success", code: "fn parse() -> Result<String, ParseError> { todo!() }", pass: true },
15    Example { label: "test helper", code: "#[cfg(test)] mod tests { fn parse() -> Result<u32, String> { Err(String::new()) } }", pass: true },
16];
17
18crate::ast_rule!(
19    string_error,
20    "Reject `String` and `&str` as function error types.",
21    "Structured error types preserve context, support source chains, and give callers stable inspection APIs.",
22    Medium,
23);
24
25fn check_string_error(ctx: &AstCtx<'_>) -> Vec<Violation> {
26    ctx.nodes::<ast::Fn>()
27        .filter(|function| !ctx.is_in_test(function))
28        .filter_map(|function| {
29            let return_type = function.ret_type()?.ty()?;
30
31            string_error_type(&return_type).then(|| {
32                let name = function
33                    .name()
34                    .map_or_else(|| "<anonymous>".to_owned(), |name| name.text().to_string());
35
36                ctx.violation(
37                    &return_type,
38                    format!(
39                        "function `{name}` returns an unstructured string error — use a canonical error type"
40                    ),
41                )
42            })
43        })
44        .collect()
45}
46
47fn string_error_type(ty: &ast::Type) -> bool {
48    let ast::Type::PathType(path_type) = ty else {
49        return false;
50    };
51    let Some(segment) = path_type.path().and_then(|path| path.segment()) else {
52        return false;
53    };
54
55    if segment
56        .name_ref()
57        .is_none_or(|name| name.text() != "Result")
58    {
59        return false;
60    }
61
62    let Some(error_type) = segment
63        .generic_arg_list()
64        .and_then(|args| args.generic_args().nth(1))
65        .and_then(|arg| match arg {
66            ast::GenericArg::TypeArg(arg) => arg.ty(),
67            _ => None,
68        })
69    else {
70        return false;
71    };
72
73    match error_type {
74        ast::Type::PathType(path_type) => {
75            let segment = path_type
76                .path()
77                .and_then(|path| path.segment())
78                .and_then(|segment| segment.name_ref());
79
80            segment.is_some_and(|name| name.text() == "String")
81        }
82        ast::Type::RefType(reference) => {
83            let path = reference
84                .ty()
85                .and_then(|ty| match ty {
86                    ast::Type::PathType(path_type) => path_type.path(),
87                    _ => None,
88                })
89                .and_then(|path| path.segment());
90
91            path.and_then(|segment| segment.name_ref())
92                .is_some_and(|name| name.text() == "str")
93        }
94        _ => false,
95    }
96}
97
98crate::tidy_ast_test!(check_string_error, {
99    crate::example_tests!(EXAMPLES, check_string_error);
100});