Skip to content

Commit 9617e62

Browse files
docs: comprehensive documentation overhaul across all crates and packages
- Fix empty @srcmap/sourcemap index.d.ts with full TypeScript definitions - Add READMEs for generator-wasm, remapping-wasm, symbolicate-wasm, trace-mapping - Add READMEs for srcmap-scopes and srcmap-symbolicate crates - Improve rustdoc on SourceMap, Mapping, OriginalLocation, GeneratedLocation, MappedRange, ParseError, SourceMapGenerator, and generator Mapping structs - Fix rustdoc HTML tag warning in LazySourceMap comment - Add test coverage for codec DecodeError Display, VLQ edge cases
1 parent 792209f commit 9617e62

13 files changed

Lines changed: 3391 additions & 23 deletions

File tree

crates/codec/src/lib.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,4 +373,47 @@ mod tests {
373373
assert_eq!(sequential, parallel);
374374
}
375375
}
376+
377+
// --- DecodeError Display tests ---
378+
379+
#[test]
380+
fn decode_error_display_invalid_base64() {
381+
let err = DecodeError::InvalidBase64 {
382+
byte: b'!',
383+
offset: 2,
384+
};
385+
assert_eq!(err.to_string(), "invalid base64 character 0x21 at offset 2");
386+
}
387+
388+
#[test]
389+
fn decode_error_display_unexpected_eof() {
390+
let err = DecodeError::UnexpectedEof { offset: 5 };
391+
assert_eq!(err.to_string(), "unexpected end of input at offset 5");
392+
}
393+
394+
#[test]
395+
fn decode_error_display_overflow() {
396+
let err = DecodeError::VlqOverflow { offset: 10 };
397+
assert_eq!(err.to_string(), "VLQ value overflow at offset 10");
398+
}
399+
400+
// --- Decode edge case: 5-field segment with name ---
401+
402+
#[test]
403+
fn decode_five_field_with_name_index() {
404+
// Ensure the name field (5th) is decoded correctly
405+
let input = "AAAAC"; // 0,0,0,0,1
406+
let decoded = decode(input).unwrap();
407+
assert_eq!(decoded[0][0], vec![0, 0, 0, 0, 1]);
408+
}
409+
410+
// --- Encode edge case: encode with only 1 line ---
411+
412+
#[test]
413+
fn encode_single_segment_one_field() {
414+
let mappings = vec![vec![vec![5_i64]]];
415+
let encoded = encode(&mappings);
416+
let decoded = decode(&encoded).unwrap();
417+
assert_eq!(decoded, mappings);
418+
}
376419
}

crates/codec/src/vlq.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,4 +353,35 @@ mod tests {
353353
let err = vlq_decode_unsigned(b"", 0).unwrap_err();
354354
assert_eq!(err, DecodeError::UnexpectedEof { offset: 0 });
355355
}
356+
357+
#[test]
358+
fn unsigned_decode_non_ascii() {
359+
let err = vlq_decode_unsigned(&[0xC3, 0x80], 0).unwrap_err();
360+
assert_eq!(
361+
err,
362+
DecodeError::InvalidBase64 {
363+
byte: 0xC3,
364+
offset: 0
365+
}
366+
);
367+
}
368+
369+
#[test]
370+
fn unsigned_decode_invalid_base64_char() {
371+
let err = vlq_decode_unsigned(b"!", 0).unwrap_err();
372+
assert_eq!(
373+
err,
374+
DecodeError::InvalidBase64 {
375+
byte: b'!',
376+
offset: 0
377+
}
378+
);
379+
}
380+
381+
#[test]
382+
fn unsigned_decode_overflow() {
383+
// 14 continuation chars to trigger overflow
384+
let err = vlq_decode_unsigned(b"ggggggggggggggA", 0).unwrap_err();
385+
assert!(matches!(err, DecodeError::VlqOverflow { .. }));
386+
}
356387
}

crates/generator/src/lib.rs

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,41 @@ use srcmap_scopes::ScopeInfo;
3131

3232
// ── Public types ───────────────────────────────────────────────────
3333

