cuke-dedup 0.1.0

Static analysis for duplicate and reusable Cucumber step definitions
Documentation
use super::shared::{bounded_findings, convert_location, report_safe};
use super::ReportContext;
use crate::model::{stable_fingerprint, AnalysisResult, Severity, SourceLocation};
use anyhow::{Context, Result};
use std::path::Path;

/// Renders active findings as a SARIF 2.1.0 log without writing it to disk.
pub fn render_sarif(result: &AnalysisResult, root: &Path) -> Result<String> {
    render_sarif_with_threshold(result, root, 0.0)
}

/// Renders SARIF using the configured duplication threshold.
pub fn render_sarif_with_threshold(
    result: &AnalysisResult,
    root: &Path,
    threshold: f64,
) -> Result<String> {
    render_sarif_context(&ReportContext::new(result, root, threshold))
}

pub(super) fn render_sarif_context(context: &ReportContext<'_>) -> Result<String> {
    let result = context.result;
    let root = context.root;
    let selected = bounded_findings(result, true);
    let findings_truncated = selected.truncated;
    let rules = result
        .findings
        .iter()
        .filter(|finding| finding.is_active())
        .map(|finding| finding.rule)
        .collect::<std::collections::BTreeSet<_>>()
        .into_iter()
        .map(|rule| {
            serde_json::json!({
                "id": rule.to_string(),
                "name": rule.to_string(),
                "shortDescription": { "text": rule.to_string() },
                "helpUri": "https://github.com/figueiredoluiz/cuke-dedup#rules",
            })
        })
        .collect::<Vec<_>>();
    let findings = selected
        .findings
        .into_iter()
        .map(|finding| {
            let primary = sarif_location(&finding.primary, root, None);
            let related = finding
                .related
                .iter()
                .enumerate()
                .map(|(index, location)| sarif_location(location, root, Some(index + 1)))
                .collect::<Vec<_>>();
            let fingerprint_input = std::iter::once(&finding.primary)
                .chain(&finding.related)
                .map(|location| location.display(root))
                .collect::<Vec<_>>()
                .join("|");
            serde_json::json!({
                "ruleId": finding.rule.to_string(),
                "level": match finding.severity {
                    Severity::Error => "error",
                    Severity::Warning => "warning",
                    Severity::Off => "note",
                },
                "message": { "text": report_safe(&finding.message) },
                "locations": [primary],
                "relatedLocations": related,
                "partialFingerprints": {
                    "cukeDedupFingerprint/v1": stable_fingerprint(
                        &format!("{}|{fingerprint_input}", finding.rule)
                    )
                },
                "properties": {
                    "suggestedAction": report_safe(&finding.suggested_action),
                    "matcherSimilarity": finding.evidence.matcher_similarity,
                    "handlerSimilarity": finding.evidence.handler_similarity,
                }
            })
        })
        .collect::<Vec<_>>();
    let retained_findings = findings.len();
    let sarif = serde_json::json!({
        "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
        "version": "2.1.0",
        "runs": [{
            "automationDetails": { "id": "cuke-dedup/" },
            "columnKind": "unicodeCodePoints",
            "tool": {
                "driver": {
                    "name": "CukeDedup",
                    "semanticVersion": context.tool_version,
                    "informationUri": "https://github.com/figueiredoluiz/cuke-dedup",
                    "rules": rules,
                }
            },
            "invocations": [{
                "executionSuccessful": true,
                "properties": {
                    "threshold": context.threshold,
                    "duplicatedDefinitions": context.summary.duplication.duplicated_definitions,
                    "totalDefinitions": context.summary.duplication.total_definitions,
                    "duplicationPercentage": context.summary.duplication.percentage,
                    "thresholdPassed": context.summary.duplication.passed,
                    "contributingRules": context.summary.duplication.rules,
                    "activeFindings": context.summary.findings,
                    "suppressedFindings": context.summary.suppressed,
                    "retainedFindings": retained_findings,
                    "findingsTruncated": findings_truncated,
                }
            }],
            "results": findings,
        }]
    });
    serde_json::to_string_pretty(&sarif).context("failed to serialize SARIF report")
}

fn sarif_location(location: &SourceLocation, root: &Path, id: Option<usize>) -> serde_json::Value {
    let converted = convert_location(location, root);
    let mut value = serde_json::json!({
        "physicalLocation": {
            "artifactLocation": { "uri": converted.path },
            "region": {
                "startLine": converted.line,
                "startColumn": converted.column,
                "endLine": converted.end_line,
                "endColumn": converted.end_column,
            }
        }
    });
    if let Some(id) = id {
        value["id"] = serde_json::json!(id);
    }
    value
}