use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
Info,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct LintFinding {
pub severity: Severity,
pub rule: String,
pub sheet: String,
pub cell: Option<String>,
pub message: String,
pub repair: String,
}
impl LintFinding {
pub fn new(
severity: Severity,
rule: impl Into<String>,
sheet: impl Into<String>,
cell: Option<String>,
message: impl Into<String>,
repair: impl Into<String>,
) -> Self {
Self {
severity,
rule: rule.into(),
sheet: sheet.into(),
cell,
message: message.into(),
repair: repair.into(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
pub struct LintReport {
pub findings: Vec<LintFinding>,
}
impl LintReport {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, finding: LintFinding) {
self.findings.push(finding);
}
pub fn extend(&mut self, findings: impl IntoIterator<Item = LintFinding>) {
self.findings.extend(findings);
}
pub fn has_errors(&self) -> bool {
self.findings.iter().any(|f| f.severity == Severity::Error)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn has_errors_gates_on_error_severity_only() {
let mut report = LintReport::new();
report.push(LintFinding::new(
Severity::Error,
"whitelist/unsupported-fn",
"1_Inputs",
Some("A2".to_string()),
"OFFSET is not in the dialect whitelist",
"Replace OFFSET with an INDEX/MATCH lookup",
));
report.push(LintFinding::new(
Severity::Warning,
"structure/hidden-row",
"1_Inputs",
None,
"row 7 is hidden",
"Unhide the row or document why it is hidden",
));
assert!(report.has_errors(), "an Error finding must trip has_errors");
assert_eq!(report.findings.len(), 2);
}
#[test]
fn has_errors_false_when_only_warnings_and_info() {
let mut report = LintReport::new();
report.extend([
LintFinding::new(
Severity::Warning,
"structure/hidden-row",
"1_Inputs",
None,
"row 7 is hidden",
"Unhide the row",
),
LintFinding::new(
Severity::Info,
"structure/note",
"0_Guide",
None,
"guide legend present",
"No action required",
),
]);
assert!(
!report.has_errors(),
"warnings and info alone must NOT trip has_errors (D-05)"
);
}
#[test]
fn lint_finding_serializes_with_repair_field() {
let finding = LintFinding::new(
Severity::Error,
"structure/external-link",
"1_Inputs",
Some("E6".to_string()),
"external link reference [1]Sheet1 found",
"Inline the referenced value; the dialect forbids external links",
);
let json = serde_json::to_value(&finding).expect("serialize finding");
assert_eq!(
json["repair"],
"Inline the referenced value; the dialect forbids external links"
);
assert_eq!(json["severity"], "error");
assert_eq!(json["rule"], "structure/external-link");
assert_eq!(json["cell"], "E6");
}
#[test]
fn lint_report_round_trips_through_json() {
let mut report = LintReport::new();
report.push(LintFinding::new(
Severity::Error,
"whitelist/unsupported-fn",
"1_Inputs",
Some("A2".into()),
"msg",
"repair",
));
let back: LintReport =
serde_json::from_value(serde_json::to_value(&report).unwrap()).unwrap();
assert_eq!(back.findings.len(), 1);
assert!(back.has_errors());
}
}