Skip to main content

bylaw_core/
diagnostic.rs

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}