cuke-dedup 0.3.0

Static analysis for duplicate and reusable Cucumber step definitions
Documentation
use crate::model::{InlineSuppression, Rule, SourceLocation};
use crate::resource_limits::MAX_SUPPRESSION_REASON_CHARS;
use crate::source_adapter::{ExtractionDiagnostic, ExtractionDiagnosticLevel, SourceFile};
use tree_sitter::Node;

pub(super) fn inline_suppressions(
    call: Node<'_>,
    lines: &[&str],
    file: &SourceFile,
    diagnostics: &mut Vec<ExtractionDiagnostic>,
) -> Vec<InlineSuppression> {
    let mut line_index = call.start_position().row.checked_sub(1);
    let mut suppressions = Vec::new();

    while let Some(index) = line_index {
        let line = lines.get(index).copied().unwrap_or_default().trim();
        let Some(directive) = line
            .strip_prefix("//")
            .map(str::trim)
            .and_then(|line| line.strip_prefix("cuke-dedup:ignore").map(str::trim))
        else {
            break;
        };
        let Some((rule, reason)) = directive.split_once("--") else {
            diagnostics.push(inline_suppression_diagnostic(
                file,
                index,
                "inline suppression must use `// cuke-dedup:ignore RULE -- REASON`",
            ));
            line_index = index.checked_sub(1);
            continue;
        };
        let rule = match rule.trim().parse::<Rule>() {
            Ok(rule) => rule,
            Err(error) => {
                diagnostics.push(inline_suppression_diagnostic(file, index, &error));
                line_index = index.checked_sub(1);
                continue;
            }
        };
        let reason = reason.trim();
        if reason.is_empty() {
            diagnostics.push(inline_suppression_diagnostic(
                file,
                index,
                "inline suppression reason must not be empty",
            ));
        } else if reason.chars().count() > MAX_SUPPRESSION_REASON_CHARS {
            diagnostics.push(inline_suppression_diagnostic(
                file,
                index,
                &format!(
                    "inline suppression reason exceeds the {MAX_SUPPRESSION_REASON_CHARS}-character limit"
                ),
            ));
        } else {
            suppressions.push(InlineSuppression {
                rule,
                reason: reason.to_owned(),
            });
        }
        line_index = index.checked_sub(1);
    }
    suppressions.reverse();
    suppressions
}

fn inline_suppression_diagnostic(
    file: &SourceFile,
    zero_based_line: usize,
    message: &str,
) -> ExtractionDiagnostic {
    ExtractionDiagnostic {
        level: ExtractionDiagnosticLevel::Error,
        location: SourceLocation::new(&file.path, zero_based_line + 1, 1, zero_based_line + 1, 1),
        message: message.to_owned(),
    }
}