From 48eb0d1dbdd83ade86c1eb7201fca2d57ab85476 Mon Sep 17 00:00:00 2001 From: lauren Date: Thu, 21 May 2026 10:03:50 -0700 Subject: [PATCH] [rust-compiler] Skip empty optional ScopeInfo maps on serialize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `bash compiler/scripts/test-babel-ast.sh` runs three sub-tests in order: `round_trip_all_fixtures`, `scope_info_round_trip`, and `scope_resolution_rename`. The first and third are green after the earlier slices in this stack, but the middle one fails 1780/1780 with diffs of the shape: --- compiler/.json --- Round-trip mismatch: + "nodeIdToScope": {}, This was previously masked because the round-trip failure earlier in the script tripped `set -e` and the scope-info test never ran. With round-trip green, it now surfaces a pre-existing serialization mismatch in `ScopeInfo`. Root cause: the Babel-side fixture producer in `compiler/scripts/babel-ast-to-json.mjs::collectScopeInfo` emits only five keys per `.scope.json` file — `scopes`, `bindings`, `nodeToScope`, `referenceToBinding`, and `programScope`. It never writes `nodeIdToScope`, `refNodeIdToBinding`, or `nodeToScopeEnd`. The Rust `ScopeInfo` struct declares those three fields with `#[serde(default)]`, so deserialization happily produces empty `HashMap` / `IndexMap` values when the keys are missing. Serialization was then emitting them as `"nodeIdToScope": {}` (etc.), which the round-trip diff treats as a mismatch. Add `skip_serializing_if = "::is_empty"` to all three fields so a fixture that came in without the key goes back out without the key. Required fields (no `#[serde(default)]`: `scopes`, `bindings`, `node_to_scope`, `reference_to_binding`, `program_scope`) are deliberately untouched — they are present in every fixture by design. Alternatives considered: - Remove `#[serde(default)]` from the three fields. Rejected: would break deserialization of legacy fixtures that legitimately omit these supplemental maps. - Wrap the maps in `Option<...>`. Rejected: changes field types and forces every reader to unwrap, with no behavior win over the existing default + skip-on-empty combination. - Custom skip function. Rejected: `HashMap::is_empty` and `IndexMap::is_empty` express intent directly and are already in scope via the existing imports. - Add a `Default` impl for `ScopeInfo`. Rejected: the issue is on the serialize side, not deserialize; `serde(default)` already supplies empty maps on deserialize correctly. Test plan: - bash compiler/scripts/test-babel-ast.sh: Before: round_trip 1779/1779, scope_info 0/1780 (FAIL), rename 1779/1779; exit 101 After: round_trip 1779/1779, scope_info 1780/1780, rename 1779/1779; exit 0 - cargo build: clean across the workspace. --- compiler/crates/react_compiler_ast/src/scope.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/compiler/crates/react_compiler_ast/src/scope.rs b/compiler/crates/react_compiler_ast/src/scope.rs index ac4e901d554d..8d11211ee8ce 100644 --- a/compiler/crates/react_compiler_ast/src/scope.rs +++ b/compiler/crates/react_compiler_ast/src/scope.rs @@ -115,7 +115,7 @@ pub struct ScopeInfo { /// Parallel to node_to_scope — same keys, but stores the end position /// of the scope-creating AST node. Used to determine if a source position /// falls within a particular scope's AST range. - #[serde(default)] + #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub node_to_scope_end: HashMap, /// Maps an Identifier AST node's start offset to the binding it resolves to. @@ -126,11 +126,19 @@ pub struct ScopeInfo { /// Maps an identifier reference's node-ID to the binding it resolves to. /// Only present for identifiers that resolve to a binding (not globals). /// Uses IndexMap to preserve insertion order. - #[serde(default, rename = "refNodeIdToBinding")] + #[serde( + default, + rename = "refNodeIdToBinding", + skip_serializing_if = "IndexMap::is_empty" + )] pub ref_node_id_to_binding: IndexMap, /// Maps a scope-creating AST node's node-ID to the scope it creates. - #[serde(default, rename = "nodeIdToScope")] + #[serde( + default, + rename = "nodeIdToScope", + skip_serializing_if = "HashMap::is_empty" + )] pub node_id_to_scope: HashMap, /// The program-level (module) scope. Always scopes[0].