Skip to main content

wowlab_tidy/languages/rust/rules/correctness/
infallible_from_weak.rs

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