Skip to main content

alint_rules/
file_max_lines.rs

1//! `file_max_lines` — files in scope must have AT MOST
2//! `max_lines` lines. Mirror of [`crate::file_min_lines`];
3//! shares the line-counting semantics so the two compose
4//! cleanly when both are applied to the same file.
5//!
6//! Catches the "everything-module" anti-pattern — a single
7//! `lib.rs` / `index.ts` / `helpers.py` that grew until it
8//! does the work of a half-dozen smaller files. The threshold
9//! is intentionally a soft signal rather than a hard limit;
10//! we ship it at `level: warning` in tutorials and rulesets,
11//! and leave the cap value to the team.
12
13use std::path::Path;
14
15use alint_core::{
16    Context, Error, Level, PerFileRule, Result, Rule, RuleSpec, Scope, Violation, eval_per_file,
17};
18use serde::Deserialize;
19
20#[derive(Debug, Deserialize)]
21#[serde(deny_unknown_fields)]
22struct Options {
23    max_lines: u64,
24}
25
26#[derive(Debug)]
27pub struct FileMaxLinesRule {
28    id: String,
29    level: Level,
30    policy_url: Option<String>,
31    message: Option<String>,
32    scope: Scope,
33    max_lines: u64,
34}
35
36impl Rule for FileMaxLinesRule {
37    alint_core::rule_common_impl!();
38
39    fn evaluate(&self, ctx: &Context<'_>) -> Result<Vec<Violation>> {
40        eval_per_file(self, ctx)
41    }
42
43    fn as_per_file(&self) -> Option<&dyn PerFileRule> {
44        Some(self)
45    }
46}
47
48impl PerFileRule for FileMaxLinesRule {
49    fn path_scope(&self) -> &Scope {
50        &self.scope
51    }
52
53    fn evaluate_file(
54        &self,
55        _ctx: &Context<'_>,
56        path: &Path,
57        bytes: &[u8],
58    ) -> Result<Vec<Violation>> {
59        let lines = count_lines(bytes);
60        if lines <= self.max_lines {
61            return Ok(Vec::new());
62        }
63        let msg = self.message.clone().unwrap_or_else(|| {
64            format!(
65                "file has {} line(s); at most {} allowed",
66                lines, self.max_lines,
67            )
68        });
69        Ok(vec![
70            Violation::new(msg).with_path(std::sync::Arc::<Path>::from(path)),
71        ])
72    }
73}
74
75/// Count lines with the same `wc -l`-style semantics as
76/// `file_min_lines::count_lines`. Kept as a private function
77/// here (rather than reused from `file_min_lines`) because
78/// inlining it makes the unit tests explicit about what this
79/// rule's threshold is being compared against — and the
80/// implementation is one line, not worth a cross-module dep.
81fn count_lines(bytes: &[u8]) -> u64 {
82    if bytes.is_empty() {
83        return 0;
84    }
85    #[allow(clippy::naive_bytecount)]
86    let newlines = bytes.iter().filter(|&&b| b == b'\n').count() as u64;
87    let trailing_unterminated = u64::from(!bytes.ends_with(b"\n"));
88    newlines + trailing_unterminated
89}
90
91pub fn build(spec: &RuleSpec) -> Result<Box<dyn Rule>> {
92    let Some(_paths) = &spec.paths else {
93        return Err(Error::rule_config(
94            &spec.id,
95            "file_max_lines requires a `paths` field",
96        ));
97    };
98    let opts: Options = spec
99        .deserialize_options()
100        .map_err(|e| Error::rule_config(&spec.id, format!("invalid options: {e}")))?;
101    Ok(Box::new(FileMaxLinesRule {
102        id: spec.id.clone(),
103        level: spec.level,
104        policy_url: spec.policy_url.clone(),
105        message: spec.message.clone(),
106        scope: Scope::from_spec(spec)?,
107        max_lines: opts.max_lines,
108    }))
109}
110
111#[cfg(test)]
112mod tests {
113    use super::count_lines;
114
115    #[test]
116    fn empty_file_is_zero_lines() {
117        assert_eq!(count_lines(b""), 0);
118    }
119
120    #[test]
121    fn matches_min_lines_semantics() {
122        // Identical accounting to file_min_lines so the two
123        // rules agree on what "this file has N lines" means.
124        assert_eq!(count_lines(b"a\n"), 1);
125        assert_eq!(count_lines(b"a\nb\n"), 2);
126        assert_eq!(count_lines(b"a\nb"), 2);
127        assert_eq!(count_lines(b"\n\n"), 2);
128        assert_eq!(count_lines(b"a\n\nb\n"), 3);
129    }
130}