use rusqlite::{Connection, params_from_iter, types::Value};
use super::super::{
prepare_code_search_statement,
relevance::{
fts_match_query, fts_path_and_language_filter_sql, language_filter_sql_for_columns,
path_filter_sql_for_column, push_language_filter_values, push_path_filter_values,
push_query_path_substring_filter_sql, push_query_path_substring_filter_values,
},
required_scope,
rows::CallRow,
};
use super::{
display::identifier_character,
identity_query::{CallIdentityDirection, CallIdentityQuery, call_identity_candidate_limit},
row_store::{CallIdentityRows, call_rows_sql, row_to_call},
};
use crate::storage::sqlite::code::search::EXACT_SEARCH_OWNER_PREDICATE_SQL;
use crate::{
domain::{CodeRepositoryStatus, CodeRetrievalRequest},
storage::StorageError,
};
struct IndirectCallBinding {
field_name: String,
target_name: String,
binding_path: String,
context_terms: Vec<String>,
}
struct IndirectCallBindings {
bindings: Vec<IndirectCallBinding>,
saturated: bool,
}
const INDIRECT_CALL_BINDING_LIMIT: usize = 80;
const MAX_INDIRECT_CALL_FIELDS: usize = 24;
const EXACT_LOCAL_BINDING_CONFIDENCE_BPS: u16 = 9_500;
pub(super) fn search_indirect_call_identity_rows(
connection: &Connection,
status: &CodeRepositoryStatus,
request: &CodeRetrievalRequest,
identity: &CallIdentityQuery,
) -> Result<CallIdentityRows, StorageError> {
if identity.direction != CallIdentityDirection::Callee {
return Ok(CallIdentityRows {
rows: Vec::new(),
saturated: false,
});
}
let bindings =
search_indirect_call_bindings(connection, status, request, identity.leaf_name())?;
if bindings.bindings.is_empty() {
return Ok(CallIdentityRows {
rows: Vec::new(),
saturated: bindings.saturated,
});
}
let mut field_names = Vec::new();
for binding in &bindings.bindings {
if !field_names.contains(&binding.field_name) {
field_names.push(binding.field_name.clone());
}
if field_names.len() >= MAX_INDIRECT_CALL_FIELDS {
break;
}
}
let path_filter = path_filter_sql_for_column("c.path", status, request);
let language_filter =
language_filter_sql_for_columns("f.language_id", "f.path", status, request);
let generated_filter = if request.exclude_generated {
"AND f.is_generated = 0"
} else {
""
};
let placeholders = placeholders(field_names.len());
let sql = call_rows_sql(&format!(
"
AND c.callee_name IN ({placeholders})
{path_filter}
{language_filter}
{generated_filter}
"
));
let direct_limit = call_identity_candidate_limit(request);
let mut values = vec![Value::Text(required_scope(status)?.to_owned())];
values.extend(field_names.into_iter().map(Value::Text));
push_path_filter_values(&mut values, &status.path_filters);
push_path_filter_values(&mut values, &request.repository.path_filters);
push_language_filter_values(&mut values, &status.language_filters);
push_language_filter_values(&mut values, &request.repository.language_filters);
push_language_filter_values(&mut values, &request.query_language_filters);
values.push(Value::Integer((direct_limit + 1) as i64));
let mut statement = prepare_code_search_statement(connection, &sql)?;
let rows = statement.query_map(params_from_iter(values), row_to_call)?;
let mut rows = rows
.collect::<Result<Vec<_>, _>>()
.map_err(StorageError::from)?;
let saturated = rows.len() > direct_limit;
rows.truncate(direct_limit);
rows.retain_mut(|row| {
let Some(binding) = best_indirect_call_binding(&bindings.bindings, row) else {
return false;
};
let same_path = row.path == binding.binding_path;
row.target_hint = Some(binding.target_name.clone());
row.resolution_state = "inferred".to_owned();
let confidence_floor = if same_path {
row.confidence_tier = "exact".to_owned();
EXACT_LOCAL_BINDING_CONFIDENCE_BPS
} else {
row.confidence_tier = "inferred".to_owned();
5_500
};
row.confidence_basis_points = row.confidence_basis_points.max(confidence_floor);
true
});
Ok(CallIdentityRows {
rows,
saturated: saturated || bindings.saturated,
})
}
fn search_indirect_call_bindings(
connection: &Connection,
status: &CodeRepositoryStatus,
request: &CodeRetrievalRequest,
target_name: &str,
) -> Result<IndirectCallBindings, StorageError> {
let structured_rows =
search_structured_indirect_binding_chunks(connection, status, request, target_name)?;
let structured_saturated = structured_rows.len() > INDIRECT_CALL_BINDING_LIMIT;
let structured_bindings = collect_indirect_call_bindings(
structured_rows
.into_iter()
.take(INDIRECT_CALL_BINDING_LIMIT),
target_name,
);
if !structured_bindings.is_empty() {
return Ok(IndirectCallBindings {
bindings: structured_bindings,
saturated: structured_saturated,
});
}
search_indirect_call_bindings_from_fts(connection, status, request, target_name)
}
fn search_structured_indirect_binding_chunks(
connection: &Connection,
status: &CodeRepositoryStatus,
request: &CodeRetrievalRequest,
target_name: &str,
) -> Result<Vec<(String, String)>, StorageError> {
let path_filter = path_filter_sql_for_column("r.path", status, request);
let language_filter =
language_filter_sql_for_columns("f.language_id", "f.path", status, request);
let mut query_path_clauses = Vec::new();
push_query_path_substring_filter_sql(
&mut query_path_clauses,
"r.path",
&request.query_path_substrings,
);
let query_path_filter = if query_path_clauses.is_empty() {
String::new()
} else {
format!("AND {}", query_path_clauses.join(" AND "))
};
let generated_filter = if request.exclude_generated {
"AND f.is_generated = 0"
} else {
""
};
let sql = format!(
"
SELECT r.path,
(
SELECT chunk.content
FROM code_repository_chunks chunk
WHERE chunk.source_scope = r.source_scope
AND chunk.path = r.path
AND chunk.line_start <= r.line_start
AND chunk.line_end >= r.line_start
ORDER BY (chunk.line_end - chunk.line_start) ASC,
chunk.line_start DESC,
chunk.chunk_id ASC
LIMIT 1
) AS source_excerpt
FROM code_repository_references r
INNER JOIN code_repository_files f
ON f.source_scope = r.source_scope AND f.path = r.path
WHERE r.source_scope = ?
AND r.name = ?
{path_filter}
{query_path_filter}
{language_filter}
{generated_filter}
ORDER BY f.is_generated ASC, r.path ASC, r.line_start ASC, r.reference_id ASC
LIMIT ?
"
);
let mut values = vec![
Value::Text(required_scope(status)?.to_owned()),
Value::Text(target_name.to_owned()),
];
push_path_filter_values(&mut values, &status.path_filters);
push_path_filter_values(&mut values, &request.repository.path_filters);
push_query_path_substring_filter_values(&mut values, &request.query_path_substrings);
push_language_filter_values(&mut values, &status.language_filters);
push_language_filter_values(&mut values, &request.repository.language_filters);
push_language_filter_values(&mut values, &request.query_language_filters);
values.push(Value::Integer((INDIRECT_CALL_BINDING_LIMIT + 1) as i64));
let mut statement = prepare_code_search_statement(connection, &sql)?;
let rows = statement.query_map(params_from_iter(values), |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?))
})?;
let rows = rows
.collect::<Result<Vec<_>, _>>()
.map_err(StorageError::from)?;
Ok(rows
.into_iter()
.filter_map(|(path, excerpt)| excerpt.map(|excerpt| (path, excerpt)))
.collect())
}
fn search_indirect_call_bindings_from_fts(
connection: &Connection,
status: &CodeRepositoryStatus,
request: &CodeRetrievalRequest,
target_name: &str,
) -> Result<IndirectCallBindings, StorageError> {
let fts_filter = fts_path_and_language_filter_sql(status, request);
let generated_filter = if request.exclude_generated {
"AND NOT EXISTS (SELECT 1 FROM code_repository_files file WHERE file.source_scope = code_repository_search.source_scope AND file.path = code_repository_search.path AND file.is_generated != 0)"
} else {
""
};
let sql = format!(
"
SELECT path, content
FROM code_repository_search
WHERE code_repository_search MATCH ?
AND source_scope = ?
AND document_kind = 'chunk'
{EXACT_SEARCH_OWNER_PREDICATE_SQL}
{fts_filter}
{generated_filter}
ORDER BY bm25(code_repository_search) ASC, record_id ASC
LIMIT ?
"
);
let mut values = vec![
Value::Text(fts_match_query(target_name)),
Value::Text(required_scope(status)?.to_owned()),
];
push_path_filter_values(&mut values, &status.path_filters);
push_path_filter_values(&mut values, &request.repository.path_filters);
push_query_path_substring_filter_values(&mut values, &request.query_path_substrings);
push_language_filter_values(&mut values, &status.language_filters);
push_language_filter_values(&mut values, &request.repository.language_filters);
push_language_filter_values(&mut values, &request.query_language_filters);
values.push(Value::Integer((INDIRECT_CALL_BINDING_LIMIT + 1) as i64));
let mut statement = prepare_code_search_statement(connection, &sql)?;
let rows = statement.query_map(params_from_iter(values), |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?;
let mut rows = rows
.collect::<Result<Vec<_>, _>>()
.map_err(StorageError::from)?;
let saturated = rows.len() > INDIRECT_CALL_BINDING_LIMIT;
rows.truncate(INDIRECT_CALL_BINDING_LIMIT);
let bindings = collect_indirect_call_bindings(rows, target_name);
Ok(IndirectCallBindings {
bindings,
saturated,
})
}
fn collect_indirect_call_bindings(
rows: impl IntoIterator<Item = (String, String)>,
target_name: &str,
) -> Vec<IndirectCallBinding> {
let mut bindings: Vec<IndirectCallBinding> = Vec::new();
for (path, excerpt) in rows {
for field_name in indirect_call_binding_fields(&excerpt, target_name) {
let context_terms =
indirect_call_binding_context_terms(&excerpt, &field_name, target_name);
let binding = IndirectCallBinding {
field_name,
target_name: target_name.to_owned(),
binding_path: path.clone(),
context_terms,
};
if !bindings.iter().any(|existing| {
existing.field_name == binding.field_name
&& existing.binding_path == binding.binding_path
}) {
bindings.push(binding);
}
}
}
bindings
}
fn best_indirect_call_binding<'a>(
bindings: &'a [IndirectCallBinding],
row: &CallRow,
) -> Option<&'a IndirectCallBinding> {
let matching_field = || {
bindings
.iter()
.filter(|binding| binding.field_name == row.callee_name)
};
matching_field()
.find(|binding| binding.binding_path == row.path)
.or_else(|| {
matching_field()
.find(|binding| row_has_indirect_target_evidence(row, &binding.target_name))
})
.or_else(|| matching_field().find(|binding| row_has_indirect_binding_context(row, binding)))
}
fn row_has_indirect_target_evidence(row: &CallRow, target_name: &str) -> bool {
matches!(row.resolution_state.as_str(), "resolved" | "inferred")
&& row.confidence_basis_points >= 5_000
&& [
row.target_hint.as_deref(),
row.callee_canonical_symbol_id.as_deref(),
row.callee_signature.as_deref(),
]
.into_iter()
.flatten()
.any(|field| line_contains_identifier(field, target_name))
}
fn row_has_indirect_binding_context(row: &CallRow, binding: &IndirectCallBinding) -> bool {
if binding.context_terms.is_empty() {
return false;
}
let row_terms = indirect_call_row_context_terms(row, &binding.field_name);
row_terms.iter().any(|row_term| {
binding
.context_terms
.iter()
.any(|binding_term| binding_term == row_term)
})
}
fn indirect_call_binding_fields(excerpt: &str, target_name: &str) -> Vec<String> {
let mut fields = Vec::new();
for line in excerpt.lines() {
if !line_contains_identifier(line, target_name) {
continue;
}
if let Some(field_name) = field_name_before_bound_target(line, target_name)
&& !fields.contains(&field_name)
{
fields.push(field_name);
}
}
fields
}
fn indirect_call_binding_context_terms(
excerpt: &str,
field_name: &str,
target_name: &str,
) -> Vec<String> {
let mut terms = Vec::new();
if excerpt
.lines()
.any(|line| line_contains_identifier(line, target_name))
{
push_indirect_context_terms(excerpt, &mut terms);
}
prune_indirect_context_terms(&mut terms, field_name, target_name);
terms
}
fn indirect_call_row_context_terms(row: &CallRow, field_name: &str) -> Vec<String> {
let mut terms = Vec::new();
for value in [
row.caller_name.as_deref(),
row.caller_canonical_symbol_id.as_deref(),
row.caller_signature.as_deref(),
]
.into_iter()
.flatten()
{
push_indirect_context_terms(value, &mut terms);
}
if let Some(excerpt) = row.caller_excerpt.as_deref() {
for line in excerpt.lines() {
push_indirect_receiver_context_terms(line, field_name, &mut terms);
}
}
prune_indirect_context_terms(&mut terms, field_name, "");
terms
}
fn push_indirect_receiver_context_terms(line: &str, field_name: &str, terms: &mut Vec<String>) {
for operator in [format!("->{field_name}"), format!(".{field_name}")] {
for (index, _) in line.match_indices(&operator) {
if let Some(surface) = trailing_receiver_surface(&line[..index]) {
push_indirect_context_terms(surface, terms);
}
}
}
}
fn trailing_receiver_surface(value: &str) -> Option<&str> {
let value = value.trim_end();
if value.is_empty() {
return None;
}
let start = value
.char_indices()
.rev()
.find(|(_, character)| {
character.is_whitespace() || matches!(character, '(' | ',' | ';' | '=' | '{')
})
.map_or(0, |(index, character)| index + character.len_utf8());
value
.get(start..)
.map(str::trim)
.filter(|value| !value.is_empty())
}
fn field_name_before_bound_target(line: &str, target_name: &str) -> Option<String> {
let target_start = identifier_start(line, target_name)?;
let before_target = line.get(..target_start)?;
let assignment_start = binding_assignment_start(before_target)?;
let left = before_target.get(..assignment_start)?.trim_end();
if left.contains('(') || left.contains(')') {
return None;
}
field_name_from_member_surface(left).filter(|field_name| field_name != target_name)
}
fn binding_assignment_start(value: &str) -> Option<usize> {
value.char_indices().rev().find_map(|(index, character)| {
if character == ':' {
return Some(index);
}
if character != '=' {
return None;
}
let previous = value.get(..index)?.chars().next_back();
let next = value.get(index + character.len_utf8()..)?.chars().next();
if previous.is_some_and(|character| matches!(character, '=' | '!' | '<' | '>'))
|| next.is_some_and(|character| matches!(character, '=' | '>'))
{
None
} else {
Some(index)
}
})
}
fn field_name_from_member_surface(value: &str) -> Option<String> {
if let Some((_, tail)) = value.rsplit_once("->") {
return leading_identifier(tail.trim_start());
}
if let Some((_, tail)) = value.rsplit_once('.') {
return leading_identifier(tail.trim_start());
}
None
}
fn leading_identifier(value: &str) -> Option<String> {
let mut end = 0usize;
for (index, character) in value.char_indices() {
if index == 0 && !identifier_start_character(character) {
return None;
}
if !identifier_character(character) {
break;
}
end = index + character.len_utf8();
}
(end > 0).then(|| value[..end].to_owned())
}
fn push_indirect_context_terms(value: &str, terms: &mut Vec<String>) {
let mut token = String::new();
for character in value.chars() {
if identifier_character(character) {
token.push(character);
} else {
push_indirect_context_token(&token, terms);
token.clear();
}
}
push_indirect_context_token(&token, terms);
}
fn push_indirect_context_token(token: &str, terms: &mut Vec<String>) {
if token.is_empty() {
return;
}
let normalized = token.to_ascii_lowercase();
push_indirect_context_term(&normalized, terms);
for part in normalized.split('_') {
push_indirect_context_term(part, terms);
}
}
fn push_indirect_context_term(term: &str, terms: &mut Vec<String>) {
if term.len() >= 3
&& !indirect_context_noise_term(term)
&& !terms.iter().any(|existing| existing == term)
{
terms.push(term.to_owned());
}
}
fn prune_indirect_context_terms(terms: &mut Vec<String>, field_name: &str, target_name: &str) {
let field_name = field_name.to_ascii_lowercase();
let target_name = target_name.to_ascii_lowercase();
terms.retain(|term| {
term != &field_name
&& (target_name.is_empty() || term != &target_name)
&& !indirect_context_noise_term(term)
});
}
fn indirect_context_noise_term(term: &str) -> bool {
matches!(
term,
"char"
| "const"
| "int"
| "return"
| "self"
| "size"
| "size_t"
| "static"
| "struct"
| "this"
| "void"
)
}
fn line_contains_identifier(line: &str, identifier: &str) -> bool {
identifier_start(line, identifier).is_some()
}
fn identifier_start(line: &str, identifier: &str) -> Option<usize> {
if identifier.is_empty() {
return None;
}
line.match_indices(identifier)
.find(|(start, _)| {
let end = start + identifier.len();
line.get(..*start).is_some_and(|prefix| {
prefix
.chars()
.next_back()
.is_none_or(|character| !identifier_character(character))
}) && line.get(end..).is_some_and(|suffix| {
suffix
.chars()
.next()
.is_none_or(|character| !identifier_character(character))
})
})
.map(|(start, _)| start)
}
fn identifier_start_character(character: char) -> bool {
character == '_' || character.is_ascii_alphabetic()
}
fn placeholders(count: usize) -> String {
std::iter::repeat_n("?", count)
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
#[path = "indirect_tests.rs"]
mod tests;