Skip to main content

reifydb_type/error/
render.rs

1// SPDX-License-Identifier: Apache-2.0
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_none() {
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 rql = diagnostic.rql.as_deref().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(rql, line);
56
57			let _ = writeln!(output, "RQL");
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		if let Some(chain) = &diagnostic.operator_chain
76			&& !chain.is_empty()
77		{
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		if let Some(help) = &diagnostic.help {
93			let _ = writeln!(output, "HELP");
94			let _ = writeln!(output, "  {}", help);
95			let _ = writeln!(output);
96		}
97
98		if let Some(col) = &diagnostic.column {
99			let _ = writeln!(output, "COLUMN");
100			let _ = writeln!(output, "  column `{}` is of type `{}`", col.name, col.r#type);
101			let _ = writeln!(output);
102		}
103
104		if !diagnostic.notes.is_empty() {
105			let _ = writeln!(output, "NOTES");
106			for note in &diagnostic.notes {
107				let _ = writeln!(output, "  • {}", note);
108			}
109		}
110	}
111
112	fn render_nested(&self, output: &mut String, diagnostic: &Diagnostic, depth: usize) {
113		let indent = if depth == 0 {
114			""
115		} else {
116			"  "
117		};
118		let prefix = if depth == 0 {
119			""
120		} else {
121			"↳ "
122		};
123
124		let _ = writeln!(output, "{}{} Error {}: {}", indent, prefix, diagnostic.code, diagnostic.message);
125
126		if let Fragment::Statement {
127			line,
128			column,
129			text,
130			..
131		} = &diagnostic.fragment
132		{
133			let fragment = text;
134			let line = line.0;
135			let col = column.0;
136			let rql = diagnostic.rql.as_deref().unwrap_or("");
137
138			let _ = writeln!(
139				output,
140				"{}  at {} (line {}, column {})",
141				indent,
142				if rql.is_empty() {
143					"unknown".to_string()
144				} else {
145					format!("\"{}\"", fragment)
146				},
147				line,
148				col
149			);
150			let _ = writeln!(output);
151
152			let line_content = get_line(rql, line);
153
154			let _ = writeln!(output, "{}  {} │ {}", indent, line, line_content);
155			let fragment_start = line_content.find(fragment.as_ref()).unwrap_or(col as usize);
156			let _ = writeln!(
157				output,
158				"{}    │ {}{}",
159				indent,
160				" ".repeat(fragment_start),
161				"~".repeat(fragment.len())
162			);
163
164			let label_text = diagnostic.label.as_deref().unwrap_or("");
165			if !label_text.is_empty() {
166				let fragment_center = fragment_start + fragment.len() / 2;
167				let label_center_offset = if label_text.len() / 2 > fragment_center {
168					0
169				} else {
170					fragment_center - label_text.len() / 2
171				};
172
173				let _ = writeln!(
174					output,
175					"{}    │ {}{}",
176					indent,
177					" ".repeat(label_center_offset),
178					label_text
179				);
180			}
181			let _ = writeln!(output);
182		}
183
184		if let Some(cause) = &diagnostic.cause {
185			self.render_nested(output, cause, depth + 1);
186		}
187
188		if let Some(help) = &diagnostic.help {
189			let _ = writeln!(output, "{}  help: {}", indent, help);
190		}
191
192		if let Some(col) = &diagnostic.column {
193			let _ = writeln!(output, "{}  column `{}` is of type `{}`", indent, col.name, col.r#type);
194		}
195
196		if !diagnostic.notes.is_empty() {
197			for note in &diagnostic.notes {
198				let _ = writeln!(output, "{}  note: {}", indent, note);
199			}
200		}
201
202		if depth > 0 {
203			let _ = writeln!(output);
204		}
205	}
206}
207
208impl DefaultRenderer {
209	pub fn render_string(diagnostic: &Diagnostic) -> String {
210		DefaultRenderer.render(diagnostic)
211	}
212}