unsafe-review-cli 0.3.6

CLI adapter for unsafe-review
Documentation
use serde_json::{Value, json};
use tower_lsp_server::ls_types::{CodeActionOrCommand, Command, Diagnostic, Position};
use unsafe_review_core::{AnalyzeOutput, CardId, ReviewCard, collect_context};

use super::TRUST_BOUNDARY;
use super::diagnostics::find_card_at_position;
use super::{CMD_OPEN_TEST, CMD_PACKET, CMD_REFRESH, CMD_WITNESS_COMMAND, CMD_WITNESS_ROUTE};

pub(super) fn code_actions_for(
    output: Option<&AnalyzeOutput>,
    diagnostics: &[Diagnostic],
    pos: Position,
) -> Vec<CodeActionOrCommand> {
    let mut actions = vec![CodeActionOrCommand::Command(Command {
        title: "Refresh unsafe-review diagnostics".into(),
        command: CMD_REFRESH.into(),
        arguments: None,
    })];
    let Some(output) = output else {
        return actions;
    };
    let Some(card) = find_card_at_position(output, diagnostics, pos) else {
        return actions;
    };
    actions.extend(card_code_actions(card));
    actions
}

fn card_code_actions(card: &ReviewCard) -> Vec<CodeActionOrCommand> {
    let card_id = card.id.0.clone();
    let mut actions = vec![
        command_action(
            format!("Copy unsafe-review packet for {card_id}"),
            CMD_PACKET,
            json!({"card_id": card_id}),
        ),
        command_action(
            format!("Explain unsafe-review witness route for {}", card.id.0),
            CMD_WITNESS_ROUTE,
            json!({"card_id": card.id.0}),
        ),
    ];
    if card.routes.iter().any(|route| route.command.is_some()) {
        actions.push(command_action(
            format!("Copy witness command for {} (does not run)", card.id.0),
            CMD_WITNESS_COMMAND,
            json!({"card_id": card.id.0}),
        ));
    }
    if let Some(test) = card.related_tests.first() {
        actions.push(command_action(
            format!("Open related test `{}`", test.name),
            CMD_OPEN_TEST,
            json!({
                "card_id": card.id.0,
                "file": test.file,
                "line": test.line,
                "name": test.name
            }),
        ));
    }
    actions
}

fn command_action(title: impl Into<String>, command: &str, argument: Value) -> CodeActionOrCommand {
    CodeActionOrCommand::Command(Command {
        title: title.into(),
        command: command.into(),
        arguments: Some(vec![argument]),
    })
}

pub(super) fn execute_card_command(
    command: &str,
    arguments: &[Value],
    output: &AnalyzeOutput,
) -> Option<Value> {
    let card_id = command_card_id(arguments)?;
    let card = output.cards.iter().find(|card| card.id.0 == card_id)?;
    match command {
        CMD_PACKET => collect_context(output, &CardId(card_id)).map(Value::String),
        CMD_WITNESS_ROUTE => card.routes.first().map(|route| {
            json!({
                "kind": "unsafe-review.witness_route",
                "card_id": card.id.0,
                "route": route.kind.as_str(),
                "reason": route.reason,
                "trust_boundary": TRUST_BOUNDARY
            })
        }),
        CMD_WITNESS_COMMAND => card.routes.iter().find_map(|route| {
            route.command.as_ref().map(|command| {
                json!({
                    "kind": "unsafe-review.witness_command",
                    "card_id": card.id.0,
                    "route": route.kind.as_str(),
                    "command": command,
                    "trust_boundary": TRUST_BOUNDARY
                })
            })
        }),
        CMD_OPEN_TEST => card.related_tests.first().map(|test| {
            json!({
                "kind": "unsafe-review.related_test",
                "card_id": card.id.0,
                "file": test.file,
                "line": test.line,
                "name": test.name
            })
        }),
        _ => None,
    }
}

fn command_card_id(arguments: &[Value]) -> Option<String> {
    arguments
        .first()
        .and_then(|argument| argument.get("card_id"))
        .and_then(Value::as_str)
        .map(ToOwned::to_owned)
}