llmgrep 3.11.1

Smart grep over Magellan code maps with schema-aligned JSON output
Documentation
use anyhow::Result;
use magellan::CodeGraph;
use serde::Serialize;
use sqlitegraph::backend::BackendDirection;
use std::path::Path;

#[derive(Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct NavSymbol {
    pub id: i64,
    pub name: String,
    pub kind: String,
    pub file: Option<String>,
    pub line: usize,
}

#[derive(Clone, Serialize)]
pub struct DepthNavSymbol {
    pub depth: u32,
    pub node: NavSymbol,
}

#[derive(Clone, Serialize)]
pub struct EdgeInfo {
    pub edge_type: String,
    pub direction: String,
    pub target: NavSymbol,
}

#[derive(Serialize)]
pub struct NavigateResponse {
    pub node: Option<NavSymbol>,
    pub resolve: Option<Vec<NavSymbol>>,
    pub edges: Option<Vec<EdgeInfo>>,
    pub callers: Option<Vec<DepthNavSymbol>>,
    pub callees: Option<Vec<DepthNavSymbol>>,
}

impl From<magellan::graph::navigator::SymbolInfo> for NavSymbol {
    fn from(si: magellan::graph::navigator::SymbolInfo) -> Self {
        NavSymbol {
            id: si.id,
            name: si.name,
            kind: si.kind,
            file: si.file_path,
            line: si.start_line,
        }
    }
}

impl From<magellan::graph::navigator::DepthSymbol> for DepthNavSymbol {
    fn from(ds: magellan::graph::navigator::DepthSymbol) -> Self {
        DepthNavSymbol {
            depth: ds.depth,
            node: ds.info.into(),
        }
    }
}

fn direction_str(d: &BackendDirection) -> &'static str {
    match d {
        BackendDirection::Outgoing => "out",
        BackendDirection::Incoming => "in",
    }
}

#[allow(clippy::too_many_arguments)]
pub fn run_navigate(
    db_path: &Path,
    symbol: &str,
    id: Option<i64>,
    show_edges: bool,
    show_callers: bool,
    show_callees: bool,
    depth: usize,
    output: crate::output::OutputFormat,
) -> Result<()> {
    let graph = CodeGraph::open(db_path)?;
    let nav = graph.navigator();

    let start_id = if let Some(eid) = id {
        eid
    } else {
        let resolved = nav.resolve(symbol)?;
        if resolved.is_empty() {
            match output {
                crate::output::OutputFormat::Json | crate::output::OutputFormat::Pretty => {
                    println!(r#"{{"error":"no symbols found for '{}'"}}"#, symbol);
                }
                crate::output::OutputFormat::Human => {
                    eprintln!("error: no symbols found for '{}'", symbol);
                }
            }
            return Ok(());
        }
        resolved[0].id
    };

    let info = nav.info(start_id)?;
    let node: Option<NavSymbol> = info.map(Into::into);

    let resolve = if id.is_none() {
        let r = nav.resolve(symbol)?;
        Some(r.into_iter().map(Into::into).collect())
    } else {
        None
    };

    let edges = if show_edges {
        let e = nav.expand(start_id)?;
        Some(
            e.into_iter()
                .map(|hop| EdgeInfo {
                    edge_type: hop.edge_type,
                    direction: direction_str(&hop.direction).to_string(),
                    target: hop.target.into(),
                })
                .collect(),
        )
    } else {
        None
    };

    let callers = if show_callers {
        Some(
            nav.k_hop_callers(start_id, depth as u32)?
                .into_iter()
                .map(Into::into)
                .collect(),
        )
    } else {
        None
    };

    let callees = if show_callees {
        Some(
            nav.k_hop_callees(start_id, depth as u32)?
                .into_iter()
                .map(Into::into)
                .collect(),
        )
    } else {
        None
    };

    let response = NavigateResponse {
        node,
        resolve,
        edges,
        callers,
        callees,
    };

    match output {
        crate::output::OutputFormat::Json | crate::output::OutputFormat::Pretty => {
            let wrapped = crate::output::json_response(&response);
            let json_str = match output {
                crate::output::OutputFormat::Pretty => serde_json::to_string_pretty(&wrapped)?,
                _ => serde_json::to_string(&wrapped)?,
            };
            println!("{}", json_str);
        }
        crate::output::OutputFormat::Human => print_human(&response),
    }

    Ok(())
}

fn print_human(resp: &NavigateResponse) {
    if let Some(ref node) = resp.node {
        println!(
            "id={} kind={} name=\"{}\" file={}",
            node.id,
            node.kind,
            node.name,
            node.file.as_deref().unwrap_or("?")
        );
    }
    if let Some(ref resolve) = resp.resolve {
        for s in resolve {
            println!(
                "id={} kind={} name=\"{}\" file={}",
                s.id,
                s.kind,
                s.name,
                s.file.as_deref().unwrap_or("?")
            );
        }
    }
    if let Some(ref callers) = resp.callers {
        println!("  callers:");
        for c in callers {
            println!(
                "    {} id={} depth={} ({})",
                c.node.name,
                c.node.id,
                c.depth,
                c.node.file.as_deref().unwrap_or("?")
            );
        }
    }
    if let Some(ref callees) = resp.callees {
        println!("  callees:");
        for c in callees {
            println!(
                "    {} id={} depth={} ({})",
                c.node.name,
                c.node.id,
                c.depth,
                c.node.file.as_deref().unwrap_or("?")
            );
        }
    }
    if let Some(ref edges) = resp.edges {
        println!("  edges:");
        for e in edges {
            println!(
                "    {} {} {} ({})",
                e.direction,
                e.edge_type,
                e.target.name,
                e.target.file.as_deref().unwrap_or("?")
            );
        }
    }
}