use anyhow::Result;
use sqlitegraph::{BackendDirection, EdgeSpec, GraphBackend, NeighborQuery, NodeSpec, SnapshotId};
use std::path::PathBuf;
use std::sync::Arc;
use crate::graph::schema::ImportNode;
use crate::ingest::ImportFact;
pub struct ImportOps {
pub backend: Arc<dyn GraphBackend>,
}
impl ImportOps {
pub fn delete_imports_in_file(&self, path: &str) -> Result<usize> {
let entity_ids = self.backend.entity_ids()?;
let snapshot = SnapshotId::current();
let mut to_delete: Vec<i64> = Vec::new();
for entity_id in entity_ids {
let node = match self.backend.get_node(snapshot, entity_id) {
Ok(n) => n,
Err(_) => continue,
};
if node.kind != "Import" {
continue;
}
let import_node: ImportNode = match serde_json::from_value(node.data) {
Ok(value) => value,
Err(_) => continue,
};
if import_node.file == path {
to_delete.push(entity_id);
}
}
to_delete.sort_unstable();
for id in &to_delete {
self.backend.delete_entity(*id)?;
}
Ok(to_delete.len())
}
pub fn index_imports(
&self,
path: &str,
file_id: i64,
imports: Vec<ImportFact>,
module_resolver: Option<&crate::graph::module_resolver::ModuleResolver>,
) -> Result<usize> {
for import_fact in &imports {
let resolved_file_id = if let Some(resolver) = module_resolver {
resolver.resolve_path(path, &import_fact.import_path)
} else {
None
};
let import_node = ImportNode {
file: path.to_string(),
import_kind: import_fact.import_kind.normalized_key().to_string(),
import_path: import_fact.import_path.clone(),
imported_names: import_fact.imported_names.clone(),
is_glob: import_fact.is_glob,
byte_start: import_fact.byte_start as u64,
byte_end: import_fact.byte_end as u64,
start_line: import_fact.start_line as u64,
start_col: import_fact.start_col as u64,
end_line: import_fact.end_line as u64,
end_col: import_fact.end_col as u64,
};
let node_spec = NodeSpec {
kind: "Import".to_string(),
name: format!(
"{} import from {}",
import_fact.import_kind.normalized_key(),
import_fact.file_path.display()
),
file_path: Some(path.to_string()),
data: {
let mut data = serde_json::to_value(import_node)?;
if let Some(resolved_id) = resolved_file_id {
if let Some(obj) = data.as_object_mut() {
obj.insert(
"resolved_file_id".to_string(),
serde_json::json!(resolved_id),
);
}
}
data
},
};
let import_id = self.backend.insert_node(node_spec)?;
if let Some(target_file_id) = resolved_file_id {
self.create_import_edge(import_id, target_file_id)?;
}
let edge_spec = EdgeSpec {
from: file_id,
to: import_id,
edge_type: "IMPORTS".to_string(),
data: serde_json::json!({
"byte_start": import_fact.byte_start,
"byte_end": import_fact.byte_end,
}),
};
self.backend.insert_edge(edge_spec)?;
}
Ok(imports.len())
}
fn create_import_edge(&self, import_id: i64, target_id: i64) -> Result<()> {
let edge_spec = EdgeSpec {
from: import_id,
to: target_id,
edge_type: "DEFINES".to_string(),
data: serde_json::json!({}),
};
self.backend.insert_edge(edge_spec)?;
Ok(())
}
}
use crate::ingest::ImportKind;
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_delete_imports_in_file() -> Result<()> {
let temp_dir = TempDir::new()?;
let db_path = temp_dir.path().join("test.db");
let mut graph = crate::CodeGraph::open(&db_path)?;
let test_file = "test.rs";
let source = b"use std::collections::HashMap;";
graph.index_file(test_file, source)?;
let normalized_path = crate::graph::files::normalize_path_for_index(test_file);
let file_id = graph
.files
.find_file_node(&normalized_path)?
.expect("file node should exist after index_file");
let imports = vec![ImportFact {
file_path: PathBuf::from(test_file),
import_kind: ImportKind::PlainUse,
import_path: vec!["std".to_string(), "collections".to_string()],
imported_names: vec!["HashSet".to_string()], is_glob: false,
byte_start: 100,
byte_end: 200,
start_line: 2,
start_col: 0,
end_line: 3,
end_col: 0,
}];
let count = graph
.imports
.index_imports(test_file, file_id.as_i64(), imports, None)
.unwrap();
assert_eq!(count, 1);
let deleted = graph.imports.delete_imports_in_file(test_file)?;
assert_eq!(deleted, 2); Ok(())
}
#[test]
fn test_index_imports_creates_nodes() -> Result<()> {
let temp_dir = TempDir::new()?;
let db_path = temp_dir.path().join("test.db");
let mut graph = crate::CodeGraph::open(&db_path)?;
let test_file = "test.rs";
let source = b"use crate::foo::bar;";
graph.index_file(test_file, source)?;
let normalized_path = crate::graph::files::normalize_path_for_index(test_file);
let file_id = graph
.files
.find_file_node(&normalized_path)?
.expect("file node should exist after index_file");
let imports = vec![ImportFact {
file_path: PathBuf::from(test_file),
import_kind: ImportKind::UseCrate,
import_path: vec!["crate".to_string(), "foo".to_string()],
imported_names: vec!["bar".to_string()],
is_glob: false,
byte_start: 0,
byte_end: 50,
start_line: 1,
start_col: 0,
end_line: 1,
end_col: 50,
}];
let count = graph
.imports
.index_imports(test_file, file_id.as_i64(), imports, None)
.unwrap();
assert_eq!(count, 1);
let snapshot = SnapshotId::current();
let entity_ids = graph.imports.backend.entity_ids().unwrap();
let import_node = entity_ids
.iter()
.find(|&&id| {
let node = graph.imports.backend.get_node(snapshot, id).unwrap();
node.kind == "Import"
})
.map(|&id| graph.imports.backend.get_node(snapshot, id).unwrap());
assert!(import_node.is_some(), "Import node should be created");
Ok(())
}
#[test]
fn test_index_imports_with_module_resolver() -> Result<()> {
let temp_dir = TempDir::new()?;
let db_path = temp_dir.path().join("test.db");
let mut graph = crate::CodeGraph::open(&db_path)?;
let lib_file = "src/lib.rs";
let foo_file = "src/foo.rs";
std::fs::create_dir_all(temp_dir.path().join("src"))?;
graph.index_file(lib_file, b"fn lib() {}")?;
graph.index_file(foo_file, b"fn foo() {}")?;
graph.module_resolver.build_module_index()?;
let normalized_lib = crate::graph::files::normalize_path_for_index(lib_file);
let file_id = graph
.files
.find_file_node(&normalized_lib)?
.expect("lib.rs file node should exist after index_file");
let imports = vec![ImportFact {
file_path: PathBuf::from(lib_file),
import_kind: ImportKind::UseCrate,
import_path: vec!["crate".to_string(), "foo".to_string()],
imported_names: vec!["foo".to_string()],
is_glob: false,
byte_start: 0,
byte_end: 50,
start_line: 1,
start_col: 0,
end_line: 1,
end_col: 50,
}];
let count = graph
.imports
.index_imports(
lib_file,
file_id.as_i64(),
imports,
Some(&graph.module_resolver),
)
.unwrap();
assert_eq!(count, 1);
let snapshot = SnapshotId::current();
let entity_ids = graph.imports.backend.entity_ids().unwrap();
let import_node_option = entity_ids
.iter()
.find(|&&id| {
let node = graph.imports.backend.get_node(snapshot, id).unwrap();
node.kind == "Import"
})
.map(|&id| graph.imports.backend.get_node(snapshot, id).unwrap());
assert!(
import_node_option.is_some(),
"Import node should be created"
);
let import_node = import_node_option.unwrap();
let resolved_id = import_node.data.get("resolved_file_id");
assert!(
resolved_id.is_some(),
"Import should have resolved_file_id in metadata"
);
Ok(())
}
#[test]
fn test_cross_file_import_edges() -> Result<()> {
let temp_dir = TempDir::new()?;
let db_path = temp_dir.path().join("test.db");
let mut graph = crate::CodeGraph::open(&db_path)?;
let lib_file = "src/lib.rs";
let helper_file = "src/helper.rs";
std::fs::create_dir_all(temp_dir.path().join("src"))?;
graph.index_file(lib_file, b"fn lib() {}")?;
graph.index_file(helper_file, b"fn helper() {}")?;
graph.module_resolver.build_module_index()?;
let normalized_lib = crate::graph::files::normalize_path_for_index(lib_file);
let lib_file_id = graph
.files
.find_file_node(&normalized_lib)?
.expect("lib.rs file node should exist after index_file");
let normalized_helper = crate::graph::files::normalize_path_for_index(helper_file);
let helper_file_id = graph
.files
.find_file_node(&normalized_helper)?
.expect("helper.rs file node should exist after index_file");
let imports = vec![ImportFact {
file_path: PathBuf::from(lib_file),
import_kind: ImportKind::UseCrate,
import_path: vec!["crate".to_string(), "helper".to_string()],
imported_names: vec!["helper".to_string()],
is_glob: false,
byte_start: 0,
byte_end: 50,
start_line: 1,
start_col: 0,
end_line: 1,
end_col: 50,
}];
let normalized_lib_for_resolve = crate::graph::files::normalize_path_for_index(lib_file);
let count = graph.imports.index_imports(
&normalized_lib_for_resolve,
lib_file_id.as_i64(),
imports,
Some(&graph.module_resolver),
)?;
assert_eq!(count, 1);
let snapshot = SnapshotId::current();
let entity_ids = graph.imports.backend.entity_ids().unwrap();
let import_node_option = entity_ids
.iter()
.find(|&&id| {
let node = graph.imports.backend.get_node(snapshot, id).unwrap();
node.kind == "Import"
})
.map(|&id| graph.imports.backend.get_node(snapshot, id).unwrap());
assert!(
import_node_option.is_some(),
"Import node should be created"
);
let import_node = import_node_option.unwrap();
let import_id = entity_ids
.iter()
.find(|&&id| {
let node = graph.imports.backend.get_node(snapshot, id).unwrap();
node.kind == "Import"
})
.unwrap();
let resolved_id = import_node.data.get("resolved_file_id");
assert!(
resolved_id.is_some(),
"Import should have resolved_file_id in metadata"
);
let resolved_value = resolved_id.unwrap().as_i64();
assert_eq!(
resolved_value,
Some(helper_file_id.as_i64()),
"resolved_file_id should match helper.rs file ID"
);
use sqlitegraph::BackendDirection;
let outgoing_edges = graph
.imports
.backend
.neighbors(
snapshot,
*import_id,
sqlitegraph::NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("DEFINES".to_string()),
},
)
.unwrap();
assert_eq!(
outgoing_edges.len(),
1,
"Import should have exactly one DEFINES edge"
);
assert_eq!(
outgoing_edges[0],
helper_file_id.as_i64(),
"DEFINES edge should point to helper.rs file"
);
Ok(())
}
}