Skip to main content

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

1use 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
136/// `Box<[T]>` and `Box<str>` are idiomatic owned DSTs, not wrapper leakage.
137fn 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
152// #t(rust_similar_fns) Smart-pointer leakage intentionally scans public function and public-struct fields, while generic nesting also owns alias policy.
153fn 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});