Skip to content

Commit 1ce85b9

Browse files
fix: ECMA-426 conformance fixes across codec and sourcemap crates
- Reject 2/3-field segments in all decoder paths (codec + 4 sourcemap decoders) - Reject nested index maps with ParseError::NestedIndexMap - Validate section ordering (strictly ascending line/column offsets) - Fix ignoreList: explicit empty [] no longer falls through to x_google_ignoreList - Fix sourceRoot double-application on roundtrip (strip prefix in to_json) - Emit scopes field in to_json() output - Support both x_ and x- prefixed extension keys - Add 11 conformance tests covering all fixes
1 parent a2b1181 commit 1ce85b9

5 files changed

Lines changed: 441 additions & 39 deletions

File tree

ROADMAP.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,37 @@ Rolldown's `collapse_sourcemaps` materializes the entire token stream into `Vec<
6262
- [x] `StreamingGenerator`: on-the-fly VLQ encoder that emits mappings in sorted order without collecting
6363
- [x] `remap_streaming()`: composition pipeline that streams through mappings without intermediate allocation
6464
- [x] Criterion benchmarks (500 / 10K / 60K mappings) — 15-20% faster than materialized `remap()`
65+
66+
Remaining nice-to-haves (low priority):
6567
- [ ] Builder pattern for `SourceMap::new()` that consumes iterators for names/sources/source_contents
6668
- [ ] Benchmark against Rolldown's current `collapse_sourcemaps`
6769

6870
---
6971

