Skip to main content

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

1#[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}