unsafe-review-cli 0.3.5

CLI adapter for unsafe-review
Documentation
use std::collections::BTreeMap;
use std::path::Path;

use tower_lsp_server::ls_types::{
    Diagnostic, DiagnosticSeverity, NumberOrString, Position, Range, Uri,
};
use unsafe_review_core::{AnalyzeOutput, Priority, ReviewCard};

use super::uri::uri_from_path;

mod data;

pub(super) fn diagnostics_by_uri(
    root: &Path,
    output: &AnalyzeOutput,
) -> BTreeMap<Uri, Vec<Diagnostic>> {
    let mut map = BTreeMap::new();
    for card in &output.cards {
        let path = root.join(&card.site.location.file);
        let Some(uri) = uri_from_path(path) else {
            continue;
        };
        map.entry(uri)
            .or_insert_with(Vec::new)
            .push(diagnostic_from_card(card));
    }
    map
}

fn diagnostic_from_card(card: &ReviewCard) -> Diagnostic {
    let line = card.site.location.line.saturating_sub(1) as u32;
    let start = Position::new(line, card.site.location.column.saturating_sub(1) as u32);
    let end = Position::new(line, start.character + lsp_width(&card.site.snippet));
    Diagnostic {
        range: Range::new(start, end),
        severity: Some(diagnostic_severity(card)),
        code: Some(NumberOrString::String(card.class.as_str().to_string())),
        source: Some("unsafe-review".into()),
        message: diagnostic_message(card),
        data: Some(data::build_diagnostic_data(card)),
        ..Default::default()
    }
}

fn diagnostic_severity(card: &ReviewCard) -> DiagnosticSeverity {
    if matches!(card.priority, Priority::High) {
        DiagnosticSeverity::WARNING
    } else {
        DiagnosticSeverity::INFORMATION
    }
}

fn diagnostic_message(card: &ReviewCard) -> String {
    format!(
        "{}: {}",
        card.operation.family.as_str(),
        card.next_action.summary
    )
}

pub(super) fn lsp_width(text: &str) -> u32 {
    text.lines()
        .next()
        .unwrap_or(text)
        .chars()
        .map(|c| c.len_utf16() as u32)
        .sum::<u32>()
        .max(1)
}

pub(super) fn find_card_at_position<'a>(
    output: &'a AnalyzeOutput,
    diagnostics: &[Diagnostic],
    pos: Position,
) -> Option<&'a ReviewCard> {
    let diagnostic = diagnostics
        .iter()
        .find(|diagnostic| range_contains(diagnostic.range, pos))?;
    let card_id = diagnostic_card_id(diagnostic)?;
    output.cards.iter().find(|card| card.id.0 == card_id)
}

pub(super) fn diagnostic_card_id(diagnostic: &Diagnostic) -> Option<String> {
    diagnostic
        .data
        .as_ref()
        .and_then(|data| data.get("card_id"))
        .and_then(serde_json::Value::as_str)
        .map(ToOwned::to_owned)
}

pub(super) fn range_contains(range: Range, pos: Position) -> bool {
    pos.line == range.start.line
        && pos.character >= range.start.character
        && pos.character <= range.end.character
}