Skip to main content

stern4rust/rules/
header_rule.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use crate::offence::Offence;
6use crate::rule::Rule;
7use crate::source_file::SourceFile;
8
9// Every .rs file opens with the repository's header.
10//
11// The expected text is data rather than a constant, because it is not the same
12// twice: this repository is MIT, the etheram repositories are Apache 2.0, the
13// year moves, and another codebase will have something else entirely. A rule
14// that hardcoded one header would be right for exactly one repository.
15//
16// Only the first divergence is reported. A file whose header is missing
17// altogether would otherwise produce one offence per header line, burying every
18// other file in the report behind it.
19pub struct HeaderRule {
20    expected: Vec<String>,
21}
22
23impl HeaderRule {
24    pub fn new(expected: Vec<String>) -> Self {
25        Self { expected }
26    }
27
28    pub fn expected(&self) -> &[String] {
29        &self.expected
30    }
31
32    // The same instruction for all three failures, because the fix is the same
33    // one: whatever is at the top of the file now, the header replaces it. The
34    // whole expected text travels on the offence, so the correction does not
35    // have to repeat it -- and a consumer applies it in one pass rather than
36    // fixing one line, re-running, and finding the next.
37    fn correction(&self) -> String {
38        format!(
39            "make the first {} lines of the file match the expected header",
40            self.expected.len()
41        )
42    }
43
44    fn expected_text(&self) -> String {
45        self.expected.join("\n")
46    }
47
48    fn missing_entirely(&self, file: &SourceFile) -> Offence {
49        Offence::new(
50            file.relative_path(),
51            1,
52            self.name(),
53            "file is empty, so it carries no header".to_string(),
54            self.correction(),
55        )
56        .with_expected(&self.expected_text())
57    }
58
59    fn ends_early(&self, file: &SourceFile) -> Offence {
60        Offence::new(
61            file.relative_path(),
62            file.lines().len(),
63            self.name(),
64            format!(
65                "file has {} lines but the header is {}",
66                file.lines().len(),
67                self.expected.len()
68            ),
69            self.correction(),
70        )
71        .with_expected(&self.expected_text())
72    }
73
74    fn line_differs(&self, file: &SourceFile, index: usize) -> Offence {
75        Offence::new(
76            file.relative_path(),
77            index + 1,
78            self.name(),
79            format!(
80                "expected {:?} but found {:?}",
81                self.expected[index],
82                file.lines()[index]
83            ),
84            self.correction(),
85        )
86        .with_expected(&self.expected_text())
87    }
88}
89
90impl Rule for HeaderRule {
91    fn name(&self) -> &'static str {
92        "header"
93    }
94
95    // The overlap is compared before the length, so a file whose very first line
96    // is wrong is reported at line 1 rather than at its end. Checking length
97    // first would tell a file with no header at all that it was "too short",
98    // which is true and useless -- the actionable fact is that line 1 is not the
99    // header.
100    fn check(&self, file: &SourceFile) -> Vec<Offence> {
101        if self.expected.is_empty() {
102            return Vec::new();
103        }
104        if file.is_empty() {
105            return vec![self.missing_entirely(file)];
106        }
107        let overlap = self.expected.len().min(file.lines().len());
108        if let Some(index) =
109            (0..overlap).find(|index| file.lines()[*index] != self.expected[*index])
110        {
111            return vec![self.line_differs(file, index)];
112        }
113        if file.lines().len() < self.expected.len() {
114            return vec![self.ends_early(file)];
115        }
116        Vec::new()
117    }
118}