wowlab_tidy/languages/rust/rules/api/
string_error.rs1use 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});