shape_diagnostics/render/
terminal.rs1use crate::{Diagnostic, Severity};
25
26pub fn render(diag: &Diagnostic) -> String {
28 let mut out = String::new();
29
30 let severity_word = match diag.severity {
32 Severity::Error => "error",
33 Severity::Warning => "warning",
34 Severity::Info => "info",
35 Severity::Hint => "hint",
36 };
37 out.push_str(severity_word);
38 out.push('[');
39 out.push_str(&diag.diagnostic_id);
40 out.push_str("]: ");
41 out.push_str(&diag.message);
42 out.push('\n');
43
44 out.push_str(" --> ");
46 out.push_str(&format_location(&diag.location));
47 out.push('\n');
48
49 if let Some(expected) = &diag.expected {
51 out.push_str(" = expected: ");
52 out.push_str(&format_witness(expected));
53 out.push('\n');
54 }
55 if let Some(found) = &diag.found {
56 out.push_str(" = found: ");
57 out.push_str(&format_witness(found));
58 out.push('\n');
59 }
60
61 for fix in &diag.fixes {
64 out.push_str(" = fix: ");
65 out.push_str(&fix.label);
66 if fix.confidence > 0.0 {
67 out.push_str(&format!(" (confidence: {:.0}%)", fix.confidence * 100.0));
68 }
69 out.push('\n');
70 }
71
72 for note in &diag.notes {
74 out.push_str(" = note: ");
75 out.push_str(¬e.message);
76 if let Some(loc) = ¬e.location {
77 out.push_str(" (");
78 out.push_str(&format_location(loc));
79 out.push(')');
80 }
81 out.push('\n');
82 }
83
84 if let Some(rule) = &diag.rule {
86 out.push_str(" = rule: ");
87 out.push_str(rule);
88 out.push('\n');
89 }
90
91 out
92}
93
94fn format_location(loc: &crate::Location) -> String {
95 let file = loc.file.as_deref().unwrap_or("<synthetic>");
96 format!("{}:{}:{}", file, loc.line, loc.col)
97}
98
99fn format_witness(w: &crate::TypeWitness) -> String {
100 match &w.witness {
101 Some(v) => format!("{} (witness: {})", w.r#type, v),
102 None => w.r#type.clone(),
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::{
110 ContextWindow, DiagnosticBuilder, DiagnosticNote, Location, Severity, SuggestedFix,
111 TypeWitness,
112 };
113 use serde_json::json;
114
115 fn sample_b0013() -> Diagnostic {
116 DiagnosticBuilder::new(
117 "B0013",
118 Severity::Error,
119 Location::new(Some("src/main.shape".into()), 12, 4, 102, 145),
120 "cannot pass the same variable to multiple parameters that require non-aliased access",
121 )
122 .expected(TypeWitness::new("int", Some(json!(42))))
123 .found(TypeWitness::new("string", Some(json!("hello"))))
124 .with_fix(SuggestedFix::new(
125 "use separate variables or clone one of the arguments",
126 0.85,
127 ))
128 .with_note(DiagnosticNote::new(
129 "conflicting argument originates here",
130 Some(Location::new(Some("src/main.shape".into()), 9, 2, 60, 70)),
131 ))
132 .context_window(ContextWindow::empty())
133 .rule("ADR-006-§1.1")
134 .build()
135 }
136
137 #[test]
138 fn terminal_render_snapshot() {
139 let diag = sample_b0013();
140 let out = render(&diag);
141 let expected = "\
143error[B0013]: cannot pass the same variable to multiple parameters that require non-aliased access
144 --> src/main.shape:12:4
145 = expected: int (witness: 42)
146 = found: string (witness: \"hello\")
147 = fix: use separate variables or clone one of the arguments (confidence: 85%)
148 = note: conflicting argument originates here (src/main.shape:9:2)
149 = rule: ADR-006-§1.1
150";
151 assert_eq!(out, expected);
152 }
153
154 #[test]
155 fn terminal_render_minimal_diagnostic() {
156 let diag = DiagnosticBuilder::new(
158 "E0100",
159 Severity::Error,
160 Location::new(Some("test.shape".into()), 3, 1, 10, 20),
161 "type mismatch",
162 )
163 .build();
164 let out = render(&diag);
165 assert_eq!(
166 out,
167 "error[E0100]: type mismatch\n --> test.shape:3:1\n"
168 );
169 }
170
171 #[test]
172 fn terminal_render_synthetic_location() {
173 let diag = DiagnosticBuilder::new(
174 "E0001",
175 Severity::Warning,
176 Location::synthetic(),
177 "config notice",
178 )
179 .build();
180 let out = render(&diag);
181 assert!(out.starts_with("warning[E0001]: config notice"));
182 assert!(out.contains("<synthetic>:0:0"));
183 }
184}