- 语言运行时
- 编译器
- 移动开发
【免费下载链接】hermes
A JavaScript engine optimized for running React Native.
本篇文章围绕 Hermes 开源仓库中 Rust 侧编译管线的诊断基础设施——hermes_diagnosticscrate 展开,系统讲解它如何用一套「分类化严重程度 + 通用诊断表示」替代直接依赖 miette / lsp_types 的常见做法,并介绍它与miette漂亮打印、hermes_estree源码定位以及hermes_parser、hermes_semantic_analysis等下游 crate 的协作方式。读完本文,你将理解这套诊断类型的内部数据结构、四级严重程度语义、构建与注解 API,以及如何在自己的工具链中复用它。
一、crate 定位:Rust 编译管线的统一诊断出口
hermes_diagnostics是 Hermes 仓库unsupported/hermes/crates/工作区中的基础 crate 之一,位于 unsupported/hermes/crates/hermes_diagnostics/。它在工作区中被多个上游 crate 依赖,是整个 Rust 侧编译管线的「诊断出口」:
- hermes_parser 将解析错误包装为
Diagnostic返回; - hermes_semantic_analysis 在作用域分析中产生诊断;
- hermes_estree_codegen 的代码生成逻辑也通过
DiagnosticsResult/Diagnostic上报问题。
其官方定位(见 README.md)是:
提供表示编译器诊断的类型,包含一种通用的诊断表示(general-purpose representation),可附带相关信息(related information),并能转换成
miette::Diagnostic,从而利用 miette 对错误的漂亮打印(pretty printing)。
这里的关键词是「通用表示」:上游 crate 只面向hermes_diagnostics这一套类型编程,而具体的展示方式(终端彩色输出、LSP 推送等)由消费方决定,从而把「诊断产生」与「诊断渲染」彻底解耦。
二、设计动机:为什么不用现成的 miette / lsp_types
README 明确指出本 crate 与 miette、lsp_types 等其他诊断库的核心差异:
与 miette、lsp_types 等诊断库不同,这里的错误严重程度(severities)被分类,允许不同工具以不同级别报告它们。例如,某个工具可以选择把 todo 错误报告出来,也可以选择忽略它们。
这正是本项目与「直接以 miette 为最终类型」的方案之间的关键分界点。miette 的Severity只有Error/Warning/Advice三档,LSP 的DiagnosticSeverity也只有 Error / Warning / Information / Hint 四档,它们描述的是「这条消息有多严重」;而 Hermes 编译器面临的问题往往是**「这条错误属于哪一类」**——是尚未实现的特性、语法合法但不支持的写法、真正的语法错误,还是编译器内部错误。只有先完成「分类」,不同的前端工具(CLI、LSP、测试框架)才能根据自身语境决定将其映射为哪种级别的展示。
因此hermes_diagnostics站在「诊断产生端」,miette 站在「诊断展示端」,二者通过impl miette::Diagnostic for Diagnostic桥接,互不越界。Cargo.toml 中的注释也印证了这一设计意图:
# TODO: consider extracting a separate hermes_miette crate which does # the translation from hermes_diagnostics::Diagnostic to miette::Diagnostic即未来甚至可能把「向 miette 转换」这一层单独抽成 crate,进一步强化职责分离。
三、核心数据结构速览
src/lib.rs与src/diagnostic.rs共同定义了整套类型。先看最常用的别名与容器:
| 类型 | 定义 | 用途 |
|---|---|---|
Diagnostics | Vec<Diagnostic> | 一批诊断,通常代表一次编译/解析过程收集到的全部问题 |
DiagnosticsResult<T> | Result<T, Diagnostics> | 可携带多条诊断的 Result 别名,是下游 crate 最常用的返回类型 |
WithDiagnostics<T> | { item: T, diagnostics: Vec<Diagnostic> } | 把「产物」与「伴随诊断」打包,From<WithDiagnostics<T>> for Result<T, Diagnostics>允许在其上做类型转换:诊断为空则取Ok(item),否则Err(diagnostics) |
diagnostics_result(result, diagnostics) | 函数 | 与WithDiagnostics等价的过程式版本,同样按诊断是否为空决定返回Ok还是Err |
其中WithDiagnostics的价值在于:有些流程(例如解析)即使出现了警告级问题,产物本身依然可用;这个容器允许调用方「先产出、再决定」,而不是在第一个问题上立即失败。
四、四级严重程度:DiagnosticSeverity 枚举
diagnostic.rs 中定义了带有thiserror派生错误消息的枚举,这是整个 crate 分类设计的落点:
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Error)] pub enum DiagnosticSeverity { /// A feature that is intended to work but not yet implemented #[error("Not implemented")] Todo, /// Syntax that is valid but intentionally not supported #[error("Unsupported")] Unsupported, /// Invalid syntax #[error("Invalid JavaScript")] InvalidSyntax, /// Internal compiler error (ICE) #[error("Internal error")] Invariant, }每一档的语义与典型场景:
- Todo(Not implemented):设计上打算支持、但当前尚未实现的特性。工具可以选择「直接报告」或「静默忽略」。典型场景是 JavaScript 新语法(如装饰器、新运算符)尚未落地时的占位提示。
- Unsupported(Unsupported):语法本身合法,但编译器有意不支持。例如 Hermes 面向嵌入式场景刻意裁剪的某些 ES 特性。这与 Todo 的区别在于:Unsupported 是「最终决策」,Todo 是「暂态」。
- InvalidSyntax(Invalid JavaScript):真正的语法错误,用户代码写错了。
- Invariant(Internal error):编译器内部错误(ICE,Internal Compiler Error),通常意味着代码库自身有 bug,而非用户代码问题。
这个枚举派生自thiserror::Error,因此每个变体都自带人类可读的错误文案("Not implemented" 等),可直接参与错误链传播;同时派生了PartialOrd / Ord,便于按严重度排序或比较。
从源码结构看,这个分类设计还允许同一工具在不同调用上下文里做差异化映射:例如 LSP 集成可以把Todo映射为 Hint、Unsupported映射为 Warning、InvalidSyntax映射为 Error;而 CLI 批量检查脚本可能直接忽略Todo,只针对InvalidSyntax失败。这正是 README 所说「不同工具以不同级别报告它们」的实现基础。
五、Diagnostic 结构:构建、注解与访问
5.1 内部布局
Diagnostic是一个胖指针包装(thin wrapper over boxed data):
#[derive(Debug)] pub struct Diagnostic(Box<DiagnosticData>);内部数据DiagnosticData包含五个字段:
struct DiagnosticData { message: Box<dyn DiagnosticDisplay>, // 人类可读的错误消息 span: SourceSpan, // 主定位(miette::SourceSpan) related_information: Vec<DiagnosticRelatedInformation>, // 次级定位 severity: DiagnosticSeverity, // 上述四级分类 data: Vec<Box<dyn DiagnosticDisplay>>, // 供 LSP code actions 使用的附加数据 }message与data使用 trait 对象Box<dyn DiagnosticDisplay>,而不是 String,是为了支持**「持有结构化数据、按需惰性格式化为文本」**的消息类型(见下文第六节)。span直接采用 miette 的SourceSpan,但构造入口统一接收hermes_estree::SourceRange,通过source_span_from_range做换算:以range.start为偏移、以range.end - range.start为长度。也就是说,上游只要持有 ESTree 源码范围,就能直接生成诊断,无需关心 miette 的偏移语义。data字段对应 LSP 诊断对象中的data字段(用于 code actions 透传上下文),相关注释给出了对 LSP 规范的引用,说明该 crate 的字段设计是「以 LSP Diagnostic 为蓝本、按需裁剪」的。
5.2 构造器与注解 API
四种严重程度各有一个对应的构造器,签名统一为(message, range):
Diagnostic::todo("...", range) // -> DiagnosticSeverity::Todo Diagnostic::unsupported("...", range) // -> DiagnosticSeverity::Unsupported Diagnostic::invalid_syntax("...", range)// -> DiagnosticSeverity::InvalidSyntax Diagnostic::invariant("...", range) // -> DiagnosticSeverity::Invariant在此基础上,annotate(message, range)为诊断追加次级定位与消息(对应 LSP 的relatedInformation),常用于「重复定义」这类错误:主消息定位到第二次定义处,annotate追加的信息定位到第一次定义处。调用可链式叠加多个注解。
let diag = Diagnostic::invalid_syntax("Duplicate identifier 'x'", second_range) .annotate("First declared here", first_range);此外还提供了一组访问器与便捷输出:
message() -> &impl DiagnosticDisplay、into_message():取消息(借用或消费);span() -> SourceSpan:取主定位;severity()、get_data()、related_information():分别取分类、附加数据、次级信息;print_without_source():不依赖源码文本的纯文本渲染,格式为message:span,次级信息以[related N] message:span逐条列出,适合在无源码上下文的场景(如单元测试、日志)快速落盘。
Diagnostic同时实现了Display(输出消息文本)与std::error::Error,因此可以无缝接入 Rust 的?错误传播体系。
六、DiagnosticDisplay:可扩展的惰性消息 trait
crate 定义了消息专用 trait:
#[typetag::serialize(tag = "type")] pub trait DiagnosticDisplay: Debug + Display + Send + Sync {}设计要点:
- 惰性格式化:实现方可以持有结构化数据(例如「引用的符号名 + 定义位置」),在
Display时才拼装完整文案,避免过早字符串化丢失类型信息; - 自动实现:借助 blanket impl,任何满足
Debug + Display + Send + Sync + typetag::Serialize的类型都会自动获得该 trait,实现者无需手动 impl:#[typetag::serialize] impl<T> DiagnosticDisplay for T where T: Debug + Display + Send + Sync + typetag::Serialize {} - 可序列化:
typetag使 trait 对象具备序列化能力(按type标签标记具体类型),为跨进程传递诊断、持久化诊断数据提供了可能。
由于Send + Sync是 trait 的硬性约束,Diagnostic整体是线程安全的——源码通过static_assertions在编译期固化这一保证:
assert_impl_all!(Diagnostic: Send, Sync);这保证了诊断可以跨线程收集、汇总(例如并行解析多个文件后集中上报),而不会引入未定义行为。
七、与 miette 的桥接:漂亮的终端错误渲染
Diagnostic实现了miette::Diagnostic,这是 README 承诺的「利用 miette 漂亮打印」的关键:
labels():把主 span 与所有related_information的 span 转换成miette::LabeledSpan。若存在次级信息,则输出多个带标签的标注;否则退化为只标注主 span,标签即消息文本。渲染效果是在源码上以箭头/下划线标注出问题位置及每处注解。help():将消息文本作为 miette 的 help 提示返回。
因此,只要持有Diagnostic,就能直接喂给miette::Report获得带源码上下文、行号、彩色标注的终端输出,无需任何手工转换。而「生成诊断」与「选择渲染方案」两个环节彻底分离:同一诊断,在 CLI 里走 miette 彩色打印,在 LSP 里转换为lsp_types::Diagnostic,在测试里用print_without_source()做断言。
八、invariant 辅助函数与诊断聚合
lib.rs 暴露了一个便捷函数,用于「条件不满足则产生诊断」的常见模式:
pub fn invariant<F>(cond: bool, f: F) -> Result<(), Diagnostic> where F: Fn() -> Diagnostic, { if cond { Ok(()) } else { Err(f()) } }用法示例:校验某个编译前提(如「函数体非空」),不满足时通过闭包惰性构造Invariant诊断并返回Err;由于闭包是惰性求值,条件满足时不会产生任何诊断开销。
此外,impl From<Diagnostic> for Diagnostics允许单条诊断通过.into()直接升级为Vec<Diagnostic>,方便在不同返回类型之间切换。
九、仓库内的真实使用场景
9.1 hermes_parser:解析错误包装
hermes_parser/src/lib.rs 展示了最典型的使用方式——把底层 C++ 解析器的错误消息转换为InvalidSyntax诊断:
use hermes_diagnostics::Diagnostic; // ... pub fn parse(source: &str, _file: &str, flags: ParserFlags) -> Result<ParseResult, Vec<Diagnostic>> { let result = HermesParser::parse(flags, &buf); if result.has_errors() { return Err(result.messages().iter().map(|diag| { let message = utf8_with_surrogates_to_string(diag.message.as_slice()).unwrap(); let start = convert_smloc(&cx, diag.loc) as u32; Diagnostic::invalid_syntax(message, SourceRange { start, end: start + 1 }) }).collect()); } // ... }这里将底层原生解析器的每条消息映射为Diagnostic::invalid_syntax,并把源码位置(SourceRange)一起打包,返回Result<_, Vec<Diagnostic>>供上层统一处理。
9.2 hermes_semantic_analysis:语义诊断
hermes_semantic_analysis/src/analyzer.rs 在作用域/引用分析中引入hermes_diagnostics::Diagnostic,语义检查阶段的「未声明变量」「重复声明」等问题都可以用Todo/InvalidSyntax/Unsupported分类表达,与解析阶段共用同一套类型体系。
9.3 hermes_estree_codegen:生成代码中的诊断
hermes_estree_codegen/src/codegen.rs 中导出的生成代码直接使用hermes_diagnostics::DiagnosticsResult与Diagnostic,说明该 crate 已深度嵌入代码生成的类型签名,是工作区内的事实标准诊断类型。
十、如何在本仓库中查看与使用
本 crate 是unsupported/hermesCargo 工作区的一员(工作区 Cargo.toml),成员通过crates/*通配符注册,hermes_diagnostics同时在[workspace.dependencies]中以路径依赖声明。若要在该工作区内复用此 crate,只需在目标 crate 的Cargo.toml中添加:
[dependencies] hermes_diagnostics = { workspace = true }依赖项为hermes_estree(提供SourceRange)、miette(提供SourceSpan与渲染 trait)、thiserror(派生严重度枚举的错误实现)、static_assertions(编译期线程安全断言)与typetag(trait 对象序列化),见 Cargo.toml。
值得注意的两个 TODO 与限制:
hermes_estree依赖被标注为「为了SourceRange而引入的较重依赖」,未来计划把SourceRange抽取为独立 crate,以解耦诊断与完整 ESTree 类型体系;- 向 miette 的转换目前内嵌在本 crate 中,未来可能抽成独立的
hermes_miettecrate。
十一、总结
hermes_diagnostics以「分类化严重程度」为核心创新,用Todo / Unsupported / InvalidSyntax / Invariant四级分类取代传统的错误/警告二分法,配合SourceRange定位、annotate次级注解、DiagnosticDisplay惰性消息 trait 与miette::Diagnostic桥接,为 Hermes Rust 编译管线提供了一套「产生、聚合、传播、渲染」职责分离的完整诊断方案。无论是解析器(hermes_parser)、语义分析(hermes_semantic_analysis)还是代码生成(hermes_estree_codegen),都能以统一方式产出诊断,再由 CLI、LSP 或测试框架按自身语境决定呈现级别——这正是嵌入式 JS 引擎在「面向不同使用场景」时需要的关键灵活性。
- 语言运行时
- 编译器
- 移动开发
【免费下载链接】hermes
A JavaScript engine optimized for running React Native.
相关推荐
ESP32智能小车终极指南:3小时打造你的第一台自动避障机器人
ESP32智能小车终极指南:3小时打造你的第一台自动避障机器人 还在为昂贵的机器人套件望而却步?想要亲手打造一台智能小车却不知从何开始?今天我要与你分享一个超实
嵌入式物联网驱动开发深入解析 Rust 编译错误 E0223:ambiguous associated type 的成因、修复与编译器诊断实现
深入解析 Rust 编译错误 E0223:ambiguous associated type 的成因、修复与编译器诊断实现 导读 本文围绕 rustc 错误代码
编程语言编译器语言运行时标准库Rust 编译器错误码 E0087 详解:多余的泛型类型参数与 E0107 诊断机制
Rust 编译器错误码 E0087 详解:多余的泛型类型参数与 E0107 诊断机制 本文围绕 Rust 编译器历史错误码 E0087("函数被传入过多类型参数
编程语言编译器语言运行时标准库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考