weavatrix-clone 0.1.3

Evidence-first Type-1/2/3 code clone engine with deterministic families and bounded near-miss verification
Documentation
use weavatrix_clone::{
    CloneConfig, CloneDetector, CloneError, CloneKind, DetectionMode, Language, Similarity,
    SourceFragment, SourceSpan,
};

#[test]
fn classifies_exact_renamed_and_near_miss_clones() {
    let exact = function("exact:a", "a.rs", "value", "result", "");
    let exact_copy = function("exact:b", "b.rs", "value", "result", "");
    let renamed = function("renamed", "c.rs", "input", "output", "");
    let near_miss = function(
        "near",
        "d.rs",
        "item",
        "answer",
        "let audit = answer.is_positive();",
    );

    let report = CloneDetector::default()
        .detect(&[exact, exact_copy, renamed, near_miss])
        .unwrap();

    assert!(
        report
            .pairs
            .iter()
            .any(|pair| pair.kind == CloneKind::Type1)
    );
    assert!(
        report
            .pairs
            .iter()
            .any(|pair| pair.kind == CloneKind::Type2)
    );
    assert!(
        report
            .pairs
            .iter()
            .any(|pair| pair.kind == CloneKind::Type3)
    );
    assert_eq!(report.statistics.verified_pairs, report.pairs.len());
    assert_eq!(report.families.len(), 1);
}

#[test]
fn exact_mode_excludes_identifier_and_near_miss_matches() {
    let exact = function("exact:a", "a.rs", "value", "result", "");
    let exact_copy = function("exact:b", "b.rs", "value", "result", "");
    let renamed = function("renamed", "c.rs", "input", "output", "");
    let detector = CloneDetector::new(CloneConfig {
        mode: DetectionMode::Exact,
        ..CloneConfig::default()
    })
    .unwrap();
    let report = detector.detect(&[exact, exact_copy, renamed]).unwrap();
    assert_eq!(report.pairs.len(), 1);
    assert_eq!(report.pairs[0].kind, CloneKind::Type1);
}

#[test]
fn preserves_behavior_defining_numbers_and_rejects_shape_only_matches() {
    let left = numeric_table("left", "left.rs", 0);
    let right = numeric_table("right", "right.rs", 10_000);
    let report = CloneDetector::default().detect(&[left, right]).unwrap();
    assert!(report.pairs.is_empty());
}

#[test]
fn output_is_stable_under_input_reordering() {
    let fragments = vec![
        function("a", "z.rs", "value", "result", ""),
        function("b", "a.rs", "item", "answer", ""),
        function("c", "m.rs", "source", "target", ""),
    ];
    let forward = CloneDetector::default().detect(&fragments).unwrap();
    let reverse = CloneDetector::default()
        .detect(&fragments.into_iter().rev().collect::<Vec<_>>())
        .unwrap();
    assert_eq!(forward, reverse);
}

#[test]
fn overlapping_fragments_are_suppressed_by_default() {
    let text = body("value", "result", "");
    let left = SourceFragment::new(
        "left",
        "same.rs",
        Language::Rust,
        SourceSpan {
            start_byte: 0,
            end_byte: text.len(),
            start_line: 1,
            end_line: 10,
        },
        &text,
    )
    .unwrap();
    let right = SourceFragment::new(
        "right",
        "same.rs",
        Language::Rust,
        SourceSpan {
            start_byte: 10,
            end_byte: text.len() + 10,
            start_line: 2,
            end_line: 11,
        },
        &text,
    )
    .unwrap();
    assert!(
        CloneDetector::default()
            .detect(&[left.clone(), right.clone()])
            .unwrap()
            .pairs
            .is_empty()
    );
    let detector = CloneDetector::new(CloneConfig {
        compare_overlapping_fragments: true,
        ..CloneConfig::default()
    })
    .unwrap();
    assert_eq!(detector.detect(&[left, right]).unwrap().pairs.len(), 1);
}

#[test]
fn invalid_configuration_and_duplicate_ids_are_explicit() {
    assert!(matches!(
        CloneDetector::new(CloneConfig {
            candidate_similarity: Similarity::from_permille(900),
            min_similarity: Similarity::from_permille(800),
            ..CloneConfig::default()
        }),
        Err(CloneError::InvalidConfig { .. })
    ));
    let fragment = function("same", "a.rs", "value", "result", "");
    assert!(matches!(
        CloneDetector::default().detect(&[fragment.clone(), fragment]),
        Err(CloneError::DuplicateFragment(_))
    ));
}

fn function(id: &str, path: &str, input: &str, output: &str, extra: &str) -> SourceFragment {
    let text = body(input, output, extra);
    SourceFragment::new(
        id,
        path,
        Language::Rust,
        SourceSpan {
            start_byte: 0,
            end_byte: text.len(),
            start_line: 1,
            end_line: 10,
        },
        text,
    )
    .unwrap()
}

fn body(input: &str, output: &str, extra: &str) -> String {
    format!(
        "fn calculate({input}: i64) -> i64 {{\n\
         let baseline = {input} * 31;\n\
         let adjusted = baseline + 7;\n\
         let {output} = if adjusted > 100 {{ adjusted / 2 }} else {{ adjusted * 2 }};\n\
         {extra}\n\
         let checked = {output}.saturating_add(baseline);\n\
         let bounded = checked.clamp(0, 10000);\n\
         bounded + adjusted + baseline\n\
         }}\n"
    )
}

fn numeric_table(id: &str, path: &str, base: usize) -> SourceFragment {
    let rows = (0..30)
        .map(|index| format!("let value_{index} = input + {};", base + index))
        .collect::<Vec<_>>()
        .join("\n");
    let text = format!("fn table(input: usize) -> usize {{\n{rows}\ninput\n}}\n");
    SourceFragment::new(
        id,
        path,
        Language::Rust,
        SourceSpan {
            start_byte: 0,
            end_byte: text.len(),
            start_line: 1,
            end_line: 33,
        },
        text,
    )
    .unwrap()
}