Skip to main content

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

1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::{ast, ast::HasName};
4use wowlab_types::sim::FastSet;
5
6use super::support::type_name;
7use crate::{AstCtx, Example, Violation};
8
9#[rustfmt::skip]
10const EXAMPLES: &[Example] = &[
11    Example {
12        label: "iter with IntoIterator for ref",
13        code: "struct Bag(Vec<u32>);\nimpl Bag {\n    fn iter(&self) -> std::slice::Iter<'_, u32> { self.0.iter() }\n}\nimpl<'a> IntoIterator for &'a Bag {\n    type Item = &'a u32;\n    type IntoIter = std::slice::Iter<'a, u32>;\n    fn into_iter(self) -> Self::IntoIter { self.0.iter() }\n}",
14        pass: true,
15    },
16    Example {
17        label: "iter without IntoIterator for ref",
18        code: "struct Bag(Vec<u32>);\nimpl Bag {\n    fn iter(&self) -> std::slice::Iter<'_, u32> { self.0.iter() }\n}",
19        pass: false,
20    },
21    Example {
22        label: "iter_mut without IntoIterator for mut ref",
23        code: "struct Bag(Vec<u32>);\nimpl Bag {\n    fn iter_mut(&mut self) -> std::slice::IterMut<'_, u32> { self.0.iter_mut() }\n}",
24        pass: false,
25    },
26    Example {
27        label: "iter_mut with IntoIterator for mut ref",
28        code: "struct Bag(Vec<u32>);\nimpl Bag {\n    fn iter_mut(&mut self) -> std::slice::IterMut<'_, u32> { self.0.iter_mut() }\n}\nimpl<'a> IntoIterator for &'a mut Bag {\n    type Item = &'a mut u32;\n    type IntoIter = std::slice::IterMut<'a, u32>;\n    fn into_iter(self) -> Self::IntoIter { self.0.iter_mut() }\n}",
29        pass: true,
30    },
31    Example {
32        label: "FromIterator without Extend",
33        code: "struct Bag(Vec<u32>);\nimpl FromIterator<u32> for Bag {\n    fn from_iter<I: IntoIterator<Item = u32>>(iter: I) -> Self { Bag(iter.into_iter().collect()) }\n}",
34        pass: false,
35    },
36    Example {
37        label: "Extend without FromIterator",
38        code: "struct Bag(Vec<u32>);\nimpl Extend<u32> for Bag {\n    fn extend<I: IntoIterator<Item = u32>>(&mut self, iter: I) { self.0.extend(iter) }\n}",
39        pass: false,
40    },
41    Example {
42        label: "FromIterator with Extend",
43        code: "struct Bag(Vec<u32>);\nimpl FromIterator<u32> for Bag {\n    fn from_iter<I: IntoIterator<Item = u32>>(iter: I) -> Self { Bag(iter.into_iter().collect()) }\n}\nimpl Extend<u32> for Bag {\n    fn extend<I: IntoIterator<Item = u32>>(&mut self, iter: I) { self.0.extend(iter) }\n}",
44        pass: true,
45    },
46    Example {
47        label: "no collection surface",
48        code: "struct Bag(Vec<u32>);\nimpl Bag {\n    fn len(&self) -> usize { self.0.len() }\n}",
49        pass: true,
50    },
51    Example {
52        label: "iter in test module",
53        code: "#[cfg(test)]\nmod tests {\n    struct Bag(Vec<u32>);\n    impl Bag {\n        fn iter(&self) -> std::slice::Iter<'_, u32> { self.0.iter() }\n    }\n}",
54        pass: true,
55    },
56    Example {
57        label: "associated fn iter without receiver",
58        code: "struct Gen;\nimpl Gen {\n    fn iter() -> std::ops::Range<u32> { 0..4 }\n}",
59        pass: true,
60    },
61    Example {
62        label: "free fn iter",
63        code: "fn iter() -> std::ops::Range<u32> { 0..4 }",
64        pass: true,
65    },
66];
67
68crate::ast_rule!(
69    collection_trait_completeness,
70    "Require collection trait counterparts: `iter()` needs `impl IntoIterator for &T`, `iter_mut()` needs `impl IntoIterator for &mut T`, and `FromIterator`/`Extend` come in pairs.",
71    "Collections missing the matching std iterator traits break for-loops over references and generic code expecting the standard surface (M-COLLECTION-TRAITS).",
72    Low,
73);
74
75#[derive(Default)]
76struct TraitImpls {
77    into_iter_ref: FastSet<String>,
78    into_iter_mut: FastSet<String>,
79    from_iter: FastSet<String>,
80    extend: FastSet<String>,
81}
82
83fn collect_trait_impls(ctx: &AstCtx<'_>) -> TraitImpls {
84    let mut found = TraitImpls::default();
85
86    for item in ctx.nodes::<ast::Impl>() {
87        record_impl(&item, &mut found);
88    }
89
90    found
91}
92
93fn record_impl(item: &ast::Impl, found: &mut TraitImpls) {
94    let Some(trait_name) = item.trait_().and_then(|ty| type_name(&ty)) else {
95        return;
96    };
97
98    if trait_name == "IntoIterator" {
99        let Some(ast::Type::RefType(reference)) = item.self_ty() else {
100            return;
101        };
102        let Some(name) = reference.ty().and_then(|ty| type_name(&ty)) else {
103            return;
104        };
105
106        if reference.mut_token().is_some() {
107            found.into_iter_mut.insert(name);
108        } else {
109            found.into_iter_ref.insert(name);
110        }
111    } else if trait_name == "FromIterator" || trait_name == "Extend" {
112        let Some(name) = item.self_ty().and_then(|ty| type_name(&ty)) else {
113            return;
114        };
115
116        if trait_name == "FromIterator" {
117            found.from_iter.insert(name);
118        } else {
119            found.extend.insert(name);
120        }
121    }
122}
123
124fn check_collection_trait_completeness(ctx: &AstCtx<'_>) -> Vec<Violation> {
125    let impls = collect_trait_impls(ctx);
126
127    ctx.nodes::<ast::Impl>()
128        .filter(|item| !ctx.is_in_test(item))
129        .flat_map(|item| match item.trait_() {
130            None => check_inherent_impl(ctx, &item, &impls),
131            Some(_) => check_pair_impl(ctx, &item, &impls),
132        })
133        .collect()
134}
135
136fn check_inherent_impl(ctx: &AstCtx<'_>, item: &ast::Impl, impls: &TraitImpls) -> Vec<Violation> {
137    let Some(name) = item.self_ty().and_then(|ty| type_name(&ty)) else {
138        return Vec::new();
139    };
140
141    let associated_items = item
142        .assoc_item_list()
143        .into_iter()
144        .flat_map(|list| list.assoc_items());
145
146    associated_items
147        .filter_map(|assoc| match assoc {
148            ast::AssocItem::Fn(method) => Some(method),
149            _ => None,
150        })
151        .filter_map(|method| {
152            let method_name = method.name()?;
153            let mutable = ref_receiver_mutability(&method)?;
154
155            if method_name.text() == "iter" && !mutable && !impls.into_iter_ref.contains(&name) {
156                Some(missing_counterpart(
157                    ctx,
158                    &method_name,
159                    &name,
160                    "iter()",
161                    "impl IntoIterator for &",
162                ))
163            } else if method_name.text() == "iter_mut"
164                && mutable
165                && !impls.into_iter_mut.contains(&name)
166            {
167                Some(missing_counterpart(
168                    ctx,
169                    &method_name,
170                    &name,
171                    "iter_mut()",
172                    "impl IntoIterator for &mut ",
173                ))
174            } else {
175                None
176            }
177        })
178        .collect()
179}
180
181fn ref_receiver_mutability(function: &ast::Fn) -> Option<bool> {
182    let receiver = function.param_list()?.self_param()?;
183
184    receiver.amp_token().map(|_| receiver.mut_token().is_some())
185}
186
187fn check_pair_impl(ctx: &AstCtx<'_>, item: &ast::Impl, impls: &TraitImpls) -> Vec<Violation> {
188    let Some(trait_ty) = item.trait_() else {
189        return Vec::new();
190    };
191    let Some(trait_name) = type_name(&trait_ty) else {
192        return Vec::new();
193    };
194    let Some(name) = item.self_ty().and_then(|ty| type_name(&ty)) else {
195        return Vec::new();
196    };
197
198    if trait_name == "FromIterator" && !impls.extend.contains(&name) {
199        vec![missing_counterpart(
200            ctx,
201            &trait_ty,
202            &name,
203            "impl FromIterator",
204            "impl Extend<_> for ",
205        )]
206    } else if trait_name == "Extend" && !impls.from_iter.contains(&name) {
207        vec![missing_counterpart(
208            ctx,
209            &trait_ty,
210            &name,
211            "impl Extend",
212            "impl FromIterator<_> for ",
213        )]
214    } else {
215        Vec::new()
216    }
217}
218
219fn missing_counterpart<N>(
220    ctx: &AstCtx<'_>,
221    node: &N,
222    type_name: &str,
223    has: &str,
224    needs: &str,
225) -> Violation
226where
227    N: ra_ap_syntax::AstNode,
228{
229    ctx.violation(
230        node,
231        format!("type `{type_name}` has `{has}` but no `{needs}{type_name}` in this file (M-COLLECTION-TRAITS)"),
232    )
233}
234
235crate::tidy_ast_test!(check_collection_trait_completeness, {
236    crate::example_tests!(EXAMPLES, check_collection_trait_completeness);
237
238    #[gtest]
239    fn one_violation_per_missing_counterpart() -> Result<()> {
240        let v = run("struct Bag(Vec<u32>);\n\
241             impl Bag {\n\
242                 fn iter(&self) -> std::slice::Iter<'_, u32> { self.0.iter() }\n\
243                 fn iter_mut(&mut self) -> std::slice::IterMut<'_, u32> { self.0.iter_mut() }\n\
244             }");
245        verify_eq!(v.len(), 2)?;
246
247        Ok(())
248    }
249});