rulewright 0.1.2

Executable engineering standards for Rust workspaces
Documentation
#[cfg(test)]
use googletest::prelude::*;

use crate::{Example, FileCtx, Violation, violation};

const PERCENT_DENOMINATOR: usize = 100;

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[];

crate::line_rule!(
    too_many_lines_in_file,
    "Flag files exceeding the configured nonblank-line threshold.",
    "A very large file often contains several responsibilities. Split it along domain or ownership boundaries with clear module names; do not scatter one cohesive implementation across arbitrary numbered files, and tune the threshold when separation would make navigation worse.",
    Medium,
    params {
        threshold: i64 = 1500,
        slack_percent: i64 = 20
    },
);

fn check_too_many_lines_in_file(ctx: &FileCtx<'_>) -> Vec<Violation> {
    let max_lines = ctx.config.get_usize(
        "rust_too_many_lines_in_file",
        &TOO_MANY_LINES_IN_FILE_PARAMS[0],
    );
    let slack_percent = ctx.config.get_usize(
        "rust_too_many_lines_in_file",
        &TOO_MANY_LINES_IN_FILE_PARAMS[1],
    );
    let budget = ctx.lines.iter().find_map(|line| {
        let crate::infra::parse::DirectiveResult::Valid(directive) =
            crate::infra::parse::directive(line)?
        else {
            return None;
        };

        (directive.scope == crate::infra::parse::Scope::File)
            .then(|| directive.values.get("rust_too_many_lines_in_file").copied())
            .flatten()
    });
    let allowed = budget.map_or(max_lines, |budget| {
        budget.saturating_mul(PERCENT_DENOMINATOR + slack_percent) / PERCENT_DENOMINATOR
    });
    let line_count = nonblank_line_count(ctx.lines.iter().copied());

    if line_count > allowed {
        let message = budget.map_or_else(
            || {
                format!(
                    "file has {line_count} nonblank lines (max {max_lines}), consider splitting into smaller modules",
                )
            },
            |budget| {
                format!(
                    "file has {line_count} nonblank lines, exceeding declared budget {budget} with {slack_percent}% slack (max {allowed})",
                )
            },
        );

        vec![violation(ctx.rel, 1, message)]
    } else {
        Vec::new()
    }
}

fn nonblank_line_count<'a>(lines: impl IntoIterator<Item = &'a str>) -> usize {
    lines
        .into_iter()
        .filter(|line| !line.trim().is_empty())
        .count()
}

crate::rulewright_test!(check_too_many_lines_in_file, {
    #[gtest]
    fn too_many_lines_fails() -> Result<()> {
        let source = (0..1501)
            .map(|i| format!("// line {i}"))
            .collect::<Vec<_>>()
            .join("\n");
        let v = run(&source);
        verify_eq!(v.len(), 1)?;
        verify_eq!(v[0].line, 1)?;
        verify_true!(v[0].message.contains("1501 nonblank lines"))?;

        Ok(())
    }

    #[gtest]
    fn exactly_max_passes() -> Result<()> {
        let source = (0..1500)
            .map(|i| format!("// line {i}"))
            .collect::<Vec<_>>()
            .join("\n");
        verify_true!(run(&source).is_empty())?;

        Ok(())
    }

    #[gtest]
    fn declared_budget_allows_slack() -> Result<()> {
        let mut lines =
            vec!["// #rw(file: rust_too_many_lines_in_file = 1300) cohesive table".to_owned()];
        lines.extend((1..=1559).map(|i| format!("// line {i}")));
        verify_true!(run(&lines.join("\n")).is_empty())?;

        Ok(())
    }

    #[gtest]
    fn declared_budget_reports_growth_beyond_slack() -> Result<()> {
        let mut lines =
            vec!["// #rw(file: rust_too_many_lines_in_file = 1300) cohesive table".to_owned()];
        lines.extend((1..=1560).map(|i| format!("// line {i}")));
        let violations = run(&lines.join("\n"));
        verify_eq!(violations.len(), 1)?;
        verify_true!(violations[0].message.contains("declared budget 1300"))?;

        Ok(())
    }

    #[gtest]
    fn readability_spacing_does_not_consume_the_file_budget() -> Result<()> {
        let source = (0..1500)
            .flat_map(|i| [format!("// line {i}"), String::new()])
            .collect::<Vec<_>>()
            .join("\n");

        verify_true!(run(&source).is_empty())
    }
});