Skip to main content

normalize_facts_rules_api/
diagnostic.rs

1//! Diagnostic output from rules.
2//!
3//! Rules produce diagnostics when they detect issues in the code.
4//! These are displayed to users and can be used for CI enforcement.
5
6use serde::{Deserialize, Serialize};
7
8/// Severity level for a diagnostic.
9#[derive(
10    Clone,
11    Copy,
12    Debug,
13    PartialEq,
14    Eq,
15    Serialize,
16    Deserialize,
17    rkyv::Archive,
18    rkyv::Serialize,
19    rkyv::Deserialize,
20)]
21#[rkyv(derive(Debug))]
22pub enum DiagnosticLevel {
23    /// Informational hint
24    Hint,
25    /// Warning (may indicate a problem)
26    Warning,
27    /// Error (definite problem)
28    Error,
29}
30
31/// A source code location.
32#[derive(
33    Clone, Debug, Serialize, Deserialize, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize,
34)]
35#[rkyv(derive(Debug))]
36pub struct Location {
37    /// File path relative to project root
38    pub file: String,
39    /// Line number (1-indexed)
40    pub line: u32,
41    /// Column number (1-indexed, optional)
42    pub column: Option<u32>,
43}
44
45impl Location {
46    /// Create a location with file and line
47    pub fn new(file: &str, line: u32) -> Self {
48        Self {
49            file: file.into(),
50            line,
51            column: None,
52        }
53    }
54
55    /// Create a location with file, line, and column
56    pub fn with_column(file: &str, line: u32, column: u32) -> Self {
57        Self {
58            file: file.into(),
59            line,
60            column: Some(column),
61        }
62    }
63}
64
65/// A diagnostic produced by a rule.
66#[derive(
67    Clone, Debug, Serialize, Deserialize, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize,
68)]
69#[rkyv(derive(Debug))]
70pub struct Diagnostic {
71    /// Rule ID that produced this diagnostic (e.g., "circular-dependency")
72    pub rule_id: String,
73    /// Severity level
74    pub level: DiagnosticLevel,
75    /// Human-readable message
76    pub message: String,
77    /// Primary location (where the issue was detected)
78    pub location: Option<Location>,
79    /// Related locations (e.g., other files in a cycle)
80    pub related: Vec<Location>,
81    /// Optional fix suggestion
82    pub suggestion: Option<String>,
83}
84
85impl Diagnostic {
86    /// Create a new diagnostic
87    pub fn new(rule_id: &str, level: DiagnosticLevel, message: &str) -> Self {
88        Self {
89            rule_id: rule_id.into(),
90            level,
91            message: message.into(),
92            location: None,
93            related: Vec::new(),
94            suggestion: None,
95        }
96    }
97
98    /// Create an error diagnostic
99    pub fn error(rule_id: &str, message: &str) -> Self {
100        Self::new(rule_id, DiagnosticLevel::Error, message)
101    }
102
103    /// Create a warning diagnostic
104    pub fn warning(rule_id: &str, message: &str) -> Self {
105        Self::new(rule_id, DiagnosticLevel::Warning, message)
106    }
107
108    /// Create a hint diagnostic
109    pub fn hint(rule_id: &str, message: &str) -> Self {
110        Self::new(rule_id, DiagnosticLevel::Hint, message)
111    }
112
113    /// Set the primary location
114    pub fn at(mut self, file: &str, line: u32) -> Self {
115        self.location = Some(Location::new(file, line));
116        self
117    }
118
119    /// Add a related location
120    pub fn with_related(mut self, file: &str, line: u32) -> Self {
121        self.related.push(Location::new(file, line));
122        self
123    }
124
125    /// Add a fix suggestion
126    pub fn with_suggestion(mut self, suggestion: &str) -> Self {
127        self.suggestion = Some(suggestion.into());
128        self
129    }
130}