alint_rules/
file_max_lines.rs1use 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
75fn 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 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}