wowlab_tidy/languages/rust/rules/api/
builder_fallible_setter.rs1use ra_ap_syntax::{ast, ast::HasName};
2
3use super::support::type_name;
4use crate::{AstCtx, Example, Violation};
5
6#[rustfmt::skip]
7const EXAMPLES: &[Example] = &[
8 Example {
9 label: "fallible setter",
10 code: "struct HostBuilder;\nimpl HostBuilder {\n fn port(self, port: u16) -> Result<Self, String> {\n Ok(self)\n }\n fn build(self) -> u32 {\n 0\n }\n}",
11 pass: false,
12 },
13 Example {
14 label: "fallible build",
15 code: "struct HostBuilder;\nimpl HostBuilder {\n fn port(self, port: u16) -> Self {\n self\n }\n fn build(self) -> Result<u32, String> {\n Ok(0)\n }\n}",
16 pass: true,
17 },
18 Example {
19 label: "fallible try_build and finish",
20 code: "struct HostBuilder;\nimpl HostBuilder {\n fn try_build(self) -> Result<u32, String> {\n Ok(0)\n }\n fn finish(self) -> Result<u32, String> {\n Ok(0)\n }\n}",
21 pass: true,
22 },
23 Example {
24 label: "fallible associated fn",
25 code: "struct HostBuilder;\nimpl HostBuilder {\n fn parse(input: &str) -> Result<Self, String> {\n Ok(HostBuilder)\n }\n}",
26 pass: true,
27 },
28 Example {
29 label: "fallible method on non-builder",
30 code: "struct Config;\nimpl Config {\n fn set(self, key: u32) -> Result<Self, String> {\n Ok(self)\n }\n}",
31 pass: true,
32 },
33 Example {
34 label: "fallible setter in test module",
35 code: "#[cfg(test)]\nmod tests {\n struct HostBuilder;\n impl HostBuilder {\n fn port(self, port: u16) -> Result<Self, String> {\n Ok(self)\n }\n }\n}",
36 pass: true,
37 },
38];
39
40const ALLOWED_FALLIBLE: &[&str] = &["build", "try_build", "finish"];
41
42crate::ast_rule!(
43 builder_fallible_setter,
44 "Flag builder setters returning `Result` — setters accept infallibly, validation belongs in `build()`.",
45 "Fallible setters force repeated error checks that add noise and still cannot guard interdependent conditions; a Result-carrying build() consolidates validation.",
46 Medium,
47);
48
49fn check_builder_fallible_setter(ctx: &AstCtx<'_>) -> Vec<Violation> {
50 let builders = ctx
51 .nodes::<ast::Impl>()
52 .filter(|item| !ctx.is_in_test(item) && item.trait_().is_none())
53 .filter(|item| {
54 item.self_ty()
55 .is_some_and(|ty| type_name(&ty).is_some_and(|name| name.ends_with("Builder")))
56 });
57
58 builders
59 .flat_map(|item| {
60 let associated_items = item
61 .assoc_item_list()
62 .into_iter()
63 .flat_map(|list| list.assoc_items());
64 let setters = associated_items
65 .filter_map(|assoc| match assoc {
66 ast::AssocItem::Fn(function) => Some(function),
67 _ => None,
68 })
69 .filter(|function| function.param_list().is_some_and(|params| params.self_param().is_some()));
70
71 setters
72 .filter(|function| {
73 let return_type = function
74 .ret_type()
75 .and_then(|ret| ret.ty())
76 .and_then(|ty| type_name(&ty));
77
78 return_type
79 .is_some_and(|name| name == "Result")
80 })
81 .filter_map(|function| {
82 let name = function.name()?;
83 let text = name.text().to_string();
84
85 (!ALLOWED_FALLIBLE.contains(&text.as_str())).then(|| {
86 ctx.violation(
87 &name,
88 format!(
89 "fallible builder setter `{text}` — accept infallibly and validate in `build()`"
90 ),
91 )
92 })
93 })
94 .collect::<Vec<_>>()
95 })
96 .collect()
97}
98
99crate::tidy_ast_test!(check_builder_fallible_setter, {
100 crate::example_tests!(EXAMPLES, check_builder_fallible_setter);
101});