本篇是 Rspack 源码解析系列第三十五篇。前面每篇都出现
Result<T>、Diagnostic::error(...),但一直没讲这些错误最终怎么变成终端里那行带×、带源码高亮、带help的输出。本文进入crates/rspack_error,看 Rspack 的错误类型如何借助 miette 构建,又如何分层渲染成可读的诊断信息。
前言:错误不是字符串,是结构化数据
普通库的错误往往是一个 String。Rspack 的错误是一个结构体 ErrorData,它携带:
text
severity 严重级别(Error / Warning)
message 主消息
src 出错的源码原文
labels 源码里的定位区间(offset + len)
help 帮助文本
source_error 嵌套的底层错误(错误链)
code 错误码(用于区分类型,不参与显示)
stack 堆栈
hide_stack 是否隐藏堆栈
正是因为错误是结构化的,渲染层才能画出源码高亮和 × / ⚠ 符号。
源码入口:ErrorData 与 Error 包装
crates/rspack_error/src/error.rs 里定义了核心结构:
rust
#[cacheable]
#[derive(Debug, Clone, Default)]
pub struct ErrorData {
pub severity: Severity,
pub message: String,
pub src: Option<String>,
pub labels: Option<Vec<Label>>,
pub help: Option<String>,
#[cacheable(omit_bounds)]
pub source_error: Option<Box<Error>>,
pub code: Option<String>,
pub details: Option<String>,
pub stack: Option<String>,
pub hide_stack: Option<bool>,
}
注意 #[cacheable]:错误本身也参与持久化缓存(第 21 篇讲过 rkyv),所以构建失败的错误可以跨进程缓存复用。
Label 描述源码区间:
rust
#[cacheable]
#[derive(Debug, Clone, Default)]
pub struct Label {
pub name: Option<String>, // 标签名,如 "parse failed"
pub offset: usize, // 源码字节偏移
pub len: usize, // 影响长度
}
Error 包装:绕开 result_large_err
ErrorData 外面再包一层 Box:
rust
#[cacheable]
#[derive(Debug, Clone, Default)]
pub struct Error(Box<ErrorData>);
impl std::ops::Deref for Error {
type Target = ErrorData;
fn deref(&self) -> &Self::Target { &self.0 }
}
注释里写明了原因:Wrap ErrorData to avoid result_large_err。clippy 的 result_large_err 会警告 Result<T, E> 里 E 太大。用 Box 包装后,Result<T, Error> 的 Error 只有一个指针大小,避免了每次返回都复制大结构体。
构造错误:error / warning / from_string
Error 提供三个常用构造入口:
rust
impl Error {
pub fn error(message: String) -> Self {
Self(Box::new(ErrorData { message, ..Default::default() }))
}
pub fn warning(message: String) -> Self {
Self(Box::new(ErrorData { severity: Severity::Warning, message, ..Default::default() }))
}
pub fn from_string(src: Option<String>, start: usize, end: usize, title: String, message: String) -> Self {
let mut error = Error::error(format!("{title}: {message}"));
error.src = src;
error.labels = Some(vec![Label { name: None, offset: start, len: end.saturating_sub(start) }]);
error
}
}
from_string 是最常用的一种:给定源码、起止位置、标题和消息,自动填好 src 和 labels,渲染层就能画出高亮区间。
错误链:source_error 与 wrap_err
source_error 让错误能嵌套,形成错误链:
rust
pub fn wrap_err<D>(self, msg: D) -> Self
where D: std::fmt::Display {
Self(Box::new(ErrorData {
message: msg.to_string(),
source_error: Some(Box::new(self)),
..Default::default()
}))
}
wrap_err 把当前错误包成新的 source_error,并在外层写一条新消息。这样构建模块时的错误链就是:Build Module Failed → Can not parse → An unexpected keyword。
源码入口:实现 MietteDiagnostic
Error 实现了 miette::Diagnostic trait,这是渲染的关键:
rust
impl MietteDiagnostic for Error {
fn code(&self) -> Option<Box<dyn Display + '_>> { ... }
fn severity(&self) -> Option<miette::Severity> {
match self.severity {
Severity::Error => Some(miette::Severity::Error),
Severity::Warning => Some(miette::Severity::Warning),
}
}
fn help(&self) -> Option<Box<dyn Display + '_>> { ... }
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
self.src.as_ref().map(|s| s as &dyn miette::SourceCode)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
let Some(labels) = &self.labels else { return None; };
Some(Box::new(labels.iter().map(|item| {
LabeledSpan::new(item.name.clone(), item.offset, item.len)
})))
}
fn diagnostic_source(&self) -> Option<&dyn MietteDiagnostic> {
self.source_error.as_ref().map(|s| &**s as &dyn MietteDiagnostic)
}
}
关键映射:
severity→ miette 的Severity,决定×(error)还是⚠(warning);src→ miette 的SourceCode,渲染源码行;labels→LabeledSpan,在源码上画╰──定位;source_error→diagnostic_source,把嵌套错误接成错误链。
From 转换:外部错误自动适配
error.rs 用宏批量实现 From:
rust
macro_rules! impl_from_error {
($($t:ty),*) => {
$(impl From<$t> for Error {
fn from(value: $t) -> Error { Error::from_error(value) }
})*
}
}
impl_from_error! { std::fmt::Error, std::io::Error, std::string::FromUtf8Error }
from_error 把标准库错误转成 Error,并把 value.source() 链到 source_error:
rust
pub fn from_error<T>(value: T) -> Self where T: std::error::Error {
let mut error = Error::error(value.to_string());
error.source_error = value.source().map(|e| Box::new(Error::from_error(e)));
error
}
anyhow::Error 也有专门的 From 实现,处理它的 source() 链。
源码入口:Renderer 分层
crates/rspack_error/src/displayer/mod.rs 暴露 Renderer,它有多个后端:
text
renderer/graphical.rs 图形化渲染(带颜色、源码高亮)
renderer/mod.rs 渲染器接口
stdio.rs 标准输出
string.rs 纯字符串(无颜色)
Renderer::new(colors_enabled) 决定是否带颜色。测试里用 Renderer::new(false) 拿纯文本快照对比。
测试里的期望输出
error.rs 末尾的测试展示了完整渲染效果:
rust
let renderer = Renderer::new(false);
assert_eq!(
renderer.render(&root_err).unwrap().trim(),
expect_display.trim()
);
期望输出是经典的 miette 风格:
text
× Build Module Failed
├─▶ × Can not parse current module.
│ ╭────
│ 1 │ const a = { const };
│ · ┬
│ · ╰── parse failed
│ ╰────
│ help: See follow info.
╰─▶ ⚠ An unexpected keyword.
╭─[1:12]
1 │ const a = { const };
· ──┬──
· ╰── keyword 1
2 │ const b = { var };
· ─┬─
· ╰── keyword 2
╰────
help: Maybe you should remove it.
注意错误链的缩进关系:外层 Build Module Failed 用 ├─▶ 连接子错误,╰─▶ 连接最后一个。每一级源码高亮都精确落在 labels 指定的 offset 上。这就是「结构化错误」带来的价值------换一个 String 做不到这种层级和定位。
Diagnostic 与 DiagnosticArray
rspack_error 里还有两个配套类型:
Diagnostic:编译期诊断,带title和message,用于seal阶段的警告(如第 39 篇 case sensitive 的警告);DiagnosticArray:诊断集合,支持批量收集。
diagnostic.rs 里 Diagnostic 是编译期错误/警告的最小单元,diagnosable.rs 定义可诊断 trait,让任何能产出诊断的对象统一处理。
Rust 角度:错误类型与渲染分离
这个 crate 的设计核心是「数据与展示分离」:
text
ErrorData 纯数据:severity / message / src / labels / help / source_error
MietteDiagnostic 适配层:把 ErrorData 映射到 miette 的渲染协议
Renderer 渲染层:graphical / string 等后端
编译逻辑只负责构造 Error(填数据),不关心怎么画。渲染层只面对 MietteDiagnostic trait,不关心错误从哪来。#[cacheable] 又让错误数据能持久化。三者正交,各自演进。
这一篇应该带走什么
ErrorData是结构化错误,带 severity、src、labels、help、source_error;Error用Box<ErrorData>包装,避免result_large_err;from_string按 offset/len 填 labels,渲染出源码高亮;source_error形成错误链,diagnostic_source交给 miette 递归渲染;Renderer分层,graphical 带颜色,string 无颜色供快照。
写在最后
错误系统讲完了。下一篇进入一个「小但高频」的插件------rspack_plugin_banner,看它如何在 process_assets 阶段给产物头部加 banner 注释,以及它如何处理 entry-only、footer、raw、test/include/exclude 这些选项。