wowlab_tidy/languages/rust/rules/api/
collection_trait_completeness.rs1#[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});