Rspack 源码解析(三十五):rspack_error 如何用 miette 渲染带源码定位的错误链

本篇是 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] 又让错误数据能持久化。三者正交,各自演进。

这一篇应该带走什么

  1. ErrorData 是结构化错误,带 severity、src、labels、help、source_error;
  2. Error 用 Box<ErrorData> 包装,避免 result_large_err;
  3. from_string 按 offset/len 填 labels,渲染出源码高亮;
  4. source_error 形成错误链,diagnostic_source 交给 miette 递归渲染;
  5. Renderer 分层,graphical 带颜色,string 无颜色供快照。

写在最后

错误系统讲完了。下一篇进入一个「小但高频」的插件------rspack_plugin_banner,看它如何在 process_assets 阶段给产物头部加 banner 注释,以及它如何处理 entry-only、footer、raw、test/include/exclude 这些选项。

相关推荐
杨航 AI26 分钟前
JEV的用法
java·前端·数据库
修炼的dance32 分钟前
OCR预处理测了108组,纠斜去噪有用,坑在顺序和力度
前端
不可能片场41 分钟前
AI视频成片 帧间差分把拼接缝照出来
前端·electron
写的都是BUG41 分钟前
APK 里的钥匙,一直就放在门口:聊聊加固真正该做什么
前端·算法
dpharness43 分钟前
我踩过的四个记忆坑,从预算到换机
前端·后端
用户7783366132111 小时前
SerpBase驱动:用原生JS实现搜索意图分流浮层
前端
阿明的小蝴蝶1 小时前
设备出厂横竖屏修改 & 默认手势导航修改(按键导航)
前端
SameX1 小时前
ArkTS 做应用内"快捷胶囊" —— 续航日记踩过的两个鸿蒙声明式 UI workaround
前端
用户11481867894841 小时前
水印的实现方式逻辑
前端·python