///|
// Reject invalid Unicode explicitly: UTF-8 encoding would otherwise panic.
fn valid_unicode(source : String) -> Bool {
let units = source.code_units()
let mut i = 0
while i < units.length() {
let c = units[i]
if c >= 0xD800 && c <= 0xDBFF {
if i + 1 >= units.length() ||
units[i + 1] < 0xDC00 ||
units[i + 1] > 0xDFFF {
return false
}
i += 2
} else if c >= 0xDC00 && c <= 0xDFFF {
return false
} else {
i += 1
}
}
true
}
///|
fn source_boundary(source : String, offset : Int32) -> Bool {
offset >= 0 &&
offset <= source.length() &&
!(offset > 0 &&
offset < source.length() &&
source.code_units()[offset] >= 0xDC00 &&
source.code_units()[offset] <= 0xDFFF)
}
///|
fn sarif_uri(path : String) -> Result[String, String] {
let path = path.replace(old="\\", new="/")
if !valid_unicode(path) ||
path.is_empty() ||
path.has_prefix("/") ||
path.contains(":") ||
path.code_units().any(c => c < 32 || c == 127) {
return Err(
"SARIF requires a repository-relative file path without control characters or a drive/scheme",
)
}
for part in path.split("/") {
if part.is_empty() || part == "." || part == ".." {
return Err("SARIF path must not contain empty, dot or parent components")
}
}
let out = StringBuilder()
let hex = "0123456789ABCDEF"
for byte in @utf8.encode(path).iter() {
let c = byte.to_int()
if (c >= 65 && c <= 90) ||
(c >= 97 && c <= 122) ||
(c >= 48 && c <= 57) ||
c == 45 ||
c == 46 ||
c == 95 ||
c == 126 ||
c == 47 {
out.write_char(c.to_char().unwrap())
} else {
out.write_string("%")
out.write_stringview(hex[c / 16:c / 16 + 1])
out.write_stringview(hex[c % 16:c % 16 + 1])
}
}
Ok(out.to_string())
}
///|
fn sarif_lines(source : String) -> Array[Int32] {
let starts = [0]
let mut i = 0
while i < source.length() {
let c = source.code_units()[i]
i += 1
if c == 13 {
if source.get(i) == Some(10) {
i += 1
}
starts.push(i)
} else if c == 10 {
starts.push(i)
}
}
starts
}
///|
fn sarif_position(starts : Array[Int32], offset : Int32) -> (Int32, Int32) {
let mut low = 0
let mut high = starts.length()
while low + 1 < high {
let mid = low + (high - low) / 2
if starts[mid] <= offset {
low = mid
} else {
high = mid
}
}
(low + 1, offset - starts[low] + 1)
}
///|
/// Export every finding and the original source as SARIF 2.1.0. Paths must be
/// repository-relative; Windows separators are normalized and UTF-8 bytes are
/// percent-encoded. Invalid paths, Unicode, diagnostic metadata or spans return
/// Err instead of dropping findings or repairing externally constructed reports.
pub fn Report::to_sarif(
self : Report,
artifact_path : String,
) -> Result[Json, String] {
let uri = match sarif_uri(artifact_path) {
Ok(uri) => uri
Err(e) => return Err(e)
}
match validate_report(self, "SARIF") {
Ok(_) => ()
Err(e) => return Err(e)
}
let lines = sarif_lines(self.source)
let rules : Array[Json] = []
let codes : Map[String, Int32] = Map([])
let results : Array[Json] = []
for d in self.diagnostics {
let rule_index = match codes.get(d.code) {
Some(index) => index
None => {
let index = rules.length()
codes[d.code] = index
rules.push({ "id": d.code })
index
}
}
let (start_line, start_column) = sarif_position(lines, d.span.start)
let (end_line, end_column) = sarif_position(lines, d.span.end)
results.push({
"ruleId": d.code,
"ruleIndex": rule_index,
"level": if d.severity == Warning {
"warning"
} else {
"error"
},
"message": { "text": d.message + "\nHelp: " + d.help },
"locations": [
{
"physicalLocation": {
"artifactLocation": { "uri": uri, "index": 0 },
"region": {
"startLine": start_line,
"startColumn": start_column,
"endLine": end_line,
"endColumn": end_column,
},
},
},
],
"properties": {
"migrationLensSeverity": Json(d.severity),
"migrationLensSpan": Json(d.span),
"migrationLensMessage": d.message,
"migrationLensHelp": d.help,
},
})
}
Ok({
"$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "MigrationLens",
"informationUri": "https://github.com/cn-xjr/MigrationLens",
"rules": Json(rules),
},
},
"columnKind": "utf16CodeUnits",
"newlineSequences": ["\r\n", "\r", "\n"],
"artifacts": [
{
"location": { "uri": uri },
"contents": { "text": self.source },
"sourceLanguage": "sql",
},
],
"invocations": [
{ "executionSuccessful": self.complete, "exitCode": self.exit_code() },
],
"results": Json(results),
"properties": {
"migrationLensComplete": self.complete,
"migrationLensStatements": self.statements,
},
},
],
})
}