Skip to main content

stern4rust/
offence.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use std::fmt::Debug;
6
7use serde::Serialize;
8
9// One thing wrong with one file. Every rule reports in this currency, so the
10// report is a single table rather than one section per rule, and a new rule
11// costs nothing in the printer.
12//
13// `subject` and `expected` are what make the JSON report worth consuming rather
14// than only reading. The description is a sentence for a person; the subject is
15// the thing the offence is about, and `expected` is the correct text where the
16// rule knows it. A rule opts into both, so a rule with nothing precise to add
17// says nothing rather than repeating its own prose in another field.
18// `correction` is required rather than optional, and that is the point. A rule
19// that can say what is wrong can say what to do about it, and making the field
20// optional would let a future rule quietly omit the half of the report that is
21// worth acting on -- the exact shape of silent gap this tool exists to catch.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
23pub struct Offence {
24    pub file: String,
25    pub line: usize,
26    pub rule: &'static str,
27    pub description: String,
28    pub correction: String,
29    pub subject: Option<String>,
30    pub expected: Option<String>,
31}
32
33impl Offence {
34    pub fn new(
35        file: &str,
36        line: usize,
37        rule: &'static str,
38        description: String,
39        correction: String,
40    ) -> Self {
41        Self {
42            file: file.to_string(),
43            line,
44            rule,
45            description,
46            correction,
47            subject: None,
48            expected: None,
49        }
50    }
51
52    pub fn with_subject(self, subject: &str) -> Self {
53        Self {
54            subject: Some(subject.to_string()),
55            ..self
56        }
57    }
58
59    pub fn with_expected(self, expected: &str) -> Self {
60        Self {
61            expected: Some(expected.to_string()),
62            ..self
63        }
64    }
65
66    // Offences are found in whatever order the rules happen to run, which puts
67    // every tree-wide one after every per-file one. Grouping by file and then by
68    // line is what lets a reader -- or a tool consuming the report -- work
69    // through one file at a time instead of jumping between them.
70    pub fn sort_key(&self) -> (&str, usize, &'static str) {
71        (&self.file, self.line, self.rule)
72    }
73}