wowlab_tidy/languages/rust/rules/interop/
foreign_reexports.rs1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::{
4 AstNode,
5 ast::{self, HasAttrs, HasName},
6};
7use wowlab_types::sim::FastSet;
8
9use crate::{AstCtx, Example, Violation, infra::workspace::WorkspaceMembers};
10
11#[rustfmt::skip]
12const EXAMPLES: &[Example] = &[
13 Example {
14 label: "pub use of foreign crate item",
15 code: "pub use serde_json::Value;",
16 pass: false,
17 },
18 Example {
19 label: "pub use re-exporting foreign crate root",
20 code: "pub use rand;",
21 pass: false,
22 },
23 Example {
24 label: "foreign item in use group",
25 code: "pub use {std::fmt::Debug, chrono::Utc};",
26 pass: false,
27 },
28 Example {
29 label: "renamed foreign re-export",
30 code: "pub use serde_json::Value as JsonValue;",
31 pass: false,
32 },
33 Example {
34 label: "pub use of crate item",
35 code: "pub use crate::foo::Bar;",
36 pass: true,
37 },
38 Example {
39 label: "pub use of self path",
40 code: "pub use self::inner::Thing;",
41 pass: true,
42 },
43 Example {
44 label: "pub use of std item",
45 code: "pub use std::fmt::Debug;",
46 pass: true,
47 },
48 Example {
49 label: "pub use of workspace crate",
50 code: "pub use wowlab_types::game::SpecId;",
51 pass: true,
52 },
53 Example {
54 label: "pub use of workspace root alias",
55 code: "pub use common::output::Table;",
56 pass: true,
57 },
58 Example {
59 label: "non-pub use of foreign crate",
60 code: "use serde_json::Value;",
61 pass: true,
62 },
63 Example {
64 label: "pub(crate) use of foreign crate",
65 code: "pub(crate) use serde_json::Value;",
66 pass: true,
67 },
68 Example {
69 label: "doc(hidden) re-export",
70 code: "#[doc(hidden)]\npub use serde_json::Value;",
71 pass: true,
72 },
73 Example {
74 label: "re-export inside __private module",
75 code: "mod __private {\n pub use serde_json::Value;\n}",
76 pass: true,
77 },
78 Example {
79 label: "re-export inside _private module",
80 code: "mod _private {\n pub use serde_json::Value;\n}",
81 pass: true,
82 },
83 Example {
84 label: "re-export in test module",
85 code: "#[cfg(test)]\nmod tests {\n pub use serde_json::Value;\n}",
86 pass: true,
87 },
88 Example {
89 label: "re-export from local module",
90 code: "mod inner {}\npub use inner::Thing;",
91 pass: true,
92 },
93 Example {
94 label: "re-export from local out-of-line module",
95 code: "pub mod infra;\npub use infra::Config;",
96 pass: true,
97 },
98 Example {
99 label: "grouped re-export from local module",
100 code: "mod inner {}\npub use inner::{Thing, Other};",
101 pass: true,
102 },
103 Example {
104 label: "grouped re-export from foreign crate",
105 code: "pub use serde_json::{Map, Value};",
106 pass: false,
107 },
108];
109
110crate::ast_rule!(
111 foreign_reexports,
112 "Flag `pub use` re-exports of items from foreign crates.",
113 "Re-exported foreign items blur type identity into aliases; users should depend on the defining crate directly (M-FOREIGN-REEXPORTS).",
114 Medium,
115 params {
116 allowed: [String] = []
117 },
118);
119
120const BUILTIN_ROOTS: &[&str] = &["crate", "self", "super", "std", "core", "alloc"];
121
122fn check_foreign_reexports(ctx: &AstCtx<'_>) -> Vec<Violation> {
123 let allowed = ctx
124 .file
125 .config
126 .get_str_array("rust_foreign_reexports", &PARAMS[0]);
127 let members = ctx.file.config.workspace().members(ctx.file.path);
128 let local_mods = local_modules(ctx);
129
130 let public_uses = ctx
131 .nodes::<ast::Use>()
132 .filter(|item| !ctx.is_in_test(item))
133 .filter(|item| !inside_private_module(item));
134
135 public_uses
136 .filter(|item| super::support::is_fully_public(item) && !is_doc_hidden(item))
137 .flat_map(|item| {
138 item.use_tree()
139 .into_iter()
140 .flat_map(collect_roots)
141 .filter_map(|root| {
142 let name = root.text().to_string();
143
144 (!is_internal_root(&name, &members)
145 && !local_mods.contains(&name)
146 && !allowed.contains(&name))
147 .then(|| {
148 ctx.violation(
149 &root,
150 format!(
151 "`pub use` re-exports foreign crate `{name}` — users should import it from `{name}` directly"
152 ),
153 )
154 })
155 })
156 .collect::<Vec<_>>()
157 })
158 .collect()
159}
160
161fn local_modules(ctx: &AstCtx<'_>) -> FastSet<String> {
162 ctx.nodes::<ast::Module>()
163 .filter_map(|module| module.name().map(|name| name.text().to_string()))
164 .collect()
165}
166
167fn is_internal_root(root: &str, members: &WorkspaceMembers) -> bool {
168 BUILTIN_ROOTS.contains(&root) || members.is_member_root(root)
169}
170
171fn collect_roots(tree: ast::UseTree) -> Vec<ast::NameRef> {
173 let mut out = Vec::new();
174 let mut stack = vec![tree];
175
176 while let Some(tree) = stack.pop() {
177 if let Some(path) = tree.path() {
178 let names = super::support::path_names(path.clone());
179
180 if let Some(root_name) = names.first()
181 && let Some(root) = path
182 .syntax()
183 .descendants()
184 .filter_map(ast::NameRef::cast)
185 .find(|name| name.text() == root_name.as_str())
186 {
187 out.push(root);
188 }
189 } else if let Some(list) = tree.use_tree_list() {
190 stack.extend(list.use_trees());
191 }
192 }
193
194 out
195}
196
197fn is_doc_hidden(item: &ast::Use) -> bool {
198 item.attrs().any(|attr| {
199 attr.simple_name().is_some_and(|name| name == "doc")
200 && attr
201 .syntax()
202 .descendants_with_tokens()
203 .filter_map(ra_ap_syntax::NodeOrToken::into_token)
204 .any(|token| token.text() == "hidden")
205 })
206}
207
208fn inside_private_module(item: &ast::Use) -> bool {
209 item.syntax()
210 .ancestors()
211 .skip(1)
212 .filter_map(ast::Module::cast)
213 .filter_map(|module| module.name())
214 .any(|name| matches!(name.text().as_str(), "_private" | "__private"))
215}
216
217crate::tidy_ast_test!(check_foreign_reexports, {
218 crate::example_tests!(EXAMPLES, check_foreign_reexports);
219
220 #[gtest]
221 fn qualified_group_reports_the_outer_root_once() -> Result<()> {
222 let violations = run("pub use serde_json::{Map, Value};");
223 verify_eq!(violations.len(), 1)?;
224 verify_true!(violations[0].message.contains("`serde_json`"))?;
225
226 Ok(())
227 }
228});