///|
/// One named source map and its generated-file aliases.
pub(all) struct RegisteredSourceMap {
name : String
generated_file : String
aliases : Array[String]
map : DecodedSourceMap
}
///|
/// Reason a registry symbolization did not produce an original location.
pub(all) enum RegistryMissReason {
MapNotFound
MappingNotFound
GeneratedOnly
} derive(Eq, Debug)
///|
/// Result of symbolizing one frame through a source-map registry.
pub(all) struct RegistrySymbolizedFrame {
frame : StackFrame
map_name : String?
result : SymbolizedFrame?
context : SourceContext?
miss_reason : RegistryMissReason?
} derive(Eq, Debug)
///|
/// Aggregate outcome for newline-separated multi-file stack frames.
pub(all) struct RegistrySymbolizationReport {
frames : Array[RegistrySymbolizedFrame]
matched : Int
map_missing : Int
mapping_missing : Int
generated_only : Int
} derive(Eq, Debug)
///|
/// Registry for symbolizing frames from multiple generated artifacts.
pub(all) struct SourceMapRegistry {
entries : Array[RegisteredSourceMap]
}
///|
/// Construct an empty registry.
pub fn SourceMapRegistry::new() -> SourceMapRegistry {
{ entries: [] }
}
///|
fn registry_keys(entry : RegisteredSourceMap) -> Array[String] {
let keys : Array[String] = [entry.generated_file]
for alternative in entry.aliases {
keys.push(alternative)
}
keys
}
///|
fn registry_key_matches(left : String, right : String) -> Bool {
left == right ||
normalize_generated_file(left) == normalize_generated_file(right)
}
///|
fn registry_entry_owns_key(entry : RegisteredSourceMap, key : String) -> Bool {
registry_keys(entry).any(existing => registry_key_matches(existing, key))
}
///|
fn validate_registry_key(key : String) -> Unit raise SourceMapError {
if key.trim().is_empty() {
raise InvalidDocument(message="source map registry keys must not be empty")
}
}
///|
/// Register a decoded source map.
///
/// Exact and normalized keys must remain unique across entries. Re-registering
/// the same logical generated file is rejected instead of silently replacing
/// a map and making symbolization order-dependent.
pub fn SourceMapRegistry::register(
self : SourceMapRegistry,
name~ : String,
generated_file~ : String,
map~ : DecodedSourceMap,
aliases? : Array[String] = [],
) -> Unit raise SourceMapError {
if name.trim().is_empty() {
raise InvalidDocument(
message="registered source map name must not be empty",
)
}
validate_registry_key(generated_file)
for alternative in aliases {
validate_registry_key(alternative)
}
let candidate : RegisteredSourceMap = {
name,
generated_file,
aliases: aliases.copy(),
map,
}
let keys = registry_keys(candidate)
for key_index, key in keys {
for previous_index in 0.. Int {
self.entries.length()
}
///|
/// Return whether the registry is empty.
pub fn SourceMapRegistry::is_empty(self : SourceMapRegistry) -> Bool {
self.entries.is_empty()
}
///|
/// Resolve an entry by exact key first, then normalized key.
pub fn SourceMapRegistry::find(
self : SourceMapRegistry,
generated_file : String,
) -> RegisteredSourceMap? {
for entry in self.entries {
if entry.generated_file == generated_file ||
entry.aliases.any(alternative => alternative == generated_file) {
return Some(entry)
}
}
let normalized = normalize_generated_file(generated_file)
for entry in self.entries {
if normalize_generated_file(entry.generated_file) == normalized ||
entry.aliases.any(alternative => {
normalize_generated_file(alternative) == normalized
}) {
return Some(entry)
}
}
None
}
///|
fn registry_unmatched_frame(
frame : StackFrame,
map_name : String?,
reason : RegistryMissReason,
) -> RegistrySymbolizedFrame {
{ frame, map_name, result: None, context: None, miss_reason: Some(reason) }
}
///|
/// Symbolize one frame using the matching generated-file map.
pub fn SourceMapRegistry::symbolize_frame(
self : SourceMapRegistry,
frame : StackFrame,
bias? : LookupBias = GreatestLowerBound,
context_radius? : Int = 0,
) -> RegistrySymbolizedFrame {
let entry = match self.find(frame.file) {
Some(entry) => entry
None => return registry_unmatched_frame(frame, None, MapNotFound)
}
let mapping = original_position_for(
entry.map,
generated=Position::new(line=frame.line, column=frame.column),
bias~,
)
match mapping {
None => registry_unmatched_frame(frame, Some(entry.name), MappingNotFound)
Some(mapping) =>
match mapping.original {
None => registry_unmatched_frame(frame, Some(entry.name), GeneratedOnly)
Some(_) => {
let symbolized = symbolize_frame(entry.map, frame, bias~)
{
frame,
map_name: Some(entry.name),
result: Some(symbolized),
context: if context_radius > 0 {
source_context_for_mapping(
entry.map,
mapping,
radius=context_radius,
)
} else {
None
},
miss_reason: None,
}
}
}
}
}
///|
/// Parse and symbolize newline-separated frames from multiple generated files.
pub fn SourceMapRegistry::symbolize(
self : SourceMapRegistry,
frames : String,
bias? : LookupBias = GreatestLowerBound,
context_radius? : Int = 0,
) -> RegistrySymbolizationReport raise SourceMapError {
let results : Array[RegistrySymbolizedFrame] = []
let mut matched = 0
let mut map_missing = 0
let mut mapping_missing = 0
let mut generated_only = 0
for line in frames.split("\n") {
let text = line.trim()
if text.is_empty() {
continue
}
let result = self.symbolize_frame(
parse_stack_frame(text.to_owned()),
bias~,
context_radius~,
)
match result.miss_reason {
Some(MapNotFound) => map_missing = map_missing + 1
Some(MappingNotFound) => mapping_missing = mapping_missing + 1
Some(GeneratedOnly) => generated_only = generated_only + 1
None => matched = matched + 1
}
results.push(result)
}
{ frames: results, matched, map_missing, mapping_missing, generated_only }
}
///|
fn registry_reason_label(reason : RegistryMissReason) -> String {
match reason {
MapNotFound => "map_not_found"
MappingNotFound => "mapping_not_found"
GeneratedOnly => "generated_only"
}
}
///|
/// Convert a registry frame result to JSON.
pub fn RegistrySymbolizedFrame::to_json(self : RegistrySymbolizedFrame) -> Json {
Json::object({
"frame": Json::object({
"file": Json::string(self.frame.file),
"line": Json::number(self.frame.line.to_double()),
"column": Json::number(self.frame.column.to_double()),
}),
"map": match self.map_name {
Some(name) => Json::string(name)
None => Json::null()
},
"result": match self.result {
Some(result) => result.to_json()
None => Json::null()
},
"context": match self.context {
Some(context) => context.to_json()
None => Json::null()
},
"miss_reason": match self.miss_reason {
Some(reason) => Json::string(registry_reason_label(reason))
None => Json::null()
},
})
}
///|
/// Render one registry symbolization result.
pub fn RegistrySymbolizedFrame::render(
self : RegistrySymbolizedFrame,
) -> String {
match self.result {
Some(result) => {
let output = StringBuilder()
output.write_string(result.render())
match self.context {
Some(context) => {
output.write_char('\n')
output.write_string(context.render())
}
None => ()
}
output.to_string()
}
None => {
let reason = self.miss_reason
.map(registry_reason_label)
.unwrap_or("unmapped")
"\{self.frame.raw} -> <\{reason}>"
}
}
}
///|
/// Convert a registry report to stable JSON.
pub fn RegistrySymbolizationReport::to_json(
self : RegistrySymbolizationReport,
) -> Json {
Json::object({
"matched": Json::number(self.matched.to_double()),
"map_missing": Json::number(self.map_missing.to_double()),
"mapping_missing": Json::number(self.mapping_missing.to_double()),
"generated_only": Json::number(self.generated_only.to_double()),
"frames": Json::array(self.frames.map(frame => frame.to_json())),
})
}
///|
/// Render all registry frame results.
pub fn RegistrySymbolizationReport::render(
self : RegistrySymbolizationReport,
) -> String {
self.frames.map(frame => frame.render()).join("\n")
}
///|
/// Return true when every frame was symbolized.
pub fn RegistrySymbolizationReport::is_complete(
self : RegistrySymbolizationReport,
) -> Bool {
self.frames.length() == self.matched
}