wowlab_tidy/languages/rust/rules/api/
unbalanced_crate_root.rs1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::{
4 AstNode,
5 ast::{self, HasVisibility, VisibilityKind},
6};
7
8use crate::{AstCtx, Example, Violation};
9
10#[rustfmt::skip]
11const EXAMPLES: &[Example] = &[
12 Example {
13 label: "balanced root",
14 code: "pub struct Client;\npub mod account;\npub mod network;",
15 pass: true,
16 },
17 Example {
18 label: "empty root over many modules",
19 code: "pub mod a;\npub mod b;\npub mod c;\npub mod d;\npub mod e;\npub mod f;\npub mod g;\npub mod h;",
20 pass: false,
21 },
22 Example {
23 label: "empty root with few modules",
24 code: "pub mod a;\npub mod b;\npub mod c;\npub mod d;\npub mod e;\npub mod f;\npub mod g;",
25 pass: true,
26 },
27 Example {
28 label: "many private modules",
29 code: "mod a;\nmod b;\nmod c;\nmod d;\nmod e;\nmod f;\nmod g;\nmod h;\npub use a::Client;",
30 pass: true,
31 },
32 Example {
33 label: "many modules with root item",
34 code: "pub struct Client;\npub mod a;\npub mod b;\npub mod c;\npub mod d;\npub mod e;\npub mod f;\npub mod g;\npub mod h;",
35 pass: true,
36 },
37];
38
39crate::ast_rule!(
40 unbalanced_crate_root,
41 "Flag `lib.rs` roots that are flat item dumps (too many pub items) or empty shells (no pub items over many pub modules).",
42 "A crate root with dozens of loose public items or nothing but module declarations is hard to navigate; balance essential items in the root with semantic submodules (M-BALANCED-MODULES).",
43 Low,
44 params {
45 max_root_items: i64 = 40,
46 min_modules_for_empty_root: i64 = 8
47 },
48);
49
50fn check_unbalanced_crate_root(ctx: &AstCtx<'_>) -> Vec<Violation> {
51 if ctx.file.rel != "lib.rs" && !ctx.file.rel.ends_with("/lib.rs") {
52 return Vec::new();
53 }
54
55 let max_root_items = ctx
56 .file
57 .config
58 .get_usize("rust_unbalanced_crate_root", &PARAMS[0]);
59 let min_modules = ctx
60 .file
61 .config
62 .get_usize("rust_unbalanced_crate_root", &PARAMS[1]);
63 let mut pub_items = 0usize;
64 let mut pub_mods = 0usize;
65
66 for item in ctx.root.syntax().children().filter_map(ast::Item::cast) {
67 if ctx.is_in_test(&item) {
68 continue;
69 }
70
71 match item {
72 ast::Item::Module(module) if is_pub(module.visibility()) => pub_mods += 1,
73 ast::Item::Use(_) | ast::Item::Module(_) => {}
74 item if item_is_public(&item) => pub_items += 1,
75 _ => {}
76 }
77 }
78
79 if pub_items > max_root_items {
80 vec![crate::violation(
81 ctx.file.rel,
82 1,
83 format!(
84 "crate root defines {pub_items} public items (max {max_root_items}) — group related items into modules (M-BALANCED-MODULES)"
85 ),
86 )]
87 } else if pub_items == 0 && pub_mods >= min_modules {
88 vec![crate::violation(
89 ctx.file.rel,
90 1,
91 format!(
92 "crate root has no public items but {pub_mods} public modules — hoist essential items into the root (M-BALANCED-MODULES)"
93 ),
94 )]
95 } else {
96 Vec::new()
97 }
98}
99
100fn item_is_public(item: &ast::Item) -> bool {
101 match item {
102 ast::Item::Const(item) => is_pub(item.visibility()),
103 ast::Item::Enum(item) => is_pub(item.visibility()),
104 ast::Item::Fn(item) => is_pub(item.visibility()),
105 ast::Item::Static(item) => is_pub(item.visibility()),
106 ast::Item::Struct(item) => is_pub(item.visibility()),
107 ast::Item::Trait(item) => is_pub(item.visibility()),
108 ast::Item::TypeAlias(item) => is_pub(item.visibility()),
109 ast::Item::Union(item) => is_pub(item.visibility()),
110 _ => false,
111 }
112}
113
114fn is_pub(visibility: Option<ast::Visibility>) -> bool {
115 visibility.is_some_and(|vis| matches!(vis.kind(), VisibilityKind::Pub))
116}
117
118#[cfg(test)]
119mod tests {
120 use std::fmt::Write as _;
121
122 use super::*;
123
124 fn run_at(rel: &str, source: &str) -> Vec<Violation> {
125 crate::test_support::check_source_ast_at(rel, source, check_unbalanced_crate_root)
126 }
127
128 #[gtest]
129 fn examples() -> Result<()> {
130 for ex in EXAMPLES {
131 let violations = run_at("crates/demo/src/lib.rs", ex.code);
132
133 verify_eq!(violations.is_empty(), ex.pass)?;
134 }
135
136 Ok(())
137 }
138
139 #[gtest]
140 fn flat_root_over_limit_fails() -> Result<()> {
141 let mut src = String::new();
142
143 for i in 0..41 {
144 let _ = writeln!(src, "pub fn f{i}() {{}}");
145 }
146
147 let v = run_at("crates/demo/src/lib.rs", &src);
148
149 verify_eq!(v.len(), 1)?;
150 verify_eq!(v[0].line, 1)?;
151
152 Ok(())
153 }
154
155 #[gtest]
156 fn flat_root_at_limit_passes() -> Result<()> {
157 let mut src = String::new();
158
159 for i in 0..40 {
160 let _ = writeln!(src, "pub fn f{i}() {{}}");
161 }
162
163 let v = run_at("crates/demo/src/lib.rs", &src);
164
165 verify_true!(v.is_empty())?;
166
167 Ok(())
168 }
169
170 #[gtest]
171 fn non_lib_file_is_not_gated() -> Result<()> {
172 let v = run_at(
173 "crates/demo/src/util.rs",
174 "pub mod a;\npub mod b;\npub mod c;\npub mod d;\npub mod e;\npub mod f;\npub mod g;\npub mod h;",
175 );
176
177 verify_true!(v.is_empty())?;
178
179 Ok(())
180 }
181}