wowlab_tidy/languages/rust/rules/api/
dyn_wrapper_in_api.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: "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
123fn 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});