1use crate::{ComponentId, DependencyEvidence, SourceSpan};
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
5#[serde(rename_all = "kebab-case")]
6pub enum Severity {
7 Info,
8 Warning,
9 #[default]
10 Error,
11}
12
13#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
14pub struct AnalysisDiagnostic {
15 pub code: String,
16 pub severity: Severity,
17 pub message: String,
18 pub span: Option<SourceSpan>,
19 pub help: Option<String>,
20}
21
22impl AnalysisDiagnostic {
23 pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
24 Self {
25 code: code.into(),
26 severity: Severity::Error,
27 message: message.into(),
28 span: None,
29 help: None,
30 }
31 }
32
33 pub fn warning(code: impl Into<String>, message: impl Into<String>) -> Self {
34 Self {
35 code: code.into(),
36 severity: Severity::Warning,
37 message: message.into(),
38 span: None,
39 help: None,
40 }
41 }
42
43 pub fn with_span(mut self, span: SourceSpan) -> Self {
44 self.span = Some(span);
45 self
46 }
47
48 pub fn with_help(mut self, help: impl Into<String>) -> Self {
49 self.help = Some(help.into());
50 self
51 }
52}
53
54#[derive(Clone, Debug, Default, Eq, PartialEq)]
55pub struct ConditionEvent {
56 pub message: String,
57 pub origin: Option<ComponentId>,
58 pub target: Option<ComponentId>,
59 pub evidence: Vec<DependencyEvidence>,
60 pub cycle: Vec<ComponentId>,
61 pub help: Option<String>,
62}
63
64impl ConditionEvent {
65 pub fn new(message: impl Into<String>) -> Self {
66 Self {
67 message: message.into(),
68 ..Self::default()
69 }
70 }
71
72 pub fn with_edge(
73 mut self,
74 origin: ComponentId,
75 target: ComponentId,
76 evidence: Vec<DependencyEvidence>,
77 ) -> Self {
78 self.origin = Some(origin);
79 self.target = Some(target);
80 self.evidence = evidence;
81 self
82 }
83
84 pub fn with_cycle(mut self, cycle: Vec<ComponentId>) -> Self {
85 self.cycle = cycle;
86 self
87 }
88
89 pub fn with_help(mut self, help: impl Into<String>) -> Self {
90 self.help = Some(help.into());
91 self
92 }
93}
94
95#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
96pub struct Violation {
97 pub rule_id: String,
98 pub severity: Severity,
99 pub message: String,
100 pub origin: Option<ComponentId>,
101 pub target: Option<ComponentId>,
102 pub evidence: Vec<DependencyEvidence>,
103 pub cycle: Vec<ComponentId>,
104 pub help: Option<String>,
105}
106
107#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
108pub struct RuleResult {
109 pub rule_id: String,
110 pub description: String,
111 pub because: Option<String>,
112 pub severity: Severity,
113 pub violations: Vec<Violation>,
114}
115
116impl RuleResult {
117 pub fn passed(&self) -> bool {
118 self.violations.is_empty()
119 }
120}
121
122#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
123pub struct EvaluationReport {
124 pub analysis_diagnostics: Vec<AnalysisDiagnostic>,
125 pub rule_results: Vec<RuleResult>,
126}
127
128impl EvaluationReport {
129 pub fn is_success(&self) -> bool {
130 let analysis_failed = self
131 .analysis_diagnostics
132 .iter()
133 .any(|diagnostic| diagnostic.severity == Severity::Error);
134 let rule_failed = self
135 .rule_results
136 .iter()
137 .any(|result| result.severity == Severity::Error && !result.violations.is_empty());
138 !analysis_failed && !rule_failed
139 }
140
141 pub fn violations(&self) -> impl Iterator<Item = &Violation> {
142 self.rule_results
143 .iter()
144 .flat_map(|result| result.violations.iter())
145 }
146
147 pub fn sort_deterministically(&mut self) {
148 self.analysis_diagnostics.sort_by(|left, right| {
149 (
150 &left.code,
151 left.span.as_ref().map(|span| span.path.as_str()),
152 &left.message,
153 )
154 .cmp(&(
155 &right.code,
156 right.span.as_ref().map(|span| span.path.as_str()),
157 &right.message,
158 ))
159 });
160 self.rule_results
161 .sort_by(|left, right| left.rule_id.cmp(&right.rule_id));
162 for result in &mut self.rule_results {
163 result.violations.sort_by(|left, right| {
164 (
165 &left.message,
166 left.origin.as_ref(),
167 left.target.as_ref(),
168 left.evidence
169 .first()
170 .and_then(|evidence| evidence.span.as_ref())
171 .map(|span| (span.path.as_str(), span.start.line, span.start.column)),
172 )
173 .cmp(&(
174 &right.message,
175 right.origin.as_ref(),
176 right.target.as_ref(),
177 right
178 .evidence
179 .first()
180 .and_then(|evidence| evidence.span.as_ref())
181 .map(|span| (span.path.as_str(), span.start.line, span.start.column)),
182 ))
183 });
184 }
185 }
186}