stern4rust/rules/source/
header_rule.rs1use crate::reporting::offence::Offence;
6use crate::reporting::rule_explanation::RuleExplanation;
7use crate::rule::Rule;
8use crate::source_file::SourceFile;
9
10pub struct HeaderRule {
21 expected: Vec<String>,
22}
23
24impl HeaderRule {
25 pub const NAME: &'static str = "header";
26
27 pub fn new(expected: Vec<String>) -> Self {
28 Self { expected }
29 }
30
31 pub fn expected(&self) -> &[String] {
32 &self.expected
33 }
34
35 fn correction(&self) -> String {
41 format!(
42 "make the first {} lines of the file match the expected header",
43 self.expected.len()
44 )
45 }
46
47 fn expected_text(&self) -> String {
48 self.expected.join("\n")
49 }
50
51 fn missing_entirely(&self, file: &SourceFile) -> Offence {
52 Offence::new(
53 file.relative_path(),
54 1,
55 self.name(),
56 "file is empty, so it carries no header".to_string(),
57 self.correction(),
58 )
59 .with_expected(&self.expected_text())
60 }
61
62 fn ends_early(&self, file: &SourceFile) -> Offence {
63 Offence::new(
64 file.relative_path(),
65 file.lines().len(),
66 self.name(),
67 format!(
68 "file has {} lines but the header is {}",
69 file.lines().len(),
70 self.expected.len()
71 ),
72 self.correction(),
73 )
74 .with_expected(&self.expected_text())
75 }
76
77 fn line_differs(&self, file: &SourceFile, index: usize) -> Offence {
78 Offence::new(
79 file.relative_path(),
80 index + 1,
81 self.name(),
82 format!(
83 "expected {:?} but found {:?}",
84 self.expected[index],
85 file.lines()[index]
86 ),
87 self.correction(),
88 )
89 .with_expected(&self.expected_text())
90 }
91}
92
93impl Rule for HeaderRule {
94 fn name(&self) -> &'static str {
95 Self::NAME
96 }
97
98 fn requirement(&self) -> Option<&'static str> {
100 Some("needs --header-file")
101 }
102
103 fn is_configured(&self) -> bool {
104 !self.expected.is_empty()
105 }
106
107 fn check(&self, file: &SourceFile) -> Vec<Offence> {
113 if self.expected.is_empty() {
114 return Vec::new();
115 }
116 if file.is_empty() {
117 return vec![self.missing_entirely(file)];
118 }
119 let overlap = self.expected.len().min(file.lines().len());
120 if let Some(index) =
121 (0..overlap).find(|index| file.lines()[*index] != self.expected[*index])
122 {
123 return vec![self.line_differs(file, index)];
124 }
125 if file.lines().len() < self.expected.len() {
126 return vec![self.ends_early(file)];
127 }
128 Vec::new()
129 }
130
131 fn check_workspace(&self, _files: &[SourceFile]) -> Vec<Offence> {
132 Vec::new()
133 }
134
135 fn explanation(&self) -> RuleExplanation {
136 RuleExplanation::new(
137 self.name(),
138 "Every .rs file opens with the repository's header.",
139 "pub struct Widget;",
140 "// Copyright 2025 ...
141// SPDX-License-Identifier: MIT
142
143pub struct Widget;",
144 )
145 }
146}