use super::*;
#[derive(Debug)]
pub(super) struct InlineFindingsSummary {
pub(super) status: String,
pub(super) findings_count: usize,
pub(super) dashboard_findings: Vec<DashboardFinding>,
}
pub(super) fn gate_class_for_check(status: crate::checks::CheckStatus) -> GateClass {
match status {
crate::checks::CheckStatus::Passed => GateClass::Pass,
crate::checks::CheckStatus::Skipped => GateClass::Skip,
crate::checks::CheckStatus::Failed | crate::checks::CheckStatus::Error => GateClass::Fail,
crate::checks::CheckStatus::Warnings => GateClass::Info,
}
}
pub(super) fn gate_class_to_str(class: GateClass) -> &'static str {
match class {
GateClass::Pass => "PASS",
GateClass::Skip => "SKIP",
GateClass::Fail => "FAIL",
GateClass::Info => "INFO",
}
}
pub(super) fn coverage_has_rust_inline_test_blind_spot(coverage: &CoverageDelta) -> bool {
coverage
.uncovered
.iter()
.any(|file| file.path.ends_with(".rs"))
}
pub(super) fn skipped_requested_security_review_caveats(
config: &Config,
checks: &[CheckResult],
skipped_checks: &[crate::checks::SkippedCheck],
) -> Vec<String> {
if !config.run_security {
return Vec::new();
}
let mut caveats: Vec<String> = skipped_checks
.iter()
.filter(|check| {
check.id == "cargo_geiger" || check.name.eq_ignore_ascii_case("cargo geiger")
})
.map(|check| format!("cargo geiger skipped for this run ({})", check.reason))
.collect();
for check in checks {
if check.name.eq_ignore_ascii_case("cargo geiger")
&& matches!(check.status, CheckStatus::Skipped)
{
let caveat = format!(
"cargo geiger skipped for this run ({})",
runtime_skip_reason(&check.output)
);
if !caveats.contains(&caveat) {
caveats.push(caveat);
}
}
}
caveats
}
pub(super) fn runtime_skip_reason(output: &str) -> String {
if output.contains("timed out") {
"timed out".to_string()
} else if output.contains("virtual manifest") {
"virtual manifest — configure -p <pkg>".to_string()
} else {
"skipped at runtime".to_string()
}
}
pub(super) fn policy_severity_to_str(level: PolicySeverity) -> &'static str {
match level {
PolicySeverity::Block => "block",
PolicySeverity::Warn => "warn",
PolicySeverity::Ignore => "ignore",
}
}
pub(super) fn classify_commit_type(message: &str) -> &'static str {
let lower = message.to_lowercase();
let first = lower.split(':').next().unwrap_or(&lower).trim();
let prefix = first.split('(').next().unwrap_or(first);
match prefix {
"feat" | "feature" => "feat",
"fix" | "bugfix" | "hotfix" => "fix",
"refactor" => "refactor",
"docs" | "doc" => "docs",
"test" | "tests" => "test",
"chore" | "build" | "ci" => "chore",
"style" | "fmt" => "style",
"perf" => "perf",
_ => "other",
}
}
pub(super) use crate::check_id::check_id_from_name;
pub(super) fn build_heuristics_gate_check(
config: &Config,
heuristics: Option<&HeuristicsResult>,
) -> (serde_json::Value, Option<String>) {
use serde_json::json;
let severity = config.policy.severity_for("heuristics_loctree");
let (status, class, dead, cycles) = if !config.run_heuristics {
("skipped", GateClass::Skip, 0usize, 0usize)
} else if let Some(h) = heuristics {
let dead = h.summary.dead_exports;
let cycles = h.summary.circular_imports;
if h.summary.total_files == 0 {
("skipped", GateClass::Skip, dead, cycles)
} else if dead > 0 || cycles > 0 {
("warnings", GateClass::Info, dead, cycles)
} else {
("passed", GateClass::Pass, dead, cycles)
}
} else {
("skipped", GateClass::Skip, 0usize, 0usize)
};
let blocking = config.policy.is_blocking(severity, class);
let blocking_issue = if blocking {
Some(format!(
"Loctree heuristics (dead_exports={}, cycles={})",
dead, cycles
))
} else {
None
};
let check = json!({
"id": "heuristics_loctree",
"name": "Loctree Heuristics",
"status": status,
"class": gate_class_to_str(class),
"severity": policy_severity_to_str(severity),
"blocking": blocking,
"duration_secs": 0.0,
"cached": false,
"evidence": "20_quality/heuristics_loctree.result.json",
"log": "20_quality/heuristics_loctree.log",
});
(check, blocking_issue)
}
pub(super) fn generate_inline_findings(
dir: &Path,
checks: &[CheckResult],
diffs: &[crate::git::Diff],
deps_delta: Option<&signal::DepsDelta>,
repo: Option<&crate::git::Repository>,
) -> Result<InlineFindingsSummary> {
use serde_json::json;
use sha2::{Digest, Sha256};
use std::collections::HashSet;
let sarif_path = dir.join("INLINE_FINDINGS.sarif");
let mut sarif_rules: Vec<serde_json::Value> = Vec::new();
let mut sarif_results: Vec<serde_json::Value> = Vec::new();
let mut known_sarif_rules = HashSet::new();
let mut dashboard_findings = Vec::new();
let mut error_count = 0usize;
let mut warning_count = 0usize;
let changed_files: HashSet<&str> = diffs
.iter()
.flat_map(|d| d.files.iter().map(|f| f.path.as_str()))
.collect();
let fingerprint = |rule_id: &str, file: &str, line: u32| -> String {
let mut hasher = Sha256::new();
hasher.update(format!("{}:{}:{}", rule_id, file, line));
format!("{:x}", hasher.finalize())
};
let is_in_diff = |file: &str| -> bool {
let stripped = file.trim_start_matches('/');
changed_files.contains(file)
|| changed_files.contains(stripped)
|| changed_files
.iter()
.any(|cf| file.ends_with(cf) || cf.ends_with(stripped))
};
struct ToolFindings {
source: &'static str,
tool_name: &'static str,
check_id: String,
findings: Vec<parsers::LintFinding>,
}
let mut tool_findings_sets: Vec<ToolFindings> = Vec::new();
for check in checks {
let class = gate_class_for_check(check.status);
if !matches!(class, GateClass::Fail | GateClass::Info) {
continue;
}
let check_id = check_id_from_name(&check.name);
match check_id.as_str() {
"eslint" => {
let parsed = parsers::eslint::parse_eslint_output(&check.output);
if !parsed.is_empty() {
tool_findings_sets.push(ToolFindings {
source: "eslint",
tool_name: "ESLint",
check_id: check_id.clone(),
findings: parsed,
});
continue;
}
}
"stylelint" => {
let parsed = parsers::stylelint::parse_stylelint_output(&check.output);
if !parsed.is_empty() {
tool_findings_sets.push(ToolFindings {
source: "stylelint",
tool_name: "Stylelint",
check_id: check_id.clone(),
findings: parsed,
});
continue;
}
}
"clippy" => {
let parsed = parsers::clippy::parse_clippy_short_output(&check.output);
if !parsed.is_empty() {
tool_findings_sets.push(ToolFindings {
source: "clippy",
tool_name: "Clippy",
check_id: check_id.clone(),
findings: parsed,
});
continue;
}
}
"cargo_test" => {
let parsed = parsers::cargo_test::parse_cargo_test_output(&check.output);
if !parsed.is_empty() {
tool_findings_sets.push(ToolFindings {
source: "cargo_test",
tool_name: "Cargo Test",
check_id: check_id.clone(),
findings: parsed,
});
continue;
}
}
"semgrep_scan" => {
let mut parsed = parsers::semgrep::parse_semgrep_json_output(&check.output);
if parsed.is_empty() {
parsed = parsers::semgrep::parse_semgrep_text(&check.output);
}
if !parsed.is_empty() {
tool_findings_sets.push(ToolFindings {
source: "semgrep",
tool_name: "Semgrep",
check_id: check_id.clone(),
findings: parsed,
});
}
continue;
}
_ => {}
}
if check.name.eq_ignore_ascii_case("cargo audit") {
let audit_findings = parse_cargo_audit_findings(&check.output);
if !audit_findings.is_empty() {
let location = cargo_audit_location_for_check(check);
let _audit_in_diff_fallback = is_in_diff("Cargo.lock");
let base_audit_cache = get_base_cargo_audit_findings(repo, diffs);
for finding in &audit_findings {
match finding.sarif_level {
"error" => error_count += 1,
"warning" => warning_count += 1,
_ => {}
}
if known_sarif_rules.insert(finding.advisory_id.clone()) {
sarif_rules.push(json!({
"id": finding.advisory_id,
"name": "cargo audit advisory",
"shortDescription": { "text": finding.title },
"helpUri": finding.help_url,
"defaultConfiguration": {
"level": finding.sarif_level
},
"properties": {
"package": finding.package_display(),
"severity": finding.severity,
"patched_versions": finding.patched_versions,
}
}));
}
let current_audit_in_diff = if let Some(cache) = &base_audit_cache {
!cache
.contains(&(finding.advisory_id.clone(), finding.package_name.clone()))
} else if let Some(deps) = deps_delta {
deps.added.contains(&finding.package_name)
|| deps.changed.contains(&finding.package_name)
} else {
_audit_in_diff_fallback
};
dashboard_findings.push(DashboardFinding {
level: finding.sarif_level,
check_name: check.name.clone(),
check_id: check_id.clone(),
message: finding.sarif_message(),
in_diff: Some(current_audit_in_diff),
});
sarif_results.push(json!({
"ruleId": finding.advisory_id,
"level": finding.sarif_level,
"message": { "text": finding.sarif_message() },
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": &location },
"region": { "startLine": 1 }
}
}],
"partialFingerprints": {
"primaryLocationLineHash": fingerprint(
&finding.advisory_id,
&location,
1,
)
},
"properties": {
"check": "cargo_audit",
"in_diff": current_audit_in_diff,
"package": finding.package_display(),
"severity": finding.severity,
}
}));
}
continue;
}
}
let level = if matches!(class, GateClass::Fail) {
error_count += 1;
"error"
} else {
warning_count += 1;
"warning"
};
let is_geiger = check_id == "cargo_geiger";
let first_line = check
.output
.lines()
.find(|line| !should_skip_inline_fallback_line(is_geiger, line))
.unwrap_or("No details provided");
let extracted = extract_file_line_from_output(&check.output);
let sarif_location = if let Some((ref file, line_num)) = extracted {
let in_diff_val = is_in_diff(file);
dashboard_findings.push(DashboardFinding {
level,
check_name: check.name.clone(),
check_id: check_id.clone(),
message: first_line.to_string(),
in_diff: Some(in_diff_val),
});
json!({
"physicalLocation": {
"artifactLocation": { "uri": file },
"region": { "startLine": line_num }
}
})
} else {
dashboard_findings.push(DashboardFinding {
level,
check_name: check.name.clone(),
check_id: check_id.clone(),
message: first_line.to_string(),
in_diff: None,
});
json!({
"physicalLocation": {
"artifactLocation": { "uri": "20_quality/full-checks.log" }
}
})
};
let rule_id = format!("prview.{}", check_id_from_name(&check.name));
if known_sarif_rules.insert(rule_id.clone()) {
sarif_rules.push(json!({
"id": rule_id,
"shortDescription": { "text": check.name },
"defaultConfiguration": { "level": level }
}));
}
sarif_results.push(json!({
"ruleId": rule_id,
"level": level,
"message": { "text": format!("{}: {}", check.name, first_line) },
"locations": [sarif_location]
}));
}
for tool_set in &tool_findings_sets {
let mut filtered_generated = 0usize;
let mut in_diff_count = 0usize;
let mut preexisting_count = 0usize;
let mut emitted_count = 0usize;
for finding in &tool_set.findings {
if parsers::is_generated_path(&finding.file) {
filtered_generated += 1;
continue;
}
match finding.level {
"error" => error_count += 1,
"warning" => warning_count += 1,
_ => {}
}
let rule_id = finding
.rule_id
.clone()
.unwrap_or_else(|| format!("prview.{}", tool_set.source));
if known_sarif_rules.insert(rule_id.clone()) {
sarif_rules.push(json!({
"id": rule_id,
"shortDescription": { "text": tool_set.tool_name },
"defaultConfiguration": { "level": finding.level }
}));
}
let in_diff = is_in_diff(&finding.file);
if in_diff {
in_diff_count += 1;
} else {
preexisting_count += 1;
}
let classification = if in_diff { "introduced" } else { "preexisting" };
dashboard_findings.push(DashboardFinding {
level: finding.level,
check_name: tool_set.tool_name.to_string(),
check_id: tool_set.check_id.clone(),
message: finding.message.clone(),
in_diff: Some(in_diff),
});
let mut location = json!({
"physicalLocation": {
"artifactLocation": { "uri": &finding.file },
"region": { "startLine": finding.line }
}
});
if let Some(col) = finding.column {
location["physicalLocation"]["region"]["startColumn"] = json!(col);
}
sarif_results.push(json!({
"ruleId": rule_id,
"level": finding.level,
"message": { "text": &finding.message },
"locations": [location],
"partialFingerprints": {
"primaryLocationLineHash": fingerprint(
&rule_id,
&finding.file,
finding.line,
)
},
"properties": {
"in_diff": in_diff,
"classification": classification,
"source": tool_set.source,
}
}));
emitted_count += 1;
}
if emitted_count > 0 {
sarif_rules.push(json!({
"id": format!("prview.summary.{}", tool_set.source),
"shortDescription": { "text": format!("{} summary", tool_set.tool_name) },
"properties": {
"total_findings": tool_set.findings.len(),
"filtered_generated": filtered_generated,
"in_diff_count": in_diff_count,
"introduced_count": in_diff_count,
"preexisting_count": preexisting_count,
}
}));
}
}
let runs = if sarif_results.is_empty() {
Vec::new()
} else {
vec![json!({
"tool": {
"driver": {
"name": "prview-inline",
"version": "1.0.0",
"informationUri": "https://github.com/vetcoders/prview",
"rules": sarif_rules
}
},
"invocations": [{
"executionSuccessful": true,
"properties": {
"total_findings": sarif_results.len(),
}
}],
"results": sarif_results
})]
};
let sarif = json!({
"version": "2.1.0",
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"runs": runs
});
if !runs.is_empty() {
fs::write(&sarif_path, serde_json::to_string_pretty(&sarif)?)?;
}
let status = if error_count > 0 {
"failed"
} else if warning_count > 0 {
"warnings"
} else {
"passed"
}
.to_string();
Ok(InlineFindingsSummary {
status,
findings_count: error_count + warning_count,
dashboard_findings,
})
}
pub(super) fn is_pathish_candidate(candidate: &str) -> bool {
if !(candidate.contains('/') || candidate.contains('\\')) {
return false;
}
const CODE_CHARS: &[char] = &[
'{', '}', '"', '=', '(', ')', ';', ',', '\'', '`', '*', '<', '>', '[', ']',
];
if candidate.contains(CODE_CHARS) || candidate.chars().any(char::is_whitespace) {
return false;
}
true
}
pub(super) fn extract_file_line_from_output(output: &str) -> Option<(String, u32)> {
for line in output.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("-->") {
let rest = rest.trim();
if let Some((file, line_col)) = rest.rsplit_once(':') {
if let Some((file2, line_str)) = file.rsplit_once(':')
&& let Ok(ln) = line_str.parse::<u32>()
&& !file2.is_empty()
&& ln > 0
{
return Some((file2.to_string(), ln));
}
if let Ok(ln) = line_col.parse::<u32>()
&& !file.is_empty()
&& ln > 0
{
return Some((file.to_string(), ln));
}
}
continue;
}
let search_start = if trimmed.len() >= 3
&& trimmed.as_bytes()[0].is_ascii_alphabetic()
&& trimmed.as_bytes()[1] == b':'
&& (trimmed.as_bytes()[2] == b'\\' || trimmed.as_bytes()[2] == b'/')
{
2 } else {
0
};
if let Some(rel_idx) = trimmed[search_start..].find(':') {
let colon_idx = search_start + rel_idx;
let candidate = &trimmed[..colon_idx];
if is_pathish_candidate(candidate) {
let rest = &trimmed[colon_idx + 1..];
let line_str: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if let Ok(ln) = line_str.parse::<u32>()
&& ln > 0
{
return Some((candidate.to_string(), ln));
}
}
}
if let Some(paren_idx) = trimmed.find('(') {
let candidate = &trimmed[..paren_idx];
if candidate.contains('/') || candidate.contains('\\') {
let rest = &trimmed[paren_idx + 1..];
let line_str: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if let Ok(ln) = line_str.parse::<u32>()
&& ln > 0
{
return Some((candidate.to_string(), ln));
}
}
}
}
None
}
pub(super) fn should_skip_inline_fallback_line(is_geiger: bool, line: &str) -> bool {
let trimmed = line.trim();
if trimmed.is_empty() {
return true;
}
if !is_geiger {
return false;
}
trimmed.starts_with("Metric output format:")
|| trimmed.contains("WARNING: Dependency file was never scanned")
|| (trimmed.chars().next().is_some_and(|c| c.is_ascii_digit())
&& trimmed.contains('/')
&& trimmed.contains("unsafe"))
}