Skip to main content

agentlint/
diagnostic.rs

1use std::cmp::Ordering;
2use std::path::PathBuf;
3use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, clap::ValueEnum)]
8#[serde(rename_all = "lowercase")]
9pub enum Severity {
10    Info,
11    #[default]
12    Warning,
13    Error,
14}
15
16impl Severity {
17    pub const fn rank(self) -> u8 {
18        match self {
19            Self::Info => 0,
20            Self::Warning => 1,
21            Self::Error => 2,
22        }
23    }
24}
25
26impl std::fmt::Display for Severity {
27    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            Self::Info => write!(formatter, "info"),
30            Self::Warning => write!(formatter, "warning"),
31            Self::Error => write!(formatter, "error"),
32        }
33    }
34}
35
36impl FromStr for Severity {
37    type Err = String;
38
39    fn from_str(value: &str) -> Result<Self, Self::Err> {
40        match value.to_ascii_lowercase().as_str() {
41            "info" | "suggestion" => Ok(Self::Info),
42            "warning" | "warn" => Ok(Self::Warning),
43            "error" => Ok(Self::Error),
44            _ => Err(format!("unknown severity `{value}`")),
45        }
46    }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub struct Span {
51    pub line: usize,
52    pub column: usize,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct Diagnostic {
57    pub rule: String,
58    pub severity: Severity,
59    pub file: PathBuf,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub line: Option<usize>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub column: Option<usize>,
64    pub message: String,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub suggestion: Option<String>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub reference: Option<String>,
69}
70
71impl Diagnostic {
72    pub fn new(
73        rule: &str,
74        severity: Severity,
75        file: impl Into<PathBuf>,
76        message: impl Into<String>,
77    ) -> Self {
78        Self {
79            rule: rule.to_owned(),
80            severity,
81            file: file.into(),
82            line: None,
83            column: None,
84            message: message.into(),
85            suggestion: None,
86            reference: None,
87        }
88    }
89
90    #[must_use]
91    pub const fn at(mut self, line: usize, column: usize) -> Self {
92        self.line = Some(line);
93        self.column = Some(column);
94        self
95    }
96
97    #[must_use]
98    pub fn suggest(mut self, suggestion: impl Into<String>) -> Self {
99        self.suggestion = Some(suggestion.into());
100        self
101    }
102
103    #[must_use]
104    pub fn reference(mut self, reference: impl Into<String>) -> Self {
105        self.reference = Some(reference.into());
106        self
107    }
108}
109
110pub fn sort_diagnostics(items: &mut [Diagnostic]) {
111    items.sort_by(|left, right| {
112        left.file
113            .cmp(&right.file)
114            .then_with(|| left.line.cmp(&right.line))
115            .then_with(|| left.column.cmp(&right.column))
116            .then_with(|| match right.severity.rank().cmp(&left.severity.rank()) {
117                Ordering::Equal => left.rule.cmp(&right.rule),
118                ordering => ordering,
119            })
120            .then_with(|| left.message.cmp(&right.message))
121    });
122}