///|
/// Stable text label for a diagnostic severity.
pub fn DiagnosticSeverity::label(self : DiagnosticSeverity) -> String {
match self {
Error => "error"
Warning => "warning"
}
}
///|
/// Convert a diagnostic to JSON.
pub fn Diagnostic::to_json(self : Diagnostic) -> Json {
let fields : Map[String, Json] = {
"severity": Json::string(self.severity.label()),
"code": Json::string(self.code),
"message": Json::string(self.message),
"path": Json::string(self.path),
}
match self.mapping_line {
Some(value) => fields["mapping_line"] = Json::number(value.to_double())
None => ()
}
match self.segment {
Some(value) => fields["segment"] = Json::number(value.to_double())
None => ()
}
match self.offset {
Some(value) => fields["offset"] = Json::number(value.to_double())
None => ()
}
Json::object(fields)
}
///|
/// Render one diagnostic in a stable single-line text format.
pub fn Diagnostic::render(self : Diagnostic) -> String {
let location = StringBuilder()
location.write_string(self.path)
match self.mapping_line {
Some(line) => location.write_string(":line \{line}")
None => ()
}
match self.segment {
Some(segment) => location.write_string(":segment \{segment}")
None => ()
}
match self.offset {
Some(offset) => location.write_string(":offset \{offset}")
None => ()
}
"[\{self.severity.label()}] \{self.code} \{location}: \{self.message}"
}
///|
/// Convert diagnostics to a JSON array.
pub fn diagnostics_to_json(diagnostics : ArrayView[Diagnostic]) -> Json {
Json::array(diagnostics.map(Diagnostic::to_json))
}
///|
/// Return true when at least one conformance error is present.
pub fn diagnostics_have_errors(diagnostics : ArrayView[Diagnostic]) -> Bool {
diagnostics.any(Diagnostic::is_error)
}
///|
fn duplicate_int(values : ArrayView[Int]) -> Int? {
let seen : Map[Int, Unit] = Map([])
for value in values {
if seen.contains(value) {
return Some(value)
}
seen[value] = ()
}
None
}
///|
fn validate_regular(
map : RegularSourceMap,
path : String,
diagnostics : Array[Diagnostic],
) -> Unit {
if map.version != 3 {
diagnostics.push(
Diagnostic::error(
code="version_not_three",
message="ECMA-426 source maps must use version 3",
path="\{path}.version",
),
)
}
match map.sources_content {
Some(content) =>
if content.length() > map.sources.length() {
diagnostics.push(
Diagnostic::error(
code="sources_content_too_long",
message="sourcesContent cannot contain more entries than sources",
path="\{path}.sourcesContent",
),
)
} else if content.length() < map.sources.length() {
diagnostics.push(
Diagnostic::warning(
code="sources_content_short",
message="missing sourcesContent entries are treated as null",
path="\{path}.sourcesContent",
),
)
}
None => ()
}
for index, ignored in map.ignore_list {
if ignored < 0 || ignored >= map.sources.length() {
diagnostics.push(
Diagnostic::error(
code="ignore_index_out_of_range",
message="ignoreList entry \{ignored} is outside the sources table",
path="\{path}.ignoreList[\{index}]",
),
)
}
}
match duplicate_int(map.ignore_list) {
Some(value) =>
diagnostics.push(
Diagnostic::warning(
code="duplicate_ignore_index",
message="ignoreList contains duplicate index \{value}",
path="\{path}.ignoreList",
),
)
None => ()
}
let mappings = decode_mappings(
map.mappings,
sources_count=map.sources.length(),
names=map.names,
) catch {
error => {
diagnostics.push(error.to_diagnostic())
[]
}
}
for mapping_index, mapping in mappings {
match mapping.original {
Some(original) =>
if map.sources[original.source_index] is None {
diagnostics.push(
Diagnostic::warning(
code="mapping_to_null_source",
message="mapping refers to a null source entry",
path="\{path}.mappings",
mapping_line=mapping.generated.line,
segment=mapping_index,
),
)
}
None => ()
}
}
}
///|
fn offset_position(base : Position, relative : Position) -> Position {
Position::new(
line=base.line + relative.line,
column=if relative.line == 0 {
base.column + relative.column
} else {
relative.column
},
)
}
///|
fn document_last_position(
document : SourceMapDocument,
) -> Position? raise SourceMapError {
match document {
Regular(map) => {
let mappings = decode_mappings(
map.mappings,
sources_count=map.sources.length(),
names=map.names,
)
if mappings.is_empty() {
None
} else {
Some(mappings[mappings.length() - 1].generated)
}
}
Indexed(map) =>
if map.sections.is_empty() {
None
} else {
let last = map.sections[map.sections.length() - 1]
match document_last_position(last.map) {
Some(position) => Some(offset_position(last.offset, position))
None => Some(last.offset)
}
}
}
}
///|
fn validate_index(
map : IndexSourceMap,
path : String,
diagnostics : Array[Diagnostic],
) -> Unit {
if map.version != 3 {
diagnostics.push(
Diagnostic::error(
code="version_not_three",
message="ECMA-426 source maps must use version 3",
path="\{path}.version",
),
)
}
let mut previous_offset : Position? = None
let mut previous_end : Position? = None
for index, section in map.sections {
let section_path = "\{path}.sections[\{index}]"
if !section.offset.is_valid() {
diagnostics.push(
Diagnostic::error(
code="invalid_section_offset",
message="section offsets must be non-negative",
path="\{section_path}.offset",
),
)
}
match previous_offset {
Some(previous) =>
if previous.compare(section.offset) >= 0 {
diagnostics.push(
Diagnostic::error(
code="sections_not_sorted",
message="section offsets must be strictly increasing",
path="\{section_path}.offset",
),
)
}
None => ()
}
match previous_end {
Some(end) =>
if end.compare(section.offset) >= 0 {
diagnostics.push(
Diagnostic::error(
code="sections_overlap",
message="section overlaps the preceding section",
path="\{section_path}.offset",
),
)
}
None => ()
}
validate_into(section.map, "\{section_path}.map", diagnostics)
previous_offset = Some(section.offset)
previous_end = document_last_position(section.map).map(position => {
offset_position(section.offset, position)
}) catch {
_ => None
}
}
}
///|
fn validate_into(
document : SourceMapDocument,
path : String,
diagnostics : Array[Diagnostic],
) -> Unit {
match document {
Regular(map) => validate_regular(map, path, diagnostics)
Indexed(map) => validate_index(map, path, diagnostics)
}
}
///|
/// Validate a parsed source map without aborting after the first problem.
pub fn validate(document : SourceMapDocument) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
validate_into(document, "$", diagnostics)
diagnostics
}
///|
/// Parse and validate a source map in one operation.
pub fn validate_json(input : StringView) -> Array[Diagnostic] {
try parse_document(input) catch {
error => return [error.to_diagnostic()]
} noraise {
document => validate(document)
}
}