1use serde::{Deserialize, Serialize};
4
5use crate::fix::Fix;
6use crate::location::Location;
7use crate::rule_id::RuleId;
8use crate::severity::Severity;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct Violation {
13 pub rule_id: RuleId,
15 pub location: Location,
17 pub message: String,
20 pub remediation: String,
22 pub severity: Severity,
24 #[serde(skip_serializing_if = "Option::is_none")]
30 pub fix: Option<Fix>,
31}
32
33pub fn sort(violations: &mut [Violation]) {
45 violations.sort_by(|a, b| {
46 a.rule_id
47 .cmp(&b.rule_id)
48 .then_with(|| a.location.file.cmp(&b.location.file))
49 .then_with(|| a.location.position.line.cmp(&b.location.position.line))
50 .then_with(|| a.location.position.column.cmp(&b.location.position.column))
51 });
52}
53
54#[must_use]
56pub fn any_failing(violations: &[Violation]) -> bool {
57 violations.iter().any(|v| v.severity.is_failing())
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use crate::location::{FilePath, Position};
64
65 fn violation(rule: &str, file: &str, line: u32, column: u32) -> Violation {
66 Violation {
67 rule_id: rule.parse().expect("valid rule id"),
68 location: Location::new(FilePath::new(file), Position::new(line, column)),
69 message: "message".to_owned(),
70 remediation: "remediation".to_owned(),
71 severity: Severity::Error,
72 fix: None,
73 }
74 }
75
76 fn keys(violations: &[Violation]) -> Vec<String> {
77 violations
78 .iter()
79 .map(|v| format!("{} {}", v.rule_id, v.location))
80 .collect()
81 }
82
83 #[test]
84 fn sorts_by_rule_then_file_then_line_then_column() {
85 let mut violations = vec![
86 violation("local/b", "src/a.ts", 1, 1),
87 violation("lanekeep/a", "src/b.ts", 1, 1),
88 violation("lanekeep/a", "src/a.ts", 2, 1),
89 violation("lanekeep/a", "src/a.ts", 1, 5),
90 violation("lanekeep/a", "src/a.ts", 1, 1),
91 ];
92 sort(&mut violations);
93
94 assert_eq!(
95 keys(&violations),
96 [
97 "lanekeep/a src/a.ts:1:1",
98 "lanekeep/a src/a.ts:1:5",
99 "lanekeep/a src/a.ts:2:1",
100 "lanekeep/a src/b.ts:1:1",
101 "local/b src/a.ts:1:1",
102 ]
103 );
104 }
105
106 #[test]
107 fn line_and_column_compare_numerically() {
108 let mut violations = vec![
112 violation("local/a", "src/a.ts", 10, 1),
113 violation("local/a", "src/a.ts", 9, 1),
114 violation("local/a", "src/a.ts", 1, 10),
115 violation("local/a", "src/a.ts", 1, 9),
116 ];
117 sort(&mut violations);
118
119 let positions: Vec<String> = violations
120 .iter()
121 .map(|v| v.location.position.to_string())
122 .collect();
123 assert_eq!(positions, ["1:9", "1:10", "9:1", "10:1"]);
124 }
125
126 #[test]
127 fn sorting_is_independent_of_input_order() {
128 let canonical = {
132 let mut v = vec![
133 violation("lanekeep/a", "src/a.ts", 1, 1),
134 violation("lanekeep/b", "src/a.ts", 1, 1),
135 violation("local/a", "src/a.ts", 1, 1),
136 violation("local/a", "src/b.ts", 3, 7),
137 ];
138 sort(&mut v);
139 keys(&v)
140 };
141
142 let base = vec![
144 violation("lanekeep/a", "src/a.ts", 1, 1),
145 violation("lanekeep/b", "src/a.ts", 1, 1),
146 violation("local/a", "src/a.ts", 1, 1),
147 violation("local/a", "src/b.ts", 3, 7),
148 ];
149 for rotation in 0..base.len() {
150 let mut rotated = base.clone();
151 rotated.rotate_left(rotation);
152 sort(&mut rotated);
153 assert_eq!(
154 keys(&rotated),
155 canonical,
156 "rotation {rotation} sorted differently"
157 );
158 }
159
160 let mut reversed = base.clone();
161 reversed.reverse();
162 sort(&mut reversed);
163 assert_eq!(keys(&reversed), canonical);
164 }
165
166 #[test]
167 fn sorting_is_idempotent() {
168 let mut violations = vec![
169 violation("local/z", "src/z.ts", 5, 5),
170 violation("lanekeep/a", "src/a.ts", 1, 1),
171 ];
172 sort(&mut violations);
173 let once = keys(&violations);
174 sort(&mut violations);
175 assert_eq!(keys(&violations), once);
176 }
177
178 #[test]
179 fn ties_keep_their_relative_order() {
180 let mut violations = vec![
183 Violation {
184 message: "first".to_owned(),
185 ..violation("local/a", "src/a.ts", 1, 1)
186 },
187 Violation {
188 message: "second".to_owned(),
189 ..violation("local/a", "src/a.ts", 1, 1)
190 },
191 ];
192 sort(&mut violations);
193
194 let messages: Vec<&str> = violations.iter().map(|v| v.message.as_str()).collect();
195 assert_eq!(messages, ["first", "second"]);
196 }
197
198 #[test]
199 fn handles_empty_and_single_element_input() {
200 let mut empty: Vec<Violation> = Vec::new();
201 sort(&mut empty);
202 assert!(empty.is_empty());
203
204 let mut one = vec![violation("local/a", "src/a.ts", 1, 1)];
205 sort(&mut one);
206 assert_eq!(one.len(), 1);
207 }
208
209 #[test]
210 fn only_error_severity_fails_the_run() {
211 let warn = Violation {
212 severity: Severity::Warn,
213 ..violation("local/a", "a.ts", 1, 1)
214 };
215 let error = violation("local/b", "a.ts", 1, 1);
216
217 assert!(!any_failing(&[]));
218 assert!(!any_failing(std::slice::from_ref(&warn)));
219 assert!(any_failing(std::slice::from_ref(&error)));
220 assert!(any_failing(&[warn, error]));
221 }
222
223 #[test]
224 fn round_trips_through_json() {
225 let original = violation("lanekeep/no-default-export", "src/a.ts", 3, 9);
226 let json = serde_json::to_string(&original).expect("serializes");
227 let back: Violation = serde_json::from_str(&json).expect("deserializes");
228 assert_eq!(back, original);
229 }
230}