wowlab_tidy/languages/rust/rules/api/
pub_api_generic_nesting.rs1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::{
4 AstNode,
5 ast::{self, HasGenericArgs, HasVisibility, VisibilityKind},
6};
7
8use super::support::path_reachable_from;
9use crate::{AstCtx, Example, Violation};
10
11#[rustfmt::skip]
12const EXAMPLES: &[Example] = &[
13 Example {
14 label: "two local generic levels in param",
15 code: "pub fn serve(s: Service<Backend<Store>>) {}",
16 pass: false,
17 },
18 Example {
19 label: "two local generic levels in return",
20 code: "pub fn build() -> Service<Backend<Store>> { make() }",
21 pass: false,
22 },
23 Example {
24 label: "two local generic levels in pub field",
25 code: "pub struct App {\n pub svc: Service<Backend<Store>>,\n}",
26 pass: false,
27 },
28 Example {
29 label: "two local generic levels in pub type alias",
30 code: "pub type Handle = Service<Backend<Store>>;",
31 pass: false,
32 },
33 Example {
34 label: "crate-prefixed local generics",
35 code: "pub fn serve(s: crate::Service<crate::Backend<Store>>) {}",
36 pass: false,
37 },
38 Example {
39 label: "local nesting under std container",
40 code: "pub fn all() -> Vec<Service<Backend<Store>>> { Vec::new() }",
41 pass: false,
42 },
43 Example {
44 label: "std nesting",
45 code: "pub fn buf(v: Vec<Vec<u8>>) {}",
46 pass: true,
47 },
48 Example {
49 label: "one local level",
50 code: "pub fn serve(s: Service<Backend>) {}",
51 pass: true,
52 },
53 Example {
54 label: "local generic with std argument",
55 code: "pub fn serve(s: Service<Vec<u8>>) {}",
56 pass: true,
57 },
58 Example {
59 label: "foreign generics",
60 code: "pub fn spawn(t: tokio::task::JoinHandle<foo::Bar<Baz>>) {}",
61 pass: true,
62 },
63 Example {
64 label: "private fn nesting",
65 code: "fn serve(s: Service<Backend<Store>>) {}",
66 pass: true,
67 },
68 Example {
69 label: "nesting in test module",
70 code: "#[cfg(test)]\nmod tests {\n pub fn serve(s: Service<Backend<Store>>) {}\n}",
71 pass: true,
72 },
73];
74
75crate::ast_rule!(
76 pub_api_generic_nesting,
77 "Flag pub fn signatures, pub struct fields, and pub type aliases nesting one local generic instantiation inside another (e.g. `Service<Backend<Store>>`).",
78 "Nested crate-local generics infect user code with type parameters and trait bounds users never asked for; flatten or alias the composition (M-SIMPLE-ABSTRACTIONS).",
79 Low,
80);
81
82const STD_CONTAINERS: &[&str] = &[
83 "Arc",
84 "BTreeMap",
85 "BTreeSet",
86 "BinaryHeap",
87 "Box",
88 "Cell",
89 "Cow",
90 "HashMap",
91 "HashSet",
92 "LinkedList",
93 "Mutex",
94 "Option",
95 "PhantomData",
96 "Pin",
97 "Rc",
98 "RefCell",
99 "Result",
100 "RwLock",
101 "Vec",
102 "VecDeque",
103 "Weak",
104];
105
106fn check_pub_api_generic_nesting(ctx: &AstCtx<'_>) -> Vec<Violation> {
107 public_api_types(ctx)
108 .flat_map(|ty| {
109 let paths = ty
110 .syntax()
111 .descendants()
112 .filter_map(ast::PathType::cast)
113 .filter(|path| path_reachable_from(&ty, path));
114 let offending_paths = paths
115 .filter_map(|path| offending_pair(&path).map(|pair| (path, pair)))
116 .filter(|(path, _)| {
117 !path.syntax().ancestors().skip(1).filter_map(ast::PathType::cast).any(|ancestor| offending_pair(&ancestor).is_some())
118 });
119
120 offending_paths
121 .map(|(outer_path, (outer, inner))| ctx.violation(
122 &outer_path,
123 format!(
124 "pub API nests local generic `{inner}<…>` inside `{outer}<…>` — flatten the abstraction or hide it behind an alias-free type (M-SIMPLE-ABSTRACTIONS)"
125 ),
126 ))
127 .collect::<Vec<_>>()
128 })
129 .collect()
130}
131
132fn local_generic_segment(path_type: &ast::PathType) -> Option<ast::PathSegment> {
133 let path = path_type.path()?;
134 let segments = path_segments(&path);
135 let first_name = segments.first()?.name_ref()?;
136 let first = first_name.text();
137 let local = segments.len() == 1 || matches!(first.as_str(), "crate" | "self" | "super");
138
139 if !local {
140 return None;
141 }
142
143 let last = segments.last()?.clone();
144 let name_ref = last.name_ref()?;
145 let name = name_ref.text();
146
147 if STD_CONTAINERS.contains(&name.as_str()) {
148 return None;
149 }
150
151 last.generic_arg_list().is_some().then_some(last)
152}
153
154fn offending_pair(path_type: &ast::PathType) -> Option<(String, String)> {
155 let outer = local_generic_segment(path_type)?;
156 let inner = outer.generic_arg_list()?.generic_args().find_map(|arg| {
157 let ast::GenericArg::TypeArg(arg) = arg else {
158 return None;
159 };
160 let ast::Type::PathType(inner) = arg.ty()? else {
161 return None;
162 };
163
164 local_generic_segment(&inner)
165 })?;
166
167 Some((
168 outer.name_ref()?.text().to_string(),
169 inner.name_ref()?.text().to_string(),
170 ))
171}
172
173fn path_segments(path: &ast::Path) -> Vec<ast::PathSegment> {
174 let mut reverse = Vec::new();
175 let mut current = Some(path.clone());
176
177 while let Some(path) = current {
178 if let Some(segment) = path.segment() {
179 reverse.push(segment);
180 }
181
182 current = path.qualifier();
183 }
184
185 reverse.into_iter().rev().collect()
186}
187
188fn public_api_types<'a>(ctx: &'a AstCtx<'a>) -> impl Iterator<Item = ast::Type> + 'a {
189 let functions = ctx
190 .nodes::<ast::Fn>()
191 .filter(|function| !ctx.is_in_test(function) && is_pub(function.visibility()))
192 .flat_map(|function| {
193 let params = function
194 .param_list()
195 .into_iter()
196 .flat_map(|params| params.params())
197 .filter_map(|param| param.ty());
198
199 params
200 .chain(function.ret_type().and_then(|ret| ret.ty()))
201 .collect::<Vec<_>>()
202 });
203 let fields = ctx
204 .nodes::<ast::RecordField>()
205 .filter(|field| !ctx.is_in_test(field) && is_pub(field.visibility()))
206 .filter_map(|field| field.ty());
207 let tuples = ctx
208 .nodes::<ast::TupleField>()
209 .filter(|field| !ctx.is_in_test(field) && is_pub(field.visibility()))
210 .filter_map(|field| field.ty());
211 let aliases = ctx
212 .nodes::<ast::TypeAlias>()
213 .filter(|alias| !ctx.is_in_test(alias) && is_pub(alias.visibility()))
214 .filter_map(|alias| alias.ty());
215
216 functions.chain(fields).chain(tuples).chain(aliases)
217}
218
219fn is_pub(visibility: Option<ast::Visibility>) -> bool {
220 visibility.is_some_and(|vis| matches!(vis.kind(), VisibilityKind::Pub))
221}
222
223crate::tidy_ast_test!(check_pub_api_generic_nesting, {
224 crate::example_tests!(EXAMPLES, check_pub_api_generic_nesting);
225
226 #[gtest]
227 fn one_violation_per_nesting_site() -> Result<()> {
228 let v = run("pub fn serve(s: Service<Backend<Store<Inner>>>) {}");
229 verify_eq!(v.len(), 1)?;
230
231 Ok(())
232 }
233});