Skip to main content

wowlab_tidy/languages/rust/rules/complexity/
too_many_lines_in_file.rs

1#[cfg(test)]
2use googletest::prelude::*;
3
4use crate::{Example, FileCtx, Violation, violation};
5
6const PERCENT_DENOMINATOR: usize = 100;
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[];
10
11crate::line_rule!(
12    too_many_lines_in_file,
13    "Flag files exceeding threshold lines.",
14    "Files over 1500 lines are a sign that the module has too many responsibilities and should be split.",
15    Medium,
16    params {
17        threshold: i64 = 1500,
18        slack_percent: i64 = 20
19    },
20);
21
22fn check_too_many_lines_in_file(ctx: &FileCtx<'_>) -> Vec<Violation> {
23    let max_lines = ctx
24        .config
25        .get_usize("rust_too_many_lines_in_file", &PARAMS[0]);
26    let slack_percent = ctx
27        .config
28        .get_usize("rust_too_many_lines_in_file", &PARAMS[1]);
29    let budget = ctx.lines.iter().find_map(|line| {
30        let crate::infra::parse::DirectiveResult::Valid(directive) =
31            crate::infra::parse::directive(line)?
32        else {
33            return None;
34        };
35
36        (directive.scope == crate::infra::parse::Scope::File)
37            .then(|| directive.values.get("rust_too_many_lines_in_file").copied())
38            .flatten()
39    });
40    let allowed = budget.map_or(max_lines, |budget| {
41        budget.saturating_mul(PERCENT_DENOMINATOR + slack_percent) / PERCENT_DENOMINATOR
42    });
43
44    if ctx.lines.len() > allowed {
45        let message = budget.map_or_else(
46            || {
47                format!(
48                    "file has {} lines (max {max_lines}), consider splitting into smaller modules",
49                    ctx.lines.len()
50                )
51            },
52            |budget| {
53                format!(
54                    "file has {} lines, exceeding declared budget {budget} with {slack_percent}% slack (max {allowed})",
55                    ctx.lines.len()
56                )
57            },
58        );
59
60        vec![violation(ctx.rel, 1, message)]
61    } else {
62        Vec::new()
63    }
64}
65
66crate::tidy_test!(check_too_many_lines_in_file, {
67    #[gtest]
68    fn too_many_lines_fails() -> Result<()> {
69        let source = (0..1501)
70            .map(|i| format!("// line {i}"))
71            .collect::<Vec<_>>()
72            .join("\n");
73        let v = run(&source);
74        verify_eq!(v.len(), 1)?;
75        verify_eq!(v[0].line, 1)?;
76        verify_true!(v[0].message.contains("1501 lines"))?;
77
78        Ok(())
79    }
80
81    #[gtest]
82    fn exactly_max_passes() -> Result<()> {
83        let source = (0..1500)
84            .map(|i| format!("// line {i}"))
85            .collect::<Vec<_>>()
86            .join("\n");
87        verify_true!(run(&source).is_empty())?;
88
89        Ok(())
90    }
91
92    #[gtest]
93    fn declared_budget_allows_slack() -> Result<()> {
94        let mut lines =
95            vec!["// #t(file: rust_too_many_lines_in_file = 1300) cohesive table".to_owned()];
96        lines.extend((1..=1559).map(|i| format!("// line {i}")));
97        verify_true!(run(&lines.join("\n")).is_empty())?;
98
99        Ok(())
100    }
101
102    #[gtest]
103    fn declared_budget_reports_growth_beyond_slack() -> Result<()> {
104        let mut lines =
105            vec!["// #t(file: rust_too_many_lines_in_file = 1300) cohesive table".to_owned()];
106        lines.extend((1..=1560).map(|i| format!("// line {i}")));
107        let violations = run(&lines.join("\n"));
108        verify_eq!(violations.len(), 1)?;
109        verify_true!(violations[0].message.contains("declared budget 1300"))?;
110
111        Ok(())
112    }
113});