34-
/// A mapping from generated position to original position.
34+
/// A mapping from a generated position to an original position.
35+
///
36+
/// Used with [`SourceMapGenerator`] to define position relationships.
37+
/// All positions are 0-based.
3538
#[derive(Debug, Clone)]
3639
pub struct Mapping {
40+
/// 0-based line in the generated output.
3741
pub generated_line: u32,
42+
/// 0-based column in the generated output.
3843
pub generated_column: u32,
44+
/// Source index from [`SourceMapGenerator::add_source`], or `None` for generated-only.
3945
pub source: Option<u32>,
46+
/// 0-based line in the original source.
4047
pub original_line: u32,
48+
/// 0-based column in the original source.
4149
pub original_column: u32,
50+
/// Name index from [`SourceMapGenerator::add_name`], or `None`.
4251
pub name: Option<u32>,
4352
}
4453

4554
/// Builder for creating source maps incrementally.
55+
///
56+
/// Register sources and names first (they return indices), then add mappings
57+
/// that reference those indices. Mappings should be added in generated-position
58+
/// order, though the builder does not require it.
59+
///
60+
/// Sources and names are automatically deduplicated.
61+
///
62+
/// # Workflow
63+
///
64+
/// 1. [`add_source`](Self::add_source) — register each original file
65+
/// 2. [`set_source_content`](Self::set_source_content) — optionally attach content
66+
/// 3. [`add_name`](Self::add_name) — register identifier names
67+
/// 4. [`add_mapping`](Self::add_mapping) / [`add_named_mapping`](Self::add_named_mapping) — add mappings
68+
/// 5. [`to_json`](Self::to_json) — serialize to JSON
4669
#[derive(Debug)]
4770
pub struct SourceMapGenerator {
4871
file: Option<String>,
@@ -1224,6 +1247,76 @@ mod tests {
12241247
);
12251248
}
12261249

