use serde::{Deserialize, Serialize};
use crate::fix::Fix;
use crate::location::Location;
use crate::rule_id::RuleId;
use crate::severity::Severity;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Violation {
pub rule_id: RuleId,
pub location: Location,
pub message: String,
pub remediation: String,
pub severity: Severity,
#[serde(skip_serializing_if = "Option::is_none")]
pub fix: Option<Fix>,
}
pub fn sort(violations: &mut [Violation]) {
violations.sort_by(|a, b| {
a.rule_id
.cmp(&b.rule_id)
.then_with(|| a.location.file.cmp(&b.location.file))
.then_with(|| a.location.position.line.cmp(&b.location.position.line))
.then_with(|| a.location.position.column.cmp(&b.location.position.column))
});
}
#[must_use]
pub fn any_failing(violations: &[Violation]) -> bool {
violations.iter().any(|v| v.severity.is_failing())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::location::{FilePath, Position};
fn violation(rule: &str, file: &str, line: u32, column: u32) -> Violation {
Violation {
rule_id: rule.parse().expect("valid rule id"),
location: Location::new(FilePath::new(file), Position::new(line, column)),
message: "message".to_owned(),
remediation: "remediation".to_owned(),
severity: Severity::Error,
fix: None,
}
}
fn keys(violations: &[Violation]) -> Vec<String> {
violations
.iter()
.map(|v| format!("{} {}", v.rule_id, v.location))
.collect()
}
#[test]
fn sorts_by_rule_then_file_then_line_then_column() {
let mut violations = vec![
violation("local/b", "src/a.ts", 1, 1),
violation("lanekeep/a", "src/b.ts", 1, 1),
violation("lanekeep/a", "src/a.ts", 2, 1),
violation("lanekeep/a", "src/a.ts", 1, 5),
violation("lanekeep/a", "src/a.ts", 1, 1),
];
sort(&mut violations);
assert_eq!(
keys(&violations),
[
"lanekeep/a src/a.ts:1:1",
"lanekeep/a src/a.ts:1:5",
"lanekeep/a src/a.ts:2:1",
"lanekeep/a src/b.ts:1:1",
"local/b src/a.ts:1:1",
]
);
}
#[test]
fn line_and_column_compare_numerically() {
let mut violations = vec![
violation("local/a", "src/a.ts", 10, 1),
violation("local/a", "src/a.ts", 9, 1),
violation("local/a", "src/a.ts", 1, 10),
violation("local/a", "src/a.ts", 1, 9),
];
sort(&mut violations);
let positions: Vec<String> = violations
.iter()
.map(|v| v.location.position.to_string())
.collect();
assert_eq!(positions, ["1:9", "1:10", "9:1", "10:1"]);
}
#[test]
fn sorting_is_independent_of_input_order() {
let canonical = {
let mut v = vec![
violation("lanekeep/a", "src/a.ts", 1, 1),
violation("lanekeep/b", "src/a.ts", 1, 1),
violation("local/a", "src/a.ts", 1, 1),
violation("local/a", "src/b.ts", 3, 7),
];
sort(&mut v);
keys(&v)
};
let base = vec![
violation("lanekeep/a", "src/a.ts", 1, 1),
violation("lanekeep/b", "src/a.ts", 1, 1),
violation("local/a", "src/a.ts", 1, 1),
violation("local/a", "src/b.ts", 3, 7),
];
for rotation in 0..base.len() {
let mut rotated = base.clone();
rotated.rotate_left(rotation);
sort(&mut rotated);
assert_eq!(
keys(&rotated),
canonical,
"rotation {rotation} sorted differently"
);
}
let mut reversed = base.clone();
reversed.reverse();
sort(&mut reversed);
assert_eq!(keys(&reversed), canonical);
}
#[test]
fn sorting_is_idempotent() {
let mut violations = vec![
violation("local/z", "src/z.ts", 5, 5),
violation("lanekeep/a", "src/a.ts", 1, 1),
];
sort(&mut violations);
let once = keys(&violations);
sort(&mut violations);
assert_eq!(keys(&violations), once);
}
#[test]
fn ties_keep_their_relative_order() {
let mut violations = vec![
Violation {
message: "first".to_owned(),
..violation("local/a", "src/a.ts", 1, 1)
},
Violation {
message: "second".to_owned(),
..violation("local/a", "src/a.ts", 1, 1)
},
];
sort(&mut violations);
let messages: Vec<&str> = violations.iter().map(|v| v.message.as_str()).collect();
assert_eq!(messages, ["first", "second"]);
}
#[test]
fn handles_empty_and_single_element_input() {
let mut empty: Vec<Violation> = Vec::new();
sort(&mut empty);
assert!(empty.is_empty());
let mut one = vec![violation("local/a", "src/a.ts", 1, 1)];
sort(&mut one);
assert_eq!(one.len(), 1);
}
#[test]
fn only_error_severity_fails_the_run() {
let warn = Violation {
severity: Severity::Warn,
..violation("local/a", "a.ts", 1, 1)
};
let error = violation("local/b", "a.ts", 1, 1);
assert!(!any_failing(&[]));
assert!(!any_failing(std::slice::from_ref(&warn)));
assert!(any_failing(std::slice::from_ref(&error)));
assert!(any_failing(&[warn, error]));
}
#[test]
fn round_trips_through_json() {
let original = violation("lanekeep/no-default-export", "src/a.ts", 3, 9);
let json = serde_json::to_string(&original).expect("serializes");
let back: Violation = serde_json::from_str(&json).expect("deserializes");
assert_eq!(back, original);
}
}