stern4rust/reporting/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, Hash, 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}