ripr 0.3.1

Static RIPR mutation-exposure analysis for Rust workspaces
Documentation
use super::state::AnalysisSnapshot;
use super::uri::file_uri_for_path;
use super::{
    COPY_CONTEXT_COMMAND, COPY_SUGGESTED_ASSERTION_COMMAND, COPY_TARGETED_TEST_BRIEF_COMMAND,
    OPEN_RELATED_TEST_COMMAND, REFRESH_COMMAND,
};
use crate::analysis::ClassifiedSeam;
use crate::analysis::test_grip_evidence::{RelatedTestGrip, RelationConfidence};
use crate::domain::OracleStrength;
use crate::output::agent_seam_packets::{
    suggested_assertion_for_classified_seam, targeted_test_brief_for_classified_seam,
};
use std::path::PathBuf;
use tower_lsp_server::ls_types::{
    CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse, Command,
    Diagnostic, LSPAny,
};

pub(super) fn code_action_response(
    params: &CodeActionParams,
    snapshot: Option<&AnalysisSnapshot>,
) -> CodeActionResponse {
    let mut actions = Vec::new();
    if let Some(context) = seam_action_context(params, snapshot) {
        push_seam_actions(&mut actions, params, snapshot, context);
    }
    if let Some(diagnostic) = params
        .context
        .diagnostics
        .iter()
        .find(|d| is_ripr_diagnostic(d) && !is_seam_diagnostic(d))
    {
        actions.push(copy_context_action(
            "Copy ripr context packet",
            "Copy ripr context",
            copy_context_target(params, diagnostic),
        ));
    }
    actions.push(CodeActionOrCommand::CodeAction(CodeAction {
        title: "Refresh ripr analysis".to_string(),
        kind: Some(CodeActionKind::SOURCE),
        command: Some(Command {
            title: "Refresh ripr analysis".to_string(),
            command: REFRESH_COMMAND.to_string(),
            arguments: Some(Vec::new()),
        }),
        ..CodeAction::default()
    }));
    actions
}

struct SeamActionContext<'a> {
    diagnostic: &'a Diagnostic,
    seam: &'a ClassifiedSeam,
}

fn seam_action_context<'a>(
    params: &'a CodeActionParams,
    snapshot: Option<&'a AnalysisSnapshot>,
) -> Option<SeamActionContext<'a>> {
    let snapshot = snapshot?;
    params
        .context
        .diagnostics
        .iter()
        .filter(|d| is_ripr_diagnostic(d) && is_seam_diagnostic(d))
        .find_map(|diagnostic| {
            snapshot
                .classified_seam_for_diagnostic(diagnostic)
                .map(|seam| SeamActionContext { diagnostic, seam })
        })
}

fn push_seam_actions(
    actions: &mut CodeActionResponse,
    params: &CodeActionParams,
    snapshot: Option<&AnalysisSnapshot>,
    context: SeamActionContext<'_>,
) {
    actions.push(copy_context_action(
        "Copy seam packet",
        "Copy seam packet",
        copy_seam_packet_target(params, context.diagnostic, context.seam),
    ));
    actions.push(copy_targeted_test_brief_action(
        context.seam,
        targeted_test_brief_for_classified_seam(context.seam),
    ));
    if let Some(assertion) = suggested_assertion_for_classified_seam(context.seam) {
        actions.push(copy_suggested_assertion_action(context.seam, assertion));
    }
    if let Some(snapshot) = snapshot
        && let Some(related) = best_related_test_for_editor(context.seam)
        && let Some(target) = related_test_target(snapshot, related)
    {
        actions.push(open_related_test_action(target));
    }
}

fn copy_context_action(title: &str, command_title: &str, target: LSPAny) -> CodeActionOrCommand {
    CodeActionOrCommand::CodeAction(CodeAction {
        title: title.to_string(),
        kind: Some(CodeActionKind::QUICKFIX),
        command: Some(Command {
            title: command_title.to_string(),
            command: COPY_CONTEXT_COMMAND.to_string(),
            arguments: Some(vec![target]),
        }),
        ..CodeAction::default()
    })
}

fn copy_targeted_test_brief_action(seam: &ClassifiedSeam, brief: String) -> CodeActionOrCommand {
    CodeActionOrCommand::CodeAction(CodeAction {
        title: "Copy targeted test brief".to_string(),
        kind: Some(CodeActionKind::QUICKFIX),
        command: Some(Command {
            title: "Copy targeted test brief".to_string(),
            command: COPY_TARGETED_TEST_BRIEF_COMMAND.to_string(),
            arguments: Some(vec![serde_json::json!({
                "seam_id": seam.seam.id().as_str(),
                "brief": brief,
            })]),
        }),
        ..CodeAction::default()
    })
}

