use llmgrep::ast::{ast_nodes_table_schema, check_ast_table_exists, AstContext};
use llmgrep::query::{
search_symbols, AstOptions, ContextOptions, DepthOptions, FqnOptions, MetricsOptions,
SearchOptions, SnippetOptions,
};
use llmgrep::AlgorithmOptions;
use rusqlite::{params, Connection};
use tempfile::TempDir;
fn setup_db_with_ast(path: &std::path::Path) -> Connection {
let conn = Connection::open(path).expect("open db");
conn.execute_batch(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY,
kind TEXT NOT NULL,
name TEXT,
data TEXT
);
CREATE TABLE graph_edges (
id INTEGER PRIMARY KEY,
from_id INTEGER,
to_id INTEGER,
edge_type TEXT
);
CREATE TABLE symbol_metrics (
symbol_id INTEGER PRIMARY KEY,
fan_in INTEGER,
fan_out INTEGER,
cyclomatic_complexity INTEGER
);",
)
.expect("create base tables");
conn.execute(ast_nodes_table_schema(), [])
.expect("create ast_nodes table");
conn
}
fn insert_symbol(
conn: &Connection,
id: i64,
name: &str,
kind: &str,
_file_id: i64,
byte_start: u64,
byte_end: u64,
) {
conn.execute(
"INSERT INTO graph_entities (id, kind, name, data) VALUES (?1, 'Symbol', ?2, ?3)",
params![
id,
name,
format!(
r#"{{"name":"{}","kind":"{}","symbol_id":"{:040x}","byte_start":{},"byte_end":{},"start_line":1,"start_col":0,"end_line":1,"end_col":{}}}"#,
name,
kind,
id,
byte_start,
byte_end,
byte_end - byte_start
)
],
)
.expect("insert symbol");
}
fn insert_file(conn: &Connection, id: i64, path: &str) {
conn.execute(
"INSERT INTO graph_entities (id, kind, data) VALUES (?1, 'File', ?2)",
params![id, format!(r#"{{"path":"{}"}}"#, path)],
)
.expect("insert file");
}
fn insert_define_edge(conn: &Connection, from_id: i64, to_id: i64) {
conn.execute(
"INSERT INTO graph_edges (from_id, to_id, edge_type) VALUES (?1, ?2, 'DEFINES')",
params![from_id, to_id],
)
.expect("insert edge");
}
fn insert_ast_node(
conn: &Connection,
id: i64,
kind: &str,
parent_id: Option<i64>,
byte_start: u64,
byte_end: u64,
) {
conn.execute(
"INSERT INTO ast_nodes (id, kind, parent_id, byte_start, byte_end) VALUES (?1, ?2, ?3, ?4, ?5)",
params![id, kind, parent_id, byte_start, byte_end],
)
.expect("insert ast node");
}
#[test]
fn test_check_ast_table_exists() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
assert!(
check_ast_table_exists(&conn).expect("failed to check AST table"),
"Should return true when ast_nodes table exists"
);
drop(conn);
let conn2 = Connection::open(&db_path).expect("open db");
conn2
.execute("DROP TABLE ast_nodes", [])
.expect("drop ast_nodes");
assert!(
!check_ast_table_exists(&conn2).expect("failed to check AST table"),
"Should return false when ast_nodes table doesn't exist"
);
}
#[test]
fn test_ast_context_serialization() {
let ctx = AstContext {
ast_id: 123,
kind: "function_item".to_string(),
parent_id: Some(122),
byte_start: 100,
byte_end: 200,
depth: None,
parent_kind: None,
children_count_by_kind: None,
decision_points: None,
};
let json = serde_json::to_string(&ctx).expect("failed to serialize context");
assert!(json.contains(r#""ast_id":123"#));
assert!(json.contains(r#""kind":"function_item""#));
assert!(json.contains(r#""parent_id":122"#));
assert!(json.contains(r#""byte_start":100"#));
assert!(json.contains(r#""byte_end":200"#));
}
#[test]
fn test_ast_context_without_parent() {
let ctx = AstContext {
ast_id: 1,
kind: "mod_item".to_string(),
parent_id: None,
byte_start: 0,
byte_end: 50,
depth: None,
parent_kind: None,
children_count_by_kind: None,
decision_points: None,
};
let json = serde_json::to_string(&ctx).expect("failed to serialize context");
assert!(json.contains(r#""parent_id":null"#));
}
#[test]
fn test_ast_kind_filter() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
insert_file(&conn, 1, "src/lib.rs");
insert_symbol(&conn, 10, "my_function", "Function", 1, 0, 100);
insert_symbol(&conn, 11, "my_block", "Block", 1, 10, 90);
insert_symbol(&conn, 12, "my_call", "Call", 1, 20, 80);
insert_define_edge(&conn, 1, 10);
insert_define_edge(&conn, 1, 11);
insert_define_edge(&conn, 1, 12);
insert_ast_node(&conn, 10, "function_item", None, 0, 100);
insert_ast_node(&conn, 11, "block", Some(10), 10, 90);
insert_ast_node(&conn, 12, "call_expression", Some(11), 20, 80);
let options = SearchOptions {
db_path: &db_path,
query: "my_",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions {
ast_kinds: vec!["function_item".to_string()],
with_ast_context: true, _phantom: std::marker::PhantomData,
},
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
for result in &response.results {
eprintln!(
"Result: {} -> ast_context: {:?}",
result.name,
result.ast_context.as_ref().map(|c| &c.kind)
);
}
assert_eq!(
response.results.len(),
3,
"Should return all symbols that overlap with function_item"
);
for result in &response.results {
assert_eq!(
result
.ast_context
.as_ref()
.expect("ast_context should be Some")
.kind,
"function_item"
);
}
}
#[test]
fn test_backward_compat_no_ast_table() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = Connection::open(&db_path).expect("open db");
conn.execute_batch(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY,
kind TEXT NOT NULL,
name TEXT,
data TEXT
);
CREATE TABLE graph_edges (
id INTEGER PRIMARY KEY,
from_id INTEGER,
to_id INTEGER,
edge_type TEXT
);
CREATE TABLE symbol_metrics (
symbol_id INTEGER PRIMARY KEY,
fan_in INTEGER,
fan_out INTEGER,
cyclomatic_complexity INTEGER
);",
)
.expect("create base tables");
insert_file(&conn, 1, "src/lib.rs");
insert_symbol(&conn, 10, "my_function", "Function", 1, 0, 100);
insert_define_edge(&conn, 1, 10);
assert!(
!check_ast_table_exists(&conn).expect("failed to check AST table"),
"ast_nodes table should not exist"
);
let options = SearchOptions {
db_path: &db_path,
query: "my_function",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions {
ast_kinds: vec!["function_item".to_string()],
with_ast_context: false,
_phantom: std::marker::PhantomData,
},
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert_eq!(response.results.len(), 1);
assert_eq!(response.results[0].name, "my_function");
assert!(
response.results[0].ast_context.is_none(),
"ast_context should be None when ast_nodes table doesn't exist"
);
}
#[test]
fn test_ast_context_population() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
insert_file(&conn, 1, "src/lib.rs");
insert_symbol(&conn, 10, "parent_function", "Function", 1, 100, 200);
insert_define_edge(&conn, 1, 10);
insert_ast_node(&conn, 10, "function_item", Some(5), 100, 200);
let options = SearchOptions {
db_path: &db_path,
query: "parent_function",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert_eq!(response.results.len(), 1);
let ast_ctx = response.results[0]
.ast_context
.as_ref()
.expect("ast_context should be present");
assert_eq!(ast_ctx.ast_id, 10);
assert_eq!(ast_ctx.kind, "function_item");
assert_eq!(ast_ctx.parent_id, Some(5));
assert_eq!(ast_ctx.byte_start, 100);
assert_eq!(ast_ctx.byte_end, 200);
}
#[test]
fn test_multiple_ast_kinds() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
insert_file(&conn, 1, "src/lib.rs");
insert_symbol(&conn, 10, "func_one", "Function", 1, 0, 100);
insert_symbol(&conn, 11, "func_two", "Function", 1, 100, 200);
insert_define_edge(&conn, 1, 10);
insert_define_edge(&conn, 1, 11);
insert_ast_node(&conn, 10, "function_item", None, 0, 100);
insert_ast_node(&conn, 11, "function_item", None, 100, 200);
let options = SearchOptions {
db_path: &db_path,
query: "func",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions {
ast_kinds: vec!["call_expression".to_string()],
with_ast_context: false,
_phantom: std::marker::PhantomData,
},
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert_eq!(
response.results.len(),
0,
"Should return no results for call_expression kind"
);
let options = SearchOptions {
db_path: &db_path,
query: "func",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions {
ast_kinds: vec!["function_item".to_string()],
with_ast_context: false,
_phantom: std::marker::PhantomData,
},
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert_eq!(
response.results.len(),
2,
"Should return both function_item results"
);
}
#[test]
fn test_ast_nodes_table_schema() {
let schema = ast_nodes_table_schema();
assert!(schema.contains("CREATE TABLE ast_nodes"));
assert!(schema.contains("id INTEGER PRIMARY KEY"));
assert!(schema.contains("parent_id"));
assert!(schema.contains("kind TEXT NOT NULL"));
assert!(schema.contains("byte_start INTEGER NOT NULL"));
assert!(schema.contains("byte_end INTEGER NOT NULL"));
}
#[test]
fn test_calculate_ast_depth() {
use llmgrep::ast::calculate_ast_depth;
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = Connection::open(&db_path).expect("open db");
conn.execute(ast_nodes_table_schema(), [])
.expect("create ast_nodes");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(1, NULL, 'mod_item', 0, 1000),
(2, 1, 'function_item', 100, 500),
(3, 2, 'block', 150, 450)",
[],
)
.expect("insert nodes");
assert_eq!(
calculate_ast_depth(&conn, 1)
.expect("failed to calculate AST depth")
.expect("AST depth should be Some"),
0,
"Root should have depth 0"
);
assert_eq!(
calculate_ast_depth(&conn, 2)
.expect("failed to calculate AST depth")
.expect("AST depth should be Some"),
1,
"Child should have depth 1"
);
assert_eq!(
calculate_ast_depth(&conn, 3)
.expect("failed to calculate AST depth")
.expect("AST depth should be Some"),
2,
"Grandchild should have depth 2"
);
}
#[test]
fn test_get_parent_kind() {
use llmgrep::ast::get_parent_kind;
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = Connection::open(&db_path).expect("open db");
conn.execute(ast_nodes_table_schema(), [])
.expect("create ast_nodes");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(1, NULL, 'mod_item', 0, 1000),
(2, 1, 'function_item', 100, 500)",
[],
)
.expect("insert nodes");
assert_eq!(
get_parent_kind(&conn, Some(1))
.expect("failed to get parent kind")
.expect("parent kind should be Some"),
"mod_item",
"Parent kind should be mod_item"
);
assert_eq!(
get_parent_kind(&conn, None).expect("failed to get parent kind for None"),
None,
"None parent_id should return None"
);
assert_eq!(
get_parent_kind(&conn, Some(999)).expect("failed to get parent kind for nonexistent"),
None,
"Non-existent parent should return None"
);
}
#[test]
fn test_count_children_by_kind() {
use llmgrep::ast::count_children_by_kind;
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = Connection::open(&db_path).expect("open db");
conn.execute(ast_nodes_table_schema(), [])
.expect("create ast_nodes");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(1, NULL, 'function_item', 0, 1000),
(2, 1, 'let_declaration', 100, 150),
(3, 1, 'let_declaration', 150, 200),
(4, 1, 'let_declaration', 200, 250),
(5, 1, 'if_expression', 250, 400),
(6, 1, 'call_expression', 300, 350),
(7, 1, 'call_expression', 350, 380)",
[],
)
.expect("insert nodes");
let counts = count_children_by_kind(&conn, 1).expect("failed to count children");
assert_eq!(
counts.get("let_declaration"),
Some(&3),
"Should have 3 let_declaration"
);
assert_eq!(
counts.get("if_expression"),
Some(&1),
"Should have 1 if_expression"
);
assert_eq!(
counts.get("call_expression"),
Some(&2),
"Should have 2 call_expression"
);
}
#[test]
fn test_count_decision_points() {
use llmgrep::ast::count_decision_points;
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = Connection::open(&db_path).expect("open db");
conn.execute(ast_nodes_table_schema(), [])
.expect("create ast_nodes");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(1, NULL, 'function_item', 0, 1000),
(2, 1, 'if_expression', 100, 200),
(3, 1, 'match_expression', 200, 300),
(4, 1, 'while_expression', 300, 400),
(5, 1, 'for_expression', 400, 500),
(6, 1, 'loop_expression', 500, 600),
(7, 1, 'conditional_expression', 600, 650),
(8, 1, 'let_declaration', 650, 700),
(9, 1, 'call_expression', 700, 750)",
[],
)
.expect("insert nodes");
let decision_points = count_decision_points(&conn, 1).expect("failed to count decision points");
assert_eq!(decision_points, 6, "Should count 6 decision points");
}
#[test]
fn test_with_ast_context_flag() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
let file_id = 1i64;
insert_file(&conn, file_id, "src/test.rs");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(1, NULL, 'mod_item', 0, 1000),
(2, 1, 'function_item', 100, 500),
(3, 2, 'let_declaration', 150, 200),
(4, 2, 'if_expression', 200, 350),
(5, 2, 'call_expression', 360, 400)",
[],
)
.expect("insert ast nodes");
insert_symbol(&conn, 100, "my_function", "Function", file_id, 0, 100);
insert_define_edge(&conn, file_id, 100);
conn.execute(
"UPDATE graph_entities SET data = json_object(
'byte_start', 150,
'byte_end', 400,
'start_line', 10,
'start_col', 0,
'end_line', 25,
'end_col', 1,
'kind', 'Function',
'name', 'my_function'
) WHERE id = 100",
[],
)
.expect("update symbol data");
conn.execute(
"INSERT INTO symbol_metrics (symbol_id, fan_in, fan_out, cyclomatic_complexity)
VALUES (100, 5, 3, 2)",
[],
)
.expect("insert metrics");
let options = SearchOptions {
db_path: &db_path,
query: "my_function",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions {
ast_kinds: vec![],
with_ast_context: true, _phantom: std::marker::PhantomData,
},
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert_eq!(response.results.len(), 1, "Should find the function");
let ast_ctx = response.results[0]
.ast_context
.as_ref()
.expect("Should have ast_context");
assert_eq!(ast_ctx.kind, "function_item");
assert_eq!(ast_ctx.depth, Some(1), "Depth should be populated");
assert_eq!(
ast_ctx.parent_kind.as_deref(),
Some("mod_item"),
"Parent kind should be populated"
);
assert!(
ast_ctx.children_count_by_kind.is_some(),
"Children count should be populated"
);
assert_eq!(
ast_ctx.decision_points,
Some(1), "Decision points should be counted"
);
}
#[test]
fn test_ast_context_without_flag() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
let file_id = 1i64;
insert_file(&conn, file_id, "src/test.rs");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(100, NULL, 'function_item', 150, 400)",
[],
)
.expect("insert ast nodes");
insert_symbol(&conn, 100, "my_function", "Function", file_id, 0, 100);
insert_define_edge(&conn, file_id, 100);
conn.execute(
"UPDATE graph_entities SET data = json_object(
'byte_start', 150,
'byte_end', 400,
'start_line', 10,
'start_col', 0,
'end_line', 25,
'end_col', 1,
'kind', 'Function',
'name', 'my_function'
) WHERE id = 100",
[],
)
.expect("update symbol data");
conn.execute(
"INSERT INTO symbol_metrics (symbol_id, fan_in, fan_out, cyclomatic_complexity)
VALUES (100, 5, 3, 2)",
[],
)
.expect("insert metrics");
let options = SearchOptions {
db_path: &db_path,
query: "my_function",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions {
ast_kinds: vec![],
with_ast_context: false, _phantom: std::marker::PhantomData,
},
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert_eq!(response.results.len(), 1, "Should find the function");
let ast_ctx = response.results[0]
.ast_context
.as_ref()
.expect("Should have basic ast_context from LEFT JOIN");
assert_eq!(ast_ctx.kind, "function_item");
assert_eq!(ast_ctx.depth, None, "Depth should not be populated");
assert_eq!(
ast_ctx.parent_kind, None,
"Parent kind should not be populated"
);
assert_eq!(
ast_ctx.children_count_by_kind, None,
"Children count should not be populated"
);
assert_eq!(
ast_ctx.decision_points, None,
"Decision points should not be populated"
);
}
#[test]
fn test_sort_by_ast_complexity() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
let file_id = 1i64;
insert_file(&conn, file_id, "src/test.rs");
insert_symbol(&conn, 100, "simple_func", "Function", file_id, 0, 100);
insert_define_edge(&conn, file_id, 100);
conn.execute(
"UPDATE graph_entities SET data = json_object(
'byte_start', 100, 'byte_end', 150,
'start_line', 10, 'start_col', 0, 'end_line', 12, 'end_col', 1,
'kind', 'Function', 'name', 'simple_func'
) WHERE id = 100",
[],
)
.expect("update symbol 1");
insert_symbol(&conn, 200, "complex_func", "Function", file_id, 0, 100);
insert_define_edge(&conn, file_id, 200);
conn.execute(
"UPDATE graph_entities SET data = json_object(
'byte_start', 200, 'byte_end', 300,
'start_line', 20, 'start_col', 0, 'end_line', 25, 'end_col', 1,
'kind', 'Function', 'name', 'complex_func'
) WHERE id = 200",
[],
)
.expect("update symbol 2");
conn.execute(
"INSERT INTO symbol_metrics (symbol_id, fan_in, fan_out, cyclomatic_complexity)
VALUES (100, 1, 0, 1)",
[],
)
.expect("insert metrics 1");
conn.execute(
"INSERT INTO symbol_metrics (symbol_id, fan_in, fan_out, cyclomatic_complexity)
VALUES (200, 2, 1, 10)",
[],
)
.expect("insert metrics 2");
let options = SearchOptions {
db_path: &db_path,
query: "func",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::AstComplexity, metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(),
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert_eq!(response.results.len(), 2, "Should find both functions");
assert_eq!(
response.results[0].name, "complex_func",
"Highest complexity should come first"
);
assert_eq!(
response.results[1].name, "simple_func",
"Lower complexity should come second"
);
}
#[test]
fn test_min_depth_filter() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
let file_id = 1i64;
insert_file(&conn, file_id, "src/test.rs");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(100, NULL, 'function_item', 100, 500), -- depth 0 (root level function)
(101, 100, 'let_declaration', 150, 200),
(102, 100, 'if_expression', 250, 350), -- depth 1 (inside function)
(103, 102, 'loop_expression', 260, 340), -- depth 2 (inside if)
(104, 100, 'let_declaration', 400, 450), -- depth 0 (sibling of if)
(105, 100, 'match_expression', 460, 490), -- depth 1 (inside function)
(106, 105, 'if_expression', 470, 480), -- depth 2 (inside match)
(107, 106, 'loop_expression', 472, 478), -- depth 3 (inside if, inside match)
(108, 100, 'let_declaration', 500, 520) -- depth 0 (sibling)",
[],
)
.expect("insert ast nodes");
let ast_node_spans: &[(i64, u64, u64)] = &[
(100, 100, 500),
(102, 250, 350),
(103, 260, 340),
(105, 460, 490),
(106, 470, 480),
(107, 472, 478),
];
for &(ast_id, byte_start, byte_end) in ast_node_spans {
insert_symbol(
&conn,
ast_id,
&format!("symbol_{}", ast_id),
"Function",
file_id,
byte_start,
byte_end,
);
insert_define_edge(&conn, file_id, ast_id);
}
let options = SearchOptions {
db_path: &db_path,
query: "symbol_",
path_filter: None,
kind_filter: None,
limit: 100,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions {
min_depth: Some(2),
max_depth: None,
inside: None,
contains: None,
},
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert!(
response.results.len() >= 2,
"Should find at least 2 symbols with depth >= 2"
);
}
#[test]
fn test_max_depth_filter() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
let file_id = 1i64;
insert_file(&conn, file_id, "src/test.rs");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(100, NULL, 'function_item', 100, 600),
(101, 100, 'if_expression', 150, 250), -- depth 1
(102, 101, 'loop_expression', 160, 240), -- depth 2
(103, 102, 'match_expression', 170, 230), -- depth 3
(104, 103, 'if_expression', 180, 220), -- depth 4
(105, 100, 'let_declaration', 260, 300)",
[],
)
.expect("insert ast nodes");
insert_symbol(&conn, 100, "test_func_depth0", "Function", file_id, 0, 100);
insert_define_edge(&conn, file_id, 100);
insert_symbol(&conn, 101, "test_if_depth1", "Function", file_id, 0, 100);
insert_define_edge(&conn, file_id, 101);
insert_symbol(&conn, 102, "test_loop_depth2", "Function", file_id, 0, 100);
insert_define_edge(&conn, file_id, 102);
let options = SearchOptions {
db_path: &db_path,
query: "test",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions {
min_depth: None,
max_depth: Some(1),
inside: None,
contains: None,
},
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert!(
response.results.len() >= 2,
"Should find at least 2 symbols with depth <= 1"
);
let names: Vec<&str> = response.results.iter().map(|r| r.name.as_str()).collect();
assert!(
names.contains(&"test_func_depth0"),
"Should include depth 0 symbol"
);
assert!(
names.contains(&"test_if_depth1"),
"Should include depth 1 symbol"
);
}
#[test]
fn test_min_max_depth_range() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
let file_id = 1i64;
insert_file(&conn, file_id, "src/test.rs");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(100, NULL, 'function_item', 100, 800),
(101, 100, 'if_expression', 150, 250), -- depth 1
(102, 101, 'loop_expression', 160, 240), -- depth 2
(103, 102, 'match_expression', 170, 230), -- depth 3
(104, 103, 'if_expression', 180, 220), -- depth 4
(105, 100, 'let_declaration', 260, 300)",
[],
)
.expect("insert ast nodes");
let ast_node_spans: &[(i64, u64, u64)] = &[
(100, 100, 800), (101, 150, 250), (102, 160, 240), (103, 170, 230), (104, 180, 220), ];
for &(ast_id, byte_start, byte_end) in ast_node_spans {
insert_symbol(
&conn,
ast_id,
&format!("depth{}", ast_id - 100),
"Function",
file_id,
byte_start,
byte_end,
);
insert_define_edge(&conn, file_id, ast_id);
}
let options = SearchOptions {
db_path: &db_path,
query: "depth",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions {
min_depth: Some(1),
max_depth: Some(2),
inside: None,
contains: None,
},
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert!(
response.results.len() >= 2,
"Should find symbols in range [1, 2]"
);
}
#[test]
fn test_inside_function_item() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
let file_id = 1i64;
insert_file(&conn, file_id, "src/test.rs");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(100, NULL, 'function_item', 0, 100),
(101, 100, 'block', 10, 90),
(102, 101, 'closure_expression', 20, 30),
(103, 101, 'let_declaration', 35, 45),
(104, NULL, 'function_item', 100, 200), -- Different function
(105, 104, 'closure_expression', 110, 120), -- Closure in different function
(106, 101, 'call_expression', 50, 60)",
[],
)
.expect("insert ast nodes");
insert_symbol(
&conn,
102,
"closure_inside_func",
"Function",
file_id,
20,
30,
);
insert_define_edge(&conn, file_id, 102);
insert_symbol(&conn, 103, "let_inside_func", "Function", file_id, 35, 45);
insert_define_edge(&conn, file_id, 103);
insert_symbol(&conn, 105, "closure_other", "Function", file_id, 110, 120);
insert_define_edge(&conn, file_id, 105);
let options = SearchOptions {
db_path: &db_path,
query: "closure",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions {
ast_kinds: vec!["closure_expression".to_string()],
with_ast_context: false,
_phantom: std::marker::PhantomData,
},
depth: DepthOptions {
min_depth: None,
max_depth: None,
inside: Some("function_item"),
contains: None,
},
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert!(
response.results.len() >= 2,
"Should find at least 2 closure_expression nodes inside function_item"
);
let names: Vec<&str> = response.results.iter().map(|r| r.name.as_str()).collect();
assert!(
names.contains(&"closure_inside_func"),
"Should find closure in first function"
);
assert!(
names.contains(&"closure_other"),
"Should find closure in other function"
);
}
#[test]
fn test_inside_block() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
let file_id = 1i64;
insert_file(&conn, file_id, "src/test.rs");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(100, NULL, 'function_item', 0, 100),
(101, 100, 'block', 10, 90),
(102, 101, 'let_declaration', 20, 30),
(103, 100, 'let_declaration', 40, 50), -- Let at function level (not in block)
(104, 101, 'call_expression', 60, 70)",
[],
)
.expect("insert ast nodes");
insert_symbol(&conn, 102, "let_in_block", "Function", file_id, 20, 30);
insert_define_edge(&conn, file_id, 102);
insert_symbol(&conn, 103, "let_at_func_level", "Function", file_id, 40, 50);
insert_define_edge(&conn, file_id, 103);
let options = SearchOptions {
db_path: &db_path,
query: "let",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions {
ast_kinds: vec!["let_declaration".to_string()],
with_ast_context: false,
_phantom: std::marker::PhantomData,
},
depth: DepthOptions {
min_depth: None,
max_depth: None,
inside: Some("block"),
contains: None,
},
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert!(
!response.results.is_empty(),
"Should find let_declaration inside block"
);
let names: Vec<&str> = response.results.iter().map(|r| r.name.as_str()).collect();
assert!(
names.contains(&"let_in_block"),
"Should include let_in_block"
);
}
#[test]
fn test_contains_if_expression() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
let file_id = 1i64;
insert_file(&conn, file_id, "src/test.rs");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(100, NULL, 'function_item', 0, 100),
(101, 100, 'if_expression', 10, 50),
(102, 101, 'let_declaration', 15, 25),
(200, NULL, 'function_item', 100, 200), -- Function without if
(201, 200, 'let_declaration', 110, 150),
(300, NULL, 'function_item', 200, 300), -- Function with multiple if expressions
(301, 300, 'if_expression', 210, 250),
(302, 300, 'if_expression', 260, 290)",
[],
)
.expect("insert ast nodes");
insert_symbol(&conn, 100, "func_with_if", "Function", file_id, 0, 100);
insert_define_edge(&conn, file_id, 100);
insert_symbol(&conn, 200, "func_plain", "Function", file_id, 0, 100);
insert_define_edge(&conn, file_id, 200);
insert_symbol(
&conn,
300,
"func_with_multiple_ifs",
"Function",
file_id,
0,
100,
);
insert_define_edge(&conn, file_id, 300);
let options = SearchOptions {
db_path: &db_path,
query: "func",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions {
ast_kinds: vec!["function_item".to_string()],
with_ast_context: false,
_phantom: std::marker::PhantomData,
},
depth: DepthOptions {
min_depth: None,
max_depth: None,
inside: None,
contains: Some("if_expression"),
},
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert!(
response.results.len() >= 2,
"Should find functions containing if_expression"
);
let names: Vec<&str> = response.results.iter().map(|r| r.name.as_str()).collect();
assert!(
names.contains(&"func_with_if"),
"Should include func_with_if"
);
assert!(
names.contains(&"func_with_multiple_ifs"),
"Should include func_with_multiple_ifs"
);
}
#[test]
fn test_contains_multiple_children() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let _conn = setup_db_with_ast(&db_path);
let file_id = 1i64;
insert_file(&_conn, file_id, "src/test.rs");
_conn
.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(100, NULL, 'function_item', 0, 100),
(101, 100, 'call_expression', 10, 20),
(102, 100, 'call_expression', 30, 40),
(103, 100, 'call_expression', 50, 60),
(104, 100, 'let_declaration', 70, 80),
(200, NULL, 'function_item', 100, 200), -- Function with single call
(201, 200, 'call_expression', 110, 120),
(300, NULL, 'function_item', 200, 300), -- Function with no calls
(301, 300, 'let_declaration', 210, 220)",
[],
)
.expect("insert ast nodes");
insert_symbol(&_conn, 100, "func_many_calls", "Function", file_id, 0, 100);
insert_define_edge(&_conn, file_id, 100);
insert_symbol(&_conn, 200, "func_one_call", "Function", file_id, 100, 200);
insert_define_edge(&_conn, file_id, 200);
insert_symbol(&_conn, 300, "func_no_calls", "Function", file_id, 200, 300);
insert_define_edge(&_conn, file_id, 300);
let options = SearchOptions {
db_path: &db_path,
query: "func",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions {
ast_kinds: vec!["function_item".to_string()],
with_ast_context: false,
_phantom: std::marker::PhantomData,
},
depth: DepthOptions {
min_depth: None,
max_depth: None,
inside: None,
contains: Some("call_expression"),
},
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert!(
response.results.len() >= 2,
"Should find functions with call_expression children"
);
let names: Vec<&str> = response.results.iter().map(|r| r.name.as_str()).collect();
assert!(
names.contains(&"func_many_calls"),
"Should include func_many_calls"
);
assert!(
names.contains(&"func_one_call"),
"Should include func_one_call"
);
}
#[test]
fn test_combined_depth_and_inside() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
let file_id = 1i64;
insert_file(&conn, file_id, "src/test.rs");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(100, NULL, 'function_item', 0, 200), -- func_outer, depth 0
(101, 100, 'if_expression', 10, 180), -- depth 1
(102, 101, 'block', 20, 170), -- depth 1
(103, 102, 'closure_expression', 30, 40), -- depth 1, inside block
(104, 101, 'if_expression', 50, 100), -- depth 2
(105, 104, 'match_expression', 60, 90), -- depth 3
(106, 102, 'let_declaration', 110, 120), -- depth 1
(107, 100, 'let_declaration', 180, 190), -- depth 0
(108, NULL, 'function_item', 200, 400), -- func_other, depth 0
(109, 108, 'if_expression', 210, 380), -- depth 1
(110, 108, 'let_declaration', 220, 230), -- depth 1
(111, 109, 'closure_expression', 240, 250), -- depth 2, inside if
(112, 109, 'block', 260, 370), -- depth 1
(113, 112, 'let_declaration', 270, 280), -- depth 1
(114, 112, 'closure_expression', 290, 300), -- depth 1, inside block
(115, NULL, 'block', 400, 500), -- orphan block, depth 0
(116, 115, 'if_expression', 410, 460), -- depth 1, inside orphan
(117, 115, 'let_declaration', 470, 480)",
[],
)
.expect("insert ast nodes");
let closure_ranges: [(i64, u64, u64); 4] = [
(103, 30, 40),
(111, 240, 250),
(114, 290, 300),
(116, 410, 460),
];
for (ast_id, byte_start, byte_end) in closure_ranges {
insert_symbol(
&conn,
ast_id,
&format!("closure_{}", ast_id),
"Function",
file_id,
byte_start,
byte_end,
);
insert_define_edge(&conn, file_id, ast_id);
}
let options = SearchOptions {
db_path: &db_path,
query: "closure",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions {
ast_kinds: vec!["closure_expression".to_string()],
with_ast_context: false,
_phantom: std::marker::PhantomData,
},
depth: DepthOptions {
min_depth: Some(1), max_depth: None,
inside: Some("function_item"), contains: None,
},
algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert!(
response.results.len() >= 2,
"Should find closures at depth >= 1 inside function_item"
);
let names: Vec<&str> = response.results.iter().map(|r| r.name.as_str()).collect();
assert!(names.contains(&"closure_103"), "Should include closure_103");
}
#[test]
fn test_backward_compat_no_depth_filter() {
let temp_dir = TempDir::new().expect("tempdir");
let db_path = temp_dir.path().join("test.db");
let conn = setup_db_with_ast(&db_path);
let file_id = 1i64;
insert_file(&conn, file_id, "src/test.rs");
conn.execute(
"INSERT INTO ast_nodes (id, parent_id, kind, byte_start, byte_end) VALUES
(100, NULL, 'function_item', 0, 100),
(101, 100, 'let_declaration', 10, 50),
(102, 100, 'if_expression', 60, 90)",
[],
)
.expect("insert ast nodes");
insert_symbol(&conn, 100, "my_function", "Function", file_id, 0, 100);
insert_define_edge(&conn, file_id, 100);
let options = SearchOptions {
db_path: &db_path,
query: "my_function",
path_filter: None,
kind_filter: None,
limit: 10,
use_regex: false,
candidates: 100,
context: ContextOptions::default(),
snippet: SnippetOptions::default(),
fqn: FqnOptions::default(),
include_score: true,
sort_by: llmgrep::SortMode::default(),
metrics: MetricsOptions::default(),
ast: AstOptions::default(),
depth: DepthOptions::default(), algorithm: AlgorithmOptions::default(),
symbol_id: None,
fqn_pattern: None,
exact_fqn: None,
language_filter: None,
coverage_filter: None,
};
let (response, _partial, _) = search_symbols(options).expect("search should succeed");
assert_eq!(response.results.len(), 1);
assert_eq!(response.results[0].name, "my_function");
let ast_ctx = response.results[0]
.ast_context
.as_ref()
.expect("ast_context should be present");
assert_eq!(
ast_ctx.depth, None,
"Depth should not be populated without depth filtering or --with-ast-context"
);
}