///|
/// Severity assigned to a validation issue.
pub enum IssueSeverity {
IssueError
IssueWarning
} derive(Debug, Eq, ToJson)
///|
/// A machine-readable validation finding.
pub struct ValidationIssue {
severity : IssueSeverity
code : String
path : String
message : String
} derive(Debug, Eq, ToJson)
///|
/// Complete validation result for one SARIF log.
pub struct ValidationReport {
issues : Array[ValidationIssue]
} derive(Debug, Eq, ToJson)
///|
/// Whether validation produced no errors.
pub fn ValidationReport::is_valid(self : ValidationReport) -> Bool {
self.issues.all(fn(issue) { issue.severity != IssueError })
}
///|
/// Count errors in a validation report.
pub fn ValidationReport::error_count(self : ValidationReport) -> Int {
self.issues.fold(init=0, fn(count, issue) {
if issue.severity == IssueError {
count + 1
} else {
count
}
})
}
///|
/// Count warnings in a validation report.
pub fn ValidationReport::warning_count(self : ValidationReport) -> Int {
self.issues.fold(init=0, fn(count, issue) {
if issue.severity == IssueWarning {
count + 1
} else {
count
}
})
}
///|
fn add_issue(
issues : Array[ValidationIssue],
severity : IssueSeverity,
code : String,
path : String,
message : String,
) -> Unit {
issues.push({ severity, code, path, message })
}
///|
fn validate_message(
message : Message,
path : String,
issues : Array[ValidationIssue],
) -> Unit {
let has_text = match message.text {
Some(text) => !text.trim().is_empty()
None => false
}
let has_markdown = match message.markdown {
Some(markdown) => !markdown.trim().is_empty()
None => false
}
if !has_text && !has_markdown {
add_issue(
issues,
IssueError,
"message.empty",
path,
"message must contain non-empty text or markdown",
)
}
}
///|
fn validate_region(
region : Region,
path : String,
issues : Array[ValidationIssue],
) -> Unit {
match region.startLine {
Some(value) if value < 1 =>
add_issue(
issues,
IssueError,
"region.startLine",
path + ".startLine",
"startLine must be greater than zero",
)
_ => ()
}
match region.startColumn {
Some(value) if value < 1 =>
add_issue(
issues,
IssueError,
"region.startColumn",
path + ".startColumn",
"startColumn must be greater than zero",
)
_ => ()
}
match (region.startLine, region.endLine) {
(Some(start), Some(finish)) if finish < start =>
add_issue(
issues,
IssueError,
"region.lineOrder",
path + ".endLine",
"endLine must not precede startLine",
)
_ => ()
}
}
///|
fn validate_result(
result : SarifResult,
path : String,
known_rules : Map[String, Unit],
issues : Array[ValidationIssue],
) -> Unit {
validate_message(result.message, path + ".message", issues)
match result.level {
Some("none" | "note" | "warning" | "error") | None => ()
Some(level) =>
add_issue(
issues,
IssueError,
"result.level",
path + ".level",
"unsupported result level '\{level}'",
)
}
match result.ruleId {
Some(rule_id) if !known_rules.contains(rule_id) =>
add_issue(
issues,
IssueWarning,
"result.unknownRule",
path + ".ruleId",
"ruleId '\{rule_id}' is not declared by the tool driver",
)
_ => ()
}
match result.locations {
Some(locations) =>
for index, location in locations {
match location.physicalLocation {
Some(physical) =>
match physical.region {
Some(region) =>
validate_region(
region,
path + ".locations[\{index}].physicalLocation.region",
issues,
)
None => ()
}
None => ()
}
}
None => ()
}
}
///|
/// Perform structural and semantic checks that are useful before uploading a
/// SARIF file to a code-scanning service.
pub fn validate(log : SarifLog) -> ValidationReport {
let issues : Array[ValidationIssue] = []
if log.version != "2.1.0" {
add_issue(
issues,
IssueError,
"log.version",
"$.version",
"MoonSARIF currently supports SARIF version 2.1.0",
)
}
if log.runs.is_empty() {
add_issue(
issues,
IssueWarning,
"log.emptyRuns",
"$.runs",
"the log contains no analysis runs",
)
}
for run_index, run in log.runs {
let run_path = "$.runs[\{run_index}]"
if run.tool.driver.name.trim().is_empty() {
add_issue(
issues,
IssueError,
"tool.name",
run_path + ".tool.driver.name",
"tool driver name must not be empty",
)
}
let known_rules : Map[String, Unit] = Map([])
match run.tool.driver.rules {
Some(rules) =>
for rule_index, rule in rules {
if rule.id.trim().is_empty() {
add_issue(
issues,
IssueError,
"rule.id",
run_path + ".tool.driver.rules[\{rule_index}].id",
"rule id must not be empty",
)
} else if known_rules.contains(rule.id) {
add_issue(
issues,
IssueError,
"rule.duplicate",
run_path + ".tool.driver.rules[\{rule_index}].id",
"duplicate rule id '\{rule.id}'",
)
} else {
known_rules[rule.id] = ()
}
}
None => ()
}
match run.results {
Some(results) =>
for result_index, result in results {
validate_result(
result,
run_path + ".results[\{result_index}]",
known_rules,
issues,
)
}
None => ()
}
}
{ issues, }
}