use std::process::ExitCode;
use clap::{CommandFactory, Parser};
use rllvm_core::{config::try_rllvm_config, error::Error};
use rllvm_query::{
Query,
cli::{ClosureDirection, QueryArgs, QueryCommand},
index::Direction,
llvm_version, mcp, open, run,
};
use tracing_subscriber::FmtSubscriber;
fn to_query(command: QueryCommand, heuristics: bool) -> Option<Query> {
Some(match command {
QueryCommand::Defs { name } => Query::Defs { name },
QueryCommand::At { file, line } => Query::At { file, line },
QueryCommand::Callers { name } => Query::Callers { name },
QueryCommand::Callees { name } => Query::Callees { name },
QueryCommand::Uses { name } => Query::Uses { name },
QueryCommand::Reach { from, to } => Query::Reach { from, to },
QueryCommand::Closure { name, direction } => Query::Closure {
name,
direction: match direction {
ClosureDirection::In => Direction::In,
ClosureDirection::Out => Direction::Out,
},
},
QueryCommand::Externals => Query::Externals,
QueryCommand::IndirectTargets { at } => Query::IndirectTargets { at, heuristics },
QueryCommand::Mcp => return None,
QueryCommand::Completions { .. } => return None,
})
}
fn run_query(args: QueryArgs) -> Result<(), Error> {
let Some(command) = args.command else {
return Ok(());
};
FmtSubscriber::builder()
.with_max_level(try_rllvm_config()?.log_level())
.with_writer(std::io::stderr)
.init();
match &command {
QueryCommand::Mcp => return serve_mcp(args.catalog.as_deref()),
QueryCommand::Completions { .. } => return Ok(()),
QueryCommand::Defs { .. }
| QueryCommand::At { .. }
| QueryCommand::Callers { .. }
| QueryCommand::Callees { .. }
| QueryCommand::Uses { .. }
| QueryCommand::Reach { .. }
| QueryCommand::Closure { .. }
| QueryCommand::Externals
| QueryCommand::IndirectTargets { .. } => {}
}
let Some(query) = to_query(command, args.heuristics) else {
return Err(Error::InvalidArguments(
"this command names a mode, not a query".to_string(),
));
};
query.validate()?;
let catalog = args.catalog.ok_or_else(|| {
Error::InvalidArguments("--catalog is required to run a query".to_string())
})?;
let result = run(&open(&catalog)?, &query)?;
let json = serde_json::to_string_pretty(&result)
.map_err(|error| Error::InvalidArguments(error.to_string()))?;
println!("{json}");
Ok(())
}
fn serve_mcp(catalog: Option<&std::path::Path>) -> Result<(), Error> {
let mut registry = mcp::Registry::new();
if let Some(catalog) = catalog {
registry.load(catalog)?;
}
let stdin = std::io::stdin();
let stdout = std::io::stdout();
mcp::serve(&mut registry, stdin.lock(), stdout.lock())
}
fn main() -> ExitCode {
let args = QueryArgs::parse();
if args.llvm_version {
println!("{}", llvm_version());
return ExitCode::SUCCESS;
}
if let Some(QueryCommand::Completions { shell }) = args.command {
let mut command = QueryArgs::command();
let name = command.get_name().to_string();
clap_complete::generate(shell, &mut command, name, &mut std::io::stdout());
return ExitCode::SUCCESS;
}
match run_query(args) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("{error}");
ExitCode::FAILURE
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cli_command_for(query: &Query) -> QueryCommand {
match query {
Query::Defs { name } => QueryCommand::Defs { name: name.clone() },
Query::At { file, line } => QueryCommand::At {
file: file.clone(),
line: *line,
},
Query::Callers { name } => QueryCommand::Callers { name: name.clone() },
Query::Callees { name } => QueryCommand::Callees { name: name.clone() },
Query::Uses { name } => QueryCommand::Uses { name: name.clone() },
Query::Reach { from, to } => QueryCommand::Reach {
from: from.clone(),
to: to.clone(),
},
Query::Closure { name, direction } => QueryCommand::Closure {
name: name.clone(),
direction: match direction {
Direction::In => ClosureDirection::In,
Direction::Out => ClosureDirection::Out,
},
},
Query::Externals => QueryCommand::Externals,
Query::IndirectTargets { at, .. } => QueryCommand::IndirectTargets { at: at.clone() },
}
}
#[test]
fn the_mcp_command_has_no_query() {
assert!(to_query(QueryCommand::Mcp, false).is_none());
}
#[test]
fn the_completions_command_has_no_query() {
let command = QueryCommand::Completions {
shell: clap_complete::Shell::Bash,
};
assert!(to_query(command, false).is_none());
}
#[test]
fn every_query_variant_round_trips_through_the_cli_command_mapping() {
let heuristics = true;
let queries = [
Query::Defs { name: "f".into() },
Query::At {
file: "t.c".into(),
line: 1,
},
Query::Callers { name: "f".into() },
Query::Callees { name: "f".into() },
Query::Uses { name: "f".into() },
Query::Reach {
from: "a".into(),
to: "b".into(),
},
Query::Closure {
name: "f".into(),
direction: Direction::In,
},
Query::Closure {
name: "f".into(),
direction: Direction::Out,
},
Query::Externals,
Query::IndirectTargets {
at: "t.c:4".into(),
heuristics,
},
];
for query in queries {
let command = cli_command_for(&query);
let round_tripped =
to_query(command, heuristics).expect("every QueryCommand but Mcp names a query");
assert_eq!(
format!("{round_tripped:?}"),
format!("{query:?}"),
"CLI round-trip must preserve every field"
);
}
}
}