use std::collections::BTreeMap;
use crate::domain::{
CodeCallRecord, CodeIndexSnapshot, CodePathTombstone, CodeRepositoryRegistration,
CodeRepositorySelector, RepositoryCodeReferenceRecord, RepositoryCodeSymbolRecord,
code_snapshot_scope_id,
};
use super::{identity, ids::stable_id};
pub(super) struct SnapshotBuild {
pub(super) repository_id: String,
pub(super) source_scope: String,
pub(super) base_resolved_commit_sha: Option<String>,
pub(super) commit: String,
pub(super) tree_hash: String,
path_filters: Vec<String>,
language_filters: Vec<String>,
full_replace: bool,
changed_path_count: usize,
pub(super) skipped_unchanged_count: usize,
pub(super) deleted_paths: Vec<String>,
pub(super) tombstones: Vec<CodePathTombstone>,
pub(super) files: Vec<crate::domain::RepositoryCodeFileRecord>,
pub(super) symbols: Vec<RepositoryCodeSymbolRecord>,
pub(super) references: Vec<RepositoryCodeReferenceRecord>,
pub(super) imports: Vec<crate::domain::CodeImportRecord>,
calls: Vec<CodeCallRecord>,
pub(super) dependencies: Vec<crate::domain::CodeDependencyRecord>,
pub(super) feature_flags: Vec<crate::domain::CodeFeatureFlagRecord>,
pub(super) chunks: Vec<crate::domain::RepositoryCodeChunkRecord>,
pub(super) diagnostics: Vec<crate::domain::CodeFileDiagnostic>,
}
pub(super) struct SnapshotScopeFilters {
pub(super) path_filters: Vec<String>,
pub(super) language_filters: Vec<String>,
}
impl SnapshotBuild {
#[cfg(test)]
pub(super) fn new(
registration: &CodeRepositoryRegistration,
commit: String,
tree_hash: String,
full_replace: bool,
changed_path_count: usize,
skipped_unchanged_count: usize,
) -> Self {
let selector = CodeRepositorySelector {
repository: registration.repository_id.clone(),
ref_selector: commit.clone(),
path_filters: Vec::new(),
language_filters: Vec::new(),
};
Self::new_with_selector(
registration,
&selector,
commit,
tree_hash,
full_replace,
changed_path_count,
skipped_unchanged_count,
)
}
pub(super) fn new_with_selector(
registration: &CodeRepositoryRegistration,
selector: &CodeRepositorySelector,
commit: String,
tree_hash: String,
full_replace: bool,
changed_path_count: usize,
skipped_unchanged_count: usize,
) -> Self {
let path_filters = merged_filters(®istration.path_filters, &selector.path_filters);
let language_filters =
merged_filters(®istration.language_filters, &selector.language_filters);
Self::new_with_scope_filters(
registration,
commit,
tree_hash,
SnapshotScopeFilters {
path_filters,
language_filters,
},
full_replace,
changed_path_count,
skipped_unchanged_count,
)
}
pub(super) fn new_with_scope_filters(
registration: &CodeRepositoryRegistration,
commit: String,
tree_hash: String,
filters: SnapshotScopeFilters,
full_replace: bool,
changed_path_count: usize,
skipped_unchanged_count: usize,
) -> Self {
let path_filters = filters.path_filters;
let language_filters = filters.language_filters;
let source_scope = code_snapshot_scope_id(
®istration.repository_id,
&tree_hash,
&path_filters,
&language_filters,
);
Self {
repository_id: registration.repository_id.clone(),
source_scope,
base_resolved_commit_sha: None,
commit,
tree_hash,
path_filters,
language_filters,
full_replace,
changed_path_count,
skipped_unchanged_count,
deleted_paths: Vec::new(),
tombstones: Vec::new(),
files: Vec::new(),
symbols: Vec::new(),
references: Vec::new(),
imports: Vec::new(),
calls: Vec::new(),
dependencies: Vec::new(),
feature_flags: Vec::new(),
chunks: Vec::new(),
diagnostics: Vec::new(),
}
}
pub(super) fn finish(mut self) -> CodeIndexSnapshot {
identity::enrich_symbol_identities(&self.repository_id, &mut self.symbols);
identity::resolve_reference_targets(&self.symbols, &mut self.references);
identity::resolve_import_targets(&self.files, &self.symbols, &mut self.imports);
let symbols_by_path = build_symbol_path_index(&self.symbols);
let symbols_by_id = build_symbol_id_index(&self.symbols);
self.calls = self
.references
.iter()
.filter(|reference| reference.kind == "call")
.map(|reference| {
let caller = caller_for_line(
&symbols_by_path,
&reference.path,
reference.line_range.start,
);
let (caller_symbol_snapshot_id, caller_name) = caller
.map(|symbol| {
(
Some(symbol.symbol_snapshot_id.clone()),
Some(symbol.name.clone()),
)
})
.unwrap_or((None, None));
let callee_name = reference
.target_symbol_snapshot_id
.as_deref()
.and_then(|symbol_id| symbols_by_id.get(symbol_id))
.map(|symbol| symbol.name.clone())
.or_else(|| reference.target_hint.clone())
.unwrap_or_else(|| reference.name.clone());
CodeCallRecord {
repository_id: reference.repository_id.clone(),
source_scope: reference.source_scope.clone(),
call_id: stable_id(
"call",
[
self.repository_id.as_str(),
self.source_scope.as_str(),
reference.reference_id.as_str(),
reference.path.as_str(),
reference.name.as_str(),
&reference.line_range.start.to_string(),
],
),
file_id: reference.file_id.clone(),
path: reference.path.clone(),
caller_symbol_snapshot_id,
caller_name,
callee_symbol_snapshot_id: reference.target_symbol_snapshot_id.clone(),
callee_name,
target_hint: reference.target_hint.clone(),
resolution_state: reference.resolution_state.clone(),
confidence_basis_points: reference.confidence_basis_points,
confidence_tier: reference.confidence_tier.clone(),
line_range: reference.line_range.clone(),
}
})
.collect();
CodeIndexSnapshot {
repository_id: self.repository_id,
source_scope: self.source_scope,
base_resolved_commit_sha: self.base_resolved_commit_sha,
resolved_commit_sha: self.commit,
tree_hash: self.tree_hash,
path_filters: self.path_filters,
language_filters: self.language_filters,
full_replace: self.full_replace,
changed_path_count: self.changed_path_count,
skipped_unchanged_count: self.skipped_unchanged_count,
deleted_paths: self.deleted_paths,
tombstones: self.tombstones,
files: self.files,
symbols: self.symbols,
references: self.references,
imports: self.imports,
calls: self.calls,
dependencies: self.dependencies,
feature_flags: self.feature_flags,
chunks: self.chunks,
diagnostics: self.diagnostics,
}
}
pub(super) fn append_file_records(&mut self, mut other: Self) {
debug_assert_eq!(self.repository_id, other.repository_id);
debug_assert_eq!(self.source_scope, other.source_scope);
self.files.append(&mut other.files);
self.symbols.append(&mut other.symbols);
self.references.append(&mut other.references);
self.imports.append(&mut other.imports);
self.calls.append(&mut other.calls);
self.dependencies.append(&mut other.dependencies);
self.feature_flags.append(&mut other.feature_flags);
self.chunks.append(&mut other.chunks);
self.diagnostics.append(&mut other.diagnostics);
}
pub(in crate::code) fn language_filters(&self) -> &[String] {
&self.language_filters
}
}
fn build_symbol_path_index(
symbols: &[RepositoryCodeSymbolRecord],
) -> BTreeMap<&str, Vec<&RepositoryCodeSymbolRecord>> {
let mut index = BTreeMap::<&str, Vec<&RepositoryCodeSymbolRecord>>::new();
for symbol in symbols {
index.entry(&symbol.path).or_default().push(symbol);
}
index
}
fn build_symbol_id_index(
symbols: &[RepositoryCodeSymbolRecord],
) -> BTreeMap<&str, &RepositoryCodeSymbolRecord> {
let mut index = BTreeMap::<&str, &RepositoryCodeSymbolRecord>::new();
for symbol in symbols {
index.insert(&symbol.symbol_snapshot_id, symbol);
}
index
}
fn caller_for_line<'a>(
symbols_by_path: &'a BTreeMap<&str, Vec<&'a RepositoryCodeSymbolRecord>>,
path: &str,
line: u32,
) -> Option<&'a RepositoryCodeSymbolRecord> {
symbols_by_path
.get(path)?
.iter()
.copied()
.filter(|symbol| symbol.line_range.start <= line && symbol.line_range.end >= line)
.max_by_key(|symbol| symbol.line_range.start)
}
pub(super) fn merged_filters(left: &[String], right: &[String]) -> Vec<String> {
let mut merged = Vec::new();
for value in left.iter().chain(right.iter()) {
if !merged.contains(value) {
merged.push(value.clone());
}
}
merged
}
#[cfg(test)]
mod tests {
use crate::domain::{
CodeRepositoryRegistration, RepositoryCodeRange, RepositoryCodeReferenceRecord,
};
use super::*;
#[test]
fn caller_lookup_uses_matching_path_and_innermost_symbol() {
let symbols = vec![
symbol("outer", "src/hot.rs", "outer", 1, 10),
symbol("inner", "src/hot.rs", "inner", 5, 8),
symbol("other", "src/other.rs", "other", 6, 6),
];
let index = build_symbol_path_index(&symbols);
let caller = caller_for_line(&index, "src/hot.rs", 6).expect("caller should resolve");
assert_eq!(caller.name, "inner");
assert!(caller_for_line(&index, "src/other.rs", 5).is_none());
assert!(caller_for_line(&index, "src/missing.rs", 6).is_none());
}
#[test]
fn call_materialization_keeps_scoped_hint_and_resolved_callee_name() {
let registration =
CodeRepositoryRegistration::new("repo", "fixture", "/tmp/repo", Vec::new(), Vec::new())
.expect("registration");
let mut build = SnapshotBuild::new(
®istration,
"commit".to_owned(),
"tree".to_owned(),
true,
1,
0,
);
build
.symbols
.push(symbol("c-definition", "src/c_entry.c", "rk_c_decode", 1, 3));
build
.references
.push(reference("ffi-call", "src/lib.rs", "ffi::rk_c_decode", 2));
let snapshot = build.finish();
let call = snapshot.calls.first().expect("call should materialize");
assert_eq!(call.callee_name, "rk_c_decode");
assert_eq!(call.target_hint.as_deref(), Some("ffi::rk_c_decode"));
assert_eq!(
call.callee_symbol_snapshot_id.as_deref(),
Some("c-definition")
);
assert_eq!(call.resolution_state, "resolved");
}
fn symbol(
symbol_snapshot_id: &str,
path: &str,
name: &str,
line_start: u32,
line_end: u32,
) -> RepositoryCodeSymbolRecord {
RepositoryCodeSymbolRecord {
repository_id: "repo".to_owned(),
source_scope: "scope".to_owned(),
symbol_snapshot_id: symbol_snapshot_id.to_owned(),
canonical_symbol_id: format!("repo://repo/{}::{name}", path.replace('/', "::")),
file_id: format!("file-{symbol_snapshot_id}"),
path: path.to_owned(),
language_id: "rust".to_owned(),
name: name.to_owned(),
qualified_name: format!("{}::{name}", path.replace('/', "::")),
kind: "function".to_owned(),
signature: format!("fn {name}()"),
doc_comment: None,
byte_range: RepositoryCodeRange { start: 0, end: 1 },
line_range: RepositoryCodeRange {
start: line_start,
end: line_end,
},
}
}
fn reference(
reference_id: &str,
path: &str,
name: &str,
line: u32,
) -> RepositoryCodeReferenceRecord {
RepositoryCodeReferenceRecord {
repository_id: "repo".to_owned(),
source_scope: "scope".to_owned(),
reference_id: reference_id.to_owned(),
file_id: format!("file-{reference_id}"),
path: path.to_owned(),
name: name.to_owned(),
kind: "call".to_owned(),
target_symbol_snapshot_id: None,
target_hint: Some(name.to_owned()),
resolution_state: "unresolved".to_owned(),
confidence_basis_points: 2_500,
confidence_tier: "ambiguous".to_owned(),
byte_range: RepositoryCodeRange { start: 0, end: 1 },
line_range: RepositoryCodeRange {
start: line,
end: line,
},
}
}
}