语言运行时编译器移动开发【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址https://gitcode.com/gh_mirrors/hermes/hermes点击查看免费下载导读hermes_semantic_analysis是 Hermes 项目中用 Rust 实现的 JavaScript 语义分析 crate负责在hermes_estree生成的 AST 之上完成名称解析name resolution与作用域信息scope information的构建为后续的 Flow/TypeScript 类型检查等高级语义功能铺路。本文将以 crate 的 README 为主线结合其 analyzer.rs、scope_manager.rs、scope_view.rs 与 analysis_test.rs 源码完整还原其架构设计、核心数据结构、两阶段分析算法、TDZ暂时性死区检测原理与测试方式帮助读者理解如何用 Rust 给 JavaScript 做静态语义分析这一经典问题在 Hermes 中的落地实现。模块定位在 hermes_estree AST 之上做语义分析crate 的 README 用三句话划定了它的边界Implements semantic analysis of JavaScript name resolution and scope information over thehermes_estreeAST. Eventually this analysis will be extended to cover Flow and TypeScript in addition to the core JS and JSX language.NOTE: this analysisassumes strict modeand does not support legacy non-strict semantics.从中可以提炼出三个关键定位输入是hermes_estree的 AST。该 crate 不自己做语法解析而是消费同工作区内hermes_estreecrate 定义的 ESTree 节点类型Program、Statement、Expression、Pattern等通过hermes_estree::Visitortrait 完成遍历。输出是名称解析 作用域信息即把每一个标识符引用Reference绑定到它对应的声明Declaration并把所有声明组织进正确的词法作用域Scope层级中。当前阶段只覆盖核心 JS 与 JSX且假定严格模式。非严格模式下的历史遗留语义如with语句、隐式全局变量等不在支持范围内未来才计划扩展到 Flow 与 TypeScript。从仓库结构看该 crate 位于 unsupported/hermes/crates/ 下的 Rust workspace 中与hermes_parser、hermes_estree、hermes_diagnostics、hermes_utils等并列属于 Hermes Rust bindings见 unsupported/hermes/Cargo.toml 的 workspace 声明中的语义分析环节其位置也呼应了 README 中 unsupported 前缀所代表的试验性/演进中属性。四个核心语义实体Scope、Declaration、Reference、Labelscope_manager.rs 定义了语义分析的全部核心数据结构。整个分析结果由四种实体组成每种实体都有对应的强类型 IDnewtype内部是usize实体ID 类型语义关键字段Scope作用域ScopeId词法作用域形成树状层级kind、parent、declarations、references、childrenDeclaration声明DeclarationId一个标识符的声明let/const/var/function/class/import…kind、name、scopeReference引用ReferenceId一次对标识符的读取/写入kindRead/Write/ReadWrite、declaration绑定目标、scopeLabel标签LabelIdbreak/continue 的跳转目标kindLoop/Other、scope、name其中Scope的定义scope_manager.rs#L472-L480直接体现了作用域树的建模方式每个作用域持有自己的声明表IndexMapString, DeclarationId按名字快速查找、引用列表和子作用域列表并通过parent指回父作用域pub struct Scope { pub id: ScopeId, pub kind: ScopeKind, pub parent: OptionScopeId, pub declarations: IndexMapString, DeclarationId, pub references: VecReferenceId, pub children: VecScopeId, }ScopeManagerscope_manager.rs#L24-L41作为总容器用若干Vec顺序存储全部实体同时维护四张AST 节点 → 语义信息的映射表pub struct ScopeManager { root: ScopeId, globals: IndexMapString, DeclarationId, scopes: VecScope, labels: VecLabel, declarations: VecDeclaration, references: VecReference, node_scopes: IndexMapAstNode, ScopeId, node_labels: IndexMapAstNode, LabelId, node_declarations: IndexMapAstNode, DeclarationId, node_references: IndexMapAstNode, ReferenceId, diagnostics: VecDiagnostic, }注意node_scopes等映射的键是AstNode它本质上是 AST 节点的指针地址见 scope_manager.rs#L541-L557 的AstNode(PointerAddress)这样可以在不修改 AST 的情况下把语义信息挂回任意 AST 节点——例如node_declaration(node)可以查询某个标识符节点对应哪个声明break_label(node)可以查询某个break语句跳向哪个标签。分析流程两阶段算法收集 → 解析入口函数位于 analyzer.rs#L42-L46pub fn analyze(ast: Program, options: AnalyzeOptions) - ScopeManager { let mut analyzer Analyzer::new(ast, options); analyzer.visit_program(ast); analyzer.complete() }整体采用两阶段two-pass算法这也是静态语义分析工具如 ESLint 的eslint-scope、Babel 的 scope 分析常见的做法阶段一visitor 遍历收集未决引用Analyzer实现了hermes_estree::Visitortrait以深度优先方式遍历整个 AST。遍历期间遇到声明节点立即调用manager.add_declaration(...)在当前或提升后的作用域登记声明遇到引用节点不立即解析而是构造一个UnresolvedReference压入self.unresolved列表analyzer.rs#L225-L240。UnresolvedReference结构体analyzer.rs#L60-L72记录了这个引用所在的作用域、AST 节点、名字、引用类型、源码范围和创建该引用时下一条声明的 IDnext_declaration后面 TDZ 检测会用到pub struct UnresolvedReference { pub scope: ScopeId, pub ast: AstNode, pub name: String, pub kind: ReferenceKind, pub range: SourceRange, pub next_declaration: DeclarationId, }阶段二complete() 统一解析遍历结束后调用complete()analyzer.rs#L87-L106此时所有声明都已登记完毕因此可以逐个回溯解析引用fn complete(mut self) - ScopeManager { for reference in self.unresolved { if let Some(declaration) self.manager.lookup_reference( reference.scope, reference.name, reference.next_declaration, ) { let id self.manager.add_reference(reference.scope, reference.kind, declaration.id); self.manager.node_references.insert(reference.ast, id); } else { self.manager.diagnostics.push(Diagnostic::invalid_syntax( Undefined variable, reference.range, )); } } self.manager }解析成功则将引用登记到对应作用域并建立node_references映射解析失败则产生Undefined variable语法诊断这正是静态分析报错如未定义变量的来源。诊断类型统一使用hermes_diagnostics::Diagnostic。作用域栈的管理遍历过程中Analyzer用current: ScopeId维护当前作用域通过enter_scope/close_scope成对进出analyzer.rs#L166-L177并提供了enter(kind, f)便捷封装进入新作用域 → 执行闭包 → 关闭作用域analyzer.rs#L156-L164。close_scope中还有assert_eq!(self.current, id)断言用于在调试期捕捉作用域进出不匹配的编码错误。作用域体系九种 ScopeKindscope_manager.rs#L459-L470 定义了完整的词法作用域种类ScopeKind触发节点说明Global脚本Script根非模块脚本的根作用域Module模块Module根模块顶层作用域import仅允许在此声明Function函数声明/表达式/箭头函数形参 函数体Class类声明/表达式类体作用域Block块语句{}普通词法块CatchClausecatch子句带形参时捕获参数专属作用域Forfor/for-in/for-of仅当循环初始化使用let/const时创建Switchswitch语句存放所有 caseStaticBlock类的静态初始化块类静态块作用域根作用域的kind由源码类型决定scope_manager.rs#L50-L55let root_kind match source_type { SourceType::Module ScopeKind::Module, SourceType::Script ScopeKind::Global, };实现中有几个值得注意的作用域细节函数体不额外建 Block 作用域visit_functionanalyzer.rs#L179-L210对函数体块直接遍历其语句列表注释明确说明Skip calling visit_block_statement to avoid creating an extra block scope避免函数作用域与函数体块作用域重复。形参中的this不产生声明函数形参遍历时跳过名为this的参数analyzer.rs#L182-L190因为它不声明变量也不可能有默认值。for 循环的let/const初始化单独建作用域visit_for_statementanalyzer.rs#L687-L715与visit_for_in_ofanalyzer.rs#L304-L335都会在初始化语句是VariableDeclarationKind::Var时跳过建作用域否则创建ScopeKind::For这精确模拟了for (let i ...)中i的词法隔离语义。catch形参单独建作用域visit_catch_clauseanalyzer.rs#L618-L633只有在带形参时才enter(ScopeKind::CatchClause, ...)不带形参则直接访问 body 块避免多余的块级作用域。声明语义八种 DeclarationKind 与变量提升scope_manager.rs#L496-L506 定义了声明种类pub enum DeclarationKind { Global, Class, Const, Var, Let, Function, CatchClause, Import, }其中Var/Let/Const由 ESTree 的VariableDeclarationKind直接转换而来scope_manager.rs#L508-L516。提升规则hoistingadd_declaration的核心逻辑是先确定声明被挂到哪个作用域即get_scope_for_declarationscope_manager.rs#L378-L411Let/Const/Import/CatchClause/Class就地声明在当前位置的作用域Var/Function向上提升hoist到最近的Function/Global/Module/StaticBlock作用域这准确实现了var提升到函数顶部的 JS 语义。重复声明检查add_declarationscope_manager.rs#L290-L376实现了严格的重复声明规则注释中给出了两个典型冲突示例function() { {let a; var a;} }在声明所在作用域就冲突function() { let a; { var a; } }var提升到含冲突let的作用域时冲突。具体规则var可以被重复声明任意多次后续声明等价于对原声明的重新赋值var a 1; var a 2;等价于var a; a 1; a 2;因此重复的var直接复用原声明的 IDvar不能与同一作用域或提升目标作用域中的let/const/class/import/function/catch形参等块级声明冲突通过is_block_scoped_declaration判定scope_manager.rs#L435-L445否则产生Duplicate declaration诊断其余声明种类在同一作用域内重复声明即报错但解析时仍绑定到第一个声明——注释说明一旦出现错误语义结果本身已无效关键是不要再因此产生二次的找不到声明误报。另外visit_import_declaration_specifieranalyzer.rs#L339-L374会在非 Module 作用域遇到import声明时立即报告import declarations are only allowed at the top-level of a module诊断三种 import specifier默认、具名、命名空间统一只登记local标识符具名导入的imported被忽略因为它引用的是被导入模块中的名字。引用语义Read / Write / ReadWrite 与赋值处理scope_manager.rs#L526-L531 定义了引用的三种种类pub enum ReferenceKind { Read, Write, ReadWrite, }analyzer.rs中对哪些标识符是引用、哪些只是名字做了非常细致的区分这是 ESTree 的一个经典坑同一个Identifier节点既可能是变量引用也可能只是属性名/字符串名如x.y中的x是引用而y不是。visit_identifieranalyzer.rs#L717-L732的注释明确说明所有非变量引用的Identifier都被上游访问器刻意跳过因此走到这里的一定是变量读取fn visit_identifier(mut self, ast: hermes_estree::Identifier) { Analyzer::visit_reference_identifier( self, ast.name, AstNode::from(ast), ReferenceKind::Read, ast.range, ); }visit_assignment_expressionanalyzer.rs#L497-L587对赋值运算符做了分流简单赋值左侧若是Pattern解构赋值调用visit_declaration_pattern(..., None)——None表示这不是声明而是重新赋值因此内部会将其视为ReferenceKind::Write的引用见 analyzer.rs#L242-L265左侧若是MemberExpression链如a.b.c x则展开到最内层.object把真正的根标识符记为Read计算属性则正常访问复合赋值/更新运算符、-等左侧必须是标识符记为ReferenceKind::ReadWrite先读后写若左侧不合法则报诊断但仍继续访问右侧以尽量发现其中的错误。解构模式的处理集中在visit_declaration_patternanalyzer.rs#L267-L302递归处理Identifier、ArrayPattern、ObjectPattern含计算属性键、Rest 元素、RestElement、AssignmentPattern带默认值默认值表达式按普通表达式访问。函数形参的声明也走这条路径因此形参解构、默认值都能被正确登记。标签解析break / continue / labeled statementbreak/continue的跳转目标解析是语义分析的另一块内容相关实体为Label与LabelKindscope_manager.rs#L482-L494pub enum LabelKind { Loop, Other }visit_labeled_statementanalyzer.rs#L734-L751根据被标注的语句是否为for/for-in/for-of/while/do-while把标签标记为LabelKind::Loop或LabelKind::Other并通过enter_label把标签压入self.labels栈循环与switch语句会创建匿名标签add_anonymous_label使无标签的break/continue也能找到最近的跳转目标lookup_break/lookup_continueanalyzer.rs#L118-L154从标签栈顶向内查找带名字的break label;要求精确匹配无名字的则取最近的标签visit_break_statement/visit_continue_statementanalyzer.rs#L600-L663解析成功后把标签 ID 关联到break/continue节点及其标签节点continue还额外校验目标标签必须是LabelKind::Loop否则报Invalid continue statement, the named label must be for a loop找不到目标则分别报 Non-syntactic break/continue 诊断。TDZ暂时性死区检测利用声明顺序做静态近似crate 内置了一个轻量的 TDZ 违规检测原理非常巧妙利用的是引用创建时点与声明时点的顺序关系。回忆UnresolvedReference中的next_declaration字段analyzer.rs#L67-L71它是该引用被创建时下一条将要产生的声明的 ID。由于ScopeManager用Vec顺序存储声明声明 ID 的递增顺序就反映了声明在代码中的出现顺序。解析阶段lookup_referencescope_manager.rs#L204-L248在沿作用域链向上查找声明时维护一个tdz_limitif let Some(tdz_limit) tdz_limit { if (declaration.kind DeclarationKind::Let || declaration.kind DeclarationKind::Const) id.0 tdz_limit.0 { return None; } }也就是说如果在引用之后才出现的let/const声明被该引用命中id next_declaration就认为这是 TDZ 违规引用发生在其初始化之前解析返回None最终产生未定义变量诊断。一个关键的修正细节是跨函数作用域时清空tdz_limitscope_manager.rs#L230-L238。因为函数内的引用即使文本上写在外部let声明之前实际执行时函数被调用时该声明可能早已初始化不能一律判为 TDZ。这是对function f() { return x; } let x 1;这类合法代码的准确处理。全局变量注入AnalyzeOptions分析结果的质量依赖于哪些名字是预定义的全局变量。入口参数AnalyzeOptionsanalyzer.rs#L48-L51只有一个字段#[derive(Debug, Default)] pub struct AnalyzeOptions { pub globals: VecString, }ScopeManager::newscope_manager.rs#L50-L88会把globals中的每个名字注册为挂在根作用域下的DeclarationKind::Global声明并维护globals: IndexMapString, DeclarationId索引。lookup_reference沿作用域链找不到任何声明时最后会查这张全局表scope_manager.rs#L240-L245if let Some(id) self.globals.get(name) { let declaration self.declaration(*id); return Some(declaration); } return None;测试用例 analysis_test.rs#L25-L40 展示了典型的注入集合let mut analysis analyze( result.ast, AnalyzeOptions { globals: vec![ Array.to_string(), Boolean.to_string(), console.to_string(), global.to_string(), Math.to_string(), Number.to_string(), setInterval.to_string(), setTimeout.to_string(), String.to_string(), ], }, );凡是未声明又不在 globals 中的标识符都会被判定为未定义变量——这也解释了为什么测试集合中必须显式列出宿主环境提供的全局 API。实际使用时应根据目标运行环境浏览器、Node、Hermes 运行时等扩充这份清单。只读调试视图ScopeManagerView 与 Debug 输出为了便于调试与快照测试crate 在 scope_view.rs 中提供了只读视图层全部以引用方式借用ScopeManager不持有所有权也不可变ScopeManagerViewscope_view.rs#L22-L47顶层视图提供root()与globals()ScopeViewscope_view.rs#L48-L166作用域视图提供id()、kind()、parent()、declarations()、references()、children()、is_descendant_of()DeclarationViewscope_view.rs#L185-L221与ReferenceViewscope_view.rs#L223-L266分别暴露声明的 id/名字/种类/所在作用域与引用的 id/种类/作用域/绑定的声明。ScopeManager::debug()scope_manager.rs#L90-L92返回ScopeManagerView且ScopeManager的Debug实现委托给它scope_manager.rs#L43-L47。各 View 均实现了详尽的Debug——例如ScopeView会递归打印声明、引用含绑定的声明名与子作用域使得println!({:#?}, analysis.debug())就能输出整棵作用域树的结构化文本。这既是日常调试手段也是下方快照测试的核心输出物。测试体系fixture 快照测试crate 的测试集中在 analysis_test.rs采用insta 快照测试配合 fixture 目录扫描#[test] fn fixtures() { glob!(fixtures/**.js, |path| { let input std::fs::read_to_string(path).unwrap(); let result parse(input, path.to_str().unwrap(), ParserFlags::default()).unwrap(); let mut analysis analyze(result.ast, AnalyzeOptions { globals: vec![...] }); let mut output String::new(); writeln!(mut output, {:#?}, analysis.debug()).unwrap(); let diagnostics analysis.diagnostics(); for diagnostic in diagnostics { writeln!(mut output, {:#?}, diagnostic).unwrap(); println!({:?}, Report::new(diagnostic) .with_source_code(NamedSource::new(path.to_string_lossy(), input.clone()))); } assert_snapshot!(format!(Input:\n{input}\n\nAnalysis:\n{output})); }); }测试流程清晰地演示了 crate 的端到端集成方式用hermes_parser::parseParserFlags::default()把 JS 源码解析为hermes_estree的ProgramAST调用hermes_semantic_analysis::analyze获得ScopeManager通过analysis.debug()的Debug输出得到完整作用域树文本同时收集analysis.diagnostics()打印诊断用miette::Report渲染为带源码上下文的错误报告与 insta 快照比对任何分析行为的变化作用域结构、绑定关系、诊断文本都会导致快照 diff 失败。assert_snapshot!把输入源码 分析输出整体存档这意味着作用域树的每个细节都被纳入了回归保护。依赖方面Cargo.toml 显示该 crate 的运行时依赖仅四个 workspace cratehermes_diagnostics诊断、hermes_estreeAST 类型、hermes_utils指针地址等工具与indexmap保序 Maphermes_parser、insta、miette、serde_json为 dev-dependencies仅用于测试。构建与运行该 crate 是 unsupported/hermes Rust workspacemembers [crates/*]的一员需要在 workspace 根目录下执行 cargo 命令# 在仓库的 unsupported/hermes 目录下 cargo build -p hermes_semantic_analysis # 编译 cargo test -p hermes_semantic_analysis # 运行快照测试注意 workspace 的[profile.release]unsupported/hermes/Cargo.toml#L40-L48沿用了较高的发布优化配置opt-level 3、lto fat、codegen-units 1、panic abort并特意为 insta 及其 diff 库similar在 dev profile 下开启opt-level 3以加速快照测试——这是 Rust 工具链中针对快照测试性能的常见调优手法。作为依赖方接入时只需在业务 crate 的Cargo.toml中声明hermes_semantic_analysis { workspace true }或path crates/hermes_semantic_analysis即可使用其公开 APIanalyze、AnalyzeOptions以及ScopeManager上的各类查询方法scope、declaration、reference、label、node_scope、node_declaration、node_reference、break_label、continue_label、is_descendant_of、diagnostics等。当前限制与后续规划结合 README 与源码中的 TODO 标记可以清晰看到该模块的边界与演进方向严格模式假设README 明确声明不兼容 legacy 非严格语义因此with语句、非严格模式下的隐式全局赋值等都不会被正确建模这是当前最重要的使用前提Flow / TypeScript 扩展目前仅覆盖核心 JS 与 JSXREADME 说明未来将扩展到 Flow 与 TypeScript——考虑到 Hermes 官方文档中 TS 语法的剥离与类型检查是活跃方向该 crate 很可能是其 Rust 侧语义基础设施尚未记录的 JSX pragma源码中visit_jsxfragment留有TODO: record the pragmasvisit_jsxopening_element也有TODO: record jsx pragma if root_name is not an FBT nameanalyzer.rs#L837-L856说明 JSX pragma 相关的语义信息收集尚未完成成员表达式展开逻辑待简化visit_assignment_expression中对成员表达式链的展开逻辑标有TODO: revisit and maybe revert this to just visit ast.left normallyanalyzer.rs#L509-L512属于已知的实现细节待优化项。从源码结构看可以推断该 crate 采用AST 不可变、语义信息旁挂的架构AstNode基于指针地址未来扩展 Flow/TS 时只需增加新的 visitor 分支而无需改动ScopeManager的核心数据结构——这是其面向演进的架构设计使然。结语hermes_semantic_analysis用约 900 行 Rust 代码完整实现了 JavaScript 静态语义分析中最硬核的部分九类词法作用域的精确构建、八类声明的登记与var/function提升、三类引用的收集与回溯解析、break/continue标签绑定以及基于声明序号的静态 TDZ 检测并配以快照测试保证行为可回归。对于想了解如何用 Rust 为 JavaScript 构建 scope 分析的读者analyzer.rs、scope_manager.rs 与 analysis_test.rs 是顺序阅读的最佳路径而它两阶段解析 指针地址旁挂语义信息 只读视图调试的整体架构也为任何 AST 驱动的静态分析工具提供了可复用的设计范式。赞分享语言运行时编译器移动开发【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址https://gitcode.com/gh_mirrors/hermes/hermes点击查看免费下载相关推荐Carbon 语言名称查找设计解析作用域、未限定名称解析、名称污染与遮蔽规则Carbon 语言名称查找设计解析作用域、未限定名称解析、名称污染与遮蔽规则 导读 本文聚焦 Carbon Language实验性编程语言的名称查找na编程语言编译器标准库ENScan_GO深度解析区块链域名分析的革命性工具ENScan_GO深度解析区块链域名分析的革命性工具 你还在为区块链域名分析效率低下而烦恼还在手动查询Ethereum域名ENS持有者信息ENScan网络安全渗透测试网页爬虫MCP 服务Hutch源码解析深入理解Ruby消息处理框架的设计原理和实现机制Hutch源码解析深入理解Ruby消息处理框架的设计原理和实现机制 Hutch是一个基于Ruby的 RabbitMQ消息处理框架 它为开发者提供了简洁而强大消息队列后端微服务上一篇华为光猫配置洞察终极方案从黑箱运维到透明管理的完整指南下一篇如何一键获取9大网盘直链告别限速困扰的完整解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考