wowlab_tidy/languages/rust/rules/correctness/
infallible_from_weak.rs1use ra_ap_syntax::ast::{self, HasGenericArgs};
2use wowlab_types::sim::FastSet;
3
4use super::{super::support::type_name, support::weak_type_name};
5use crate::{AstCtx, Example, Violation};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9 Example {
10 label: "From alongside TryFrom",
11 code: "pub struct Month(u8);\nimpl TryFrom<u8> for Month {\n type Error = String;\n fn try_from(v: u8) -> Result<Self, String> { Err(String::new()) }\n}\nimpl From<u8> for Month {\n fn from(v: u8) -> Self { Month(v) }\n}",
12 pass: false,
13 },
14 Example {
15 label: "From alongside inherent Result constructor",
16 code: "pub struct Port(u16);\nimpl Port {\n pub fn new(v: u16) -> Result<Self, String> { Ok(Port(v)) }\n}\nimpl From<u16> for Port {\n fn from(v: u16) -> Self { Port(v) }\n}",
17 pass: false,
18 },
19 Example {
20 label: "From from str alongside TryFrom",
21 code: "pub struct Tag(String);\nimpl TryFrom<String> for Tag {\n type Error = String;\n fn try_from(v: String) -> Result<Self, String> { Err(v) }\n}\nimpl From<&str> for Tag {\n fn from(v: &str) -> Self { Tag(v.to_string()) }\n}",
22 pass: false,
23 },
24 Example {
25 label: "From without fallible construction",
26 code: "pub struct Month(u8);\nimpl From<u8> for Month {\n fn from(v: u8) -> Self { Month(v) }\n}",
27 pass: true,
28 },
29 Example {
30 label: "From from strong type",
31 code: "pub struct Inner(u8);\npub struct Outer(Inner);\nimpl TryFrom<u8> for Outer {\n type Error = String;\n fn try_from(v: u8) -> Result<Self, String> { Err(String::new()) }\n}\nimpl From<Inner> for Outer {\n fn from(v: Inner) -> Self { Outer(v) }\n}",
32 pass: true,
33 },
34 Example {
35 label: "infallible constructor only",
36 code: "pub struct Port(u16);\nimpl Port {\n pub fn new(v: u16) -> Self { Port(v) }\n}\nimpl From<u16> for Port {\n fn from(v: u16) -> Self { Port(v) }\n}",
37 pass: true,
38 },
39 Example {
40 label: "From in test module",
41 code: "#[cfg(test)]\nmod tests {\n pub struct Month(u8);\n impl TryFrom<u16> for Month {\n type Error = String;\n fn try_from(v: u16) -> Result<Self, String> { Err(String::new()) }\n }\n impl From<u8> for Month {\n fn from(v: u8) -> Self { Month(v) }\n }\n}",
42 pass: true,
43 },
44];
45
46crate::ast_rule!(
47 infallible_from_weak,
48 "Flag `impl From<weak>` next to fallible construction of the same type.",
49 "An infallible conversion from a weak type bypasses the invariant the fallible constructor exists to guard; offer only TryFrom.",
50 Medium,
51);
52
53fn single_type_arg(segment: &ast::PathSegment) -> Option<ast::Type> {
54 let args = segment.generic_arg_list()?;
55 let mut iter = args.generic_args();
56
57 match (iter.next(), iter.next()) {
58 (Some(ast::GenericArg::TypeArg(arg)), None) => arg.ty(),
59 _ => None,
60 }
61}
62
63fn returns_result_self(function: &ast::Fn, self_name: &str) -> bool {
64 let Some(ast::Type::PathType(path_type)) = function.ret_type().and_then(|ret| ret.ty()) else {
65 return false;
66 };
67 let Some(segment) = path_type.path().and_then(|path| path.segment()) else {
68 return false;
69 };
70
71 if segment
72 .name_ref()
73 .is_none_or(|name| name.text() != "Result")
74 {
75 return false;
76 }
77
78 let Some(ok) = segment.generic_arg_list().and_then(|args| {
79 args.generic_args().find_map(|arg| match arg {
80 ast::GenericArg::TypeArg(arg) => arg.ty(),
81 _ => None,
82 })
83 }) else {
84 return false;
85 };
86
87 match ok {
88 ast::Type::PathType(path) => {
89 let name = path
90 .path()
91 .and_then(|path| path.segment())
92 .and_then(|segment| segment.name_ref());
93
94 name.is_some_and(|name| {
95 matches!(name.text().as_str(), "Self") || name.text() == self_name
96 })
97 }
98 _ => false,
99 }
100}
101
102fn check_infallible_from_weak(ctx: &AstCtx<'_>) -> Vec<Violation> {
103 let mut from_weak = Vec::new();
104 let mut fallible = FastSet::default();
105
106 for item_impl in ctx
107 .nodes::<ast::Impl>()
108 .filter(|item_impl| !ctx.is_in_test(item_impl))
109 {
110 collect_impl(&item_impl, &mut from_weak, &mut fallible);
111 }
112
113 let mut violations = Vec::new();
114
115 for (self_name, weak, location) in from_weak {
116 if fallible.contains(&self_name) {
117 violations.push(ctx.violation(
118 &location,
119 format!(
120 "infallible `From<{weak}> for {self_name}` alongside fallible construction — route weak types through TryFrom"
121 ),
122 ));
123 }
124 }
125
126 violations
127}
128
129fn collect_impl(
130 item: &ast::Impl,
131 from_weak: &mut Vec<(String, String, ast::NameRef)>,
132 fallible: &mut FastSet<String>,
133) {
134 let Some(self_name) = item.self_ty().and_then(|ty| type_name(&ty)) else {
135 return;
136 };
137 let Some(trait_type) = item.trait_() else {
138 let associated_items = item
139 .assoc_item_list()
140 .into_iter()
141 .flat_map(|list| list.assoc_items());
142 let has_fallible_ctor = associated_items
143 .filter_map(|assoc| match assoc {
144 ast::AssocItem::Fn(function) => Some(function),
145 _ => None,
146 })
147 .any(|function| returns_result_self(&function, &self_name));
148
149 if has_fallible_ctor {
150 fallible.insert(self_name);
151 }
152
153 return;
154 };
155 let ast::Type::PathType(path_type) = trait_type else {
156 return;
157 };
158 let Some(segment) = path_type.path().and_then(|path| path.segment()) else {
159 return;
160 };
161 let Some(name) = segment.name_ref() else {
162 return;
163 };
164
165 if name.text() == "TryFrom" {
166 fallible.insert(self_name);
167 } else if name.text() == "From" {
168 if let Some(weak) = single_type_arg(&segment).as_ref().and_then(weak_type_name) {
169 from_weak.push((self_name, weak, name));
170 }
171 }
172}
173
174crate::tidy_ast_test!(check_infallible_from_weak, {
175 crate::example_tests!(EXAMPLES, check_infallible_from_weak);
176});