wowlab_tidy/languages/rust/rules/hygiene/
inline_test_module_size.rs1#[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});