wowlab_tidy/languages/rust/rules/api/
pub_api_smart_pointers.rs1use ra_ap_syntax::{
2 AstNode,
3 ast::{self, HasGenericArgs, HasVisibility, VisibilityKind},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10 Example {
11 label: "arc parameter",
12 code: "pub fn process(data: Arc<Mutex<Shared>>) {}",
13 pass: false,
14 },
15 Example {
16 label: "box return",
17 code: "pub fn build() -> Box<Processed> { Box::new(Processed) }",
18 pass: false,
19 },
20 Example {
21 label: "rc refcell parameter",
22 code: "pub fn init(config: Rc<RefCell<Config>>) {}",
23 pass: false,
24 },
25 Example {
26 label: "pub wrapper field",
27 code: "pub struct Server {\n pub state: Arc<State>,\n}",
28 pass: false,
29 },
30 Example {
31 label: "pub method with wrapper return",
32 code: "pub struct S;\nimpl S {\n pub fn shared(&self) -> Rc<Data> { self.data.clone() }\n}",
33 pass: false,
34 },
35 Example {
36 label: "private fn with wrapper",
37 code: "fn helper(data: Arc<Config>) {}",
38 pass: true,
39 },
40 Example {
41 label: "plain reference api",
42 code: "pub fn process(data: &Data) -> State { data.state() }",
43 pass: true,
44 },
45 Example {
46 label: "box dyn left to dyn rule",
47 code: "pub fn run(handler: Box<dyn Handler>) {}",
48 pass: true,
49 },
50 Example {
51 label: "boxed slice",
52 code: "pub fn take(buf: Box<[u8]>) {}",
53 pass: true,
54 },
55 Example {
56 label: "boxed str",
57 code: "pub fn name() -> Box<str> { String::new().into_boxed_str() }",
58 pass: true,
59 },
60 Example {
61 label: "private wrapper field",
62 code: "pub struct Server {\n state: Arc<State>,\n}",
63 pass: true,
64 },
65 Example {
66 label: "wrapper not outermost",
67 code: "pub fn all(items: Vec<Arc<Config>>) {}",
68 pass: true,
69 },
70 Example {
71 label: "wrapper in test module",
72 code: "#[cfg(test)]\nmod tests {\n pub fn process(data: Arc<Mutex<Shared>>) {}\n}",
73 pass: true,
74 },
75];
76
77crate::ast_rule!(
78 pub_api_smart_pointers,
79 "Flag `Rc`/`Arc`/`Box`/`RefCell`/`Cell`/`Mutex`/`RwLock` as the outermost type of pub fn params, returns, and pub struct fields.",
80 "Smart pointers in public APIs leak implementation details and infect downstream signatures; accept and return plain types (M-AVOID-WRAPPERS).",
81 Medium,
82);
83
84const WRAPPERS: &[&str] = &["Arc", "Box", "Cell", "Mutex", "Rc", "RefCell", "RwLock"];
85
86fn check_pub_api_smart_pointers(ctx: &AstCtx<'_>) -> Vec<Violation> {
87 public_api_types(ctx)
88 .filter_map(|ty| {
89 flagged_wrapper(&ty).map(|wrapper| {
90 ctx.violation(
91 &ty,
92 format!(
93 "pub API exposes `{wrapper}` — accept/return plain types and keep wrappers internal (M-AVOID-WRAPPERS)"
94 ),
95 )
96 })
97 })
98 .collect()
99}
100
101fn flagged_wrapper(ty: &ast::Type) -> Option<&'static str> {
102 let ast::Type::PathType(type_path) = ty else {
103 return None;
104 };
105 let segment = type_path.path()?.segment()?;
106 let name_ref = segment.name_ref()?;
107 let name = name_ref.text();
108 let wrapper = WRAPPERS
109 .iter()
110 .find(|wrapper| *name == ***wrapper)
111 .copied()?;
112
113 if let Some(inner) = first_type_arg(&segment) {
114 if matches!(wrapper, "Arc" | "Box" | "Rc") && matches!(inner, ast::Type::DynTraitType(_)) {
115 return None;
116 }
117
118 if wrapper == "Box" && is_dst(&inner) {
119 return None;
120 }
121 }
122
123 Some(wrapper)
124}
125
126fn first_type_arg(segment: &ast::PathSegment) -> Option<ast::Type> {
127 segment
128 .generic_arg_list()?
129 .generic_args()
130 .find_map(|arg| match arg {
131 ast::GenericArg::TypeArg(arg) => arg.ty(),
132 _ => None,
133 })
134}
135
136fn is_dst(ty: &ast::Type) -> bool {
138 match ty {
139 ast::Type::SliceType(_) => true,
140 ast::Type::PathType(path) => {
141 let segment = path
142 .path()
143 .and_then(|path| path.segment())
144 .and_then(|segment| segment.name_ref());
145
146 segment.is_some_and(|name| name.text() == "str")
147 }
148 _ => false,
149 }
150}
151
152fn public_api_types<'a>(ctx: &'a AstCtx<'a>) -> impl Iterator<Item = ast::Type> + 'a {
154 let functions = ctx
155 .nodes::<ast::Fn>()
156 .filter(|function| !ctx.is_in_test(function) && is_pub(function.visibility()))
157 .flat_map(|function| {
158 let params = function
159 .param_list()
160 .into_iter()
161 .flat_map(|params| params.params())
162 .filter_map(|param| param.ty());
163
164 params
165 .chain(function.ret_type().and_then(|ret| ret.ty()))
166 .collect::<Vec<_>>()
167 });
168 let fields = ctx
169 .nodes::<ast::RecordField>()
170 .filter(|field| {
171 !ctx.is_in_test(field) && is_pub(field.visibility()) && inside_public_struct(field)
172 })
173 .filter_map(|field| field.ty());
174 let tuple_fields = ctx
175 .nodes::<ast::TupleField>()
176 .filter(|field| {
177 !ctx.is_in_test(field) && is_pub(field.visibility()) && inside_public_struct(field)
178 })
179 .filter_map(|field| field.ty());
180
181 functions.chain(fields).chain(tuple_fields)
182}
183
184fn inside_public_struct<N>(field: &N) -> bool
185where
186 N: AstNode,
187{
188 field
189 .syntax()
190 .ancestors()
191 .find_map(ast::Struct::cast)
192 .is_some_and(|item| is_pub(item.visibility()))
193}
194
195fn is_pub(visibility: Option<ast::Visibility>) -> bool {
196 visibility.is_some_and(|vis| matches!(vis.kind(), VisibilityKind::Pub))
197}
198
199crate::tidy_ast_test!(check_pub_api_smart_pointers, {
200 crate::example_tests!(EXAMPLES, check_pub_api_smart_pointers);
201});