Skip to main content

wowlab_tidy/languages/rust/rules/hygiene/
inline_test_module_size.rs

1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::{
4    AstNode,
5    ast::{self, HasAttrs, HasName},
6};
7
8use crate::{AstCtx, Example, Violation};
9
10#[rustfmt::skip]
11const EXAMPLES: &[Example] = &[
12    Example {
13        label: "small test module",
14        code: "#[cfg(test)]\nmod tests {\n    #[test]\n    fn t() {}\n}",
15        pass: true,
16    },
17    Example {
18        label: "non-test module",
19        code: "mod helpers {\n    fn h() {}\n}",
20        pass: true,
21    },
22];
23
24crate::ast_rule!(
25    inline_test_module_size,
26    "Flag `#[cfg(test)] mod` blocks spanning more than threshold lines.",
27    "Oversized inline test modules drown the business logic in the same file; tests touching only public API are integration tests and belong under `tests/`.",
28    Low,
29    params {
30        threshold: i64 = 200
31    },
32);
33
34fn check_inline_test_module_size(ctx: &AstCtx<'_>) -> Vec<Violation> {
35    let threshold = ctx
36        .file
37        .config
38        .get_usize("rust_inline_test_module_size", &PARAMS[0]);
39
40    ctx.nodes::<ast::Module>()
41        .filter(is_cfg_test_module)
42        .filter_map(|module| {
43            let item_list = module.item_list()?;
44            let range = item_list.syntax().text_range();
45            let start = ctx.line_index.line_col(range.start()).line as usize + 1;
46            let end = ctx.line_index.line_col(range.end()).line as usize + 1;
47            let lines = end.saturating_sub(start);
48
49            if lines <= threshold {
50                return None;
51            }
52
53            let name = module.name()?;
54
55            Some(ctx.violation(
56                &name,
57                format!(
58                    "#[cfg(test)] mod `{name}` is {lines} lines long (max {threshold}) — move public-API tests under tests/"
59                ),
60            ))
61        })
62        .collect()
63}
64
65fn is_cfg_test_module(module: &ast::Module) -> bool {
66    module.attrs().any(|attr| {
67        attr.simple_name().is_some_and(|name| name == "cfg")
68            && attr
69                .syntax()
70                .descendants_with_tokens()
71                .filter_map(ra_ap_syntax::NodeOrToken::into_token)
72                .any(|token| token.text() == "test")
73    })
74}
75
76crate::tidy_ast_test!(check_inline_test_module_size, {
77    use std::fmt::Write as _;
78
79    crate::example_tests!(EXAMPLES, check_inline_test_module_size);
80
81    fn module_with_lines(attr: &str, count: usize) -> String {
82        let mut src = format!("{attr}\nmod tests {{\n");
83        for i in 0..count {
84            let _ = writeln!(src, "    fn t{i}() {{}}");
85        }
86        src.push_str("}\n");
87        src
88    }
89
90    #[gtest]
91    fn oversized_test_module_fails() -> Result<()> {
92        let v = run(&module_with_lines("#[cfg(test)]", 205));
93        verify_eq!(v.len(), 1)?;
94        verify_true!(v[0].message.contains("tests"))?;
95
96        Ok(())
97    }
98
99    #[gtest]
100    fn oversized_plain_module_passes() -> Result<()> {
101        let v = run(&module_with_lines("#[rustfmt::skip]", 205));
102        verify_true!(v.is_empty())?;
103
104        Ok(())
105    }
106
107    #[gtest]
108    fn test_module_at_threshold_passes() -> Result<()> {
109        let v = run(&module_with_lines("#[cfg(test)]", 150));
110        verify_true!(v.is_empty())?;
111
112        Ok(())
113    }
114});