onion_frontend/diagnostics/
mod.rs

1
2//! 诊断信息与错误报告核心模块。
3//!
4//! 提供统一的诊断对象接口、错误定位、分级报告与彩色格式化输出,
5//! 支持编译器前端、分析器等场景下的高质量错误与警告展示。
6//!
7//! # 主要功能
8//! - 诊断对象的统一 trait(Diagnostic)
9//! - 错误/警告分级(ReportSeverity)
10//! - 源码精确定位(SourceLocation)
11//! - 彩色人类可读报告格式化
12//! - 诊断信息的批量收集与报告(见 collector 子模块)
13//!
14//! # 典型用法
15//! ```ignore
16//! let diag: Box<dyn Diagnostic> = ...;
17//! println!("{}", diag.format_report());
18//! ```
19
20use std::fmt::Debug;
21
22use colored::*;
23use serde::{Deserialize, Serialize};
24use unicode_width::UnicodeWidthStr;
25
26use crate::parser::Source;
27pub mod collector;
28
29/// 诊断信息的严重级别。
30///
31/// 用于区分错误(Error)与警告(Warning),影响报告格式与处理逻辑。
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum ReportSeverity {
34    Error,
35    Warning,
36}
37
38/// 源码错误的精确定位信息。
39///
40/// 包含字符偏移、源码内容与文件路径等信息,支持多文件、多行定位。
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SourceLocation {
43    pub span: (usize, usize), // (start, end) character offset
44    pub source: Source,       // The source object containing content and file path
45}
46
47/// 诊断对象统一 trait。
48///
49/// 任何可报告给用户的诊断信息都应实现该 trait,
50/// 支持分级、标题、消息、定位、帮助与深拷贝。
51///
52/// # 关键方法
53/// - `severity()`: 严重级别
54/// - `title()`: 简要标题
55/// - `message()`: 详细消息
56/// - `location()`: 源码定位
57/// - `help()`: 可选帮助信息
58/// - `copy()`: 深拷贝
59/// - `format_report()`: 彩色格式化输出
60pub 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    /// 默认的格式化方法,将诊断信息转换为彩色的、人类可读的字符串。
69    ///
70    /// 支持多行源码高亮、文件路径、行号、下划线标记与帮助信息。
71    /// 适用于终端输出与日志记录。
72    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        // --- 1. 打印标题和主要信息 ---
81        report.push_str(&format!(
82            "{}: {}",
83            self.title().color(title_color).bold(),
84            self.message()
85        ));
86
87        // --- 2. 打印带代码上下文的位置信息 ---
88        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            // 打印文件位置箭头
98            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            // 打印代码行和下划线
118            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                    // 计算下划线
128                    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            // 如果没有位置信息,给一个提示
146            report.push_str(&format!(
147                "\n{}\n",
148                "Note: Location information not available for this diagnostic.".italic()
149            ));
150        }
151
152        // --- 3. 打印帮助信息 ---
153        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
161/// 辅助函数:构建单行的下划线字符串。
162fn 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        // --- 情况A: 单行错误 ---
172        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        // --- 情况B: 多行的第一行 ---
194        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        // --- 情况C: 多行的最后一行 ---
207        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        // --- 情况D: 多行的中间行 ---
215        "^".repeat(line_text.width()).to_string()
216    }
217}
218
219/// 从源码字符串中根据字符偏移查找行号和列号 (0-indexed)。
220///
221/// 直接使用原始源码,支持多平台换行符。
222fn find_line_and_col(source: &str, char_pos: usize) -> (usize, usize) {
223    let chars: Vec<char> = source.chars().collect();
224
225    // 如果位置超出范围,返回最后位置
226    if char_pos >= chars.len() {
227        let line_count = source.lines().count();
228        return (line_count.saturating_sub(1), 0);
229    }
230
231    // 逐字符遍历,正确处理换行符
232    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            // 检查是否是 \r\n (Windows)
245            if i + 1 < chars.len() && chars[i + 1] == '\n' {
246                // 这是 \r\n,跳过 \r,让 \n 处理换行
247                continue;
248            } else {
249                // 这是单独的 \r (旧Mac风格)
250                current_line += 1;
251                current_col = 0;
252            }
253        } else {
254            current_col += 1;
255        }
256    }
257
258    // 如果到达这里,说明 char_pos 就是最后一个字符
259    (current_line, current_col)
260}