cuke-dedup 0.1.0

Static analysis for duplicate and reusable Cucumber step definitions
Documentation
use crate::model::{
    stable_fingerprint, DefinitionComparison, MatcherDiff, MatcherKind, StepDefinition,
};

pub(super) fn matcher_difference(left: &StepDefinition, right: &StepDefinition) -> String {
    format!("`{}` ↔ `{}`", left.matcher, right.matcher)
}

pub(super) fn definition_comparison(
    left: &StepDefinition,
    right: &StepDefinition,
) -> DefinitionComparison {
    DefinitionComparison {
        left_fingerprint: definition_semantic_fingerprint(left),
        right_fingerprint: definition_semantic_fingerprint(right),
        left_matcher: left.matcher.clone(),
        right_matcher: right.matcher.clone(),
        left_handler: left.handler.source_snippet.clone(),
        right_handler: right.handler.source_snippet.clone(),
        matcher_diff: matcher_diff(&left.matcher, &right.matcher),
    }
}

fn definition_semantic_fingerprint(definition: &StepDefinition) -> String {
    let matcher_kind = match definition.matcher_kind {
        MatcherKind::CucumberExpression => "cucumber-expression",
        MatcherKind::RegularExpression => "regular-expression",
    };
    stable_fingerprint(&format!(
        "{matcher_kind}\u{0}{}\u{0}{}\u{0}{}",
        definition.normalized_matcher,
        definition.matcher_flags,
        definition.handler.alpha_normalized
    ))
}

fn matcher_diff(left: &str, right: &str) -> MatcherDiff {
    let left_chars: Vec<_> = left.chars().collect();
    let right_chars: Vec<_> = right.chars().collect();
    let prefix_len = left_chars
        .iter()
        .zip(&right_chars)
        .take_while(|(left, right)| left == right)
        .count();
    let max_suffix = left_chars
        .len()
        .saturating_sub(prefix_len)
        .min(right_chars.len().saturating_sub(prefix_len));
    let suffix_len = left_chars
        .iter()
        .rev()
        .zip(right_chars.iter().rev())
        .take(max_suffix)
        .take_while(|(left, right)| left == right)
        .count();

    MatcherDiff {
        prefix: left_chars[..prefix_len].iter().collect(),
        left_change: left_chars[prefix_len..left_chars.len() - suffix_len]
            .iter()
            .collect(),
        right_change: right_chars[prefix_len..right_chars.len() - suffix_len]
            .iter()
            .collect(),
        suffix: left_chars[left_chars.len() - suffix_len..].iter().collect(),
    }
}

pub(super) fn handler_evidence(left: &StepDefinition, right: &StepDefinition) -> String {
    if left.handler.exact == right.handler.exact {
        "Handlers have the same exact syntax fingerprint".to_owned()
    } else if left.handler.normalized == right.handler.normalized {
        "Handlers differ only in comments or formatting".to_owned()
    } else if left.handler.alpha_normalized == right.handler.alpha_normalized {
        "Handlers differ only in parameter or local-variable names".to_owned()
    } else if left.handler.structural == right.handler.structural {
        "Handlers share the same structure after literal normalization".to_owned()
    } else {
        "Handler behavior signatures differ".to_owned()
    }
}