1250+
#[test]
1251+
fn set_source_content_out_of_bounds() {
1252+
let mut builder = SourceMapGenerator::new(None);
1253+
// No sources added, index 0 is out of bounds
1254+
builder.set_source_content(0, "content".to_string());
1255+
// Should silently do nothing
1256+
let json = builder.to_json();
1257+
assert!(!json.contains("content"));
1258+
}
1259+
1260+
#[test]
1261+
fn add_to_ignore_list_dedup() {
1262+
let mut builder = SourceMapGenerator::new(None);
1263+
let idx = builder.add_source("vendor.js");
1264+
builder.add_to_ignore_list(idx);
1265+
builder.add_to_ignore_list(idx); // duplicate - should be deduped
1266+
let json = builder.to_json();
1267+
// Should only appear once in ignoreList
1268+
let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1269+
assert_eq!(sm.ignore_list, vec![0]);
1270+
}
1271+
1272+
#[test]
1273+
fn to_decoded_map_with_source_root() {
1274+
let mut builder = SourceMapGenerator::new(None);
1275+
builder.set_source_root("src/".to_string());
1276+
let src = builder.add_source("app.ts");
1277+
builder.add_mapping(0, 0, src, 0, 0);
1278+
let sm = builder.to_decoded_map();
1279+
// Sources should be prefixed with source_root
1280+
assert_eq!(sm.sources, vec!["src/app.ts"]);
1281+
}
1282+
1283+
#[test]
1284+
fn json_escaping_special_chars() {
1285+
let mut builder = SourceMapGenerator::new(None);
1286+
let src = builder.add_source("a.js");
1287+
// Content with special chars: quotes, backslash, newline, carriage return, tab, control char
1288+
builder.set_source_content(src, "line1\nline2\r\ttab\\\"\x01end".to_string());
1289+
builder.add_mapping(0, 0, src, 0, 0);
1290+
let json = builder.to_json();
1291+
// Verify it's valid JSON by parsing
1292+
let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1293+
assert_eq!(
1294+
sm.sources_content,
1295+
vec![Some("line1\nline2\r\ttab\\\"\x01end".to_string())]
1296+
);
1297+
}
1298+
1299+
#[test]
1300+
fn json_escaping_in_names() {
1301+
let mut builder = SourceMapGenerator::new(None);
1302+
let src = builder.add_source("a.js");
1303+
let name = builder.add_name("func\"with\\special");
1304+
builder.add_named_mapping(0, 0, src, 0, 0, name);
1305+
let json = builder.to_json();
1306+
let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1307+
assert_eq!(sm.names[0], "func\"with\\special");
1308+
}
1309+
1310+
#[test]
1311+
fn json_escaping_in_sources() {
1312+
let mut builder = SourceMapGenerator::new(None);
1313+
let src = builder.add_source("path/with\"quotes.js");
1314+
builder.add_mapping(0, 0, src, 0, 0);
1315+
let json = builder.to_json();
1316+
let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1317+
assert_eq!(sm.sources[0], "path/with\"quotes.js");
1318+
}
1319+
12271320
#[cfg(feature = "parallel")]
12281321
mod parallel_tests {
12291322
use super::*;

crates/scopes/README.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# srcmap-scopes
2+
3+
[![crates.io](https://img.shields.io/crates/v/srcmap-scopes.svg)](https://crates.io/crates/srcmap-scopes)
4+
[![docs.rs](https://docs.rs/srcmap-scopes/badge.svg)](https://docs.rs/srcmap-scopes)
5+
6+
Scopes and variables decoder/encoder for source maps ([ECMA-426](https://tc39.es/ecma426/)).
7+
8+
Implements the "Scopes" proposal for source maps, enabling debuggers to reconstruct original scope trees, variable bindings, and inlined function call sites from generated code.
9+
10+
## Usage
11+
12+
```rust
13+
use srcmap_scopes::{
14+
decode_scopes, encode_scopes, Binding, GeneratedRange,
15+
OriginalScope, Position, ScopeInfo,
16+
};
17+
18+
// Build scope info
19+
let info = ScopeInfo {
20+
scopes: vec![Some(OriginalScope {
21+
start: Position { line: 0, column: 0 },
22+
end: Position { line: 5, column: 0 },
23+
name: None,
24+
kind: Some("global".to_string()),
25+
is_stack_frame: false,
26+
variables: vec!["x".to_string()],
27+
children: vec![],
28+
})],
29+
ranges: vec![GeneratedRange {
30+
start: Position { line: 0, column: 0 },
31+
end: Position { line: 5, column: 0 },
32+
is_stack_frame: false,
33+
is_hidden: false,
34+
definition: Some(0),
35+
call_site: None,
36+
bindings: vec![Binding::Expression("_x".to_string())],
37+
children: vec![],
38+
}],
39+
};
40+
41+
// Encode to VLQ
42+
let mut names = vec!["global".to_string(), "x".to_string(), "_x".to_string()];
43+
let encoded = encode_scopes(&info, &mut names);
44+
45+
// Decode back
46+
let decoded = decode_scopes(&encoded, &names, 1).unwrap();
47+
assert_eq!(decoded.scopes.len(), 1);
48+
```
49+
50+
## Key types
51+
52+
| Type | Description |
53+
|------|-------------|
54+
| `ScopeInfo` | Top-level container: original scopes + generated ranges |
55+
| `OriginalScope` | A scope in authored source code (tree structure) |
56+
| `GeneratedRange` | A range in generated output mapped to an original scope |
57+
| `Binding` | Variable binding: expression, unavailable, or sub-range bindings |
58+
| `CallSite` | Inlined function call site in original source |
59+
| `Position` | 0-based line/column pair |
60+
61+
## How it works
62+
63+
The scopes format uses tag-based VLQ encoding where each item is prefixed with a tag byte identifying the item type (scope start, scope end, range start, etc.) followed by flags and VLQ-encoded values. This enables efficient serialization of tree-structured scope and range data into a single flat string.
64+
65+
## Part of [srcmap](https://github.com/BartWaardenburg/srcmap)
66+
67+
Used by `srcmap-sourcemap` (decode) and `srcmap-generator` (encode). See the [main repo](https://github.com/BartWaardenburg/srcmap) for the full source map SDK.
68+
69+
## License
70+
71+
MIT

0 commit comments

Comments
 (0)