use rusqlite::Connection;
#[allow(unused_imports)]
use super::symbols::{IndexedSymbol, SymbolKind};
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CallEdge {
pub caller: String,
pub callee: String,
pub file: String,
pub line: u32,
}
#[allow(dead_code)]
pub fn store_edges(
conn: &Connection,
edges: &[CallEdge],
project_id: i64,
) -> anyhow::Result<usize> {
let tx = conn.unchecked_transaction()?;
let mut count = 0;
for edge in edges {
tx.execute(
"INSERT INTO call_graph (caller, callee, file, line, project_id) VALUES (?1, ?2, ?3, ?4, ?5)",
rusqlite::params![edge.caller, edge.callee, edge.file, edge.line as i64, project_id],
)?;
count += 1;
}
tx.commit()?;
Ok(count)
}
#[cfg(feature = "tree-sitter")]
#[derive(Debug, Clone)]
pub struct KgEdge {
pub source: String,
pub kind: String,
pub target: String,
pub file: String,
pub line: u32,
}
#[cfg(feature = "tree-sitter")]
#[allow(dead_code)]
pub fn store_kg_edges(
conn: &Connection,
edges: &[KgEdge],
project_id: i64,
) -> anyhow::Result<usize> {
let tx = conn.unchecked_transaction()?;
let mut count = 0;
for edge in edges {
tx.execute(
"INSERT INTO edges (source, kind, target, file, line, project_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![edge.source, edge.kind, edge.target, edge.file, edge.line as i64, project_id],
)?;
count += 1;
}
tx.commit()?;
Ok(count)
}
#[cfg(feature = "tree-sitter")]
#[allow(dead_code)]
pub fn clear_kg_edges_for_file(
conn: &Connection,
file: &str,
project_id: i64,
) -> anyhow::Result<()> {
conn.execute(
"DELETE FROM edges WHERE file = ?1 AND project_id = ?2",
rusqlite::params![file, project_id],
)?;
Ok(())
}
#[allow(dead_code)]
pub fn clear_edges_for_file(conn: &Connection, file: &str, project_id: i64) -> anyhow::Result<()> {
conn.execute(
"DELETE FROM call_graph WHERE file = ?1 AND project_id = ?2",
rusqlite::params![file, project_id],
)?;
Ok(())
}
pub fn find_callers(
conn: &Connection,
project_id: i64,
symbol_name: &str,
limit: usize,
) -> anyhow::Result<Vec<CallerResult>> {
let pattern = format!("%{symbol_name}%");
let mut stmt = conn.prepare(
"SELECT DISTINCT cg.caller, cg.file, cg.line
FROM call_graph cg
WHERE cg.callee LIKE ?1 AND cg.project_id = ?2
LIMIT ?3",
)?;
let rows = stmt.query_map(
rusqlite::params![pattern, project_id, limit as i64],
|row| {
Ok(CallerResult {
caller: row.get(0)?,
file: row.get(1)?,
line: row.get::<_, i64>(2)? as u32,
})
},
)?;
Ok(rows.filter_map(|r| r.ok()).collect())
}
pub fn find_callers_cross_project(
conn: &Connection,
symbol_name: &str,
limit: usize,
) -> anyhow::Result<Vec<CrossProjectCallerResult>> {
let pattern = format!("%{symbol_name}%");
let mut stmt = conn.prepare(
"SELECT DISTINCT cg.caller, cg.file, cg.line, p.root_path
FROM call_graph cg
JOIN projects p ON cg.project_id = p.id
WHERE cg.callee LIKE ?1
LIMIT ?2",
)?;
let rows = stmt.query_map(rusqlite::params![pattern, limit as i64], |row| {
Ok(CrossProjectCallerResult {
caller: row.get(0)?,
file: row.get(1)?,
line: row.get::<_, i64>(2)? as u32,
project_root: row.get(3)?,
})
})?;
Ok(rows.filter_map(|r| r.ok()).collect())
}
#[allow(dead_code)]
pub fn find_callees(
conn: &Connection,
project_id: i64,
symbol_name: &str,
limit: usize,
) -> anyhow::Result<Vec<CalleeResult>> {
let pattern = format!("%{symbol_name}%");
let mut stmt = conn.prepare(
"SELECT DISTINCT cg.callee, cg.file, cg.line
FROM call_graph cg
WHERE cg.caller LIKE ?1 AND cg.project_id = ?2
LIMIT ?3",
)?;
let rows = stmt.query_map(
rusqlite::params![pattern, project_id, limit as i64],
|row| {
Ok(CalleeResult {
callee: row.get(0)?,
file: row.get(1)?,
line: row.get::<_, i64>(2)? as u32,
})
},
)?;
Ok(rows.filter_map(|r| r.ok()).collect())
}
pub fn impact_analysis(
conn: &Connection,
project_id: i64,
symbol_name: &str,
depth: u32,
) -> anyhow::Result<Vec<ImpactNode>> {
let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut result = Vec::new();
let mut current_level = vec![symbol_name.to_string()];
let mut current_depth = 0u32;
while current_depth < depth && !current_level.is_empty() {
let mut next_level = Vec::new();
for sym in ¤t_level {
if !visited.insert(sym.clone()) {
continue;
}
let callers = find_callers(conn, project_id, sym, 100)?;
for caller in callers {
let node = ImpactNode {
symbol: caller.caller.clone(),
file: caller.file.clone(),
line: caller.line,
depth: current_depth + 1,
};
if !visited.contains(&caller.caller) {
next_level.push(caller.caller.clone());
}
result.push(node);
}
}
current_level = next_level;
current_depth += 1;
}
result.sort_by(|a, b| {
a.depth
.cmp(&b.depth)
.then_with(|| a.file.cmp(&b.file))
.then_with(|| a.line.cmp(&b.line))
});
Ok(result)
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct CallerResult {
pub caller: String,
pub file: String,
pub line: u32,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct CrossProjectCallerResult {
pub caller: String,
pub file: String,
pub line: u32,
pub project_root: String,
}
#[allow(dead_code)]
#[derive(Debug, Clone, serde::Serialize)]
pub struct CalleeResult {
pub callee: String,
pub file: String,
pub line: u32,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ImpactNode {
pub symbol: String,
pub file: String,
pub line: u32,
pub depth: u32,
}
pub fn trace_path(
conn: &Connection,
project_id: i64,
symbol_name: &str,
depth: u32,
direction: TraceDirection,
) -> anyhow::Result<Vec<TraceNode>> {
let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut result = Vec::new();
let mut current_level = vec![(symbol_name.to_string(), 0u32)];
visited.insert(symbol_name.to_string());
while !current_level.is_empty() {
let mut next_level = Vec::new();
for (sym, d) in ¤t_level {
if *d >= depth {
continue;
}
let neighbors = match direction {
TraceDirection::Outgoing => find_callees_edges(conn, project_id, sym, 100)?,
TraceDirection::Incoming => find_callers_edges(conn, project_id, sym, 100)?,
};
for neighbor in neighbors {
let name = match direction {
TraceDirection::Outgoing => neighbor.target,
TraceDirection::Incoming => neighbor.source,
};
let is_new = visited.insert(name.clone());
let node = TraceNode {
symbol: name.clone(),
file: neighbor.file,
line: neighbor.line,
kind: neighbor.kind,
depth: d + 1,
};
if is_new {
next_level.push((name.clone(), d + 1));
}
result.push(node);
}
}
current_level = next_level;
}
result.sort_by(|a, b| a.depth.cmp(&b.depth).then_with(|| a.file.cmp(&b.file)));
Ok(result)
}
#[derive(Debug, Clone, Copy)]
pub enum TraceDirection {
Outgoing,
Incoming,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct TraceNode {
pub symbol: String,
pub file: String,
pub line: u32,
pub kind: String,
pub depth: u32,
}
fn find_callees_edges(
conn: &Connection,
project_id: i64,
symbol_name: &str,
limit: usize,
) -> anyhow::Result<Vec<EdgeRow>> {
let pattern = format!("%{symbol_name}%");
let mut stmt = conn.prepare(
"SELECT source, kind, target, file, line
FROM edges
WHERE source LIKE ?1 AND project_id = ?2
LIMIT ?3",
)?;
let rows: Vec<EdgeRow> = stmt
.query_map(
rusqlite::params![pattern, project_id, limit as i64],
|row| {
Ok(EdgeRow {
source: row.get(0)?,
kind: row.get(1)?,
target: row.get(2)?,
file: row.get(3)?,
line: row.get::<_, i64>(4)? as u32,
})
},
)?
.filter_map(|r| r.ok())
.collect();
if !rows.is_empty() {
return Ok(rows);
}
let mut stmt = conn.prepare(
"SELECT caller, 'CALLS', callee, file, line
FROM call_graph
WHERE caller LIKE ?1 AND project_id = ?2
LIMIT ?3",
)?;
let rows: Vec<EdgeRow> = stmt
.query_map(
rusqlite::params![pattern, project_id, limit as i64],
|row| {
Ok(EdgeRow {
source: row.get(0)?,
kind: row.get(1)?,
target: row.get(2)?,
file: row.get(3)?,
line: row.get::<_, i64>(4)? as u32,
})
},
)?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
}
fn find_callers_edges(
conn: &Connection,
project_id: i64,
symbol_name: &str,
limit: usize,
) -> anyhow::Result<Vec<EdgeRow>> {
let pattern = format!("%{symbol_name}%");
let mut stmt = conn.prepare(
"SELECT source, kind, target, file, line
FROM edges
WHERE target LIKE ?1 AND project_id = ?2
LIMIT ?3",
)?;
let rows: Vec<EdgeRow> = stmt
.query_map(
rusqlite::params![pattern, project_id, limit as i64],
|row| {
Ok(EdgeRow {
source: row.get(0)?,
kind: row.get(1)?,
target: row.get(2)?,
file: row.get(3)?,
line: row.get::<_, i64>(4)? as u32,
})
},
)?
.filter_map(|r| r.ok())
.collect();
if !rows.is_empty() {
return Ok(rows);
}
let mut stmt = conn.prepare(
"SELECT caller, 'CALLS', callee, file, line
FROM call_graph
WHERE callee LIKE ?1 AND project_id = ?2
LIMIT ?3",
)?;
let rows: Vec<EdgeRow> = stmt
.query_map(
rusqlite::params![pattern, project_id, limit as i64],
|row| {
Ok(EdgeRow {
source: row.get(0)?,
kind: row.get(1)?,
target: row.get(2)?,
file: row.get(3)?,
line: row.get::<_, i64>(4)? as u32,
})
},
)?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
}
struct EdgeRow {
source: String,
kind: String,
target: String,
file: String,
line: u32,
}
pub fn arch_overview(conn: &Connection, project_id: i64) -> anyhow::Result<ArchOverview> {
let mut stmt = conn.prepare(
"SELECT
CASE
WHEN instr(file, '/') > 0 THEN substr(file, 1, instr(file, '/') - 1)
ELSE file
END AS module,
COUNT(*) AS symbol_count,
COUNT(DISTINCT kind) AS edge_types
FROM symbols
WHERE project_id = ?1
GROUP BY module
ORDER BY symbol_count DESC",
)?;
let modules: Vec<ModuleInfo> = stmt
.query_map(rusqlite::params![project_id], |row| {
Ok(ModuleInfo {
name: row.get(0)?,
symbol_count: row.get::<_, i64>(1)? as usize,
edge_types: row.get::<_, i64>(2)? as usize,
})
})?
.filter_map(|r| r.ok())
.collect();
let edge_counts = get_edge_kind_counts(conn, project_id);
Ok(ArchOverview {
modules,
edge_counts,
})
}
fn get_edge_kind_counts(conn: &Connection, project_id: i64) -> Vec<(String, i64)> {
let mut counts = Vec::new();
if let Ok(mut s) = conn.prepare(
"SELECT kind, COUNT(*) FROM edges WHERE project_id = ?1 GROUP BY kind ORDER BY COUNT(*) DESC",
) {
if let Ok(rows) = s.query_map(rusqlite::params![project_id], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
}) {
counts.extend(rows.flatten());
}
}
if counts.is_empty() {
if let Ok(mut s) =
conn.prepare("SELECT 'CALLS', COUNT(*) FROM call_graph WHERE project_id = ?1")
{
if let Ok(rows) = s.query_map(rusqlite::params![project_id], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
}) {
counts.extend(rows.flatten());
}
}
}
counts
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ArchOverview {
pub modules: Vec<ModuleInfo>,
pub edge_counts: Vec<(String, i64)>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ModuleInfo {
pub name: String,
pub symbol_count: usize,
pub edge_types: usize,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct DeadCodeResult {
pub name: String,
pub kind: String,
pub file: String,
pub line: u32,
pub signature: Option<String>,
pub reason: String,
}
#[derive(Debug, Clone, Default)]
pub struct DeadCodeOptions {
pub include_tests: bool,
pub min_lines: Option<u32>,
pub entry_point_patterns: Vec<String>,
}
const WELL_KNOWN_NAMES: &[&str] = &[
"main",
"new",
"drop",
"default",
"clone",
"eq",
"hash",
"fmt",
"debug",
"display",
"from_str",
"into",
"from",
"try_from",
"try_into",
"deref",
"deref_mut",
"index",
"index_mut",
"add",
"sub",
"mul",
"div",
"rem",
"neg",
"not",
"bitand",
"bitor",
"bitxor",
"shl",
"shr",
"partial_eq",
"partial_ord",
"eq_ignore_ascii_case",
"is_ascii",
"to_lowercase",
"to_uppercase",
"trim",
"trim_start",
"trim_end",
"as_str",
"as_bytes",
"as_mut",
"as_ref",
"into_iter",
"iter",
"next",
"map",
"filter",
"fold",
"collect",
"len",
"is_empty",
"contains",
"push",
"pop",
"insert",
"remove",
"clear",
"get",
"get_mut",
"with_capacity",
"to_vec",
"to_string",
"parse",
"unwrap",
"expect",
"ok",
"err",
"is_ok",
"is_err",
"is_some",
"is_none",
"take",
"replace",
"lock",
"read",
"write",
"flush",
"close",
"connect",
"accept",
"bind",
"listen",
"send",
"recv",
"shutdown",
"spawn",
"join",
"sleep",
"yield_now",
"block_on",
"from_secs",
"from_millis",
"elapsed",
"instant",
"system_time",
"now",
"duration_since",
];
const FRAMEWORK_ENTRY_PREFIXES: &[&str] = &[
"handle_",
"load",
"create",
"preload",
"update",
"render",
"component_did_mount",
"on_mount",
"on_destroy",
"handle",
"process",
"execute",
"serve",
"start",
"stop",
"run",
"resolve_",
];
const FRAMEWORK_ENTRY_SUFFIXES: &[&str] = &[
"Handler",
"Listener",
"Callback",
"Middleware",
"Route",
"Hook",
"Plugin",
"Provider",
];
pub fn find_dead_code(
conn: &Connection,
project_id: i64,
opts: &DeadCodeOptions,
) -> anyhow::Result<Vec<DeadCodeResult>> {
let placeholders: Vec<&str> = WELL_KNOWN_NAMES.iter().map(|_| "?").collect();
let not_in_clause = placeholders.join(", ");
let mut sql = String::from(
"SELECT s.name, s.kind, s.file, s.line, s.signature
FROM symbols s
WHERE s.project_id = ?1
AND s.kind IN ('function', 'method', 'procedure')
AND s.name NOT IN (",
);
sql.push_str(¬_in_clause);
sql.push_str(
")
AND NOT EXISTS (
SELECT 1 FROM call_graph cg
WHERE cg.callee = s.name AND cg.project_id = ?1
)",
);
if !opts.include_tests {
sql.push_str(" AND s.name NOT LIKE 'test_%' AND s.name NOT LIKE '%_test'");
}
sql.push_str(" AND (s.signature IS NULL OR s.signature NOT LIKE '%cora: keep%')");
for prefix in FRAMEWORK_ENTRY_PREFIXES {
sql.push_str(&format!(" AND LOWER(s.name) NOT LIKE '{prefix}%'"));
}
for suffix in FRAMEWORK_ENTRY_SUFFIXES {
sql.push_str(&format!(" AND s.name NOT LIKE '%{suffix}'"));
}
let _ = &opts.entry_point_patterns;
if opts.min_lines.is_some() {
}
sql.push_str(" ORDER BY s.file, s.line");
let mut stmt = conn.prepare(&sql)?;
let project_id_ref = &project_id;
let mut params_vec: Vec<&dyn rusqlite::ToSql> = Vec::with_capacity(1 + WELL_KNOWN_NAMES.len());
params_vec.push(project_id_ref);
for name in WELL_KNOWN_NAMES {
params_vec.push(name);
}
let rows = stmt.query_map(params_vec.as_slice(), |row| {
let name: String = row.get(0)?;
let kind: String = row.get(1)?;
let file: String = row.get(2)?;
let line: u32 = row.get::<_, i64>(3)? as u32;
let signature: Option<String> = row.get(4)?;
Ok(DeadCodeResult {
name,
kind,
file,
line,
signature,
reason: "no callers found".to_string(),
})
})?;
let results: Vec<DeadCodeResult> = rows
.filter_map(|r| r.ok())
.filter(|r| {
let name_lower = r.name.to_lowercase();
!opts.entry_point_patterns.iter().any(|p| {
if p.starts_with('*') && p.ends_with('*') && p.len() > 2 {
name_lower.contains(&p[1..p.len() - 1].to_lowercase())
} else if let Some(suffix) = p.strip_prefix('*') {
name_lower.ends_with(&suffix.to_lowercase())
} else if let Some(prefix) = p.strip_suffix('*') {
name_lower.starts_with(&prefix.to_lowercase())
} else {
name_lower == p.to_lowercase()
}
})
})
.collect();
Ok(results)
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct UnusedImportResult {
pub target: String,
pub source: String,
pub file: String,
pub line: u32,
}
pub fn find_unused_imports(
conn: &Connection,
file: &str,
project_id: i64,
) -> anyhow::Result<Vec<UnusedImportResult>> {
let mut imports_stmt = conn.prepare(
"SELECT source, target, line FROM edges
WHERE file = ?1 AND kind = 'IMPORTS' AND project_id = ?2",
)?;
let imports: Vec<(String, String, u32)> = imports_stmt
.query_map(rusqlite::params![file, project_id], |row| {
let source: String = row.get(0)?;
let target: String = row.get(1)?;
let line: u32 = row.get::<_, i64>(2)? as u32;
Ok((source, target, line))
})?
.filter_map(|r| r.ok())
.collect();
if imports.is_empty() {
return Ok(Vec::new());
}
let mut unused = Vec::new();
for (source, target, line) in &imports {
let is_used = conn.query_row(
"SELECT COUNT(*) FROM (
SELECT 1 FROM call_graph cg
WHERE cg.callee = ?1 AND cg.file = ?2 AND cg.project_id = ?3
UNION
SELECT 1 FROM edges e
WHERE e.target = ?1 AND e.file = ?2 AND e.kind != 'IMPORTS' AND e.project_id = ?3
)",
rusqlite::params![target, file, project_id],
|row| row.get::<_, i64>(0),
)? > 0;
if !is_used {
let is_referenced = conn.query_row(
"SELECT COUNT(*) FROM edges e
WHERE (e.source = ?1 OR e.target = ?1)
AND e.file = ?2
AND e.project_id = ?3
AND e.kind != 'IMPORTS'",
rusqlite::params![target, file, project_id],
|row| row.get::<_, i64>(0),
)?;
if is_referenced == 0 {
unused.push(UnusedImportResult {
target: target.clone(),
source: source.clone(),
file: file.to_string(),
line: *line,
});
}
}
}
Ok(unused)
}
pub fn find_dead_code_in_file(
conn: &Connection,
file: &str,
project_id: i64,
include_tests: bool,
) -> anyhow::Result<Vec<DeadCodeResult>> {
let mut sql = String::from(
"SELECT s.name, s.kind, s.file, s.line, s.signature
FROM symbols s
WHERE s.file = ?1
AND s.project_id = ?2
AND s.kind IN ('function', 'method', 'procedure')
AND s.name NOT IN (",
);
let placeholders: Vec<&str> = WELL_KNOWN_NAMES.iter().map(|_| "?").collect();
sql.push_str(&placeholders.join(", "));
sql.push_str(
")
AND NOT EXISTS (
SELECT 1 FROM call_graph cg
WHERE cg.callee = s.name AND cg.project_id = ?2
)",
);
if !include_tests {
sql.push_str(" AND s.name NOT LIKE 'test_%' AND s.name NOT LIKE '%_test'");
}
sql.push_str(" ORDER BY s.line");
let mut stmt = conn.prepare(&sql)?;
let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> =
Vec::with_capacity(2 + WELL_KNOWN_NAMES.len());
params_vec.push(Box::new(file.to_string()));
params_vec.push(Box::new(project_id));
for name in WELL_KNOWN_NAMES {
params_vec.push(Box::new(*name));
}
let params_refs: Vec<&dyn rusqlite::ToSql> = params_vec.iter().map(|b| b.as_ref()).collect();
let rows = stmt.query_map(params_refs.as_slice(), |row| {
let name: String = row.get(0)?;
let kind: String = row.get(1)?;
let file: String = row.get(2)?;
let line: u32 = row.get::<_, i64>(3)? as u32;
let signature: Option<String> = row.get(4)?;
Ok(DeadCodeResult {
name,
kind,
file,
line,
signature,
reason: "no callers found".to_string(),
})
})?;
Ok(rows.filter_map(|r| r.ok()).collect())
}
#[cfg(test)]
mod tests {
use super::*;
fn mem_conn() -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
super::super::schema::run_migrations(&conn).unwrap();
conn
}
fn test_project(conn: &Connection) -> i64 {
super::super::schema::get_or_create_project(conn, "/tmp/test-project").unwrap()
}
#[test]
fn test_store_and_find_callers() {
let conn = mem_conn();
let project_id = test_project(&conn);
let edges = vec![
CallEdge {
caller: "main".to_string(),
callee: "authenticate".to_string(),
file: "main.rs".to_string(),
line: 10,
},
CallEdge {
caller: "handler".to_string(),
callee: "authenticate".to_string(),
file: "handler.rs".to_string(),
line: 25,
},
];
store_edges(&conn, &edges, project_id).unwrap();
let callers = find_callers(&conn, project_id, "authenticate", 10).unwrap();
assert_eq!(callers.len(), 2);
let names: Vec<&str> = callers.iter().map(|c| c.caller.as_str()).collect();
assert!(names.contains(&"main"));
assert!(names.contains(&"handler"));
}
#[test]
fn test_find_callers_cross_project() {
let conn = mem_conn();
let project_a =
super::super::schema::get_or_create_project(&conn, "/tmp/project-a").unwrap();
let project_b =
super::super::schema::get_or_create_project(&conn, "/tmp/project-b").unwrap();
store_edges(
&conn,
&[CallEdge {
caller: "handler_a".to_string(),
callee: "shared_util".to_string(),
file: "handler.rs".to_string(),
line: 5,
}],
project_a,
)
.unwrap();
store_edges(
&conn,
&[CallEdge {
caller: "handler_b".to_string(),
callee: "shared_util".to_string(),
file: "lib.rs".to_string(),
line: 10,
}],
project_b,
)
.unwrap();
let scoped = find_callers(&conn, project_a, "shared_util", 10).unwrap();
assert_eq!(scoped.len(), 1);
assert_eq!(scoped[0].caller, "handler_a");
let scoped_b = find_callers(&conn, project_b, "shared_util", 10).unwrap();
assert_eq!(scoped_b.len(), 1);
assert_eq!(scoped_b[0].caller, "handler_b");
let cross = find_callers_cross_project(&conn, "shared_util", 10).unwrap();
assert_eq!(cross.len(), 2);
let roots: Vec<&str> = cross.iter().map(|c| c.project_root.as_str()).collect();
assert!(roots.contains(&"/tmp/project-a"));
assert!(roots.contains(&"/tmp/project-b"));
}
#[test]
fn test_find_callees() {
let conn = mem_conn();
let project_id = test_project(&conn);
let edges = vec![
CallEdge {
caller: "main".to_string(),
callee: "init".to_string(),
file: "main.rs".to_string(),
line: 5,
},
CallEdge {
caller: "main".to_string(),
callee: "run".to_string(),
file: "main.rs".to_string(),
line: 10,
},
];
store_edges(&conn, &edges, project_id).unwrap();
let callees = find_callees(&conn, project_id, "main", 10).unwrap();
assert_eq!(callees.len(), 2);
}
#[test]
fn test_clear_edges_for_file() {
let conn = mem_conn();
let project_id = test_project(&conn);
store_edges(
&conn,
&[CallEdge {
caller: "a".to_string(),
callee: "b".to_string(),
file: "test.rs".to_string(),
line: 1,
}],
project_id,
)
.unwrap();
clear_edges_for_file(&conn, "test.rs", project_id).unwrap();
let callers = find_callers(&conn, project_id, "b", 10).unwrap();
assert!(callers.is_empty());
}
#[test]
fn test_impact_analysis_depth() {
let conn = mem_conn();
let project_id = test_project(&conn);
let edges = vec![
CallEdge {
caller: "b".to_string(),
callee: "c".to_string(),
file: "b.rs".to_string(),
line: 1,
},
CallEdge {
caller: "a".to_string(),
callee: "b".to_string(),
file: "a.rs".to_string(),
line: 1,
},
];
store_edges(&conn, &edges, project_id).unwrap();
let impact = impact_analysis(&conn, project_id, "c", 3).unwrap();
assert!(impact.iter().any(|n| n.symbol == "b" && n.depth == 1));
assert!(impact.iter().any(|n| n.symbol == "a" && n.depth == 2));
}
#[test]
fn test_trace_path_outgoing() {
let conn = mem_conn();
let pid = test_project(&conn);
for (caller, callee) in [("a", "b"), ("b", "c"), ("a", "d")] {
store_edges(
&conn,
&[CallEdge {
caller: caller.into(),
callee: callee.into(),
file: "x.rs".into(),
line: 1,
}],
pid,
)
.unwrap();
}
let nodes = trace_path(&conn, pid, "a", 2, TraceDirection::Outgoing).unwrap();
let syms: Vec<&str> = nodes.iter().map(|n| n.symbol.as_str()).collect();
assert!(syms.contains(&"b"));
assert!(syms.contains(&"c"));
assert!(syms.contains(&"d"));
}
#[test]
fn test_trace_path_incoming() {
let conn = mem_conn();
let pid = test_project(&conn);
for (caller, callee) in [("a", "c"), ("b", "c")] {
store_edges(
&conn,
&[CallEdge {
caller: caller.into(),
callee: callee.into(),
file: "y.rs".into(),
line: 1,
}],
pid,
)
.unwrap();
}
let nodes = trace_path(&conn, pid, "c", 1, TraceDirection::Incoming).unwrap();
let syms: Vec<&str> = nodes.iter().map(|n| n.symbol.as_str()).collect();
assert!(syms.contains(&"a"));
assert!(syms.contains(&"b"));
}
#[test]
fn test_arch_overview() {
let conn = mem_conn();
let pid = test_project(&conn);
store_edges(
&conn,
&[CallEdge {
caller: "main".into(),
callee: "run".into(),
file: "src/main.rs".into(),
line: 1,
}],
pid,
)
.unwrap();
conn.execute(
"INSERT INTO symbols (name, kind, file, line, project_id) VALUES (?1, ?2, ?3, ?4, ?5)",
rusqlite::params!["main", "function", "src/main.rs", 1i64, pid],
)
.unwrap();
let overview = arch_overview(&conn, pid).unwrap();
assert_eq!(overview.modules.len(), 1);
assert_eq!(overview.modules[0].name, "src");
}
#[test]
fn test_find_dead_code() {
let conn = mem_conn();
let pid = test_project(&conn);
let insert_sym = |name: &str, kind: &str, file: &str, line: i64| {
conn.execute(
"INSERT INTO symbols (name, kind, file, line, signature, language, project_id)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
rusqlite::params![name, kind, file, line, "", "rust", pid],
)
.unwrap();
};
insert_sym("used_fn", "function", "src/lib.rs", 10);
insert_sym("orphan_fn", "function", "src/lib.rs", 20);
insert_sym("unused_method", "method", "src/impl.rs", 5);
insert_sym("test_unused", "function", "src/tests.rs", 1);
insert_sym("new", "function", "src/lib.rs", 30);
store_edges(
&conn,
&[CallEdge {
caller: "caller_fn".into(),
callee: "used_fn".into(),
file: "src/lib.rs".into(),
line: 50,
}],
pid,
)
.unwrap();
let dead = find_dead_code(&conn, pid, &DeadCodeOptions::default()).unwrap();
let names: Vec<&str> = dead.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"orphan_fn"));
assert!(names.contains(&"unused_method"));
assert!(!names.contains(&"used_fn"));
assert!(!names.contains(&"new"));
assert!(!names.contains(&"test_unused"));
let dead_with_tests = find_dead_code(
&conn,
pid,
&DeadCodeOptions {
include_tests: true,
min_lines: None,
entry_point_patterns: vec![],
},
)
.unwrap();
let names_t: Vec<&str> = dead_with_tests.iter().map(|d| d.name.as_str()).collect();
assert!(names_t.contains(&"test_unused"));
assert!(names_t.contains(&"orphan_fn"));
let orphan = dead.iter().find(|d| d.name == "orphan_fn").unwrap();
assert_eq!(orphan.kind, "function");
assert_eq!(orphan.file, "src/lib.rs");
assert_eq!(orphan.line, 20);
assert_eq!(orphan.reason, "no callers found");
}
}