use crate::catalogue::{self, RuleMeta};
use crate::config::ToolConfig;
use crate::report::{ComplianceReport, Severity, Violation};
use serde_json::{json, Value};
use std::path::{Component, Path, PathBuf};
const PARSE_RULE_ID: &str = "JSS-PARSE-000";
const INFORMATION_URI: &str = "https://github.com/kollerma/jss-style-checker";
const SCHEMA_URI: &str = "https://json.schemastore.org/sarif-2.1.0.json";
fn sarif_level(severity: Severity) -> &'static str {
match severity {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Info => "note",
}
}
fn normalize_lexically(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
for comp in p.components() {
match comp {
Component::ParentDir => {
out.pop();
}
Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
fn resolve_path(p: &Path) -> PathBuf {
if let Ok(c) = std::fs::canonicalize(p) {
return c;
}
let abs = if p.is_absolute() {
p.to_path_buf()
} else {
std::env::current_dir().unwrap_or_default().join(p)
};
let mut tail: Vec<std::ffi::OsString> = Vec::new();
let mut cur = abs.as_path();
loop {
if let Ok(base) = std::fs::canonicalize(cur) {
let mut out = base;
for c in tail.iter().rev() {
out.push(c);
}
return out;
}
match (cur.parent(), cur.file_name()) {
(Some(parent), Some(name)) => {
tail.push(name.to_os_string());
cur = parent;
}
_ => return normalize_lexically(&abs),
}
}
}
fn relative_path(target: &Path, base: &Path) -> PathBuf {
let target_comps: Vec<Component> = target.components().collect();
let base_comps: Vec<Component> = base.components().collect();
let mut common = 0;
while common < target_comps.len()
&& common < base_comps.len()
&& target_comps[common] == base_comps[common]
{
common += 1;
}
let mut result = PathBuf::new();
for _ in common..base_comps.len() {
result.push("..");
}
for comp in &target_comps[common..] {
result.push(comp.as_os_str());
}
if result.as_os_str().is_empty() {
result.push(".");
}
result
}
fn as_posix(p: &Path) -> String {
p.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/")
}
fn relativise(file: &str, source_root: &Path) -> String {
let file_path = Path::new(file);
let abs_file = if file_path.is_absolute() {
resolve_path(file_path)
} else {
resolve_path(&source_root.join(file_path))
};
let abs_root = resolve_path(source_root);
as_posix(&relative_path(&abs_file, &abs_root))
}
fn location_value(v: &Violation, source_root: &Path) -> Value {
let mut region = serde_json::Map::new();
region.insert("startLine".to_string(), json!(v.line));
if let Some(col) = v.column {
region.insert("startColumn".to_string(), json!(col));
}
json!({
"physicalLocation": {
"artifactLocation": {"uri": relativise(&v.file, source_root)},
"region": region,
}
})
}
fn result_value(v: &Violation, source_root: &Path) -> Value {
let mut obj = serde_json::Map::new();
obj.insert("ruleId".to_string(), json!(v.rule_id));
obj.insert("level".to_string(), json!(sarif_level(v.severity)));
obj.insert("message".to_string(), json!({"text": v.message}));
obj.insert(
"locations".to_string(),
json!([location_value(v, source_root)]),
);
if let Some(fix) = &v.fix {
obj.insert(
"fixes".to_string(),
json!([{
"description": {"text": fix.description},
"artifactChanges": [{
"artifactLocation": {"uri": relativise(&v.file, source_root)},
"replacements": [{
"deletedRegion": {
"byteOffset": fix.start,
"byteLength": fix.end - fix.start,
},
"insertedContent": {"text": fix.replacement},
}],
}],
}]),
);
}
Value::Object(obj)
}
fn notification_value(v: &Violation, source_root: &Path) -> Value {
json!({
"descriptor": {"id": PARSE_RULE_ID},
"level": "error",
"message": {"text": v.message},
"locations": [location_value(v, source_root)],
})
}
fn rule_descriptor(id: &str, meta: &RuleMeta) -> Value {
let short_text = if !meta.guide_section.is_empty() && meta.guide_section != "internal" {
format!("{} ({})", meta.message_template, meta.guide_section)
} else {
meta.message_template.to_string()
};
let mut obj = serde_json::Map::new();
obj.insert("id".to_string(), json!(id));
obj.insert("name".to_string(), json!(id));
obj.insert("shortDescription".to_string(), json!({"text": short_text}));
obj.insert(
"fullDescription".to_string(),
json!({"text": meta.message_template}),
);
obj.insert(
"defaultConfiguration".to_string(),
json!({"level": sarif_level(meta.severity)}),
);
obj.insert("properties".to_string(), json!({"tags": [meta.category]}));
if let Some(url) = meta.guide_url {
obj.insert("helpUri".to_string(), json!(url));
}
Value::Object(obj)
}
fn internal_parse_rule_descriptor() -> Value {
json!({
"id": PARSE_RULE_ID,
"name": PARSE_RULE_ID,
"shortDescription": {"text": "Parser failed to process the input file."},
"fullDescription": {
"text": "Emitted when the parser could not analyse a file. The file is reported under runs[0].invocations[0].toolExecutionNotifications rather than runs[0].results."
},
"defaultConfiguration": {"level": "error"},
"properties": {"tags": ["parse"]},
})
}
fn catalogue_rules() -> Vec<Value> {
let mut rules: Vec<Value> = catalogue::all_rules()
.iter()
.map(|m| rule_descriptor(m.rule_id, m))
.collect();
rules.push(internal_parse_rule_descriptor());
rules.sort_by(|a, b| a["id"].as_str().unwrap().cmp(b["id"].as_str().unwrap()));
rules
}
fn violation_sort_key(v: &Violation) -> (String, u32, u32, String) {
(
v.file.clone(),
v.line,
v.column.unwrap_or(0),
v.rule_id.clone(),
)
}
pub fn to_payload(report: &ComplianceReport, config: &ToolConfig) -> Value {
let source_root = config.source_root.as_path();
let mut parse_failures: Vec<&Violation> = Vec::new();
let mut other_violations: Vec<&Violation> = Vec::new();
for v in &report.violations {
if v.rule_id == PARSE_RULE_ID {
parse_failures.push(v);
} else {
other_violations.push(v);
}
}
parse_failures.sort_by_key(|v| violation_sort_key(v));
other_violations.sort_by_key(|v| violation_sort_key(v));
let results: Vec<Value> = other_violations
.iter()
.map(|v| result_value(v, source_root))
.collect();
let notifications: Vec<Value> = parse_failures
.iter()
.map(|v| notification_value(v, source_root))
.collect();
json!({
"$schema": SCHEMA_URI,
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "jss-lint",
"version": report.tool_version,
"informationUri": INFORMATION_URI,
"rules": catalogue_rules(),
}
},
"invocations": [{
"executionSuccessful": true,
"toolExecutionNotifications": notifications,
}],
"results": results,
}],
})
}
pub fn render(report: &ComplianceReport, config: &ToolConfig) -> String {
let payload = to_payload(report, config);
let mut out = String::new();
crate::json_output::write_value(&payload, 0, &mut out);
out.push('\n');
out
}