Skip to main content

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

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