Skip to main content

reifydb_type/error/
render.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2025 ReifyDB
3
4use std::fmt::Write;
5
6use super::Diagnostic;
7use crate::fragment::Fragment;
8
9pub trait DiagnosticRenderer {
10	fn render(&self, diagnostic: &Diagnostic) -> String;
11}
12
13pub struct DefaultRenderer;
14
15pub fn get_line(source: &str, line: u32) -> &str {
16	source.lines().nth((line - 1) as usize).unwrap_or("")
17}
18
19impl DiagnosticRenderer for DefaultRenderer {
20	fn render(&self, diagnostic: &Diagnostic) -> String {
21		let mut output = String::new();
22
23		if !diagnostic.cause.is_some() {
24			self.render_flat(&mut output, diagnostic);
25		} else {
26			self.render_nested(&mut output, diagnostic, 0);
27		}
28
29		output
30	}
31}
32
33impl DefaultRenderer {
34	fn render_flat(&self, output: &mut String, diagnostic: &Diagnostic) {
35		let _ = writeln!(output, "Error {}", diagnostic.code);
36		let _ = writeln!(output, "  {}", diagnostic.message);
37		let _ = writeln!(output);
38
39		if let Fragment::Statement {
40			line,
41			column,
42			text,
43			..
44		} = &diagnostic.fragment
45		{
46			let fragment = text;
47			let line = line.0;
48			let col = column.0;
49			let statement = diagnostic.statement.as_ref().map(|x| x.as_str()).unwrap_or("");
50
51			let _ = writeln!(output, "LOCATION");
52			let _ = writeln!(output, "  line {}, column {}", line, col);
53			let _ = writeln!(output);
54
55			let line_content = get_line(statement, line);
56
57			let _ = writeln!(output, "CODE");
58			let _ = writeln!(output, "  {} │ {}", line, line_content);
59			let fragment_start = line_content.find(fragment.as_ref()).unwrap_or(col as usize);
60			let _ = writeln!(output, "    │ {}{}", " ".repeat(fragment_start), "~".repeat(fragment.len()));
61			let _ = writeln!(output, "    │");
62
63			let label_text = diagnostic.label.as_deref().unwrap_or("");
64			let fragment_center = fragment_start + fragment.len() / 2;
65			let label_center_offset = if label_text.len() / 2 > fragment_center {
66				0
67			} else {
68				fragment_center - label_text.len() / 2
69			};
70
71			let _ = writeln!(output, "    │ {}{}", " ".repeat(label_center_offset), label_text);
72			let _ = writeln!(output);
73		}
74
75		// Render operator chain if present
76		if let Some(chain) = &diagnostic.operator_chain {
77			if !chain.is_empty() {
78				let _ = writeln!(output, "OPERATOR CHAIN");
79				for (i, entry) in chain.iter().enumerate() {
80					let _ = writeln!(
81						output,
82						"  {}. {} (node_id={}, version={})",
83						i + 1,
84						entry.operator_name,
85						entry.node_id,
86						entry.operator_version
87					);
88				}
89				let _ = writeln!(output);
90			}
91		}
92
93		if let Some(help) = &diagnostic.help {
94			let _ = writeln!(output, "HELP");
95			let _ = writeln!(output, "  {}", help);
96			let _ = writeln!(output);
97		}
98
99		if let Some(col) = &diagnostic.column {
100			let _ = writeln!(output, "COLUMN");
101			let _ = writeln!(output, "  column `{}` is of type `{}`", col.name, col.r#type);
102			let _ = writeln!(output);
103		}
104
105		if !diagnostic.notes.is_empty() {
106			let _ = writeln!(output, "NOTES");
107			for note in &diagnostic.notes {
108				let _ = writeln!(output, "  • {}", note);
109			}
110		}
111	}
112
113	fn render_nested(&self, output: &mut String, diagnostic: &Diagnostic, depth: usize) {
114		let indent = if depth == 0 {
115			""
116		} else {
117			"  "
118		};
119		let prefix = if depth == 0 {
120			""
121		} else {
122			"↳ "
123		};
124
125		// Main error line
126		let _ = writeln!(output, "{}{} Error {}: {}", indent, prefix, diagnostic.code, diagnostic.message);
127
128		// Location info
129		if let Fragment::Statement {
130			line,
131			column,
132			text,
133			..
134		} = &diagnostic.fragment
135		{
136			let fragment = text;
137			let line = line.0;
138			let col = column.0;
139			let statement = diagnostic.statement.as_ref().map(|x| x.as_str()).unwrap_or("");
140
141			let _ = writeln!(
142				output,
143				"{}  at {} (line {}, column {})",
144				indent,
145				if statement.is_empty() {
146					"unknown".to_string()
147				} else {
148					format!("\"{}\"", fragment)
149				},
150				line,
151				col
152			);
153			let _ = writeln!(output);
154
155			// Code visualization
156			let line_content = get_line(statement, line);
157
158			let _ = writeln!(output, "{}  {} │ {}", indent, line, line_content);
159			let fragment_start = line_content.find(fragment.as_ref()).unwrap_or(col as usize);
160			let _ = writeln!(
161				output,
162				"{}    │ {}{}",
163				indent,
164				" ".repeat(fragment_start),
165				"~".repeat(fragment.len())
166			);
167
168			let label_text = diagnostic.label.as_deref().unwrap_or("");
169			if !label_text.is_empty() {
170				let fragment_center = fragment_start + fragment.len() / 2;
171				let label_center_offset = if label_text.len() / 2 > fragment_center {
172					0
173				} else {
174					fragment_center - label_text.len() / 2
175				};
176
177				let _ = writeln!(
178					output,
179					"{}    │ {}{}",
180					indent,
181					" ".repeat(label_center_offset),
182					label_text
183				);
184			}
185			let _ = writeln!(output);
186		}
187
188		// Handle nested cause first (if exists)
189		if let Some(cause) = &diagnostic.cause {
190			self.render_nested(output, cause, depth + 1);
191		}
192
193		// Help section
194		if let Some(help) = &diagnostic.help {
195			let _ = writeln!(output, "{}  help: {}", indent, help);
196		}
197
198		// Column info
199		if let Some(col) = &diagnostic.column {
200			let _ = writeln!(output, "{}  column `{}` is of type `{}`", indent, col.name, col.r#type);
201		}
202
203		// Notes
204		if !diagnostic.notes.is_empty() {
205			for note in &diagnostic.notes {
206				let _ = writeln!(output, "{}  note: {}", indent, note);
207			}
208		}
209
210		// Add spacing between diagnostic levels
211		if depth > 0 {
212			let _ = writeln!(output);
213		}
214	}
215}
216
217impl DefaultRenderer {
218	pub fn render_string(diagnostic: &Diagnostic) -> String {
219		DefaultRenderer.render(diagnostic)
220	}
221}