use std::collections::BTreeMap;
use owo_colors::OwoColorize;
use crate::{
PathStep, QueryResult, QueryResults,
bind::BindingStatus,
facts::{CallSiteFact, CallTarget, FunctionId, SourceLocation},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextMode {
Adaptive,
Full,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Color {
Always,
Never,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Paint {
Heading,
Note,
Location,
Symbol,
Resolved,
Uncertain,
Absent,
Muted,
}
#[derive(Clone, Copy)]
struct Ctx {
mode: TextMode,
color: Color,
}
pub(crate) const MIB: u64 = 1024 * 1024;
const GIB: u64 = 1024 * MIB;
#[doc(hidden)]
pub fn human_bytes(bytes: u64) -> String {
if bytes >= GIB {
format!("{:.1} GB", bytes as f64 / GIB as f64)
} else {
format!("{:.1} MB", bytes as f64 / MIB as f64)
}
}
const NO_LOCATION: &str = "<no location>";
const NOTE: &str = "note:";
const ZERO_STEP_REACH: &str = "the origin is already the destination, reached in zero steps";
const MATCHED_NOTHING: &str = "the name or location matched nothing in the selected scope";
fn paint(text: &str, color: Color, style: Paint) -> String {
if color == Color::Never {
return text.to_string();
}
match style {
Paint::Heading => format!("{}", text.bold()),
Paint::Note => format!("{}", text.yellow()),
Paint::Location => format!("{}", text.cyan()),
Paint::Symbol => format!("{}", text.bold()),
Paint::Resolved => format!("{}", text.green()),
Paint::Uncertain => format!("{}", text.yellow()),
Paint::Absent => format!("{}", text.red()),
Paint::Muted => format!("{}", text.dimmed()),
}
}
fn serde_name<T: serde::Serialize>(value: &T) -> String {
match serde_json::to_value(value) {
Ok(serde_json::Value::String(spelling)) => spelling,
Ok(other) => format!("<unrenderable:{other}>"),
Err(err) => format!("<unrenderable:{err}>"),
}
}
const UNKNOWN: &str = "<unknown>";
fn option_or_unknown<T: std::fmt::Display>(value: &Option<T>) -> String {
match value {
Some(value) => value.to_string(),
None => UNKNOWN.to_string(),
}
}
fn name(symbols: &BTreeMap<String, String>, symbol: &str, ctx: Ctx) -> String {
let readable = match (symbols.get(symbol), ctx.mode) {
(Some(demangled), TextMode::Adaptive) => demangled.clone(),
(Some(demangled), TextMode::Full) => {
return format!(
"{} {}",
paint(demangled, ctx.color, Paint::Symbol),
paint(&format!("[{symbol}]"), ctx.color, Paint::Muted)
);
}
(None, _) => symbol.to_string(),
};
paint(&readable, ctx.color, Paint::Symbol)
}
fn location(location: Option<&SourceLocation>, ctx: Ctx) -> String {
match location {
Some(location) => paint(
&format!("{}:{}", location.file.display(), location.line),
ctx.color,
Paint::Location,
),
None => paint(NO_LOCATION, ctx.color, Paint::Muted),
}
}
fn symbol_list(symbols: &BTreeMap<String, String>, ids: &[FunctionId], ctx: Ctx) -> String {
ids.iter()
.map(|id| name(symbols, &id.symbol, ctx))
.collect::<Vec<_>>()
.join(", ")
}
fn call_target(symbols: &BTreeMap<String, String>, target: &CallTarget, ctx: Ctx) -> String {
match target {
CallTarget::Direct { callee } => format!(
"{} {}",
paint("direct", ctx.color, Paint::Resolved),
name(symbols, &callee.symbol, ctx)
),
CallTarget::Indirect {
signature,
llvm_target_bound,
} => {
let kind = paint("indirect", ctx.color, Paint::Uncertain);
let signature = paint(signature, ctx.color, Paint::Muted);
match llvm_target_bound {
Some(bound) => format!(
"{kind} {signature} {} {}",
paint("bound:", ctx.color, Paint::Resolved),
symbol_list(symbols, bound, ctx)
),
None => format!(
"{kind} {signature} {}",
paint("unresolved", ctx.color, Paint::Absent)
),
}
}
CallTarget::Intrinsic { name: intrinsic } => format!(
"{} {}",
paint("intrinsic", ctx.color, Paint::Muted),
paint(intrinsic, ctx.color, Paint::Muted)
),
CallTarget::InlineAsm => paint("inline-asm", ctx.color, Paint::Muted),
}
}
const CALL_SITE_INDENT: &str = " ";
const NO_INDENT: &str = "";
fn call_sites(
symbols: &BTreeMap<String, String>,
sites: &[CallSiteFact],
indent: &str,
ctx: Ctx,
out: &mut String,
) {
for site in sites {
out.push_str(&format!(
"{indent}{} {}\n",
location(site.location.as_ref(), ctx),
call_target(symbols, &site.target, ctx)
));
}
}
#[derive(Clone, Copy, Default)]
struct Shown {
missing_location: bool,
indirect_call_site: bool,
}
fn shown_in_sites(sites: &[CallSiteFact], shown: &mut Shown) {
for site in sites {
shown.missing_location |= site.location.is_none();
shown.indirect_call_site |= matches!(site.target, CallTarget::Indirect { .. });
}
}
fn shown_in(results: &QueryResults) -> Shown {
let mut shown = Shown::default();
match results {
QueryResults::Defs(entries) | QueryResults::FfiExports(entries) => {
shown.missing_location = entries.iter().any(|entry| entry.location.is_none());
}
QueryResults::At(entries) => {
for entry in entries {
shown_in_sites(&entry.call_sites, &mut shown);
}
}
QueryResults::Callers(entries) => {
for entry in entries {
shown_in_sites(&entry.call_sites, &mut shown);
}
}
QueryResults::Callees(sites) => shown_in_sites(sites, &mut shown),
QueryResults::Uses(uses) => {
shown.missing_location = uses
.iter()
.any(|use_fact| use_fact.in_function.is_some() && use_fact.location.is_none());
}
QueryResults::Reach(path) => {
shown.indirect_call_site = path
.iter()
.flatten()
.any(|step| matches!(step, PathStep::BoundedIndirect { .. }));
}
QueryResults::IndirectTargets(entries) => {
shown.missing_location = entries.iter().any(|entry| entry.location.is_none());
shown.indirect_call_site = !entries.is_empty();
}
QueryResults::Closure(_) | QueryResults::Externals(_) => {}
}
shown
}
fn walks_call_edges(results: &QueryResults) -> bool {
match results {
QueryResults::Callers(_) | QueryResults::Closure(_) | QueryResults::Reach(_) => true,
QueryResults::Defs(_)
| QueryResults::At(_)
| QueryResults::Callees(_)
| QueryResults::Uses(_)
| QueryResults::Externals(_)
| QueryResults::FfiExports(_)
| QueryResults::IndirectTargets(_) => false,
}
}
fn empty_meaning(results: &QueryResults) -> &'static str {
match results {
QueryResults::Reach(_) => "no path over resolved edges; not proof of unreachability",
QueryResults::Callees(_) => "the target defined no outgoing call sites",
QueryResults::Externals(_) => "every symbol in scope bound to a definition",
QueryResults::FfiExports(_) => "no function attributed to Rust exports an unmangled symbol",
QueryResults::Callers(_) => "no call to the target was found in the selected scope",
QueryResults::Uses(_) => "no non-call use of the target was found in the selected scope",
QueryResults::Closure(_) => {
"no functions were found in the selected scope for that direction"
}
QueryResults::Defs(_) | QueryResults::At(_) | QueryResults::IndirectTargets(_) => {
MATCHED_NOTHING
}
}
}
fn footer(result: &QueryResult, color: Color, out: &mut String) {
let mut notes: Vec<String> = Vec::new();
let unresolved_name = result
.resolution
.iter()
.any(|resolution| resolution.matched.is_none());
if result.results.is_empty() {
let meaning = empty_meaning(&result.results);
if !(unresolved_name && meaning == MATCHED_NOTHING) {
notes.push(meaning.to_string());
}
}
if let QueryResults::Reach(Some(steps)) = &result.results
&& steps.is_empty()
{
notes.push(ZERO_STEP_REACH.to_string());
}
for resolution in &result.resolution {
match resolution.matched {
None => notes.push(format!("'{}' matched nothing", resolution.requested)),
Some(crate::NameMatch::Fuzzy) => notes.push(format!(
"'{}' matched loosely, gathering {} symbol(s)",
resolution.requested,
resolution.symbols.len()
)),
Some(_) => {}
}
}
let analysis = &result.analysis;
let module_counts = [
("verified", analysis.verified),
("failed", analysis.failed),
("missing", analysis.missing),
("changed", analysis.changed),
("unsupported", analysis.unsupported),
("not built", analysis.not_built),
];
let unread: Vec<String> = module_counts
.iter()
.filter(|(_, count)| *count > 0)
.map(|(label, count)| format!("{count} {label}"))
.collect();
if !unread.is_empty() {
let unread_modules: usize = module_counts.iter().map(|(_, count)| count).sum();
notes.push(format!(
"{} of {} modules were not analyzed: {}",
unread_modules,
analysis.modules.len(),
unread.join(", ")
));
}
let shown = shown_in(&result.results);
let uncertainty = &result.uncertainty;
if uncertainty.indirect_call_sites > 0
&& (shown.indirect_call_site || walks_call_edges(&result.results))
{
notes.push(format!(
"{} indirect call site(s), {} with an LLVM target bound",
uncertainty.indirect_call_sites, uncertainty.sites_with_llvm_target_bound
));
}
if uncertainty.ambiguous_bindings > 0 {
notes.push(format!(
"{} ambiguous binding(s)",
uncertainty.ambiguous_bindings
));
}
if uncertainty.functions_without_location > 0 && shown.missing_location {
notes.push(format!(
"{} function(s) without a source location",
uncertainty.functions_without_location
));
}
if uncertainty.locations_from_modified_sources > 0 {
notes.push(format!(
"{} location(s) from modified sources",
uncertainty.locations_from_modified_sources
));
}
if uncertainty.conditional_path_steps > 0 {
notes.push(format!(
"{} step(s) hold only if the call takes the member the path chose",
uncertainty.conditional_path_steps
));
}
if let Some(unknown) = uncertainty.functions_of_unknown_language
&& unknown > 0
{
notes.push(format!(
"{unknown} unmangled definition(s) could not be attributed to a language and were not searched: they have no debug info, and their module's producers are mixed or absent"
));
}
if let QueryResults::IndirectTargets(results) = &result.results
&& results
.iter()
.any(|entry| entry.address_taken_inventory.is_some())
{
notes.push("address-taken candidates are signature-matched, never call edges".to_string());
}
if let QueryResults::IndirectTargets(results) = &result.results {
for assumption in results
.iter()
.filter(|entry| entry.llvm_target_bound.is_some())
.flat_map(|entry| &entry.assumptions)
{
if !notes.iter().any(|note| note == assumption) {
notes.push(assumption.clone());
}
}
}
if let Some(cache) = analysis.cache.as_ref().filter(|cache| cache.over_threshold) {
notes.push(format!(
"facts cache is {}, over query_cache_warn_mb ({} MB); prune with rllvm-query cache clear",
human_bytes(cache.disk_bytes),
cache.warn_bytes / MIB
));
}
if notes.is_empty() {
return;
}
if !out.is_empty() {
out.push('\n');
}
let prefix = paint(NOTE, color, Paint::Note);
for note in notes {
out.push_str(&format!("{prefix} {note}\n"));
}
}
fn full_sections(result: &QueryResult, color: Color, out: &mut String) {
let scope = &result.scope;
out.push_str(&format!("\n{}\n", paint("scope", color, Paint::Heading)));
out.push_str(&format!(
" entries: {} selected of {}\n",
scope.selected_entries, scope.total_entries
));
out.push_str(&format!(
" whole_program_complete: {}\n",
match scope.whole_program_complete {
Some(complete) => complete.to_string(),
None => "unstated".to_string(),
}
));
for limitation in &scope.limitations {
out.push_str(&format!(" limitation: {limitation}\n"));
}
let analysis = &result.analysis;
out.push_str(&format!("\n{}\n", paint("analysis", color, Paint::Heading)));
out.push_str(&format!(
" verified: {} analyzed: {} changed: {} missing: {}\n",
analysis.verified, analysis.analyzed, analysis.changed, analysis.missing
));
out.push_str(&format!(
" failed: {} unsupported: {} not_built: {}\n",
analysis.failed, analysis.unsupported, analysis.not_built
));
if let Some(cache) = &analysis.cache {
out.push_str(&format!(
" cache: {} hit, {} miss, {} written, {}\n",
cache.hits,
cache.misses,
cache.written,
human_bytes(cache.disk_bytes)
));
}
for module in &analysis.modules {
out.push_str(&format!(
" {} {} ir_stage: {} debug_info: {}\n",
module.id,
serde_name(&module.status),
option_or_unknown(&module.ir_stage),
option_or_unknown(&module.debug_info)
));
}
let uncertainty = &result.uncertainty;
out.push_str(&format!(
"\n{}\n",
paint("uncertainty", color, Paint::Heading)
));
out.push_str(&format!(
" indirect_call_sites: {}\n sites_with_llvm_target_bound: {}\n",
uncertainty.indirect_call_sites, uncertainty.sites_with_llvm_target_bound
));
out.push_str(&format!(
" functions_without_location: {}\n locations_from_modified_sources: {}\n",
uncertainty.functions_without_location, uncertainty.locations_from_modified_sources
));
out.push_str(&format!(
" ambiguous_bindings: {}\n conditional_path_steps: {}\n",
uncertainty.ambiguous_bindings, uncertainty.conditional_path_steps
));
if let Some(unknown) = uncertainty.functions_of_unknown_language {
out.push_str(&format!(" functions_of_unknown_language: {unknown}\n"));
}
let full = Ctx {
mode: TextMode::Full,
color,
};
for binding in &uncertainty.frontier {
let tint = match binding.status {
BindingStatus::Unique => Paint::Resolved,
BindingStatus::Ambiguous => Paint::Uncertain,
BindingStatus::Unbound => Paint::Absent,
};
out.push_str(&format!(
" frontier: {} ({}, {} candidate(s))\n",
name(&result.symbols, &binding.symbol, full),
paint(&serde_name(&binding.status), color, tint),
binding.candidates.len()
));
}
let provenance = &result.provenance;
out.push_str(&format!(
"\n{}\n",
paint("provenance", color, Paint::Heading)
));
let origin = &provenance.catalog_origin;
out.push_str(&format!(
" catalog: {} {}\n",
origin.kind,
origin.input.display()
));
if let Some(sha256) = &origin.sha256 {
out.push_str(&format!(" catalog_sha256: {sha256}\n"));
}
out.push_str(&format!(" llvm: {}\n", provenance.llvm_version));
out.push_str(&format!(
" rllvm-query: {}\n",
provenance.rllvm_query_version
));
}
pub fn render(result: &QueryResult, mode: TextMode, color: Color) -> String {
let ctx = Ctx { mode, color };
let mut out = String::new();
render_results(result, ctx, &mut out);
footer(result, color, &mut out);
if mode == TextMode::Full {
full_sections(result, color, &mut out);
}
out
}
fn render_results(result: &QueryResult, ctx: Ctx, out: &mut String) {
let symbols = &result.symbols;
match &result.results {
QueryResults::Defs(entries) | QueryResults::FfiExports(entries) => {
for entry in entries {
out.push_str(&format!(
"{} {}\n",
location(entry.location.as_ref(), ctx),
name(symbols, &entry.function.symbol, ctx)
));
}
}
QueryResults::Closure(functions) => {
for function in functions {
out.push_str(&format!("{}\n", name(symbols, &function.symbol, ctx)));
}
}
QueryResults::Externals(bindings) => {
for binding in bindings {
let tint = match binding.status {
BindingStatus::Unique => Paint::Resolved,
BindingStatus::Ambiguous => Paint::Uncertain,
BindingStatus::Unbound => Paint::Absent,
};
out.push_str(&format!(
"{} {}\n",
name(symbols, &binding.symbol, ctx),
paint(&serde_name(&binding.status), ctx.color, tint)
));
}
}
QueryResults::At(entries) => {
for entry in entries {
out.push_str(&format!("{}\n", name(symbols, &entry.function.symbol, ctx)));
call_sites(symbols, &entry.call_sites, CALL_SITE_INDENT, ctx, out);
}
}
QueryResults::Callers(entries) => {
for entry in entries {
out.push_str(&format!("{}\n", name(symbols, &entry.function.symbol, ctx)));
call_sites(symbols, &entry.call_sites, CALL_SITE_INDENT, ctx, out);
}
}
QueryResults::Callees(sites) => call_sites(symbols, sites, NO_INDENT, ctx, out),
QueryResults::Uses(uses) => {
for use_fact in uses {
let holder = match (&use_fact.in_function, &use_fact.in_global) {
(Some(id), _) => name(symbols, &id.symbol, ctx),
(None, Some(global)) => name(symbols, global, ctx),
(None, None) => paint("<no function>", ctx.color, Paint::Muted),
};
out.push_str(&format!(
"{} in {} at {}\n",
paint(&serde_name(&use_fact.kind), ctx.color, Paint::Uncertain),
holder,
location(use_fact.location.as_ref(), ctx)
));
}
}
QueryResults::Reach(path) => {
for step in path.iter().flatten() {
match step {
PathStep::Call(site) => out.push_str(&format!(
"{} {}\n",
paint("call", ctx.color, Paint::Resolved),
name(symbols, &site.function.symbol, ctx)
)),
PathStep::BoundedIndirect {
site,
chosen,
bound,
} => {
out.push_str(&format!(
"{} {} chose {} of {}\n",
paint("bounded-indirect", ctx.color, Paint::Uncertain),
name(symbols, &site.function.symbol, ctx),
name(symbols, &chosen.symbol, ctx),
symbol_list(symbols, bound, ctx)
));
}
PathStep::Binding(binding) => {
let tint = match binding.status {
BindingStatus::Unique => Paint::Resolved,
BindingStatus::Ambiguous => Paint::Uncertain,
BindingStatus::Unbound => Paint::Absent,
};
out.push_str(&format!(
"{} {} ({}, {} candidate(s))\n",
paint("binding", ctx.color, tint),
name(symbols, &binding.symbol, ctx),
paint(&serde_name(&binding.status), ctx.color, tint),
binding.candidates.len()
));
}
PathStep::Alias { alias, target } => out.push_str(&format!(
"{} {} (of {})\n",
paint("alias", ctx.color, Paint::Resolved),
name(symbols, &alias.symbol, ctx),
name(symbols, &target.symbol, ctx)
)),
}
}
}
QueryResults::IndirectTargets(results) => {
for entry in results {
out.push_str(&format!(
"{} {}\n",
location(entry.location.as_ref(), ctx),
paint(&entry.signature, ctx.color, Paint::Muted)
));
match (&entry.llvm_target_bound, entry.unresolved) {
(Some(bound), _) => out.push_str(&format!(
" {} {}\n",
paint("bound:", ctx.color, Paint::Resolved),
symbol_list(symbols, bound, ctx)
)),
(None, true) => out.push_str(&format!(
" {}\n",
paint("unresolved", ctx.color, Paint::Absent)
)),
(None, false) => {}
}
if let (Some(inventory), Some(compatible)) =
(&entry.address_taken_inventory, &entry.signature_compatible)
{
out.push_str(&format!(
" {}\n",
paint(
&format!(
"address-taken candidates: {} of {} address-taken function(s) match the signature",
compatible.len(),
inventory.len()
),
ctx.color,
Paint::Uncertain
)
));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
CacheReport, Query,
facts::{FunctionFact, Language, Linkage, ModuleAnalysis, ModuleReport, UseFact, UseKind},
index::{Direction, Session},
run,
testing::*,
};
#[test]
fn defs_prints_one_line_per_definition() {
let session = session_with_cxx_symbols();
let result = run(
&session,
&Query::Defs {
name: "main".into(),
},
)
.unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert_eq!(text.split("\n\n").next().unwrap(), "<no location> main");
}
#[test]
fn a_mangled_symbol_prints_its_demangled_reading() {
let session = session_with_cxx_symbols();
let result = run(
&session,
&Query::Defs {
name: "_Z5twiceIiET_S0_".into(),
},
)
.unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(text.contains("int twice<int>(int)"), "got: {text}");
assert!(!text.contains("_Z5twiceIiET_S0_"), "mangled leaked: {text}");
}
#[test]
fn full_mode_keeps_the_mangled_symbol_beside_the_reading() {
let session = session_with_cxx_symbols();
let result = run(
&session,
&Query::Defs {
name: "_Z5twiceIiET_S0_".into(),
},
)
.unwrap();
let text = render(&result, TextMode::Full, Color::Never);
assert!(text.contains("int twice<int>(int)"), "got: {text}");
assert!(text.contains("[_Z5twiceIiET_S0_]"), "got: {text}");
}
#[test]
fn an_externals_answer_with_no_unbound_symbols_explains_itself_in_the_footer() {
let session = session_from(&[("a", "b")]);
let result = run(&session, &Query::Externals).unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(text.contains("every symbol in scope bound to a definition"));
assert!(text.starts_with(NOTE), "leading blank line: {text:?}");
}
#[test]
fn color_never_emits_no_escape_codes() {
for style in [
Paint::Heading,
Paint::Note,
Paint::Location,
Paint::Symbol,
Paint::Resolved,
Paint::Uncertain,
Paint::Absent,
Paint::Muted,
] {
assert_eq!(paint("x", Color::Never, style), "x");
assert!(
paint("x", Color::Always, style).contains('\u{1b}'),
"{style:?} produced no escape code"
);
}
}
#[test]
fn a_coloured_answer_carries_the_same_words_as_a_plain_one() {
let session = session_with_bounded_indirect();
let result = run(&session, &Query::Callees { name: "a".into() }).unwrap();
let plain = render(&result, TextMode::Adaptive, Color::Never);
let coloured = render(&result, TextMode::Adaptive, Color::Always);
assert!(coloured.contains('\u{1b}'), "nothing was coloured");
let stripped: String = {
let mut out = String::new();
let mut chars = coloured.chars();
while let Some(c) = chars.next() {
if c == '\u{1b}' {
for c in chars.by_ref() {
if c == 'm' {
break;
}
}
} else {
out.push(c);
}
}
out
};
assert_eq!(stripped, plain);
}
#[test]
fn callers_reports_each_call_site_with_its_location() {
let caller = function("m", "a", true, Linkage::Internal);
let callee = function("m", "b", true, Linkage::Internal);
let mut site = direct_call(&caller, &callee, 0);
site.location = Some(source_location("caller.c", 7));
let session = Session::new(facts(vec![caller, callee], vec![site]), Vec::new());
let result = run(&session, &Query::Callers { name: "b".into() }).unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(text.contains("caller.c:7"), "got: {text}");
}
#[test]
fn callees_names_the_target_kind() {
let session = session_with_bounded_indirect();
let result = run(&session, &Query::Callees { name: "a".into() }).unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(
text.lines()
.any(|line| line.contains("indirect") && line.contains("i32 (i32, i32)")),
"no rendered call-site row: {text}"
);
}
#[test]
fn callees_rows_start_at_column_zero_and_callers_rows_nest() {
let session = session_with_bounded_indirect();
let text_of = |query| {
render(
&run(&session, &query).unwrap(),
TextMode::Adaptive,
Color::Never,
)
};
let callees = text_of(Query::Callees { name: "a".into() });
let row = callees.lines().next().unwrap_or_default();
assert!(row.contains("indirect"), "no row rendered: {callees}");
assert!(!row.starts_with(' '), "callees row is indented: {row:?}");
let callers = text_of(Query::Callers {
name: "target".into(),
});
let mut lines = callers.lines();
assert_eq!(lines.next(), Some("a"), "got: {callers}");
let nested = lines.next().unwrap_or_default();
assert!(
nested.starts_with(" ") && nested.contains("indirect"),
"caller call site must stay nested: {callers}"
);
}
#[test]
fn at_lists_the_functions_mapped_to_a_line() {
let session = session_from_source_lines(&[("parser.c", 4)]);
let result = run(
&session,
&Query::At {
file: "parser.c".into(),
line: 4,
},
)
.unwrap();
assert!(render(&result, TextMode::Adaptive, Color::Never).contains("only"));
}
#[test]
fn a_found_path_prints_its_steps_in_order() {
let session = session_from(&[("a", "b")]);
let result = run(
&session,
&Query::Reach {
from: "a".into(),
to: "b".into(),
},
)
.unwrap();
assert!(render(&result, TextMode::Adaptive, Color::Never).contains("call"));
}
#[test]
fn a_zero_step_path_says_the_origin_is_already_the_destination() {
let session = session_from(&[("a", "b")]);
let result = run(
&session,
&Query::Reach {
from: "a".into(),
to: "a".into(),
},
)
.unwrap();
assert!(
matches!(&result.results, QueryResults::Reach(Some(steps)) if steps.is_empty()),
"fixture changed: expected a found path with no steps"
);
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(text.contains("reached in zero steps"), "got: {text:?}");
assert!(
!text.contains("not proof of unreachability"),
"a found path must not read as an absent one: {text:?}"
);
}
#[test]
fn a_conditional_step_says_it_is_conditional() {
let session = session_with_bounded_indirect();
let result = run(
&session,
&Query::Reach {
from: "a".into(),
to: "target".into(),
},
)
.unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(text.contains("bounded-indirect"), "got: {text}");
}
#[test]
fn indirect_targets_prints_the_signature_and_says_unresolved() {
let session = session_with_address_taken_function();
let result = run(
&session,
&Query::IndirectTargets {
at: "t.c:4".into(),
heuristics: false,
},
)
.unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(text.contains("unresolved"), "got: {text}");
assert!(
!text.contains("only within the captured scope"),
"got: {text}"
);
}
#[test]
fn heuristic_candidates_count_only_signature_matches() {
let session = session_with_address_taken_function();
let result = run(
&session,
&Query::IndirectTargets {
at: "t.c:4".into(),
heuristics: true,
},
)
.unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(
text.contains("address-taken candidates: 1 of 2 address-taken function(s) match"),
"got: {text}"
);
}
#[test]
fn an_indirect_bound_says_it_holds_only_in_the_captured_scope() {
let session = session_with_bounded_indirect();
let result = run(
&session,
&Query::IndirectTargets {
at: "t.c:9".into(),
heuristics: false,
},
)
.unwrap();
assert!(
matches!(&result.results, QueryResults::IndirectTargets(entries)
if entries.iter().any(|entry| entry.llvm_target_bound.is_some())),
"fixture changed: expected a bounded site at t.c:9"
);
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(text.contains("bound:"), "got: {text}");
assert!(
text.contains("only within the captured scope"),
"a text reader must be told the bound is scope-local: {text}"
);
}
#[test]
fn an_absent_path_says_what_absence_means() {
let session = session_with_indirect_gap();
let result = run(
&session,
&Query::Reach {
from: "a".into(),
to: "c".into(),
},
)
.unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(text.contains("no path over resolved edges"), "got: {text}");
assert!(text.contains("not proof of unreachability"), "got: {text}");
}
#[test]
fn a_nonzero_uncertainty_count_reaches_the_reader() {
let session = session_with_ambiguous_bindings();
let result = run(&session, &Query::Externals).unwrap();
assert!(result.uncertainty.ambiguous_bindings > 0, "fixture changed");
assert!(
render(&result, TextMode::Adaptive, Color::Never).contains("ambiguous binding"),
"a non-zero ambiguity must not be silent"
);
}
#[test]
fn the_missing_location_note_follows_the_answer_not_the_program() {
let located = FunctionFact {
location: Some(source_location("a.c", 1)),
..function("m", "located", true, Linkage::Internal)
};
let unlocated = function("m", "unlocated", true, Linkage::Internal);
let session = Session::new(facts(vec![located, unlocated], vec![]), Vec::new());
let defs_of = |name: &str| {
let result = run(&session, &Query::Defs { name: name.into() }).unwrap();
assert!(result.uncertainty.functions_without_location > 0, "fixture");
render(&result, TextMode::Adaptive, Color::Never)
};
let quiet = defs_of("located");
assert!(
!quiet.contains("without a source location"),
"an answer that shows no missing location must not carry the note: {quiet:?}"
);
let loud = defs_of("unlocated");
assert!(loud.contains(NO_LOCATION), "got: {loud:?}");
assert!(loud.contains("without a source location"), "got: {loud:?}");
}
#[test]
fn a_use_in_a_global_initializer_does_not_blame_functions_for_its_location() {
let run_fn = function("m", "run", true, Linkage::Internal);
let mut base = facts(vec![run_fn.clone()], vec![]);
base.uses = vec![UseFact {
used: run_fn.id,
in_function: None,
in_global: Some("table".into()),
location: None,
kind: UseKind::GlobalInitializer,
}];
let session = Session::new(base, Vec::new());
let result = run(&session, &Query::Uses { name: "run".into() }).unwrap();
assert!(result.uncertainty.functions_without_location > 0, "fixture");
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(text.contains("in table"), "got: {text:?}");
assert!(
!text.contains("without a source location"),
"a global's use has no function to lack a location: {text:?}"
);
let session = session_with_address_taken_function();
let result = run(&session, &Query::Uses { name: "add".into() }).unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(text.contains("without a source location"), "got: {text:?}");
}
#[test]
fn the_indirect_call_note_follows_the_answer_not_the_program() {
let session = session_with_bounded_indirect();
let text_of = |query| {
let result = run(&session, &query).unwrap();
assert!(result.uncertainty.indirect_call_sites > 0, "fixture");
render(&result, TextMode::Adaptive, Color::Never)
};
let callees = text_of(Query::Callees { name: "a".into() });
assert!(callees.contains("indirect call site"), "got: {callees:?}");
let defs = text_of(Query::Defs {
name: "target".into(),
});
assert!(
!defs.contains("indirect call site"),
"an answer with no indirect site must not carry the note: {defs:?}"
);
}
#[test]
fn a_walk_over_call_edges_carries_the_indirect_note_it_shows_no_row_for() {
let session = session_with_indirect_gap();
let text_of = |query| {
let result = run(&session, &query).unwrap();
assert!(
result.uncertainty.indirect_call_sites > 0
&& result.uncertainty.sites_with_llvm_target_bound == 0,
"fixture changed: expected an unbounded indirect site"
);
assert!(!result.results.is_empty(), "fixture changed: empty answer");
render(&result, TextMode::Adaptive, Color::Never)
};
let callers = text_of(Query::Callers { name: "b".into() });
assert!(
!callers
.lines()
.any(|line| !line.starts_with(NOTE) && line.contains("indirect")),
"fixture changed: expected only direct rows: {callers:?}"
);
assert!(callers.contains("indirect call site"), "got: {callers:?}");
let closure = text_of(Query::Closure {
name: "a".into(),
direction: Direction::Out,
});
assert!(closure.contains("indirect call site"), "got: {closure:?}");
}
#[test]
fn an_answer_that_walks_no_call_edges_still_omits_the_indirect_note() {
let session = session_with_indirect_gap();
let text_of = |query| {
let result = run(&session, &query).unwrap();
assert!(result.uncertainty.indirect_call_sites > 0, "fixture");
render(&result, TextMode::Adaptive, Color::Never)
};
let defs = text_of(Query::Defs { name: "a".into() });
assert!(!defs.contains("indirect call site"), "got: {defs:?}");
let externals = text_of(Query::Externals);
assert!(
!externals.contains("indirect call site"),
"got: {externals:?}"
);
}
#[test]
fn a_clean_answer_prints_no_footer() {
let text = render(&clean_callers_answer(), TextMode::Adaptive, Color::Never);
assert!(
!text.contains("note:"),
"clean answer gained a footer: {text}"
);
}
fn clean_callers_answer() -> QueryResult {
let caller = FunctionFact {
location: Some(source_location("a.c", 1)),
..function("m", "a", true, Linkage::Internal)
};
let callee = FunctionFact {
location: Some(source_location("a.c", 2)),
..function("m", "b", true, Linkage::Internal)
};
let site = direct_call(&caller, &callee, 0);
let session = Session::new(facts(vec![caller, callee], vec![site]), Vec::new());
run(&session, &Query::Callers { name: "b".into() }).unwrap()
}
#[test]
fn a_cache_past_its_threshold_says_how_to_prune() {
let mut result = clean_callers_answer();
result.analysis.cache = Some(CacheReport {
disk_bytes: 2 * MIB,
warn_bytes: MIB,
over_threshold: true,
..Default::default()
});
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(
text.contains(
"facts cache is 2.0 MB, over query_cache_warn_mb (1 MB); prune with rllvm-query cache clear"
),
"got: {text}"
);
}
#[test]
fn a_cache_under_its_threshold_prints_no_note() {
let mut result = clean_callers_answer();
result.analysis.cache = Some(CacheReport {
disk_bytes: MIB / 2,
warn_bytes: MIB,
over_threshold: false,
..Default::default()
});
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(!text.contains("facts cache"), "got: {text}");
}
#[test]
fn a_failed_module_is_reported_even_when_results_are_present() {
let mut facts = facts_with_one_failed_module();
facts
.functions
.push(function("a", "present", true, Linkage::Internal));
let result = run(
&Session::new(facts, vec![]),
&Query::Defs {
name: "present".into(),
},
)
.unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(text.contains("<no location> present"), "got: {text}");
assert!(text.contains("failed"), "got: {text}");
}
#[test]
fn several_unread_categories_sum_to_the_total_modules_not_read() {
let mut facts = facts(vec![], vec![]);
facts.scope.total_entries = 5;
facts.scope.selected_entries = 5;
facts.modules = vec![
report("f1", ModuleAnalysis::Failed),
report("f2", ModuleAnalysis::Failed),
report("f3", ModuleAnalysis::Failed),
report("m1", ModuleAnalysis::Missing),
report("m2", ModuleAnalysis::Missing),
];
let result = run(&Session::new(facts, vec![]), &Query::Externals).unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(
text.contains("5 of 5 modules were not analyzed: 3 failed, 2 missing"),
"got: {text}"
);
}
#[test]
fn a_verified_but_unextracted_module_is_reported() {
let facts = facts_with_one_verified_module();
let result = run(&Session::new(facts, vec![]), &Query::Externals).unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(text.contains("1 verified"), "got: {text}");
}
#[test]
fn full_mode_prints_scope_analysis_and_provenance() {
let session = session_from(&[("a", "b")]);
let result = run(&session, &Query::Callers { name: "b".into() }).unwrap();
let text = render(&result, TextMode::Full, Color::Never);
for heading in ["scope", "analysis", "uncertainty", "provenance"] {
assert!(text.contains(heading), "missing {heading}: {text}");
}
}
#[test]
fn full_mode_prints_the_catalog_origin_scope_completeness_and_ir_stage() {
let mut facts = facts(vec![], vec![]);
facts.scope.whole_program_complete = Some(false);
facts.modules = vec![ModuleReport {
ir_stage: Some("linked".into()),
debug_info: Some(true),
..report("m", ModuleAnalysis::Analyzed)
}];
let result = run(&Session::new(facts, vec![]), &Query::Externals).unwrap();
let text = render(&result, TextMode::Full, Color::Never);
assert!(
text.contains("whole_program_complete: false"),
"got: {text}"
);
assert!(
text.contains("ir_stage: linked debug_info: true"),
"got: {text}"
);
assert!(text.contains("catalog: test"), "got: {text}");
}
#[test]
fn full_mode_prints_unknown_for_absent_ir_stage_and_debug_info() {
let mut facts = facts(vec![], vec![]);
facts.modules = vec![report("m", ModuleAnalysis::Analyzed)];
let result = run(&Session::new(facts, vec![]), &Query::Externals).unwrap();
let text = render(&result, TextMode::Full, Color::Never);
assert!(
text.contains("ir_stage: <unknown> debug_info: <unknown>"),
"got: {text}"
);
assert!(!text.contains("None"), "got: {text}");
}
#[test]
fn full_mode_prints_zero_counts_the_footer_omits() {
let session = session_from(&[("a", "b")]);
let result = run(&session, &Query::Callers { name: "b".into() }).unwrap();
assert!(render(&result, TextMode::Full, Color::Never).contains("ambiguous_bindings: 0"));
}
#[test]
fn full_mode_prints_the_frontier_of_ambiguous_bindings() {
let session = session_with_ambiguous_bindings();
let result = run(&session, &Query::Externals).unwrap();
assert!(
!result.uncertainty.frontier.is_empty(),
"fixture changed: expected a non-empty frontier"
);
let text = render(&result, TextMode::Full, Color::Never);
assert!(text.contains("frontier: target"), "got: {text}");
assert!(text.contains("ambiguous"), "got: {text}");
assert!(text.contains("2 candidate(s))"), "got: {text}");
}
#[test]
fn a_binding_status_reads_the_same_word_in_text_as_in_json() {
let session = session_with_ambiguous_bindings();
let result = run(&session, &Query::Externals).unwrap();
let binding = result
.uncertainty
.frontier
.iter()
.find(|binding| binding.symbol == "target")
.expect("fixture changed: expected a frontier entry for `target`");
let envelope_spelling = match serde_json::to_value(binding.status) {
Ok(serde_json::Value::String(spelling)) => spelling,
other => panic!("BindingStatus did not serialize to a JSON string: {other:?}"),
};
let text = render(&result, TextMode::Full, Color::Never);
let frontier_line = text
.lines()
.find(|line| line.trim_start().starts_with("frontier: target"))
.expect("fixture changed: expected a frontier line for `target`");
assert!(
frontier_line.contains(&envelope_spelling),
"text `{frontier_line}` does not carry the envelope's `{envelope_spelling}`"
);
}
#[test]
fn a_name_that_matched_nothing_says_so_once() {
let session = session_from(&[("a", "b")]);
let result = run(
&session,
&Query::Defs {
name: "absent".into(),
},
)
.unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(text.contains("'absent' matched nothing"), "got: {text}");
assert_eq!(
text.lines().filter(|line| line.starts_with(NOTE)).count(),
1,
"one caveat, stated once: {text}"
);
}
#[test]
fn a_loosely_matched_name_says_how_many_symbols_it_gathered() {
let session = session_with_cxx_symbols();
let result = run(
&session,
&Query::Defs {
name: "twice".into(),
},
)
.unwrap();
let text = render(&result, TextMode::Adaptive, Color::Never);
assert!(
text.contains("'twice' matched loosely, gathering 3 symbol(s)"),
"got: {text}"
);
}
#[test]
fn an_empty_callers_answer_says_no_call_was_found() {
let session = session_from(&[("a", "b")]);
let result = run(&session, &Query::Callers { name: "a".into() }).unwrap();
assert!(result.results.is_empty(), "fixture changed");
assert!(
render(&result, TextMode::Adaptive, Color::Never)
.contains("no call to the target was found in the selected scope")
);
}
#[test]
fn an_empty_uses_answer_says_no_non_call_use_was_found() {
let session = session_from(&[("a", "b")]);
let result = run(&session, &Query::Uses { name: "a".into() }).unwrap();
assert!(result.results.is_empty(), "fixture changed");
assert!(
render(&result, TextMode::Adaptive, Color::Never)
.contains("no non-call use of the target was found in the selected scope")
);
}
fn ffi_exports_text(functions: Vec<FunctionFact>) -> String {
let session = Session::new(facts(functions, vec![]), vec![]);
let result = run(&session, &Query::FfiExports).unwrap();
render(&result, TextMode::Adaptive, Color::Never)
}
#[test]
fn an_ffi_exports_answer_names_what_it_could_not_attribute() {
let text = ffi_exports_text(vec![attributed("mystery", Linkage::External, None)]);
assert!(
text.contains(
"1 unmangled definition(s) could not be attributed to a language and were not searched: they have no debug info"
),
"got: {text}"
);
assert!(
text.contains("no function attributed to Rust exports an unmangled symbol"),
"got: {text}"
);
}
#[test]
fn an_ffi_exports_row_prints_like_a_definition() {
let mut export = attributed("lib_add", Linkage::External, Some(Language::Rust));
export.location = Some(source_location("lib.rs", 3));
let text = ffi_exports_text(vec![export]);
assert_eq!(
text.lines().next().unwrap(),
"lib.rs:3 lib_add",
"got: {text}"
);
}
#[test]
fn an_empty_closure_answer_says_no_functions_were_found() {
let session = session_from(&[("a", "b")]);
let result = run(
&session,
&Query::Closure {
name: "b".into(),
direction: Direction::Out,
},
)
.unwrap();
assert!(result.results.is_empty(), "fixture changed");
assert!(
render(&result, TextMode::Adaptive, Color::Never)
.contains("no functions were found in the selected scope for that direction")
);
}
#[test]
fn byte_counts_print_in_mb_below_a_gib_and_gb_above() {
assert_eq!(human_bytes(0), "0.0 MB");
assert_eq!(human_bytes(3_774_873), "3.6 MB");
assert_eq!(human_bytes(GIB - 1), "1024.0 MB");
assert_eq!(human_bytes(GIB), "1.0 GB");
assert_eq!(human_bytes(1_363_148_800), "1.3 GB");
}
}