Skip to main content

wowlab_tidy/languages/rust/rules/api/
dyn_wrapper_in_api.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: "arc dyn parameter",
15        code: "pub fn start(db: Arc<dyn Database>) {}",
16        pass: false,
17    },
18    Example {
19        label: "box dyn parameter",
20        code: "pub fn run(handler: Box<dyn Handler>) {}",
21        pass: false,
22    },
23    Example {
24        label: "rc dyn pub field",
25        code: "pub struct App {\n    pub db: Rc<dyn Store>,\n}",
26        pass: false,
27    },
28    Example {
29        label: "nested arc dyn parameter",
30        code: "pub fn start(db: Option<Arc<dyn Database>>) {}",
31        pass: false,
32    },
33    Example {
34        label: "box dyn return",
35        code: "pub fn handler() -> Box<dyn Handler> { make() }",
36        pass: false,
37    },
38    Example {
39        label: "box dyn error return",
40        code: "pub fn run() -> Result<(), Box<dyn std::error::Error>> { Ok(()) }",
41        pass: true,
42    },
43    Example {
44        label: "box dyn error send sync",
45        code: "pub fn run() -> Box<dyn Error + Send + Sync> { make() }",
46        pass: true,
47    },
48    Example {
49        label: "private newtype wrapper",
50        code: "pub struct DynStore(Arc<dyn Store>);",
51        pass: true,
52    },
53    Example {
54        label: "private fn with dyn wrapper",
55        code: "fn wire(db: Arc<dyn Database>) {}",
56        pass: true,
57    },
58    Example {
59        label: "generic parameter instead",
60        code: "pub fn start(db: impl Database) {}",
61        pass: true,
62    },
63    Example {
64        label: "dyn wrapper in test module",
65        code: "#[cfg(test)]\nmod tests {\n    pub fn start(db: Arc<dyn Database>) {}\n}",
66        pass: true,
67    },
68];
69
70crate::ast_rule!(
71    dyn_wrapper_in_api,
72    "Flag `Rc<dyn …>`/`Arc<dyn …>`/`Box<dyn …>` in pub fn params, returns, and pub struct fields.",
73    "Visible dyn wrappers lock APIs into object safety and infect user code; wrap trait objects in a private newtype (M-DI-HIERARCHY).",
74    Low,
75);
76
77const DYN_WRAPPERS: &[&str] = &["Arc", "Box", "Rc"];
78
79fn check_dyn_wrapper_in_api(ctx: &AstCtx<'_>) -> Vec<Violation> {
80    public_api_types(ctx)
81        .flat_map(|ty| {
82            let paths = ty
83                .syntax()
84                .descendants()
85                .filter_map(ast::PathType::cast)
86                .filter(|path_type| path_reachable_from(&ty, path_type));
87
88            paths
89                .filter_map(|path_type| dyn_wrapper(&path_type).map(|wrapper| (path_type, wrapper)))
90                .map(|(path_type, wrapper)| {
91                    ctx.violation(
92                        &path_type,
93                        format!("`{wrapper}<dyn …>` in pub API — hide the trait object behind a private newtype wrapper (M-DI-HIERARCHY)"),
94                    )
95                })
96                .collect::<Vec<_>>()
97        })
98        .collect()
99}
100
101fn dyn_wrapper(path_type: &ast::PathType) -> Option<&'static str> {
102    let segment = path_type.path()?.segment()?;
103    let name_ref = segment.name_ref()?;
104    let name = name_ref.text();
105    let wrapper = DYN_WRAPPERS
106        .iter()
107        .find(|wrapper| *name == ***wrapper)
108        .copied()?;
109    let inner = segment
110        .generic_arg_list()?
111        .generic_args()
112        .find_map(|arg| match arg {
113            ast::GenericArg::TypeArg(arg) => arg.ty(),
114            _ => None,
115        })?;
116    let ast::Type::DynTraitType(object) = inner else {
117        return None;
118    };
119
120    (!is_error_object(&object)).then_some(wrapper)
121}
122
123/// `Box<dyn Error>` returns are the sanctioned error-object pattern.
124fn is_error_object(object: &ast::DynTraitType) -> bool {
125    object
126        .syntax()
127        .descendants()
128        .filter_map(ast::NameRef::cast)
129        .any(|name| name.text() == "Error")
130}
131
132fn public_api_types<'a>(ctx: &'a AstCtx<'a>) -> impl Iterator<Item = ast::Type> + 'a {
133    let functions = ctx
134        .nodes::<ast::Fn>()
135        .filter(|function| !ctx.is_in_test(function) && is_pub(function.visibility()))
136        .flat_map(|function| {
137            let params = function
138                .param_list()
139                .into_iter()
140                .flat_map(|params| params.params())
141                .filter_map(|param| param.ty());
142
143            params
144                .chain(function.ret_type().and_then(|ret| ret.ty()))
145                .collect::<Vec<_>>()
146        });
147    let fields = ctx
148        .nodes::<ast::RecordField>()
149        .filter(|field| {
150            !ctx.is_in_test(field) && is_pub(field.visibility()) && inside_public_struct(field)
151        })
152        .filter_map(|field| field.ty());
153    let tuples = ctx
154        .nodes::<ast::TupleField>()
155        .filter(|field| {
156            !ctx.is_in_test(field) && is_pub(field.visibility()) && inside_public_struct(field)
157        })
158        .filter_map(|field| field.ty());
159
160    functions.chain(fields).chain(tuples)
161}
162
163fn inside_public_struct<N>(field: &N) -> bool
164where
165    N: AstNode,
166{
167    field
168        .syntax()
169        .ancestors()
170        .find_map(ast::Struct::cast)
171        .is_some_and(|item| is_pub(item.visibility()))
172}
173
174fn is_pub(visibility: Option<ast::Visibility>) -> bool {
175    visibility.is_some_and(|vis| matches!(vis.kind(), VisibilityKind::Pub))
176}
177
178crate::tidy_ast_test!(check_dyn_wrapper_in_api, {
179    crate::example_tests!(EXAMPLES, check_dyn_wrapper_in_api);
180
181    #[gtest]
182    fn pub_newtype_field_flagged() -> Result<()> {
183        let v = run("pub struct DynStore(pub Arc<dyn Store>);");
184        verify_eq!(v.len(), 1)?;
185
186        Ok(())
187    }
188});