#![warn(unreachable_pub)]
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
path::{Path, PathBuf},
};
use serde::Serialize;
use rllvm_core::{
catalog::{CatalogOrigin, CatalogScope, ModuleCatalog},
error::Error,
};
#[doc(hidden)]
pub mod cli;
pub mod extract;
pub use extract::{ModuleFacts, llvm_version};
pub mod facts;
pub use facts::*;
pub mod load;
pub mod bind;
pub use bind::{BindingCandidate, BindingStatus, SymbolBinding};
pub mod index;
pub use index::{Direction, NameMatch, NameResolution, PathStep, ReachResult, Session};
pub mod mcp;
#[cfg(test)]
pub(crate) mod testing;
#[derive(Clone, Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Query {
Defs { name: String },
At { file: String, line: u32 },
Callers { name: String },
Callees { name: String },
Uses { name: String },
Reach { from: String, to: String },
Closure { name: String, direction: Direction },
Externals,
IndirectTargets { at: String, heuristics: bool },
}
#[derive(Debug, Serialize)]
pub struct DefEntry {
pub function: FunctionId,
pub configuration_id: Option<String>,
pub location: Option<SourceLocation>,
}
#[derive(Debug, Serialize)]
pub struct AtEntry {
pub function: FunctionId,
pub call_sites: Vec<CallSiteFact>,
}
#[derive(Debug, Serialize)]
pub struct CallerEntry {
pub function: FunctionId,
pub call_sites: Vec<CallSiteFact>,
}
#[derive(Debug, Serialize)]
pub struct IndirectTargetsResult {
pub site: CallSiteId,
pub location: Option<SourceLocation>,
pub signature: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub llvm_target_bound: Option<Vec<FunctionId>>,
pub unresolved: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub address_taken_inventory: Option<Vec<FunctionId>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature_compatible: Option<Vec<FunctionId>>,
pub assumptions: Vec<String>,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum QueryResults {
Defs(Vec<DefEntry>),
At(Vec<AtEntry>),
Callers(Vec<CallerEntry>),
Callees(Vec<CallSiteFact>),
Uses(Vec<UseFact>),
Reach(Option<Vec<PathStep>>),
Closure(Vec<FunctionId>),
Externals(Vec<SymbolBinding>),
IndirectTargets(Vec<IndirectTargetsResult>),
}
impl QueryResults {
pub fn is_empty(&self) -> bool {
match self {
QueryResults::Defs(items) => items.is_empty(),
QueryResults::At(items) => items.is_empty(),
QueryResults::Callers(items) => items.is_empty(),
QueryResults::Callees(items) => items.is_empty(),
QueryResults::Uses(items) => items.is_empty(),
QueryResults::Reach(path) => path.is_none(),
QueryResults::Closure(items) => items.is_empty(),
QueryResults::Externals(items) => items.is_empty(),
QueryResults::IndirectTargets(items) => items.is_empty(),
}
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct Analysis {
pub verified: usize,
pub analyzed: usize,
pub changed: usize,
pub missing: usize,
pub failed: usize,
pub unsupported: usize,
pub not_built: usize,
pub modules: Vec<ModuleReport>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct Uncertainty {
pub indirect_call_sites: usize,
pub sites_with_llvm_target_bound: usize,
pub functions_without_location: usize,
pub locations_from_modified_sources: usize,
pub ambiguous_bindings: usize,
pub frontier: Vec<SymbolBinding>,
pub conditional_path_steps: usize,
}
#[derive(Clone, Debug, Serialize)]
pub struct Resolution {
pub requested: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub matched: Option<NameMatch>,
pub symbols: Vec<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct Provenance {
pub catalog_origin: CatalogOrigin,
pub llvm_version: String,
pub rllvm_query_version: String,
}
#[derive(Debug, Serialize)]
pub struct QueryResult {
pub schema_version: u32,
pub query: Query,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub resolution: Vec<Resolution>,
pub results: QueryResults,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub symbols: BTreeMap<String, String>,
pub scope: CatalogScope,
pub analysis: Analysis,
pub uncertainty: Uncertainty,
pub provenance: Provenance,
}
pub fn open(catalog: &Path) -> Result<Session, Error> {
session_from_loaded(load::load_catalog(catalog)?)
}
pub fn open_catalog(catalog: ModuleCatalog, catalog_dir: &Path) -> Result<Session, Error> {
session_from_loaded(load::load_catalog_value(catalog, catalog_dir)?)
}
fn session_from_loaded(loaded: load::Loaded) -> Result<Session, Error> {
let configurations: HashMap<String, Option<String>> = loaded
.pending
.iter()
.map(|module| (module.id.clone(), module.record.configuration_id.clone()))
.collect();
let mut functions: Vec<FunctionFact> = Vec::new();
let mut call_sites: Vec<CallSiteFact> = Vec::new();
let mut uses: Vec<UseFact> = Vec::new();
let mut reports = loaded.reports.clone();
let unreadable = load::for_each_module(&loaded, |module| {
match extract::extract(&module, &loaded.source_status) {
Ok(facts) => {
if let Some(report) = reports.iter_mut().find(|report| report.id == module.id) {
report.status = ModuleAnalysis::Analyzed;
if !facts.diagnostics.is_empty() {
let joined = facts.diagnostics.join("; ");
report.diagnostic = Some(match report.diagnostic.take() {
Some(existing) => format!("{existing}; {joined}"),
None => joined,
});
}
}
functions.extend(facts.functions);
call_sites.extend(facts.call_sites);
uses.extend(facts.uses);
}
Err(error) => {
tracing::warn!(module = %module.id, %error, "module failed to extract");
record_failure(&mut reports, &module.id, error.to_string());
}
}
Ok(())
})?;
for (id, error) in unreadable {
tracing::warn!(module = %id, %error, "module could not be read");
record_failure(&mut reports, &id, error.to_string());
}
let bindings = bind::bind(&functions, &configurations);
let facts = ProgramFacts {
functions,
call_sites,
uses,
scope: loaded.scope.clone(),
origin: loaded.origin.clone(),
modules: reports,
};
drop(loaded);
Ok(Session::new(facts, bindings))
}
pub fn run(session: &Session, query: &Query) -> Result<QueryResult, Error> {
let mut reach_frontier: Option<Vec<SymbolBinding>> = None;
let mut conditional_path_steps = 0;
let results = match query {
Query::Defs { name } => QueryResults::Defs(defs(session, name)),
Query::At { file, line } => QueryResults::At(at_entries(session, Path::new(file), *line)),
Query::Callers { name } => QueryResults::Callers(callers(session, name)),
Query::Callees { name } => QueryResults::Callees(session.callees(name)),
Query::Uses { name } => QueryResults::Uses(uses_of(session, name)),
Query::Reach { from, to } => {
let reach = session.reach(from, to);
reach_frontier = Some(reach.frontier);
conditional_path_steps = reach
.path
.iter()
.flatten()
.filter(|step| matches!(step, PathStep::BoundedIndirect { .. }))
.count();
QueryResults::Reach(reach.path)
}
Query::Closure { name, direction } => {
QueryResults::Closure(session.closure(name, *direction))
}
Query::Externals => QueryResults::Externals(externals(session)),
Query::IndirectTargets { at, heuristics } => {
QueryResults::IndirectTargets(indirect_targets(session, at, *heuristics)?)
}
};
let frontier = reach_frontier.unwrap_or_else(|| ambiguous_bindings(session));
let symbols = symbols_in(session, &results, &frontier);
Ok(QueryResult {
schema_version: 2,
query: query.clone(),
resolution: query
.names()
.into_iter()
.map(|name| resolution_of(session, name))
.collect(),
results,
symbols,
scope: session.scope().clone(),
analysis: analysis_of(session.modules()),
uncertainty: uncertainty_of(session, frontier, conditional_path_steps),
provenance: Provenance {
catalog_origin: session.origin().clone(),
llvm_version: llvm_version(),
rllvm_query_version: env!("CARGO_PKG_VERSION").to_string(),
},
})
}
fn resolution_of(session: &Session, name: &str) -> Resolution {
let resolved = session.resolve(name);
let mut symbols: Vec<String> = resolved
.iter()
.flat_map(|resolution| &resolution.ids)
.map(|id| id.symbol.clone())
.collect();
symbols.sort_unstable();
symbols.dedup();
Resolution {
requested: name.to_string(),
matched: resolved.map(|resolution| resolution.matched),
symbols,
}
}
fn symbols_in(
session: &Session,
results: &QueryResults,
frontier: &[SymbolBinding],
) -> BTreeMap<String, String> {
let mut names = BTreeSet::new();
results.collect_symbols(&mut names);
for binding in frontier {
collect_binding(binding, &mut names);
}
names
.into_iter()
.filter_map(|name| {
session
.demangled(&name)
.map(|reading| (name, reading.to_string()))
})
.collect()
}
impl QueryResults {
fn collect_symbols(&self, into: &mut BTreeSet<String>) {
match self {
QueryResults::Defs(entries) => {
for entry in entries {
collect_function(&entry.function, into);
}
}
QueryResults::At(entries) => {
for entry in entries {
collect_function(&entry.function, into);
collect_call_sites(&entry.call_sites, into);
}
}
QueryResults::Callers(entries) => {
for entry in entries {
collect_function(&entry.function, into);
collect_call_sites(&entry.call_sites, into);
}
}
QueryResults::Callees(sites) => collect_call_sites(sites, into),
QueryResults::Uses(uses) => {
for use_fact in uses {
collect_function(&use_fact.used, into);
if let Some(id) = &use_fact.in_function {
collect_function(id, into);
}
}
}
QueryResults::Reach(path) => {
for step in path.iter().flatten() {
collect_step(step, into);
}
}
QueryResults::Closure(ids) => {
for id in ids {
collect_function(id, into);
}
}
QueryResults::Externals(bindings) => {
for binding in bindings {
collect_binding(binding, into);
}
}
QueryResults::IndirectTargets(entries) => {
for entry in entries {
collect_function(&entry.site.function, into);
let lists = [
&entry.llvm_target_bound,
&entry.address_taken_inventory,
&entry.signature_compatible,
];
for id in lists.into_iter().flatten().flatten() {
collect_function(id, into);
}
}
}
}
}
}
fn collect_function(id: &FunctionId, into: &mut BTreeSet<String>) {
into.insert(id.symbol.clone());
}
fn collect_call_sites(sites: &[CallSiteFact], into: &mut BTreeSet<String>) {
for site in sites {
collect_function(&site.id.function, into);
match &site.target {
CallTarget::Direct { callee } => collect_function(callee, into),
CallTarget::Indirect {
llvm_target_bound, ..
} => {
for id in llvm_target_bound.iter().flatten() {
collect_function(id, into);
}
}
CallTarget::Intrinsic { .. } | CallTarget::InlineAsm => {}
}
}
}
fn collect_step(step: &PathStep, into: &mut BTreeSet<String>) {
match step {
PathStep::Call(site) => collect_function(&site.function, into),
PathStep::BoundedIndirect {
site,
chosen,
bound,
} => {
collect_function(&site.function, into);
collect_function(chosen, into);
for id in bound {
collect_function(id, into);
}
}
PathStep::Binding(binding) => collect_binding(binding, into),
}
}
fn collect_binding(binding: &SymbolBinding, into: &mut BTreeSet<String>) {
into.insert(binding.symbol.clone());
for candidate in &binding.candidates {
collect_function(&candidate.function, into);
}
}
fn record_failure(reports: &mut [ModuleReport], id: &str, reason: String) {
if let Some(report) = reports.iter_mut().find(|report| report.id == id) {
report.status = ModuleAnalysis::Failed;
report.diagnostic = Some(reason);
}
}
fn ambiguous_bindings(session: &Session) -> Vec<SymbolBinding> {
session
.bindings()
.iter()
.filter(|binding| binding.status == BindingStatus::Ambiguous)
.cloned()
.collect()
}
fn defs(session: &Session, name: &str) -> Vec<DefEntry> {
session
.definitions(name)
.into_iter()
.map(|function| DefEntry {
function: function.id.clone(),
configuration_id: configuration_of(session, &function.id.module_id),
location: function.location.clone(),
})
.collect()
}
fn configuration_of(session: &Session, module_id: &str) -> Option<String> {
session
.modules()
.iter()
.find(|module| module.id == module_id)
.and_then(|module| module.configuration_id.clone())
}
fn at_entries(session: &Session, file: &Path, line: u32) -> Vec<AtEntry> {
let call_sites = session.call_sites_at(file, line);
session
.functions_at(file, line)
.iter()
.map(|id| {
let call_sites = call_sites
.iter()
.filter(|site| &site.id.function == id)
.map(|site| (*site).clone())
.collect();
AtEntry {
function: id.clone(),
call_sites,
}
})
.collect()
}
fn callers(session: &Session, name: &str) -> Vec<CallerEntry> {
let mut grouped: BTreeMap<FunctionId, Vec<CallSiteFact>> = BTreeMap::new();
for site in session.callers(name) {
grouped
.entry(site.id.function.clone())
.or_default()
.push(site);
}
grouped
.into_iter()
.map(|(function, call_sites)| CallerEntry {
function,
call_sites,
})
.collect()
}
fn uses_of(session: &Session, name: &str) -> Vec<UseFact> {
session
.uses()
.iter()
.filter(|use_fact| use_fact.used.symbol == name)
.cloned()
.collect()
}
fn externals(session: &Session) -> Vec<SymbolBinding> {
session
.bindings()
.iter()
.filter(|binding| binding.status == BindingStatus::Unbound)
.cloned()
.collect()
}
fn indirect_targets(
session: &Session,
at: &str,
heuristics: bool,
) -> Result<Vec<IndirectTargetsResult>, Error> {
let (file, line) = parse_location(at)?;
let inventory = heuristics.then(|| address_taken_inventory(session));
let assumptions = vec![
"Soundness holds only within the captured scope.".to_string(),
"dlopen and a callback registered by code outside the captured scope both escape this bound.".to_string(),
];
Ok(session
.call_sites_at(&file, line)
.into_iter()
.filter_map(|site| {
let CallTarget::Indirect {
signature,
llvm_target_bound,
} = &site.target
else {
return None;
};
let signature_compatible = inventory.as_ref().map(|functions| {
functions
.iter()
.filter(|id| {
session
.function(id)
.is_some_and(|function| &function.signature == signature)
})
.cloned()
.collect()
});
Some(IndirectTargetsResult {
site: site.id.clone(),
location: site.location.clone(),
signature: signature.clone(),
unresolved: llvm_target_bound.is_none(),
llvm_target_bound: llvm_target_bound.clone(),
address_taken_inventory: inventory.clone(),
signature_compatible,
assumptions: assumptions.clone(),
})
})
.collect())
}
fn address_taken_inventory(session: &Session) -> Vec<FunctionId> {
let mut seen: BTreeSet<FunctionId> = BTreeSet::new();
let mut inventory = Vec::new();
for use_fact in session.uses() {
if seen.insert(use_fact.used.clone()) {
inventory.push(use_fact.used.clone());
}
}
inventory
}
impl Query {
fn names(&self) -> Vec<&str> {
match self {
Query::Defs { name }
| Query::Callers { name }
| Query::Callees { name }
| Query::Uses { name }
| Query::Closure { name, .. } => vec![name],
Query::Reach { from, to } => vec![from, to],
Query::At { .. } | Query::Externals | Query::IndirectTargets { .. } => Vec::new(),
}
}
pub fn validate(&self) -> Result<(), Error> {
match self {
Query::IndirectTargets { at, .. } => parse_location(at).map(|_| ()),
_ => Ok(()),
}
}
}
fn parse_location(at: &str) -> Result<(PathBuf, u32), Error> {
let invalid = || {
Error::InvalidArguments(format!(
"invalid location `{at}`: expected `file:line`, e.g. `parser.c:8`"
))
};
let (file, line) = at.rsplit_once(':').ok_or_else(invalid)?;
let line: u32 = line.parse().map_err(|_| invalid())?;
Ok((PathBuf::from(file), line))
}
fn analysis_of(modules: &[ModuleReport]) -> Analysis {
let mut analysis = Analysis {
modules: modules.to_vec(),
..Default::default()
};
for module in modules {
match module.status {
ModuleAnalysis::Verified => analysis.verified += 1,
ModuleAnalysis::Analyzed => analysis.analyzed += 1,
ModuleAnalysis::Changed => analysis.changed += 1,
ModuleAnalysis::Missing => analysis.missing += 1,
ModuleAnalysis::Failed => analysis.failed += 1,
ModuleAnalysis::Unsupported => analysis.unsupported += 1,
ModuleAnalysis::NotBuilt => analysis.not_built += 1,
}
}
analysis
}
fn uncertainty_of(
session: &Session,
frontier: Vec<SymbolBinding>,
conditional_path_steps: usize,
) -> Uncertainty {
let call_sites = session.call_sites();
let indirect_call_sites = call_sites
.iter()
.filter(|site| matches!(&site.target, CallTarget::Indirect { .. }))
.count();
let sites_with_llvm_target_bound = call_sites
.iter()
.filter(|site| {
matches!(
&site.target,
CallTarget::Indirect {
llvm_target_bound: Some(_),
..
}
)
})
.count();
let functions_without_location = session
.functions()
.iter()
.filter(|function| function.location.is_none())
.count();
let is_modified = |location: &Option<SourceLocation>| {
location
.as_ref()
.is_some_and(|location| location.source_status == SourceStatus::Modified)
};
let locations_from_modified_sources = session
.functions()
.iter()
.filter(|function| is_modified(&function.location))
.count()
+ call_sites
.iter()
.filter(|site| is_modified(&site.location))
.count()
+ session
.uses()
.iter()
.filter(|use_fact| is_modified(&use_fact.location))
.count();
Uncertainty {
indirect_call_sites,
sites_with_llvm_target_bound,
functions_without_location,
locations_from_modified_sources,
ambiguous_bindings: session
.bindings()
.iter()
.filter(|binding| binding.status == BindingStatus::Ambiguous)
.count(),
frontier,
conditional_path_steps,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rllvm_core::catalog::{
ModuleCatalog, ModuleRecord, ModuleStatus, hash_bytes, write_catalog,
};
use crate::{load::load_catalog, testing::*};
#[test]
fn a_module_is_analyzed_only_after_it_parses() {
let scratch = tempfile::tempdir().unwrap();
let loaded = load_catalog(&write_catalog_with_one_module(&scratch)).unwrap();
assert_eq!(loaded.reports[0].status, ModuleAnalysis::Verified);
}
#[test]
fn scope_counts_survive_a_failed_module() {
let facts = facts_with_one_failed_module();
let result = run(&Session::new(facts, vec![]), &Query::Externals).unwrap();
assert_eq!(result.scope.selected_entries, 2);
assert_eq!(result.analysis.analyzed, 1);
assert_eq!(result.analysis.failed, 1);
assert_eq!(result.analysis.modules[0].debug_info, Some(true));
}
#[test]
fn an_empty_reach_names_the_indirect_sites_it_could_not_follow() {
let session = session_with_indirect_gap();
let result = run(
&session,
&Query::Reach {
from: "a".into(),
to: "c".into(),
},
)
.unwrap();
assert!(result.results.is_empty());
assert_eq!(result.uncertainty.indirect_call_sites, 1);
}
#[test]
fn a_trivial_reach_is_a_found_path_not_an_absent_one() {
let session = session_from(&[("a", "b")]);
let trivial = run(
&session,
&Query::Reach {
from: "a".into(),
to: "a".into(),
},
)
.unwrap();
assert!(
!trivial.results.is_empty(),
"a==a must be a found path, not an absent one"
);
let missing = run(
&session,
&Query::Reach {
from: "a".into(),
to: "absent".into(),
},
)
.unwrap();
assert!(missing.results.is_empty());
}
#[test]
fn heuristics_are_absent_unless_requested() {
let session = session_with_address_taken_function();
let result = run(
&session,
&Query::IndirectTargets {
at: "t.c:4".into(),
heuristics: false,
},
)
.unwrap();
let json = serde_json::to_value(&result).unwrap();
assert!(json.to_string().find("address_taken_inventory").is_none());
}
#[test]
fn requested_heuristics_stay_in_their_own_field() {
let session = session_with_address_taken_function();
let result = run(
&session,
&Query::IndirectTargets {
at: "t.c:4".into(),
heuristics: true,
},
)
.unwrap();
let json = serde_json::to_value(&result).unwrap();
let entry = &json["results"][0];
assert!(
entry["address_taken_inventory"]
.as_array()
.unwrap()
.iter()
.any(|f| f["symbol"] == "add"),
"inventory must list address-taken functions when requested"
);
assert!(
entry["llvm_target_bound"].is_null(),
"a heuristic must never appear as an LLVM-provided bound"
);
}
#[test]
fn at_returns_nothing_for_a_line_with_no_instructions() {
let session = session_from_source_lines(&[("t.c", 2), ("t.c", 10)]);
let result = run(
&session,
&Query::At {
file: "t.c".into(),
line: 5,
},
)
.unwrap();
assert!(result.results.is_empty());
}
#[test]
fn callees_of_b_keeps_the_unresolved_indirect_site() {
let session = session_with_indirect_gap();
let result = run(&session, &Query::Callees { name: "b".into() }).unwrap();
let QueryResults::Callees(sites) = &result.results else {
panic!("Query::Callees must produce QueryResults::Callees");
};
let indirect = sites
.iter()
.find(|site| matches!(&site.target, CallTarget::Indirect { .. }))
.expect("the unresolved indirect call site must not be dropped");
assert_eq!(indirect.id.instruction_index, 1);
assert_eq!(indirect.location, None);
}
#[test]
fn ambiguous_bindings_counts_the_scope_not_the_walk() {
let session = session_with_ambiguous_bindings();
let reach = run(
&session,
&Query::Reach {
from: "caller".into(),
to: "target".into(),
},
)
.unwrap();
assert_eq!(
reach.uncertainty.frontier.len(),
1,
"the frontier is what this walk reached"
);
assert_eq!(
reach.uncertainty.ambiguous_bindings, 2,
"the count is program-wide, not the frontier's length"
);
let externals = run(&session, &Query::Externals).unwrap();
assert_eq!(externals.uncertainty.ambiguous_bindings, 2);
}
#[test]
fn a_path_through_a_bounded_indirect_call_is_flagged_conditional() {
let conditional = run(
&session_with_bounded_indirect(),
&Query::Reach {
from: "a".into(),
to: "target".into(),
},
)
.unwrap();
assert!(!conditional.results.is_empty(), "a path must be found");
assert_eq!(conditional.uncertainty.conditional_path_steps, 1);
let direct = run(
&session_from(&[("a", "b"), ("b", "c")]),
&Query::Reach {
from: "a".into(),
to: "c".into(),
},
)
.unwrap();
assert!(!direct.results.is_empty());
assert_eq!(
direct.uncertainty.conditional_path_steps, 0,
"a path of direct calls is not conditional"
);
}
#[test]
fn an_unparseable_location_is_an_error_not_an_empty_answer() {
let session = session_with_address_taken_function();
let error = run(
&session,
&Query::IndirectTargets {
at: "parser.c".into(),
heuristics: false,
},
)
.expect_err("a location without a line must not answer");
assert!(error.to_string().contains("parser.c"), "{error}");
assert!(
run(
&session,
&Query::IndirectTargets {
at: "parser.c:notaline".into(),
heuristics: false,
},
)
.is_err(),
"a non-numeric line must not answer either"
);
}
#[test]
fn a_module_extraction_never_saw_is_counted_verified() {
let result = run(
&Session::new(facts_with_one_verified_module(), vec![]),
&Query::Externals,
)
.unwrap();
assert_eq!(result.analysis.verified, 1);
assert_eq!(result.analysis.analyzed, 0);
assert_eq!(
result.scope.selected_entries, 1,
"scope still quotes the catalog"
);
}
#[test]
fn an_answer_carries_the_reading_of_every_mangled_symbol_it_prints() {
let result = run(
&session_with_cxx_symbols(),
&Query::Defs {
name: "_Z5twiceIiET_S0_".into(),
},
)
.unwrap();
assert_eq!(
result.symbols.get("_Z5twiceIiET_S0_").map(String::as_str),
Some("int twice<int>(int)")
);
assert!(
!result.symbols.contains_key("main"),
"a C name has no reading, so it gets no entry: {:?}",
result.symbols
);
}
#[test]
fn the_symbol_table_covers_the_answer_not_the_whole_scope() {
let result = run(
&session_with_cxx_symbols(),
&Query::Defs {
name: "int twice<int>(int)".into(),
},
)
.unwrap();
assert_eq!(
result.symbols.keys().collect::<Vec<_>>(),
vec!["_Z5twiceIiET_S0_"],
"only the one function this answer names"
);
}
#[test]
fn a_c_only_answer_carries_no_symbol_table_at_all() {
let result = run(
&session_from(&[("a", "b")]),
&Query::Defs { name: "a".into() },
)
.unwrap();
assert!(result.symbols.is_empty());
let json = serde_json::to_value(&result).unwrap();
assert!(
json.get("symbols").is_none(),
"an empty table is omitted rather than printed as {{}}"
);
}
#[test]
fn an_answer_says_which_tier_resolved_its_name() {
let session = session_with_cxx_symbols();
let exact = run(
&session,
&Query::Defs {
name: "_Z5twiceIiET_S0_".into(),
},
)
.unwrap();
assert_eq!(exact.resolution[0].requested, "_Z5twiceIiET_S0_");
assert_eq!(exact.resolution[0].matched, Some(NameMatch::Mangled));
assert_eq!(exact.resolution[0].symbols.len(), 1);
let fuzzy = run(
&session,
&Query::Defs {
name: "twice".into(),
},
)
.unwrap();
assert_eq!(fuzzy.resolution[0].matched, Some(NameMatch::Fuzzy));
assert_eq!(
fuzzy.resolution[0].symbols,
["_Z5twiceIdET_S0_", "_Z5twiceIiET_S0_", "_ZN2ns5twiceEv"],
"the block names every symbol the fuzzy tier gathered"
);
assert!(!fuzzy.results.is_empty(), "all three still answer");
}
#[test]
fn a_name_that_matches_nothing_reports_no_tier() {
let result = run(
&session_with_cxx_symbols(),
&Query::Defs {
name: "absent".into(),
},
)
.unwrap();
assert!(result.results.is_empty());
assert_eq!(result.resolution[0].matched, None);
assert!(result.resolution[0].symbols.is_empty());
}
#[test]
fn resolution_is_reported_once_per_name_the_query_takes() {
let session = session_from(&[("a", "b")]);
let reach = run(
&session,
&Query::Reach {
from: "a".into(),
to: "b".into(),
},
)
.unwrap();
assert_eq!(
reach
.resolution
.iter()
.map(|entry| entry.requested.as_str())
.collect::<Vec<_>>(),
["a", "b"]
);
let externals = run(&session, &Query::Externals).unwrap();
assert!(externals.resolution.is_empty());
let json = serde_json::to_value(&externals).unwrap();
assert!(json.get("resolution").is_none(), "omitted when empty");
}
fn write_catalog_with_one_module(scratch: &tempfile::TempDir) -> PathBuf {
let module_path = scratch.path().join("m.bc");
let bytes = b"not real bitcode, only its hash matters here";
std::fs::write(&module_path, bytes).unwrap();
let mut record = ModuleRecord::new("m");
record.path = Some(PathBuf::from("m.bc"));
record.content_sha256 = Some(hash_bytes(bytes));
record.status = ModuleStatus::Available;
let catalog = ModuleCatalog::new(
CatalogOrigin {
kind: "test".into(),
input: PathBuf::from("test"),
sha256: None,
},
"test",
vec![record],
);
let catalog_path = scratch.path().join("catalog.json");
write_catalog(&catalog_path, &catalog).unwrap();
catalog_path
}
}