Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions crates/diffguard-core/src/check/annotations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use diffguard_types::Finding;

pub(super) fn render_annotations(findings: &[Finding]) -> Vec<String> {
findings
.iter()
.map(|f| {
let level = match f.severity {
diffguard_types::Severity::Info => "notice",
diffguard_types::Severity::Warn => "warning",
diffguard_types::Severity::Error => "error",
};
format!(
"::{level} file={path},line={line}::{rule} {msg}",
level = level,
path = f.path,
line = f.line,
rule = f.rule_id,
msg = f.message
)
})
.collect()
}

#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;

fn test_finding(severity: diffguard_types::Severity) -> Finding {
Finding {
rule_id: "test.rule".to_string(),
severity,
message: "Test message".to_string(),
path: "src/lib.rs".to_string(),
line: 42,
column: Some(3),
match_text: "match".to_string(),
snippet: "let x = match;".to_string(),
}
}

proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]

#[test]
fn property_annotations_format_matches_expected(
severity in prop_oneof![Just(diffguard_types::Severity::Info), Just(diffguard_types::Severity::Warn), Just(diffguard_types::Severity::Error)],
line in 1u32..1000,
) {
let mut finding = test_finding(severity);
finding.line = line;

let annotations = render_annotations(&[finding.clone()]);
prop_assert_eq!(annotations.len(), 1);

let level = match severity {
diffguard_types::Severity::Info => "notice",
diffguard_types::Severity::Warn => "warning",
diffguard_types::Severity::Error => "error",
};

let expected = format!(
"::{level} file={path},line={line}::{rule} {msg}",
level = level,
path = finding.path,
line = finding.line,
rule = finding.rule_id,
msg = finding.message
);

prop_assert_eq!(annotations[0].as_str(), expected.as_str());
}
}
}
33 changes: 33 additions & 0 deletions crates/diffguard-core/src/check/diff_prep.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use std::collections::BTreeSet;
use std::path::Path;

use diffguard_diff::{DiffLine, parse_unified_diff};

use super::CheckPlan;
use super::path_filter::compile_filter_globs;

/// Parse the unified diff and apply secondary filters: path globs, allowed-lines, dedup.
///
/// Returns the filtered, deduplicated list of diff lines ready for evaluation.
pub(super) fn prepare_diff_lines(
plan: &CheckPlan,
diff_text: &str,
) -> Result<Vec<DiffLine>, anyhow::Error> {
let (mut diff_lines, _stats) = parse_unified_diff(diff_text, plan.scope)?;

if !plan.path_filters.is_empty() {
let filters = compile_filter_globs(&plan.path_filters)?;
diff_lines.retain(|l| filters.is_match(Path::new(&l.path)));
}

if let Some(allowed_lines) = &plan.allowed_lines {
diff_lines.retain(|l| allowed_lines.contains(&(l.path.clone(), l.line)));
}

// Multiple diff sources (or unusual diffs) can contain duplicates for the same
// path/line/content tuple. Keep first occurrence to preserve deterministic ordering.
let mut seen = BTreeSet::<(String, u32, String)>::new();
diff_lines.retain(|l| seen.insert((l.path.clone(), l.line, l.content.clone())));

Ok(diff_lines)
}
62 changes: 62 additions & 0 deletions crates/diffguard-core/src/check/false_positive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use std::collections::{BTreeMap, BTreeSet};

use diffguard_types::{Finding, VerdictCounts};

use crate::fingerprint::compute_fingerprint;

/// Per-rule tally of false-positive filtering: (total_filtered, info, warn, error).
pub(super) type PerRuleFalsePositive = BTreeMap<String, (u32, u32, u32, u32)>;

pub(super) struct FalsePositiveOutcome {
pub kept_findings: Vec<Finding>,
pub adjusted_counts: VerdictCounts,
pub false_positive_findings: u32,
pub per_rule: PerRuleFalsePositive,
}

/// Drop findings whose fingerprints appear in `false_positive_fingerprints`, adjusting
/// verdict counts and recording per-rule false-positive tallies for analytics.
pub(super) fn apply_false_positive_filter(
findings: Vec<Finding>,
initial_counts: &VerdictCounts,
false_positive_fingerprints: &BTreeSet<String>,
) -> FalsePositiveOutcome {
let mut kept = Vec::with_capacity(findings.len());
let mut adjusted = initial_counts.clone();
let mut total_fp = 0u32;
let mut per_rule = PerRuleFalsePositive::new();

for finding in findings {
let fingerprint = compute_fingerprint(&finding);
if false_positive_fingerprints.contains(&fingerprint) {
total_fp = total_fp.saturating_add(1);
let entry = per_rule
.entry(finding.rule_id.clone())
.or_insert((0, 0, 0, 0));
entry.0 = entry.0.saturating_add(1);
match finding.severity {
diffguard_types::Severity::Info => {
adjusted.info = adjusted.info.saturating_sub(1);
entry.1 = entry.1.saturating_add(1);
}
diffguard_types::Severity::Warn => {
adjusted.warn = adjusted.warn.saturating_sub(1);
entry.2 = entry.2.saturating_add(1);
}
diffguard_types::Severity::Error => {
adjusted.error = adjusted.error.saturating_sub(1);
entry.3 = entry.3.saturating_add(1);
}
}
continue;
}
kept.push(finding);
}

FalsePositiveOutcome {
kept_findings: kept,
adjusted_counts: adjusted,
false_positive_findings: total_fp,
per_rule,
}
}
Loading