///|
/// Deterministic text, JSON, Markdown and SARIF renderers.
fn json_escape(value : String) -> String {
let out = StringBuilder()
for c in value {
match c {
'"' => out.write_string("\\\"")
'\\' => out.write_string("\\\\")
'\n' => out.write_string("\\n")
'\r' => out.write_string("\\r")
'\t' => out.write_string("\\t")
_ => out.write_char(c)
}
}
out.to_string()
}
///|
fn json_string(value : String) -> String {
"\"" + json_escape(value) + "\""
}
///|
fn finding_json(finding : Finding) -> String {
"{" +
"\"code\":" +
json_string(finding.code) +
"," +
"\"kind\":" +
json_string(finding.kind.name()) +
"," +
"\"severity\":" +
json_string(finding.severity.name()) +
"," +
"\"message\":" +
json_string(finding.message) +
"," +
"\"primary\":" +
json_string(finding.primary_id) +
"," +
"\"secondary\":" +
json_string(finding.secondary_id) +
"," +
"\"shortcut\":" +
json_string(finding.shortcut) +
"," +
"\"context\":" +
json_string(finding.context) +
"," +
"\"source\":" +
json_string(finding.source) +
"," +
"\"line\":" +
finding.line.to_string() +
"," +
"\"suggestion\":" +
json_string(finding.suggestion) +
"}"
}
///|
/// Serialize an analysis without relying on a third-party JSON package.
pub fn analysis_to_json(analysis : Analysis) -> String {
let findings : Array[String] = []
for item in analysis.findings {
findings.push(finding_json(item))
}
let ok = if analysis.ok() { "true" } else { "false" }
"{" +
"\"keymap\":" +
json_string(analysis.keymap_name) +
"," +
"\"checked_bindings\":" +
analysis.checked_bindings.to_string() +
"," +
"\"enabled_bindings\":" +
analysis.enabled_bindings.to_string() +
"," +
"\"errors\":" +
analysis.error_count.to_string() +
"," +
"\"warnings\":" +
analysis.warning_count.to_string() +
"," +
"\"infos\":" +
analysis.info_count.to_string() +
"," +
"\"score\":" +
analysis.score.to_string() +
"," +
"\"fingerprint\":" +
json_string(analysis.fingerprint) +
"," +
"\"ok\":" +
ok +
"," +
"\"findings\":[" +
findings.join(",") +
"]" +
"}"
}
///|
/// Serialize parser diagnostics for CI consumers.
pub fn parse_diagnostics_to_json(
diagnostics : Array[ParseDiagnostic],
) -> String {
let rows : Array[String] = []
for diagnostic in diagnostics {
rows.push(
"{" +
"\"line\":" +
diagnostic.line.to_string() +
"," +
"\"code\":" +
json_string(diagnostic.code) +
"," +
"\"message\":" +
json_string(diagnostic.message) +
"," +
"\"source\":" +
json_string(diagnostic.source) +
"}",
)
}
"[" + rows.join(",") + "]"
}
///|
/// Human-readable report suitable for a pull request comment.
pub fn analysis_to_text(analysis : Analysis) -> String {
let lines : Array[String] = [
"MoonKeyguard analysis",
"======================",
analysis.summary(),
]
if analysis.findings.length() == 0 {
lines.push("No findings. The keymap is internally consistent.")
} else {
lines.push("")
for item in analysis.findings {
lines.push(item.to_line())
if item.suggestion.length() > 0 {
lines.push(" suggestion: " + item.suggestion)
}
}
}
lines.join("\n")
}
///|
/// Render a concise Markdown table for documentation and release notes.
pub fn analysis_to_markdown(analysis : Analysis) -> String {
let lines : Array[String] = [
"## MoonKeyguard report",
"",
"| Metric | Value |",
"| --- | ---: |",
"| Keymap | " + analysis.keymap_name + " |",
"| Checked bindings | " + analysis.checked_bindings.to_string() + " |",
"| Enabled bindings | " + analysis.enabled_bindings.to_string() + " |",
"| Errors | " + analysis.error_count.to_string() + " |",
"| Warnings | " + analysis.warning_count.to_string() + " |",
"| Score | " + analysis.score.to_string() + "/100 |",
"| Fingerprint | `" + analysis.fingerprint + "` |",
"",
"| Severity | Code | Shortcut | Context | Message |",
"| --- | --- | --- | --- | --- |",
]
for item in analysis.findings {
lines.push(
"| " +
item.severity.name() +
" | " +
item.code +
" | `" +
item.shortcut +
"` | `" +
item.context +
"` | " +
item.message +
" |",
)
}
lines.join("\n")
}
///|
fn sarif_level(severity : Severity) -> String {
match severity {
Error => "error"
Warning => "warning"
Info => "note"
}
}
///|
fn finding_sarif(finding : Finding) -> String {
let location = if finding.source.length() == 0 {
""
} else {
",\"locations\":[{\"physicalLocation\":{\"artifactLocation\":{\"uri\":" +
json_string(finding.source) +
"},\"region\":{\"startLine\":" +
finding.line.to_string() +
"}}}]"
}
"{" +
"\"ruleId\":" +
json_string(finding.code) +
"," +
"\"level\":" +
json_string(sarif_level(finding.severity)) +
"," +
"\"message\":{\"text\":" +
json_string(finding.message) +
"}" +
location +
"}"
}
///|
/// SARIF 2.1.0 output lets GitHub annotate keymap files in CI.
pub fn analysis_to_sarif(analysis : Analysis) -> String {
let results : Array[String] = []
for item in analysis.findings {
results.push(finding_sarif(item))
}
"{\"version\":\"2.1.0\",\"$schema\":\"https://json.schemastore.org/sarif-2.1.0.json\",\"runs\":[{\"tool\":{\"driver\":{\"name\":\"MoonKeyguard\",\"version\":\"0.2.0\"}},\"results\":[" +
results.join(",") +
"]}]}"
}
///|
/// Return a conventional process exit code for a CI gate.
pub fn analysis_exit_code(
analysis : Analysis,
fail_on_warning? : Bool = false,
) -> Int {
if analysis.error_count > 0 || (fail_on_warning && analysis.warning_count > 0) {
1
} else {
0
}
}