use std::{
io::{IsTerminal, Read},
path::{Path, PathBuf},
process::ExitCode,
};
use clap::{CommandFactory, Parser};
use rllvm_core::{
config::try_rllvm_config,
error::Error,
utils::{print_stdout, split_response_arguments},
};
use rllvm_query::{
AnchorParts, AnchorSurface, Color, FactsCache, Overlay, OverlaySummary, Query, Record, Session,
TextMode, Verification, anchor_from_parts, check_anchor_parts,
cli::{CacheAction, ClosureDirection, OverlayAction, QueryArgs, QueryCommand, QueryModifiers},
default_overlay_path, emit_guards_module, emit_slice,
index::Direction,
llvm_version, mcp, open_with_cache,
overlay::verdict_label,
run_with_overlay,
};
use tracing_subscriber::FmtSubscriber;
fn to_query(command: QueryCommand, modifiers: QueryModifiers) -> Option<Planned> {
let QueryModifiers {
heuristics,
include_overlay,
include_annotations,
min_confidence,
} = modifiers;
if let QueryCommand::Guards {
at,
operand,
ret,
function,
local,
from,
emit_module,
} = command
{
return Some(Planned::Guards(GuardsRequest {
parts: AnchorParts {
at,
operand,
ret,
function,
local,
},
from,
emit_module,
}));
}
Some(Planned::Ready(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,
include_overlay,
min_confidence,
include_annotations,
},
QueryCommand::Slice { from, to, .. } => Query::Slice {
from,
to,
include_overlay,
min_confidence,
include_annotations,
},
QueryCommand::Closure { name, direction } => Query::Closure {
name,
direction: match direction {
ClosureDirection::In => Direction::In,
ClosureDirection::Out => Direction::Out,
},
include_overlay,
min_confidence,
},
QueryCommand::Externals => Query::Externals,
QueryCommand::FfiExports => Query::FfiExports,
QueryCommand::IndirectTargets { at } => Query::IndirectTargets { at, heuristics },
QueryCommand::ResolutionCandidates => Query::ResolutionCandidates,
QueryCommand::Guards { .. } => return None,
QueryCommand::Mcp => return None,
QueryCommand::Completions { .. } => return None,
QueryCommand::Cache { .. } => return None,
QueryCommand::Overlay { .. } => return None,
}))
}
#[derive(Debug)]
enum Planned {
Ready(Query),
Guards(GuardsRequest),
}
#[derive(Debug, PartialEq)]
struct GuardsRequest {
parts: AnchorParts,
from: Option<String>,
emit_module: Option<PathBuf>,
}
impl Planned {
fn validate(&self) -> Result<(), Error> {
match self {
Planned::Ready(query) => query.validate(),
Planned::Guards(request) => check_anchor_parts(&request.parts, AnchorSurface::Cli)
.map_err(Error::InvalidArguments),
}
}
fn into_query(self, session: &Session) -> Result<Query, Error> {
match self {
Planned::Ready(query) => Ok(query),
Planned::Guards(request) => Ok(Query::Guards {
anchor: anchor_from_parts(session, &request.parts, AnchorSurface::Cli)
.map_err(Error::InvalidArguments)?,
at: request.parts.use_site().map(str::to_string),
from: request.from,
emit_module: request.emit_module,
}),
}
}
}
#[derive(Parser)]
#[command(name = "rllvm-query", no_binary_name = true)]
struct QueryLine {
#[command(flatten)]
modifiers: QueryModifiers,
#[command(subcommand)]
command: QueryCommand,
}
fn parse_queries(input: &str, modifiers: QueryModifiers) -> Result<Vec<(String, Planned)>, Error> {
let mut queries = Vec::new();
for (index, line) in input.lines().enumerate() {
let line = line.trim();
if line.is_empty() {
continue;
}
let at_line = |reason: String| {
let reason = reason.split("\n\n").next().unwrap_or_default();
let reason = reason.strip_prefix("error: ").unwrap_or(reason);
let reason = reason.split_whitespace().collect::<Vec<_>>().join(" ");
Error::InvalidArguments(format!("line {}: {reason}", index + 1))
};
let arguments = split_response_arguments(line);
let parsed =
QueryLine::try_parse_from(&arguments).map_err(|error| at_line(error.to_string()))?;
if let QueryCommand::Slice {
emit_module: Some(_),
..
}
| QueryCommand::Guards {
emit_module: Some(_),
..
} = parsed.command
{
return Err(at_line(
"`--emit-module` writes a file for one slice: run it on the command line"
.to_string(),
));
}
let query = to_query(parsed.command, modifiers.with(parsed.modifiers))
.ok_or_else(|| at_line(format!("`{}` names a mode, not a query", arguments[0])))?;
query.validate().map_err(|error| match error {
Error::InvalidArguments(reason) => at_line(reason),
other => at_line(other.to_string()),
})?;
queries.push((line.to_string(), query));
}
Ok(queries)
}
fn set_llvm_bindir(session: &mut Session) {
session.set_llvm_bindir(
try_rllvm_config()
.and_then(|config| config.llvm_bindir().cloned())
.map_err(|error| error.to_string()),
);
}
fn facts_cache() -> Result<Option<FactsCache>, Error> {
Ok(FactsCache::from_config(try_rllvm_config()?))
}
fn run_cache(action: Option<&CacheAction>, format: Format) -> Result<(), Error> {
let config = try_rllvm_config()?;
let cache = FactsCache::configured(config)?;
match action {
Some(CacheAction::Clear { stale }) => {
let cleared = cache.clear(*stale);
match format {
Format::Json => print_stdout(&format!(
"{}\n",
serde_json::to_string(&cleared)
.map_err(|error| Error::InvalidArguments(error.to_string()))?
)),
Format::Text(_) => {
let orphans = if cleared.orphans > 0 {
format!(" and {} orphaned temp file(s)", cleared.orphans)
} else {
String::new()
};
print_stdout(&format!(
"removed {} entries{orphans}, {}\n",
cleared.entries,
rllvm_query::render::human_bytes(cleared.bytes)
))
}
}
}
None => {
let usage = cache.usage(config.query_cache_enabled());
match format {
Format::Json => print_stdout(&format!(
"{}\n",
serde_json::to_string_pretty(&usage)
.map_err(|error| Error::InvalidArguments(error.to_string()))?
)),
Format::Text(_) => print_stdout(&rllvm_query::render_usage(&usage)),
}
}
}
}
fn parse_records(input: &str) -> Result<Vec<(usize, Record)>, Error> {
let mut records = Vec::new();
for (index, line) in input.lines().enumerate() {
if line.trim().is_empty() {
continue;
}
let record = Record::parse_labeled(line, format_args!("line {}", index + 1))?;
records.push((index + 1, record));
}
Ok(records)
}
fn overlay_path(catalog: &Path, overlay: Option<&Path>) -> PathBuf {
overlay.map_or_else(|| default_overlay_path(catalog), Path::to_path_buf)
}
fn overlay_for<'o>(
opened: &'o mut Option<Overlay>,
session: &Session,
catalog: &Path,
named: Option<&Path>,
query: &Query,
) -> Result<Option<&'o Overlay>, Error> {
if query.walks_overlay() && opened.is_none() {
let path = overlay_path(catalog, named);
if (named.is_some() || query.includes_annotations())
&& let Err(error) = std::fs::metadata(&path)
&& error.kind() == std::io::ErrorKind::NotFound
{
return Err(Error::InvalidArguments(if named.is_some() {
format!(
"{}: no such overlay; --overlay names an existing file for a walk to read",
path.display()
)
} else {
format!(
"{}: no such overlay; --annotations needs one holding annotations",
path.display()
)
}));
}
*opened = Some(Overlay::open(session, &path)?);
}
Ok(opened.as_ref())
}
fn run_overlay(
action: &OverlayAction,
catalog: Option<&Path>,
overlay: Option<&Path>,
format: Format,
) -> Result<(), Error> {
let catalog = catalog.ok_or_else(|| {
Error::InvalidArguments("--catalog is required for the overlay command".to_string())
})?;
let path = overlay_path(catalog, overlay);
let records = match action {
OverlayAction::Record => {
let mut stdin = std::io::stdin();
if stdin.is_terminal() {
return Err(Error::InvalidArguments(
"no records given: pipe them on stdin, one JSON object per line".to_string(),
));
}
let mut input = String::new();
stdin.read_to_string(&mut input)?;
parse_records(&input)?
}
OverlayAction::List | OverlayAction::Compact => Vec::new(),
};
let mut session = open_with_cache(catalog, facts_cache()?.as_ref())?;
set_llvm_bindir(&mut session);
let mut overlay = Overlay::open(&session, &path)?;
let text = match action {
OverlayAction::Record => {
let recorded = records.len();
overlay.record_lines(&session, records)?;
let saved = overlay.save()?;
format!("recorded {recorded}, saved {saved} to {}\n", path.display())
}
OverlayAction::List => overlay_table(&overlay.summary(&session)),
OverlayAction::Compact => {
overlay.compact()?;
format!(
"compacted {}: {} edges, {} annotations\n",
path.display(),
overlay.edges().count(),
overlay.annotations().len()
)
}
};
match format {
Format::Json => print_stdout(&format!(
"{}\n",
serde_json::to_string_pretty(&overlay.summary(&session))
.map_err(|error| Error::InvalidArguments(error.to_string()))?
)),
Format::Text(_) => print_stdout(&text),
}
}
fn overlay_table(summary: &OverlaySummary) -> String {
let verdict = |verification: Option<&Verification>| {
verdict_label(verification.map(|verification| verification.verdict))
};
let edges = summary.edges.iter().map(|entry| {
format!(
"{} {} {} {} site(s)\n",
entry.edge.key,
entry.edge.confidence,
verdict(entry.edge.verification.as_ref()),
entry.sites
)
});
let annotations = summary.annotations.iter().map(|annotation| {
format!(
"{} {} {} {}\n",
annotation.key,
annotation.verdict,
annotation.confidence,
verdict(annotation.verification.as_ref())
)
});
edges.chain(annotations).collect()
}
fn run_stdin_queries(
catalog: &Path,
overlay: Option<&Path>,
modifiers: QueryModifiers,
format: Format,
) -> Result<(), Error> {
let mut stdin = std::io::stdin();
if stdin.is_terminal() {
return Err(Error::InvalidArguments(
"no query given: name one, or pipe queries on stdin".to_string(),
));
}
let mut input = String::new();
stdin.read_to_string(&mut input)?;
let queries = parse_queries(&input, modifiers)?;
if queries.is_empty() {
return Ok(());
}
init_logging()?;
let mut session = open_with_cache(catalog, facts_cache()?.as_ref())?;
set_llvm_bindir(&mut session);
let named = overlay;
let mut opened = None;
for (line, query) in queries {
let query = query.into_query(&session)?;
let overlay = overlay_for(&mut opened, &session, catalog, named, &query)?;
let result = run_with_overlay(&session, overlay, &query)?;
let answer = match format {
Format::Json => serde_json::to_string(&result)
.map(|json| format!("{json}\n"))
.map_err(|error| Error::InvalidArguments(error.to_string()))?,
Format::Text(mode) => format!(
"== {line}\n{}",
rllvm_query::render(&result, mode, stdout_color())
),
};
print_stdout(&answer)?;
}
Ok(())
}
#[derive(Clone, Copy)]
enum Format {
Json,
Text(TextMode),
}
impl Format {
fn of(args: &QueryArgs) -> Format {
if args.json {
Format::Json
} else if args.full {
Format::Text(TextMode::Full)
} else {
Format::Text(TextMode::Adaptive)
}
}
}
fn init_logging() -> Result<(), Error> {
FmtSubscriber::builder()
.with_max_level(try_rllvm_config()?.log_level())
.with_writer(std::io::stderr)
.init();
Ok(())
}
fn run_query(args: QueryArgs) -> Result<(), Error> {
let format = Format::of(&args);
let Some(command) = args.command else {
return match &args.catalog {
Some(catalog) => {
run_stdin_queries(catalog, args.overlay.as_deref(), args.modifiers, format)
}
None => Ok(()),
};
};
init_logging()?;
match &command {
QueryCommand::Mcp => return serve_mcp(args.catalog.as_deref()),
QueryCommand::Completions { .. } => return Ok(()),
QueryCommand::Cache { action } => {
if args.catalog.is_some() {
return Err(Error::InvalidArguments(
"the cache command reads no catalog; drop --catalog".into(),
));
}
return run_cache(action.as_ref(), format);
}
QueryCommand::Overlay { action } => {
return run_overlay(
action,
args.catalog.as_deref(),
args.overlay.as_deref(),
format,
);
}
QueryCommand::Defs { .. }
| QueryCommand::At { .. }
| QueryCommand::Callers { .. }
| QueryCommand::Callees { .. }
| QueryCommand::Uses { .. }
| QueryCommand::Reach { .. }
| QueryCommand::Slice { .. }
| QueryCommand::Closure { .. }
| QueryCommand::Externals
| QueryCommand::FfiExports
| QueryCommand::IndirectTargets { .. }
| QueryCommand::ResolutionCandidates
| QueryCommand::Guards { .. } => {}
}
let emit_module = match &command {
QueryCommand::Slice { emit_module, .. } | QueryCommand::Guards { emit_module, .. } => {
emit_module.clone()
}
_ => None,
};
let Some(query) = to_query(command, args.modifiers) 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 mut session = open_with_cache(&catalog, facts_cache()?.as_ref())?;
set_llvm_bindir(&mut session);
let query = query.into_query(&session)?;
let mut opened = None;
let overlay = overlay_for(
&mut opened,
&session,
&catalog,
args.overlay.as_deref(),
&query,
)?;
let result = run_with_overlay(&session, overlay, &query)?;
let emitted = match &emit_module {
Some(out) => {
let config = try_rllvm_config()?;
Some(match &query {
Query::Guards { anchor, from, .. } => emit_guards_module(
&session,
&catalog,
anchor,
from.as_deref(),
config.llvm_link_filepath(),
config.llvm_bindir()?,
out,
)?,
_ => emit_slice(
&session,
&catalog,
&result,
config.llvm_link_filepath(),
config.llvm_bindir()?,
out,
)?,
})
}
None => None,
};
match format {
Format::Json => {
let mut answer = serde_json::to_value(&result)
.map_err(|error| Error::InvalidArguments(error.to_string()))?;
if let Some(emitted) = &emitted {
answer["emitted"] = serde_json::json!(emitted);
}
let json = serde_json::to_string_pretty(&answer)
.map_err(|error| Error::InvalidArguments(error.to_string()))?;
print_stdout(&format!("{json}\n"))?;
}
Format::Text(mode) => {
let mut text = emitted.map_or_else(String::new, |emitted| {
format!(
"wrote {}: {} functions from {} modules\n",
emitted.path.display(),
emitted.functions,
emitted.modules
)
});
text.push_str(&rllvm_query::render(&result, mode, stdout_color()));
print_stdout(&text)?;
}
}
Ok(())
}
fn stdout_color() -> Color {
match supports_color::on(supports_color::Stream::Stdout) {
Some(_) => Color::Always,
None => Color::Never,
}
}
fn serve_mcp(catalog: Option<&std::path::Path>) -> Result<(), Error> {
let config = try_rllvm_config()?;
let mut registry = mcp::Registry::with_cache(facts_cache()?).with_llvm_tools(
config.llvm_link_filepath().clone(),
config.llvm_bindir().cloned(),
);
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 {
return rllvm_core::error::report(print_stdout(&format!("{}\n", llvm_version())));
}
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::*;
use rllvm_query::{Confidence, GuardAnchor};
fn ready(planned: &Planned) -> &Query {
match planned {
Planned::Ready(query) => query,
Planned::Guards(_) => panic!("a guards line waits on the catalog"),
}
}
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::Slice { from, to, .. } => QueryCommand::Slice {
from: from.clone(),
to: to.clone(),
emit_module: None,
},
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::FfiExports => QueryCommand::FfiExports,
Query::IndirectTargets { at, .. } => QueryCommand::IndirectTargets { at: at.clone() },
Query::ResolutionCandidates => QueryCommand::ResolutionCandidates,
Query::Guards {
anchor,
at: use_site,
from,
emit_module,
} => {
let (mut operand, mut ret, mut function, mut local) = (None, false, None, None);
let at = match anchor {
GuardAnchor::Argument { site, operand: n } => {
operand = Some(*n);
Some(serde_json::to_string(site).unwrap())
}
GuardAnchor::Return { site } => {
ret = true;
Some(serde_json::to_string(site).unwrap())
}
GuardAnchor::Local { function: f, name } => {
function = Some(f.clone());
local = Some(name.clone());
use_site.clone()
}
};
QueryCommand::Guards {
at,
operand,
ret,
function,
local,
from: from.clone(),
emit_module: emit_module.clone(),
}
}
other => panic!("{other:?} has no CLI command"),
}
}
const MANAGEMENT_TOOLS: [&str; 8] = [
"load_catalog",
"inventory",
"list_catalogs",
"unload_catalog",
"load_overlay",
"record_edges",
"save_overlay",
"list_overlay",
];
#[test]
fn every_query_tool_has_a_cli_subcommand() {
let mut output = Vec::new();
mcp::serve(
&mut mcp::Registry::new(),
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}\n".as_bytes(),
&mut output,
)
.unwrap();
let reply: serde_json::Value = serde_json::from_slice(&output).unwrap();
let tools: Vec<&str> = reply["result"]["tools"]
.as_array()
.unwrap()
.iter()
.map(|tool| tool["name"].as_str().unwrap())
.filter(|name| !MANAGEMENT_TOOLS.contains(name))
.collect();
assert_eq!(tools.len(), 13, "{tools:?}");
let command = QueryArgs::command();
for tool in tools {
assert!(
command.find_subcommand(tool.replace('_', "-")).is_some(),
"the `{tool}` tool has no `rllvm-query` subcommand"
);
}
}
#[test]
fn each_line_of_stdin_is_one_query_in_command_line_syntax() {
let queries = parse_queries(
"callees quiche_accept\n\n defs 'int twice<int>(int)'\nindirect-targets t.c:4 --heuristics\n",
QueryModifiers::default(),
)
.unwrap();
let lines: Vec<&str> = queries.iter().map(|(line, _)| line.as_str()).collect();
assert_eq!(
lines,
[
"callees quiche_accept",
"defs 'int twice<int>(int)'",
"indirect-targets t.c:4 --heuristics"
]
);
assert_eq!(
format!("{:?}", ready(&queries[1].1)),
format!(
"{:?}",
Query::Defs {
name: "int twice<int>(int)".into()
}
)
);
assert!(matches!(
ready(&queries[2].1),
Query::IndirectTargets {
heuristics: true,
..
}
));
}
#[test]
fn heuristics_on_the_command_line_reach_every_stdin_query() {
let heuristics = QueryModifiers {
heuristics: true,
..QueryModifiers::default()
};
let queries = parse_queries("indirect-targets t.c:4\n", heuristics).unwrap();
assert!(matches!(
ready(&queries[0].1),
Query::IndirectTargets {
heuristics: true,
..
}
));
}
#[test]
fn a_stdin_line_may_ask_for_the_overlay() {
let queries = parse_queries(
"reach main handler --include-overlay --min-confidence medium\nreach main handler\n",
QueryModifiers::default(),
)
.unwrap();
assert!(matches!(
ready(&queries[0].1),
Query::Reach {
include_overlay: true,
min_confidence: Some(Confidence::Medium),
..
}
));
assert!(ready(&queries[0].1).walks_overlay());
assert!(
!ready(&queries[1].1).walks_overlay(),
"each line asks for itself"
);
let error = parse_queries(
"reach main handler --min-confidence high\n",
QueryModifiers::default(),
)
.map(|_| ())
.unwrap_err()
.to_string();
assert!(error.contains("line 1:"), "{error}");
}
#[test]
fn a_stdin_slice_may_not_emit_a_module() {
let queries = parse_queries(
"slice main handler --include-overlay\n",
QueryModifiers::default(),
)
.unwrap();
assert!(matches!(
ready(&queries[0].1),
Query::Slice {
include_overlay: true,
..
}
));
assert!(ready(&queries[0].1).walks_overlay());
let error = parse_queries(
"callees a\nslice main handler --emit-module out.bc\n",
QueryModifiers::default(),
)
.map(|_| ())
.unwrap_err()
.to_string();
assert!(
error.contains("line 2:") && error.contains("--emit-module"),
"{error}"
);
}
#[test]
fn a_bad_stdin_line_is_reported_by_its_number() {
for (input, expected) in [
("callees a\nfrobnicate b\n", "line 2:"),
("\ncallees a --json\n", "line 2:"),
("callees a\n--catalog c.json callees b\n", "line 2:"),
("mcp\n", "line 1:"),
("completions bash\n", "line 1:"),
("callees a\ncallees b\nindirect-targets main.c\n", "line 3:"),
] {
let error = parse_queries(input, QueryModifiers::default())
.map(|_| ())
.expect_err(input)
.to_string();
assert!(error.contains(expected), "{input:?} gave {error:?}");
assert_eq!(error.lines().count(), 1, "{input:?} gave {error:?}");
}
}
#[test]
fn empty_stdin_names_no_query() {
assert!(
parse_queries("", QueryModifiers::default())
.unwrap()
.is_empty()
);
assert!(
parse_queries("\n \n", QueryModifiers::default())
.unwrap()
.is_empty()
);
}
#[test]
fn the_mcp_command_has_no_query() {
assert!(to_query(QueryCommand::Mcp, QueryModifiers::default()).is_none());
}
#[test]
fn the_completions_command_has_no_query() {
let command = QueryCommand::Completions {
shell: clap_complete::Shell::Bash,
};
assert!(to_query(command, QueryModifiers::default()).is_none());
}
#[test]
fn the_overlay_command_has_no_query() {
let command = QueryCommand::Overlay {
action: OverlayAction::List,
};
assert!(to_query(command, QueryModifiers::default()).is_none());
}
#[test]
fn a_bad_record_on_stdin_is_reported_by_its_number() {
let good = r#"{"op":"retract","edge":{"via_field":{"record":"ops","offset":8},"to":{"module_id":"m","symbol":"h3"}},"reason":"r"}"#;
let lines: Vec<usize> = parse_records(&format!("\n{good}\n\n{good}\n"))
.unwrap()
.into_iter()
.map(|(line, _)| line)
.collect();
assert_eq!(lines, [2, 4], "each record keeps its line for validation");
for (input, expected) in [
(format!("{good}\n{{\n"), "line 2:"),
(format!("\n{good}\n{{\"op\":\"annotate\"}}\n"), "line 3:"),
] {
let error = parse_records(&input).map(|_| ()).unwrap_err().to_string();
assert!(error.contains(expected), "{input:?} gave {error:?}");
}
}
#[test]
fn every_query_variant_round_trips_through_the_cli_command_mapping() {
let modifiers = QueryModifiers {
heuristics: true,
include_overlay: true,
include_annotations: true,
min_confidence: Some(Confidence::High),
};
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(),
include_overlay: true,
min_confidence: Some(Confidence::High),
include_annotations: true,
},
Query::Slice {
from: "a".into(),
to: "b".into(),
include_overlay: true,
min_confidence: Some(Confidence::High),
include_annotations: true,
},
Query::Closure {
name: "f".into(),
direction: Direction::In,
include_overlay: true,
min_confidence: Some(Confidence::High),
},
Query::Closure {
name: "f".into(),
direction: Direction::Out,
include_overlay: true,
min_confidence: Some(Confidence::High),
},
Query::Externals,
Query::FfiExports,
Query::IndirectTargets {
at: "t.c:4".into(),
heuristics: true,
},
];
for query in queries {
let command = cli_command_for(&query);
let Some(Planned::Ready(round_tripped)) = to_query(command, modifiers) else {
panic!("{query:?} should be ready to run");
};
assert_eq!(
format!("{round_tripped:?}"),
format!("{query:?}"),
"CLI round-trip must preserve every field"
);
}
}
#[test]
fn a_guards_query_round_trips_through_its_flags() {
let site = rllvm_query::CallSiteId {
function: rllvm_query::FunctionId {
module_id: "m".into(),
symbol: "f".into(),
},
block_index: 1,
instruction_index: 2,
};
let parse_len = GuardAnchor::Local {
function: "parse".into(),
name: "len".into(),
};
for (anchor, at, from) in [
(
GuardAnchor::Argument {
site: site.clone(),
operand: 3,
},
None,
Some("main".to_string()),
),
(GuardAnchor::Return { site: site.clone() }, None, None),
(parse_len.clone(), None, None),
(parse_len, Some("t.c:9".to_string()), None),
] {
let query = Query::Guards {
anchor: anchor.clone(),
at: at.clone(),
from: from.clone(),
emit_module: Some("out.bc".into()),
};
let command = cli_command_for(&query);
let Some(Planned::Guards(request)) = to_query(command, QueryModifiers::default())
else {
panic!("{query:?} should wait on the catalog");
};
let site_json = serde_json::to_string(&site).unwrap();
let expected = match &anchor {
GuardAnchor::Argument { operand, .. } => GuardsRequest {
parts: AnchorParts {
at: Some(site_json),
operand: Some(*operand),
ret: false,
function: None,
local: None,
},
from,
emit_module: Some("out.bc".into()),
},
GuardAnchor::Return { .. } => GuardsRequest {
parts: AnchorParts {
at: Some(site_json),
operand: None,
ret: true,
function: None,
local: None,
},
from,
emit_module: Some("out.bc".into()),
},
GuardAnchor::Local { function, name } => GuardsRequest {
parts: AnchorParts {
at,
operand: None,
ret: false,
function: Some(function.clone()),
local: Some(name.clone()),
},
from,
emit_module: Some("out.bc".into()),
},
};
assert_eq!(request, expected);
}
}
#[test]
fn guards_flags_parse_into_a_request() {
let queries = parse_queries(
"guards --at t.c:4 --operand 0 --from main\nguards --at t.c:4 --return\nguards --function parse --local len\nguards --function parse --local len --at t.c:9\n",
QueryModifiers::default(),
)
.unwrap();
let requests: Vec<&GuardsRequest> = queries
.iter()
.map(|(_, planned)| match planned {
Planned::Guards(request) => request,
Planned::Ready(query) => panic!("{query:?}"),
})
.collect();
assert_eq!(requests[0].parts.at.as_deref(), Some("t.c:4"));
assert_eq!(requests[0].parts.operand, Some(0));
assert_eq!(requests[0].from.as_deref(), Some("main"));
assert!(requests[1].parts.ret && requests[1].parts.operand.is_none());
assert_eq!(requests[2].parts.function.as_deref(), Some("parse"));
assert_eq!(requests[2].parts.local.as_deref(), Some("len"));
assert_eq!(requests[3].parts.at.as_deref(), Some("t.c:9"));
assert_eq!(requests[3].parts.local.as_deref(), Some("len"));
}
#[test]
fn a_guards_line_naming_no_value_is_rejected_by_its_number() {
for (input, expected) in [
("guards\n", "name a value"),
("guards --operand 0\n", "need --at"),
("guards --at t.c:4\n", "needs --operand"),
("guards --at t.c:4 --operand 0 --return\n", "give one"),
("guards --local n\n", "needs --function"),
("guards --function f\n", "needs --local"),
(
"guards --at t.c:4 --return --local n --function f\n",
"give one",
),
(
"callees a\nguards --at t.c:4 --return --emit-module o.bc\n",
"--emit-module",
),
] {
let error = parse_queries(input, QueryModifiers::default())
.map(|_| ())
.expect_err(input)
.to_string();
assert!(
error.contains("line") && error.contains(expected),
"{input:?} gave {error:?}"
);
}
}
#[test]
fn the_output_flags_select_the_renderer() {
let selected = [
&["rllvm-query", "--full", "callers", "add"][..],
&["rllvm-query", "--json", "callers", "add"],
&["rllvm-query", "callers", "add"],
]
.map(
|argv| match Format::of(&QueryArgs::try_parse_from(argv).unwrap()) {
Format::Json => None,
Format::Text(mode) => Some(mode),
},
);
assert_eq!(
selected,
[Some(TextMode::Full), None, Some(TextMode::Adaptive)]
);
}
}