onion_frontend/diagnostics/
mod.rs1
2use std::fmt::Debug;
21
22use colored::*;
23use serde::{Deserialize, Serialize};
24use unicode_width::UnicodeWidthStr;
25
26use crate::parser::Source;
27pub mod collector;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum ReportSeverity {
34 Error,
35 Warning,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SourceLocation {
43 pub span: (usize, usize), pub source: Source, }
46
47pub trait Diagnostic: Debug + Send + Sync {
61 fn severity(&self) -> ReportSeverity;
62 fn title(&self) -> String;
63 fn message(&self) -> String;
64 fn location(&self) -> Option<SourceLocation>;
65 fn help(&self) -> Option<String>;
66 fn copy(&self) -> Box<dyn Diagnostic>;
67
68 fn format_report(&self) -> String {
73 let (title_color, primary_color) = match self.severity() {
74 ReportSeverity::Error => (Color::BrightRed, Color::BrightRed),
75 ReportSeverity::Warning => (Color::Yellow, Color::Yellow),
76 };
77
78 let mut report = String::new();
79
80 report.push_str(&format!(
82 "{}: {}",
83 self.title().color(title_color).bold(),
84 self.message()
85 ));
86
87 if let Some(loc) = self.location() {
89 let source_content: String = loc.source.iter().collect();
90 let lines: Vec<&str> = source_content.lines().collect();
91
92 let (start_char, end_char) = loc.span;
93
94 let (start_line_idx, start_col_char) = find_line_and_col(&source_content, start_char);
95 let (end_line_idx, end_col_char) = find_line_and_col(&source_content, end_char);
96
97 match loc.source.file_path() {
99 Some(path) => {
100 report.push_str(&format!(
101 "\n {} {}:{} in {}\n",
102 "-->".bright_blue().bold(),
103 (start_line_idx + 1).to_string().bright_cyan(),
104 (start_col_char + 1).to_string().bright_cyan(),
105 path.display().to_string().bright_yellow().underline()
106 ));
107 }
108 None => {
109 report.push_str(&format!(
110 "\n {} {}:{}\n",
111 "-->".bright_blue().bold(),
112 (start_line_idx + 1).to_string().bright_cyan(),
113 (start_col_char + 1).to_string().bright_cyan()
114 ));
115 }
116 }
117 for i in start_line_idx..=end_line_idx {
119 if let Some(line_text) = lines.get(i) {
120 report.push_str(&format!(
121 " {:>4} {} {}\n",
122 (i + 1).to_string().bright_cyan(),
123 "|".bright_blue().bold(),
124 line_text.white()
125 ));
126
127 let underline = build_underline(
129 i,
130 start_line_idx,
131 end_line_idx,
132 start_col_char,
133 end_col_char,
134 line_text,
135 );
136
137 report.push_str(&format!(
138 " {} {}\n",
139 "|".bright_blue().bold(),
140 underline.color(primary_color).bold()
141 ));
142 }
143 }
144 } else {
145 report.push_str(&format!(
147 "\n{}\n",
148 "Note: Location information not available for this diagnostic.".italic()
149 ));
150 }
151
152 if let Some(help_text) = self.help() {
154 report.push_str(&format!("\n{}: {}", "Help".bright_green(), help_text));
155 }
156
157 report
158 }
159}
160
161fn build_underline(
163 current_line_idx: usize,
164 start_line_idx: usize,
165 end_line_idx: usize,
166 start_col_char: usize,
167 end_col_char: usize,
168 line_text: &str,
169) -> String {
170 if current_line_idx == start_line_idx && current_line_idx == end_line_idx {
171 let prefix_width = line_text
173 .chars()
174 .take(start_col_char)
175 .collect::<String>()
176 .width();
177 let error_width = if end_col_char > start_col_char {
178 line_text
179 .chars()
180 .skip(start_col_char)
181 .take(end_col_char - start_col_char)
182 .collect::<String>()
183 .width()
184 } else {
185 1
186 };
187 format!(
188 "{}{}",
189 " ".repeat(prefix_width),
190 "^".repeat(error_width.max(1))
191 )
192 } else if current_line_idx == start_line_idx {
193 let prefix_width = line_text
195 .chars()
196 .take(start_col_char)
197 .collect::<String>()
198 .width();
199 let error_width = line_text.width() - prefix_width;
200 format!(
201 "{}{}",
202 " ".repeat(prefix_width),
203 "^".repeat(error_width.max(1))
204 )
205 } else if current_line_idx == end_line_idx {
206 let error_width = line_text
208 .chars()
209 .take(end_col_char)
210 .collect::<String>()
211 .width();
212 "^".repeat(error_width.max(1)).to_string()
213 } else {
214 "^".repeat(line_text.width()).to_string()
216 }
217}
218
219fn find_line_and_col(source: &str, char_pos: usize) -> (usize, usize) {
223 let chars: Vec<char> = source.chars().collect();
224
225 if char_pos >= chars.len() {
227 let line_count = source.lines().count();
228 return (line_count.saturating_sub(1), 0);
229 }
230
231 let mut current_line = 0;
233 let mut current_col = 0;
234
235 for (i, &ch) in chars.iter().enumerate() {
236 if i == char_pos {
237 return (current_line, current_col);
238 }
239
240 if ch == '\n' {
241 current_line += 1;
242 current_col = 0;
243 } else if ch == '\r' {
244 if i + 1 < chars.len() && chars[i + 1] == '\n' {
246 continue;
248 } else {
249 current_line += 1;
251 current_col = 0;
252 }
253 } else {
254 current_col += 1;
255 }
256 }
257
258 (current_line, current_col)
260}