72+
## Function Name Mappings
73+
74+
**ECMA-426, standardized**[bloomberg.github.io/js-blog/post/standardizing-source-maps](https://bloomberg.github.io/js-blog/post/standardizing-source-maps/)
75+
76+
A `"sourcesFunctionMappings"` array parallel to `"sources"`, where each entry is a VLQ-encoded string mapping generated positions to original function names. Previously the `x_com_bloomberg_sourcesFunctionMappings` extension, now standardized in ECMA-426.
77+
78+
This is a simpler alternative to the full scopes proposal for resolving minified function names in stack traces. Tools that don't need full scope/binding information can use this field alone. For tools that support scopes, `FindOriginalFunctionName` from the scopes proposal supersedes this — but both should be supported for interop, since most source maps in the wild won't have scopes data.
79+
80+
### What to implement
81+
82+
- [ ] Parse `sourcesFunctionMappings` field in `srcmap-sourcemap`
83+
- [ ] Decode per-source function name mappings (VLQ → function name index at position)
84+
- [ ] `original_function_name_for(source, line, col)` lookup API
85+
- [ ] Generate `sourcesFunctionMappings` in `srcmap-generator`
86+
- [ ] Preserve through remapping/concatenation
87+
- [ ] Use in symbolicate crate as fallback when scopes data is absent
88+
- [ ] WASM bindings
89+
- [ ] CLI: show function mappings in `srcmap info`
90+
91+
---
92+
7093
## Scopes Spec Alignment
7194

72-
**ECMA-426 scopes proposal, actively evolving** — srcmap already has `srcmap-scopes`, but the spec is still changing.
95+
**ECMA-426 scopes proposal, actively evolving** — srcmap already has `srcmap-scopes`, but the spec is still changing. The scopes proposal is the comprehensive approach to debugging metadata — it supersedes `sourcesFunctionMappings` for function name resolution and adds variable bindings, scope chains, and inlined frame reconstruction.
7396

7497
### Open issues to track
7598

@@ -105,7 +128,7 @@ Rolldown's `collapse_sourcemaps` materializes the entire token stream into `Vec<
105128

106129
---
107130

108-
## Sources Hash
131+
## Sources Hash `[Stage 1 — not implementing yet]`
109132

110133
**ECMA-426 proposal, Stage 1**[tc39/ecma426#208](https://github.com/nicolo-ribaudo/ecma-426/blob/main/proposals/range-mappings.md)
111134

@@ -136,7 +159,7 @@ Hash algorithm is implementation-defined (SHA-256 recommended). Format is a pref
136159

137160
---
138161

139-
## Debug ID Extraction
162+
## Debug ID Extraction `[Stage 2 — not implementing yet]`
140163

141164
**ECMA-426 proposal, Stage 2**[tc39/ecma426#207](https://github.com/nicolo-ribaudo/ecma-426/blob/main/proposals/range-mappings.md)
142165

@@ -188,7 +211,7 @@ Split the mappings string into chunks and encode VLQ segments in parallel using
188211

189212
---
190213

191-
## Mappings v2 Encoding
214+
## Mappings v2 Encoding `[Discussion — not implementing yet]`
192215

193216
**ECMA-426 discussion**[tc39/ecma426#155](https://github.com/nicolo-ribaudo/ecma-426/blob/main/proposals/range-mappings.md)
194217

@@ -223,7 +246,7 @@ This is early-stage and may never land. Worth implementing as an experimental en
223246

224247
---
225248

226-
## Env Metadata
249+
## Env Metadata `[Stage 1 — not implementing yet]`
227250

228251
**ECMA-426 proposal, Stage 1**[tc39/ecma426 proposals/env.md](https://github.com/nicolo-ribaudo/ecma-426/blob/main/proposals/range-mappings.md)
229252

crates/codec/src/decode.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,33 @@ pub fn decode(input: &str) -> Result<SourceMapMappings, DecodeError> {
7171

7272
// Build segment with exact allocation (1, 4, or 5 fields)
7373
let segment: Segment = if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
74-
// Fields 2-4: source, original line, original column
74+
// Field 2: source index
7575
let (delta, consumed) = vlq_decode(bytes, pos)?;
7676
source_index += delta;
7777
pos += consumed;
7878

79+
// Reject 2-field segments (only 1, 4, or 5 are valid per ECMA-426)
80+
if pos >= len || bytes[pos] == b',' || bytes[pos] == b';' {
81+
return Err(DecodeError::InvalidSegmentLength {
82+
fields: 2,
83+
offset: pos,
84+
});
85+
}
86+
87+
// Field 3: original line
7988
let (delta, consumed) = vlq_decode(bytes, pos)?;
8089
original_line += delta;
8190
pos += consumed;
8291

92+
// Reject 3-field segments (only 1, 4, or 5 are valid per ECMA-426)
93+
if pos >= len || bytes[pos] == b',' || bytes[pos] == b';' {
94+
return Err(DecodeError::InvalidSegmentLength {
95+
fields: 3,
96+
offset: pos,
97+
});
98+
}
99+
100+
// Field 4: original column
83101
let (delta, consumed) = vlq_decode(bytes, pos)?;
84102
original_column += delta;
85103
pos += consumed;

crates/codec/src/lib.rs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ pub enum DecodeError {
195195
UnexpectedEof { offset: usize },
196196
/// A VLQ value exceeded the maximum representable range.
197197
VlqOverflow { offset: usize },
198+
/// A segment has an invalid number of fields (only 1, 4, or 5 are valid per ECMA-426).
199+
InvalidSegmentLength { fields: u8, offset: usize },
198200
}
199201

200202
impl fmt::Display for DecodeError {
@@ -212,6 +214,12 @@ impl fmt::Display for DecodeError {
212214
Self::VlqOverflow { offset } => {
213215
write!(f, "VLQ value overflow at offset {offset}")
214216
}
217+
Self::InvalidSegmentLength { fields, offset } => {
218+
write!(
219+
f,
220+
"invalid segment with {fields} fields at offset {offset} (expected 1, 4, or 5)"
221+
)
222+
}
215223
}
216224
}
217225
}
@@ -373,12 +381,42 @@ mod tests {
373381
}
374382

375383
#[test]
376-
fn decode_truncated_segment() {
377-
// "AC" = two VLQ values (0, 1) — starts a 4-field segment but only has 2 values
384+
fn decode_truncated_segment_two_fields() {
385+
// "AC" = two VLQ values (0, 1) — 2-field segment is invalid per ECMA-426
378386
let err = decode("AC").unwrap_err();
379387
assert!(matches!(
380388
err,
381-
DecodeError::UnexpectedEof { .. } | DecodeError::InvalidBase64 { .. }
389+
DecodeError::InvalidSegmentLength { fields: 2, .. }
390+
));
391+
}
392+
393+
#[test]
394+
fn decode_truncated_segment_three_fields() {
395+
// "ACA" = three VLQ values (0, 1, 0) — 3-field segment is invalid per ECMA-426
396+
let err = decode("ACA").unwrap_err();
397+
assert!(matches!(
398+
err,
399+
DecodeError::InvalidSegmentLength { fields: 3, .. }
400+
));
401+
}
402+
403+
#[test]
404+
fn decode_two_field_segment_followed_by_separator() {
405+
// "AC,AAAA" — first segment has 2 fields, invalid
406+
let err = decode("AC,AAAA").unwrap_err();
407+
assert!(matches!(
408+
err,
409+
DecodeError::InvalidSegmentLength { fields: 2, .. }
410+
));
411+
}
412+
413+
#[test]
414+
fn decode_three_field_segment_followed_by_separator() {
415+
// "ACA;AAAA" — first segment has 3 fields, invalid
416+
let err = decode("ACA;AAAA").unwrap_err();
417+
assert!(matches!(
418+
err,
419+
DecodeError::InvalidSegmentLength { fields: 3, .. }
382420
));
383421
}
384422

@@ -532,6 +570,18 @@ mod tests {
532570
assert_eq!(err.to_string(), "VLQ value overflow at offset 10");
533571
}
534572

573+
#[test]
574+
fn decode_error_display_invalid_segment_length() {
575+
let err = DecodeError::InvalidSegmentLength {
576+
fields: 2,
577+
offset: 3,
578+
};
579+
assert_eq!(
580+
err.to_string(),
581+
"invalid segment with 2 fields at offset 3 (expected 1, 4, or 5)"
582+
);
583+
}
584+
535585
// --- Decode edge case: 5-field segment with name ---
536586

537587
#[test]

0 commit comments

Comments
 (0)