use anyhow::{Context, Result};
use rusqlite::Connection;
use std::collections::{HashMap, HashSet, VecDeque};
use crate::cache::CacheManager;
use crate::models::{Dependency, DependencyInfo, ImportType};
pub struct DependencyIndex {
cache: CacheManager,
}
impl DependencyIndex {
pub fn new(cache: CacheManager) -> Self {
Self { cache }
}
pub fn get_cache(&self) -> &CacheManager {
&self.cache
}
pub fn insert_dependency(
&self,
file_id: i64,
imported_path: String,
resolved_file_id: Option<i64>,
import_type: ImportType,
line_number: usize,
imported_symbols: Option<Vec<String>>,
) -> Result<()> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for dependency insert")?;
let import_type_str = match import_type {
ImportType::Internal => "internal",
ImportType::External => "external",
ImportType::Stdlib => "stdlib",
};
let symbols_json = imported_symbols
.as_ref()
.map(|syms| serde_json::to_string(syms).unwrap_or_else(|_| "[]".to_string()));
conn.execute(
"INSERT INTO file_dependencies (file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols)
VALUES (?, ?, ?, ?, ?, ?)",
rusqlite::params![
file_id,
imported_path,
resolved_file_id,
import_type_str,
line_number as i64,
symbols_json,
],
)?;
Ok(())
}
pub fn insert_export(
&self,
file_id: i64,
exported_symbol: Option<String>,
source_path: String,
resolved_source_id: Option<i64>,
line_number: usize,
) -> Result<()> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for export insert")?;
conn.execute(
"INSERT INTO file_exports (file_id, exported_symbol, source_path, resolved_source_id, line_number)
VALUES (?, ?, ?, ?, ?)",
rusqlite::params![
file_id,
exported_symbol,
source_path,
resolved_source_id,
line_number as i64,
],
)?;
Ok(())
}
pub fn batch_insert_dependencies(&self, dependencies: &[Dependency]) -> Result<()> {
if dependencies.is_empty() {
return Ok(());
}
let db_path = self.cache.path().join("meta.db");
let mut conn = Connection::open(&db_path)
.context("Failed to open meta.db for batch dependency insert")?;
let tx = conn.transaction()?;
for dep in dependencies {
let import_type_str = match dep.import_type {
ImportType::Internal => "internal",
ImportType::External => "external",
ImportType::Stdlib => "stdlib",
};
let symbols_json = dep
.imported_symbols
.as_ref()
.map(|syms| serde_json::to_string(syms).unwrap_or_else(|_| "[]".to_string()));
tx.execute(
"INSERT INTO file_dependencies (file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols)
VALUES (?, ?, ?, ?, ?, ?)",
rusqlite::params![
dep.file_id,
dep.imported_path,
dep.resolved_file_id,
import_type_str,
dep.line_number as i64,
symbols_json,
],
)?;
}
tx.commit()?;
log::debug!("Batch inserted {} dependencies", dependencies.len());
Ok(())
}
pub fn get_dependencies(&self, file_id: i64) -> Result<Vec<Dependency>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for dependency lookup")?;
let mut stmt = conn.prepare(
"SELECT file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols
FROM file_dependencies
WHERE file_id = ?
ORDER BY line_number",
)?;
let deps = stmt
.query_map([file_id], |row| {
let import_type_str: String = row.get(3)?;
let import_type = match import_type_str.as_str() {
"internal" => ImportType::Internal,
"external" => ImportType::External,
"stdlib" => ImportType::Stdlib,
_ => ImportType::External,
};
let symbols_json: Option<String> = row.get(5)?;
let imported_symbols = symbols_json.and_then(|json| {
serde_json::from_str(&json).ok()
});
Ok(Dependency {
file_id: row.get(0)?,
imported_path: row.get(1)?,
resolved_file_id: row.get(2)?,
import_type,
line_number: row.get::<_, i64>(4)? as usize,
imported_symbols,
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(deps)
}
pub fn get_dependents(&self, file_id: i64) -> Result<Vec<i64>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for reverse dependency lookup")?;
let mut stmt = conn.prepare(
"SELECT DISTINCT file_id
FROM file_dependencies
WHERE resolved_file_id = ?
ORDER BY file_id"
)?;
let dependents: Vec<i64> = stmt
.query_map([file_id], |row| row.get(0))?
.collect::<Result<Vec<_>, _>>()?;
Ok(dependents)
}
pub fn get_dependencies_info(&self, file_id: i64) -> Result<Vec<DependencyInfo>> {
let deps = self.get_dependencies(file_id)?;
let dep_infos = deps
.into_iter()
.map(|dep| {
let path = if let Some(resolved_id) = dep.resolved_file_id {
self.get_file_path(resolved_id).unwrap_or(dep.imported_path)
} else {
dep.imported_path
};
DependencyInfo {
path,
line: Some(dep.line_number),
symbols: dep.imported_symbols,
}
})
.collect();
Ok(dep_infos)
}
pub fn get_transitive_deps(&self, file_id: i64, max_depth: usize) -> Result<HashMap<i64, usize>> {
let mut visited = HashMap::new();
let mut queue = VecDeque::new();
queue.push_back((file_id, 0));
visited.insert(file_id, 0);
while let Some((current_id, depth)) = queue.pop_front() {
if depth >= max_depth {
continue;
}
let deps = self.get_dependencies(current_id)?;
for dep in deps {
if let Some(resolved_id) = dep.resolved_file_id {
if !visited.contains_key(&resolved_id) {
visited.insert(resolved_id, depth + 1);
queue.push_back((resolved_id, depth + 1));
}
}
}
}
Ok(visited)
}
pub fn detect_circular_dependencies(&self) -> Result<Vec<Vec<i64>>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for circular dependency analysis")?;
let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
let mut stmt = conn.prepare(
"SELECT file_id, resolved_file_id
FROM file_dependencies
WHERE resolved_file_id IS NOT NULL"
)?;
let dependencies: Vec<(i64, i64)> = stmt
.query_map([], |row| {
Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
})?
.collect::<Result<Vec<_>, _>>()?;
for (file_id, target_id) in dependencies {
graph.entry(file_id).or_insert_with(Vec::new).push(target_id);
}
let all_files = self.get_all_file_ids()?;
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
let mut path = Vec::new();
let mut cycles = Vec::new();
for file_id in all_files {
if !visited.contains(&file_id) {
self.dfs_cycle_detect(
file_id,
&graph,
&mut visited,
&mut rec_stack,
&mut path,
&mut cycles,
)?;
}
}
Ok(cycles)
}
fn dfs_cycle_detect(
&self,
file_id: i64,
graph: &HashMap<i64, Vec<i64>>,
visited: &mut HashSet<i64>,
rec_stack: &mut HashSet<i64>,
path: &mut Vec<i64>,
cycles: &mut Vec<Vec<i64>>,
) -> Result<()> {
visited.insert(file_id);
rec_stack.insert(file_id);
path.push(file_id);
if let Some(dependencies) = graph.get(&file_id) {
for &target_id in dependencies {
if !visited.contains(&target_id) {
self.dfs_cycle_detect(target_id, graph, visited, rec_stack, path, cycles)?;
} else if rec_stack.contains(&target_id) {
if let Some(cycle_start) = path.iter().position(|&id| id == target_id) {
let cycle = path[cycle_start..].to_vec();
cycles.push(cycle);
}
}
}
}
path.pop();
rec_stack.remove(&file_id);
Ok(())
}
pub fn get_file_paths(&self, file_ids: &[i64]) -> Result<HashMap<i64, String>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for file path lookup")?;
let mut paths = HashMap::new();
for &file_id in file_ids {
if let Ok(path) = conn.query_row(
"SELECT path FROM files WHERE id = ?",
[file_id],
|row| row.get::<_, String>(0),
) {
paths.insert(file_id, path);
}
}
Ok(paths)
}
fn get_file_path(&self, file_id: i64) -> Result<String> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for file path lookup")?;
let path = conn.query_row(
"SELECT path FROM files WHERE id = ?",
[file_id],
|row| row.get::<_, String>(0),
)?;
Ok(path)
}
fn get_all_file_ids(&self) -> Result<Vec<i64>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for file ID lookup")?;
let mut stmt = conn.prepare("SELECT id FROM files")?;
let file_ids = stmt
.query_map([], |row| row.get(0))?
.collect::<Result<Vec<_>, _>>()?;
Ok(file_ids)
}
pub fn find_hotspots(&self, limit: Option<usize>, min_dependents: usize) -> Result<Vec<(i64, usize)>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for hotspot analysis")?;
let mut stmt = conn.prepare(
"SELECT resolved_file_id, COUNT(*) as count
FROM file_dependencies
WHERE resolved_file_id IS NOT NULL
GROUP BY resolved_file_id
ORDER BY count DESC"
)?;
let mut hotspots: Vec<(i64, usize)> = stmt
.query_map([], |row| {
Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)? as usize))
})?
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.filter(|(_, count)| *count >= min_dependents)
.collect();
if let Some(lim) = limit {
hotspots.truncate(lim);
}
Ok(hotspots)
}
pub fn find_unused_files(&self) -> Result<Vec<i64>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for unused files analysis")?;
let mut used_files = HashSet::new();
let mut stmt = conn.prepare(
"SELECT DISTINCT resolved_file_id
FROM file_dependencies
WHERE resolved_file_id IS NOT NULL"
)?;
let direct_imports: Vec<i64> = stmt
.query_map([], |row| row.get(0))?
.collect::<Result<Vec<_>, _>>()?;
used_files.extend(&direct_imports);
for file_id in direct_imports {
let barrel_chain = self.resolve_through_barrel_exports(file_id)?;
used_files.extend(barrel_chain);
}
let mut stmt = conn.prepare("SELECT id FROM files ORDER BY id")?;
let all_files: Vec<i64> = stmt
.query_map([], |row| row.get(0))?
.collect::<Result<Vec<_>, _>>()?;
let unused: Vec<i64> = all_files
.into_iter()
.filter(|id| !used_files.contains(id))
.collect();
Ok(unused)
}
pub fn resolve_through_barrel_exports(&self, barrel_file_id: i64) -> Result<Vec<i64>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for barrel export resolution")?;
let mut resolved_files = Vec::new();
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(barrel_file_id);
visited.insert(barrel_file_id);
while let Some(current_id) = queue.pop_front() {
resolved_files.push(current_id);
let mut stmt = conn.prepare(
"SELECT resolved_source_id
FROM file_exports
WHERE file_id = ? AND resolved_source_id IS NOT NULL"
)?;
let exported_files: Vec<i64> = stmt
.query_map([current_id], |row| row.get(0))?
.collect::<Result<Vec<_>, _>>()?;
for exported_id in exported_files {
if !visited.contains(&exported_id) {
visited.insert(exported_id);
queue.push_back(exported_id);
}
}
}
Ok(resolved_files)
}
pub fn find_islands(&self) -> Result<Vec<Vec<i64>>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for island analysis")?;
let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
let mut stmt = conn.prepare(
"SELECT file_id, resolved_file_id
FROM file_dependencies
WHERE resolved_file_id IS NOT NULL"
)?;
let dependencies: Vec<(i64, i64)> = stmt
.query_map([], |row| {
Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
})?
.collect::<Result<Vec<_>, _>>()?;
for (file_id, target_id) in dependencies {
graph.entry(file_id).or_insert_with(Vec::new).push(target_id);
graph.entry(target_id).or_insert_with(Vec::new).push(file_id);
}
let all_files = self.get_all_file_ids()?;
for file_id in &all_files {
graph.entry(*file_id).or_insert_with(Vec::new);
}
let mut visited = HashSet::new();
let mut islands = Vec::new();
for &file_id in &all_files {
if !visited.contains(&file_id) {
let mut island = Vec::new();
self.dfs_island(&file_id, &graph, &mut visited, &mut island);
islands.push(island);
}
}
islands.sort_by(|a, b| b.len().cmp(&a.len()));
log::info!("Found {} islands (connected components)", islands.len());
Ok(islands)
}
fn dfs_island(
&self,
file_id: &i64,
graph: &HashMap<i64, Vec<i64>>,
visited: &mut HashSet<i64>,
island: &mut Vec<i64>,
) {
visited.insert(*file_id);
island.push(*file_id);
if let Some(neighbors) = graph.get(file_id) {
for &neighbor in neighbors {
if !visited.contains(&neighbor) {
self.dfs_island(&neighbor, graph, visited, island);
}
}
}
}
fn build_resolution_cache(&self) -> Result<HashMap<String, i64>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for building resolution cache")?;
let mut stmt = conn.prepare(
"SELECT DISTINCT imported_path FROM file_dependencies"
)?;
let imported_paths: Vec<String> = stmt
.query_map([], |row| row.get(0))?
.collect::<Result<Vec<_>, _>>()?;
let total_paths = imported_paths.len();
log::info!("Building resolution cache for {} unique imported paths", total_paths);
let mut cache = HashMap::new();
for imported_path in imported_paths {
if let Ok(Some(file_id)) = self.resolve_imported_path_to_file_id(&imported_path) {
cache.insert(imported_path, file_id);
}
}
log::info!(
"Resolution cache built: {} resolved, {} unresolved",
cache.len(),
total_paths - cache.len()
);
Ok(cache)
}
pub fn clear_dependencies(&self, file_id: i64) -> Result<()> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for dependency clearing")?;
conn.execute(
"DELETE FROM file_dependencies WHERE file_id = ?",
[file_id],
)?;
Ok(())
}
pub fn resolve_imported_path_to_file_id(&self, imported_path: &str) -> Result<Option<i64>> {
let path_variants = generate_path_variants(imported_path);
for variant in &path_variants {
if let Ok(Some(file_id)) = self.get_file_id_by_path(variant) {
log::trace!("Resolved '{}' → '{}' (file_id: {})", imported_path, variant, file_id);
return Ok(Some(file_id));
}
}
Ok(None)
}
pub fn get_file_id_by_path(&self, path: &str) -> Result<Option<i64>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for file ID lookup")?;
let normalized_path = normalize_path_for_lookup(path);
match conn.query_row(
"SELECT id FROM files WHERE path = ?",
[&normalized_path],
|row| row.get::<_, i64>(0),
) {
Ok(id) => return Ok(Some(id)),
Err(rusqlite::Error::QueryReturnedNoRows) => {
}
Err(e) => return Err(e.into()),
}
let mut stmt = conn.prepare(
"SELECT id, path FROM files WHERE path LIKE '%' || ?"
)?;
let matches: Vec<(i64, String)> = stmt
.query_map([&normalized_path], |row| {
Ok((row.get(0)?, row.get(1)?))
})?
.collect::<Result<Vec<_>, _>>()?;
match matches.len() {
0 => Ok(None),
1 => Ok(Some(matches[0].0)),
_ => {
let paths: Vec<String> = matches.iter().map(|(_, p)| p.clone()).collect();
anyhow::bail!(
"Ambiguous path '{}' matches multiple files:\n {}\n\nPlease be more specific.",
path,
paths.join("\n ")
);
}
}
}
pub fn get_resolution_stats(&self) -> Result<Vec<(String, usize, usize, f64)>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for resolution stats")?;
let mut stmt = conn.prepare(
"SELECT
CASE
WHEN f.path LIKE '%.py' THEN 'Python'
WHEN f.path LIKE '%.go' THEN 'Go'
WHEN f.path LIKE '%.ts' THEN 'TypeScript'
WHEN f.path LIKE '%.rs' THEN 'Rust'
WHEN f.path LIKE '%.js' OR f.path LIKE '%.jsx' THEN 'JavaScript'
WHEN f.path LIKE '%.php' THEN 'PHP'
WHEN f.path LIKE '%.java' THEN 'Java'
WHEN f.path LIKE '%.kt' THEN 'Kotlin'
WHEN f.path LIKE '%.rb' THEN 'Ruby'
WHEN f.path LIKE '%.c' OR f.path LIKE '%.h' THEN 'C'
WHEN f.path LIKE '%.cpp' OR f.path LIKE '%.cc' OR f.path LIKE '%.hpp' THEN 'C++'
WHEN f.path LIKE '%.cs' THEN 'C#'
WHEN f.path LIKE '%.zig' THEN 'Zig'
ELSE 'Other'
END as language,
COUNT(*) as total,
SUM(CASE WHEN d.resolved_file_id IS NOT NULL THEN 1 ELSE 0 END) as resolved
FROM file_dependencies d
JOIN files f ON d.file_id = f.id
WHERE d.import_type = 'internal'
GROUP BY language
ORDER BY language",
)?;
let mut stats = Vec::new();
let rows = stmt.query_map([], |row| {
let language: String = row.get(0)?;
let total: i64 = row.get(1)?;
let resolved: i64 = row.get(2)?;
let rate = if total > 0 {
(resolved as f64 / total as f64) * 100.0
} else {
0.0
};
Ok((language, total as usize, resolved as usize, rate))
})?;
for row in rows {
stats.push(row?);
}
Ok(stats)
}
pub fn get_all_internal_dependencies(&self) -> Result<Vec<(String, String, Option<String>)>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for internal dependencies")?;
let mut stmt = conn.prepare(
"SELECT
f.path,
d.imported_path,
f2.path as resolved_path
FROM file_dependencies d
JOIN files f ON d.file_id = f.id
LEFT JOIN files f2 ON d.resolved_file_id = f2.id
WHERE d.import_type = 'internal'
ORDER BY f.path",
)?;
let mut deps = Vec::new();
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, Option<String>>(2)?,
))
})?;
for row in rows {
deps.push(row?);
}
Ok(deps)
}
pub fn get_dependency_count_by_type(&self) -> Result<Vec<(String, usize)>> {
let db_path = self.cache.path().join("meta.db");
let conn = Connection::open(&db_path)
.context("Failed to open meta.db for dependency count")?;
let mut stmt = conn.prepare(
"SELECT import_type, COUNT(*) as count
FROM file_dependencies
GROUP BY import_type
ORDER BY import_type",
)?;
let mut counts = Vec::new();
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, i64>(1)? as usize,
))
})?;
for row in rows {
counts.push(row?);
}
Ok(counts)
}
}
fn generate_path_variants(import_path: &str) -> Vec<String> {
let path = import_path.replace('\\', "/").replace("::", "/");
let path = path.trim_matches('"').trim_matches('\'');
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if components.is_empty() {
return vec![];
}
let mut variants = Vec::new();
for start_idx in 0..components.len() {
let suffix = components[start_idx..].join("/");
if !suffix.ends_with(".php") {
variants.push(format!("{}.php", suffix));
} else {
variants.push(suffix.clone());
}
if !suffix.contains('.') {
variants.push(format!("{}.rs", suffix));
variants.push(format!("{}.ts", suffix));
variants.push(format!("{}.js", suffix));
variants.push(format!("{}.py", suffix));
}
}
variants
}
fn normalize_path_for_lookup(path: &str) -> String {
let mut normalized = path.trim_start_matches("./").to_string();
if normalized.starts_with("../") {
normalized = normalized.trim_start_matches("../").to_string();
}
if normalized.starts_with('/') || normalized.starts_with('\\') {
let markers = ["services", "src", "app", "lib", "packages", "modules"];
let mut found_marker = false;
for marker in &markers {
if let Some(idx) = normalized.find(marker) {
normalized = normalized[idx..].to_string();
found_marker = true;
break;
}
}
if !found_marker {
use std::path::Path;
let path_obj = Path::new(&normalized);
if let Some(filename) = path_obj.file_name() {
normalized = filename.to_string_lossy().to_string();
}
}
}
normalized
}
pub fn resolve_rust_import(
import_path: &str,
current_file: &str,
project_root: &std::path::Path,
) -> Option<String> {
use std::path::{Path, PathBuf};
if !import_path.starts_with("crate::")
&& !import_path.starts_with("super::")
&& !import_path.starts_with("self::")
{
return None;
}
let current_path = Path::new(current_file);
let mut resolved_path: Option<PathBuf> = None;
if import_path.starts_with("crate::") {
let crate_root = if project_root.join("src/lib.rs").exists() {
project_root.join("src")
} else if project_root.join("src/main.rs").exists() {
project_root.join("src")
} else {
project_root.join("src")
};
let path_parts: Vec<&str> = import_path
.strip_prefix("crate::")
.unwrap()
.split("::")
.collect();
resolved_path = resolve_module_path(&crate_root, &path_parts);
} else if import_path.starts_with("super::") {
if let Some(current_dir) = current_path.parent() {
if let Some(parent_dir) = current_dir.parent() {
let path_parts: Vec<&str> = import_path
.strip_prefix("super::")
.unwrap()
.split("::")
.collect();
resolved_path = resolve_module_path(parent_dir, &path_parts);
}
}
} else if import_path.starts_with("self::") {
if let Some(current_dir) = current_path.parent() {
let path_parts: Vec<&str> = import_path
.strip_prefix("self::")
.unwrap()
.split("::")
.collect();
resolved_path = resolve_module_path(current_dir, &path_parts);
}
}
resolved_path.and_then(|p| {
p.strip_prefix(project_root)
.ok()
.map(|rel| rel.to_string_lossy().to_string())
})
}
fn resolve_module_path(start_dir: &std::path::Path, components: &[&str]) -> Option<std::path::PathBuf> {
if components.is_empty() {
return None;
}
let mut current = start_dir.to_path_buf();
for &component in &components[..components.len() - 1] {
let dir_path = current.join(component);
let mod_file = dir_path.join("mod.rs");
if mod_file.exists() {
current = dir_path;
} else {
return None;
}
}
let last_component = components.last().unwrap();
let file_path = current.join(format!("{}.rs", last_component));
if file_path.exists() {
return Some(file_path);
}
let dir_path = current.join(last_component);
let mod_file = dir_path.join("mod.rs");
if mod_file.exists() {
return Some(mod_file);
}
None
}
pub fn resolve_rust_mod_declaration(
mod_name: &str,
current_file: &str,
_project_root: &std::path::Path,
) -> Option<String> {
use std::path::Path;
let current_path = Path::new(current_file);
let current_dir = current_path.parent()?;
let sibling = current_dir.join(format!("{}.rs", mod_name));
if sibling.exists() {
return Some(sibling.to_string_lossy().to_string());
}
let dir_mod = current_dir.join(mod_name).join("mod.rs");
if dir_mod.exists() {
return Some(dir_mod.to_string_lossy().to_string());
}
None
}
pub fn resolve_php_import(
import_path: &str,
_current_file: &str,
project_root: &std::path::Path,
) -> Option<String> {
const VENDOR_NAMESPACES: &[&str] = &[
"Illuminate\\", "Symfony\\", "Laravel\\", "Psr\\",
"Doctrine\\", "Monolog\\", "PHPUnit\\", "Carbon\\",
"GuzzleHttp\\", "Composer\\", "Predis\\", "League\\"
];
for vendor_ns in VENDOR_NAMESPACES {
if import_path.starts_with(vendor_ns) {
return None;
}
}
let file_path = import_path.replace('\\', "/");
let path_candidates = vec![
{
let parts: Vec<&str> = file_path.split('/').collect();
if let Some(first) = parts.first() {
let mut result = vec![first.to_lowercase()];
result.extend(parts[1..].iter().map(|s| s.to_string()));
result.join("/") + ".php"
} else {
file_path.clone() + ".php"
}
},
file_path.clone() + ".php",
file_path.to_lowercase() + ".php",
];
for candidate in &path_candidates {
let full_path = project_root.join(candidate);
if full_path.exists() {
return Some(candidate.clone());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn setup_test_cache() -> (TempDir, CacheManager) {
let temp = TempDir::new().unwrap();
let cache = CacheManager::new(temp.path());
cache.init().unwrap();
cache.update_file("src/main.rs", "rust", 100).unwrap();
cache.update_file("src/lib.rs", "rust", 50).unwrap();
cache.update_file("src/utils.rs", "rust", 30).unwrap();
(temp, cache)
}
#[test]
fn test_insert_and_get_dependencies() {
let (_temp, cache) = setup_test_cache();
let deps_index = DependencyIndex::new(cache);
let main_id = 1i64;
let lib_id = 2i64;
deps_index
.insert_dependency(
main_id,
"crate::lib".to_string(),
Some(lib_id),
ImportType::Internal,
5,
None,
)
.unwrap();
let deps = deps_index.get_dependencies(main_id).unwrap();
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].imported_path, "crate::lib");
assert_eq!(deps[0].resolved_file_id, Some(lib_id));
assert_eq!(deps[0].import_type, ImportType::Internal);
}
#[test]
fn test_reverse_lookup() {
let (_temp, cache) = setup_test_cache();
let deps_index = DependencyIndex::new(cache);
let main_id = 1i64;
let lib_id = 2i64;
let utils_id = 3i64;
deps_index
.insert_dependency(
main_id,
"crate::lib".to_string(),
Some(lib_id),
ImportType::Internal,
5,
None,
)
.unwrap();
deps_index
.insert_dependency(
utils_id,
"crate::lib".to_string(),
Some(lib_id),
ImportType::Internal,
3,
None,
)
.unwrap();
let dependents = deps_index.get_dependents(lib_id).unwrap();
assert_eq!(dependents.len(), 2);
assert!(dependents.contains(&main_id));
assert!(dependents.contains(&utils_id));
}
#[test]
fn test_transitive_dependencies() {
let (_temp, cache) = setup_test_cache();
let deps_index = DependencyIndex::new(cache);
let file1 = 1i64;
let file2 = 2i64;
let file3 = 3i64;
deps_index
.insert_dependency(
file1,
"file2".to_string(),
Some(file2),
ImportType::Internal,
1,
None,
)
.unwrap();
deps_index
.insert_dependency(
file2,
"file3".to_string(),
Some(file3),
ImportType::Internal,
1,
None,
)
.unwrap();
let transitive = deps_index.get_transitive_deps(file1, 2).unwrap();
assert_eq!(transitive.len(), 3);
assert_eq!(transitive.get(&file1), Some(&0));
assert_eq!(transitive.get(&file2), Some(&1));
assert_eq!(transitive.get(&file3), Some(&2));
}
#[test]
fn test_batch_insert() {
let (_temp, cache) = setup_test_cache();
let deps_index = DependencyIndex::new(cache);
let deps = vec![
Dependency {
file_id: 1,
imported_path: "std::collections".to_string(),
resolved_file_id: None,
import_type: ImportType::Stdlib,
line_number: 1,
imported_symbols: Some(vec!["HashMap".to_string()]),
},
Dependency {
file_id: 1,
imported_path: "crate::lib".to_string(),
resolved_file_id: Some(2),
import_type: ImportType::Internal,
line_number: 2,
imported_symbols: None,
},
];
deps_index.batch_insert_dependencies(&deps).unwrap();
let retrieved = deps_index.get_dependencies(1).unwrap();
assert_eq!(retrieved.len(), 2);
}
#[test]
fn test_clear_dependencies() {
let (_temp, cache) = setup_test_cache();
let deps_index = DependencyIndex::new(cache);
deps_index
.insert_dependency(
1,
"crate::lib".to_string(),
Some(2),
ImportType::Internal,
1,
None,
)
.unwrap();
assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 1);
deps_index.clear_dependencies(1).unwrap();
assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 0);
}
#[test]
fn test_resolve_rust_import_crate() {
use std::fs;
use tempfile::TempDir;
let temp = TempDir::new().unwrap();
let project_root = temp.path();
fs::create_dir_all(project_root.join("src")).unwrap();
fs::write(project_root.join("src/lib.rs"), "").unwrap();
fs::write(project_root.join("src/models.rs"), "").unwrap();
let resolved = resolve_rust_import(
"crate::models",
"src/query.rs",
project_root,
);
assert_eq!(resolved, Some("src/models.rs".to_string()));
}
#[test]
fn test_resolve_rust_import_super() {
use std::fs;
use tempfile::TempDir;
let temp = TempDir::new().unwrap();
let project_root = temp.path();
fs::create_dir_all(project_root.join("src/parsers")).unwrap();
fs::write(project_root.join("src/models.rs"), "").unwrap();
fs::write(project_root.join("src/parsers/rust.rs"), "").unwrap();
let current_file = project_root.join("src/parsers/rust.rs");
let resolved = resolve_rust_import(
"super::models",
¤t_file.to_string_lossy(),
project_root,
);
assert_eq!(resolved, Some("src/models.rs".to_string()));
}
#[test]
fn test_resolve_rust_import_external() {
use tempfile::TempDir;
let temp = TempDir::new().unwrap();
let project_root = temp.path();
let resolved = resolve_rust_import(
"serde::Serialize",
"src/models.rs",
project_root,
);
assert_eq!(resolved, None);
let resolved = resolve_rust_import(
"std::collections::HashMap",
"src/models.rs",
project_root,
);
assert_eq!(resolved, None);
}
#[test]
fn test_resolve_rust_mod_declaration() {
use std::fs;
use tempfile::TempDir;
let temp = TempDir::new().unwrap();
let project_root = temp.path();
fs::create_dir_all(project_root.join("src")).unwrap();
fs::write(project_root.join("src/lib.rs"), "").unwrap();
fs::write(project_root.join("src/parser.rs"), "").unwrap();
let resolved = resolve_rust_mod_declaration(
"parser",
&project_root.join("src/lib.rs").to_string_lossy(),
project_root,
);
assert!(resolved.is_some());
assert!(resolved.unwrap().ends_with("src/parser.rs"));
}
#[test]
fn test_resolve_rust_import_nested() {
use std::fs;
use tempfile::TempDir;
let temp = TempDir::new().unwrap();
let project_root = temp.path();
fs::create_dir_all(project_root.join("src/models")).unwrap();
fs::write(project_root.join("src/models/mod.rs"), "").unwrap();
fs::write(project_root.join("src/models/language.rs"), "").unwrap();
let resolved = resolve_rust_import(
"crate::models::language",
"src/query.rs",
project_root,
);
assert_eq!(resolved, Some("src/models/language.rs".to_string()));
}
}