fn copy_suggested_assertion_action(
    seam: &ClassifiedSeam,
    assertion: String,
) -> CodeActionOrCommand {
    CodeActionOrCommand::CodeAction(CodeAction {
        title: "Copy suggested assertion".to_string(),
        kind: Some(CodeActionKind::QUICKFIX),
        command: Some(Command {
            title: "Copy suggested assertion".to_string(),
            command: COPY_SUGGESTED_ASSERTION_COMMAND.to_string(),
            arguments: Some(vec![serde_json::json!({
                "seam_id": seam.seam.id().as_str(),
                "assertion": assertion,
            })]),
        }),
        ..CodeAction::default()
    })
}

fn open_related_test_action(target: LSPAny) -> CodeActionOrCommand {
    CodeActionOrCommand::CodeAction(CodeAction {
        title: "Open best related test".to_string(),
        kind: Some(CodeActionKind::QUICKFIX),
        command: Some(Command {
            title: "Open best related test".to_string(),
            command: OPEN_RELATED_TEST_COMMAND.to_string(),
            arguments: Some(vec![target]),
        }),
        ..CodeAction::default()
    })
}

fn is_ripr_diagnostic(diagnostic: &Diagnostic) -> bool {
    diagnostic.source.as_deref() == Some("ripr")
}

fn is_seam_diagnostic(diagnostic: &Diagnostic) -> bool {
    diagnostic
        .data
        .as_ref()
        .and_then(|data| data.get("seam_id"))
        .and_then(|value| value.as_str())
        .is_some()
}

fn copy_context_target(params: &CodeActionParams, diagnostic: &Diagnostic) -> LSPAny {
    let mut target = serde_json::Map::new();
    target.insert(
        "uri".to_string(),
        serde_json::Value::String(params.text_document.uri.as_str().to_string()),
    );
    target.insert(
        "line".to_string(),
        serde_json::Value::Number(serde_json::Number::from(
            params.range.start.line.saturating_add(1),
        )),
    );
    if let Some(data) = &diagnostic.data
        && let Some(obj) = data.as_object()
    {
        if let Some(finding_id) = obj.get("finding_id").and_then(|v| v.as_str()) {
            target.insert(
                "finding_id".to_string(),
                serde_json::Value::String(finding_id.to_string()),
            );
        }
        if let Some(probe_id) = obj.get("probe_id").and_then(|v| v.as_str()) {
            target.insert(
                "probe_id".to_string(),
                serde_json::Value::String(probe_id.to_string()),
            );
        }
        if let Some(seam_id) = obj.get("seam_id").and_then(|v| v.as_str()) {
            target.insert(
                "seam_id".to_string(),
                serde_json::Value::String(seam_id.to_string()),
            );
        }
        if let Some(seam_kind) = obj.get("seam_kind").and_then(|v| v.as_str()) {
            target.insert(
                "seam_kind".to_string(),
                serde_json::Value::String(seam_kind.to_string()),
            );
        }
    }
    serde_json::Value::Object(target)
}

fn copy_seam_packet_target(
    params: &CodeActionParams,
    diagnostic: &Diagnostic,
    seam: &ClassifiedSeam,
) -> LSPAny {
    let mut target = copy_context_target(params, diagnostic);
    if let Some(obj) = target.as_object_mut() {
        obj.insert(
            "line".to_string(),
            serde_json::Value::Number(serde_json::Number::from(seam.seam.display_line())),
        );
        obj.insert(
            "seam_id".to_string(),
            serde_json::Value::String(seam.seam.id().as_str().to_string()),
        );
        obj.insert(
            "seam_kind".to_string(),
            serde_json::Value::String(seam.seam.kind().as_str().to_string()),
        );
    }
    target
}

fn best_related_test_for_editor(seam: &ClassifiedSeam) -> Option<&RelatedTestGrip> {
    seam.evidence
        .related_tests
        .iter()
        .find(|test| test.oracle_strength == OracleStrength::Strong)
        .or_else(|| {
            seam.evidence
                .related_tests
                .iter()
                .min_by_key(|test| relation_confidence_rank(test.relation_confidence))
        })
}

fn relation_confidence_rank(confidence: RelationConfidence) -> u8 {
    match confidence {
        RelationConfidence::High => 0,
        RelationConfidence::Medium => 1,
        RelationConfidence::Low => 2,
        RelationConfidence::Opaque => 3,
    }
}

fn related_test_target(snapshot: &AnalysisSnapshot, related: &RelatedTestGrip) -> Option<LSPAny> {
    let path = absolute_related_test_path(snapshot, related);
    let uri = file_uri_for_path(&path).ok()?;
    Some(serde_json::json!({
        "uri": uri.as_str(),
        "line": related.line,
        "test_name": related.test_name.as_str(),
    }))
}

fn absolute_related_test_path(snapshot: &AnalysisSnapshot, related: &RelatedTestGrip) -> PathBuf {
    if related.file.is_absolute() {
        related.file.clone()
    } else {
        snapshot.root.join(&related.file)
    }
}