use anyhow::Result;
use rusqlite::Connection;
use serde::Serialize;
use std::collections::HashMap;
pub static AST_SHORTHANDS: &[(&str, &str)] = &[
("loops", "for_expression,while_expression,loop_expression"),
("conditionals", "if_expression,match_expression,match_arm"),
(
"functions",
"function_item,closure_expression,async_function_item",
),
(
"declarations",
"struct_item,enum_item,let_declaration,const_item,static_item,type_alias_item",
),
("unsafe", "unsafe_block"),
("types", "struct_item,enum_item,type_alias_item,union_item"),
("macros", "macro_invocation,macro_definition,macro_rule"),
("mods", "mod_item"),
("traits", "trait_item,trait_impl_item"),
("impls", "impl_item"),
];
#[derive(Debug, Clone, Serialize)]
pub struct AstContext {
pub ast_id: i64,
pub kind: String,
pub parent_id: Option<i64>,
pub byte_start: u64,
pub byte_end: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub depth: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub children_count_by_kind: Option<HashMap<String, u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decision_points: Option<u64>,
}
pub fn check_ast_table_exists(conn: &Connection) -> Result<bool> {
let mut stmt =
conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='ast_nodes'")?;
Ok(stmt.exists([])?)
}
pub const fn ast_nodes_table_schema() -> &'static str {
"CREATE TABLE ast_nodes (
id INTEGER PRIMARY KEY,
parent_id INTEGER,
kind TEXT NOT NULL,
byte_start INTEGER NOT NULL,
byte_end INTEGER NOT NULL
)"
}
pub fn calculate_ast_depth(conn: &Connection, ast_id: i64) -> Result<Option<u64>> {
let sql = r#"
WITH RECURSIVE node_ancestry AS (
-- Base case: root nodes (parent_id IS NULL)
SELECT id, parent_id, 0 as depth
FROM ast_nodes
WHERE parent_id IS NULL
UNION ALL
-- Recursive case: add 1 to parent depth
SELECT a.id, a.parent_id, na.depth + 1
FROM ast_nodes a
JOIN node_ancestry na ON a.parent_id = na.id
)
SELECT depth FROM node_ancestry WHERE id = ?
"#;
match conn.query_row(sql, [ast_id], |row| row.get::<_, u64>(0)) {
Ok(depth) => Ok(Some(depth)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn calculate_decision_depth(conn: &Connection, ast_id: i64) -> Result<Option<u64>> {
let sql = r#"
WITH RECURSIVE decision_ancestry AS (
-- Base case: start from the node itself, count 1 if it's a decision point
SELECT id, parent_id,
CASE WHEN kind IN (
'if_expression', 'match_expression', 'for_expression',
'while_expression', 'loop_expression'
) THEN 1 ELSE 0 END as depth
FROM ast_nodes
WHERE id = ?
UNION ALL
-- Recursive case: traverse to parent, add 1 if parent is a decision point
SELECT a.id, a.parent_id,
da.depth + CASE WHEN a.kind IN (
'if_expression', 'match_expression', 'for_expression',
'while_expression', 'loop_expression'
) THEN 1 ELSE 0 END
FROM ast_nodes a
JOIN decision_ancestry da ON a.id = da.parent_id
WHERE a.parent_id IS NOT NULL
)
SELECT MAX(depth) FROM decision_ancestry
"#;
match conn.query_row(sql, [ast_id], |row| row.get::<_, u64>(0)) {
Ok(depth) => Ok(Some(depth)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn get_parent_kind(conn: &Connection, parent_id: Option<i64>) -> Result<Option<String>> {
let Some(pid) = parent_id else {
return Ok(None);
};
let sql = "SELECT kind FROM ast_nodes WHERE id = ?";
match conn.query_row(sql, [pid], |row| row.get::<_, String>(0)) {
Ok(kind) => Ok(Some(kind)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn count_children_by_kind(conn: &Connection, ast_id: i64) -> Result<HashMap<String, u64>> {
let sql = r#"
SELECT kind, COUNT(*) as count
FROM ast_nodes
WHERE parent_id = ?
GROUP BY kind
"#;
let mut stmt = conn.prepare(sql)?;
let rows = stmt.query_map([ast_id], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, u64>(1)?))
})?;
let mut counts = HashMap::new();
for row in rows {
let (kind, count) = row?;
counts.insert(kind, count);
}
Ok(counts)
}
pub fn count_decision_points(conn: &Connection, ast_id: i64) -> Result<u64> {
let sql = r#"
SELECT COUNT(*) FROM ast_nodes
WHERE parent_id = ?
AND kind IN (
'if_expression', 'match_expression', 'while_expression',
'for_expression', 'loop_expression', 'conditional_expression'
)
"#;
conn.query_row(sql, [ast_id], |row| row.get(0))
.map_err(Into::into)
}
pub fn get_ast_context_for_symbol(
conn: &Connection,
_file_path: &str,
byte_start: u64,
byte_end: u64,
include_enriched: bool,
) -> Result<Option<AstContext>> {
get_ast_context_for_symbol_with_preference(
conn,
_file_path,
byte_start,
byte_end,
include_enriched,
&[],
)
}
pub fn get_ast_context_for_symbol_with_preference(
conn: &Connection,
_file_path: &str,
byte_start: u64,
byte_end: u64,
include_enriched: bool,
preferred_kinds: &[String],
) -> Result<Option<AstContext>> {
let (ast_id, parent_id, kind, ast_byte_start, ast_byte_end) = if !preferred_kinds.is_empty() {
let placeholders = preferred_kinds
.iter()
.map(|_| "?")
.collect::<Vec<_>>()
.join(",");
let sql = format!(
"SELECT id, parent_id, kind, byte_start, byte_end
FROM ast_nodes
WHERE byte_start <= ? AND byte_end >= ? AND kind IN ({})
ORDER BY ABS(byte_start - ?) + ABS(byte_end - ?)
LIMIT 1",
placeholders
);
let byte_end_i64 = byte_end as i64;
let byte_start_i64 = byte_start as i64;
let mut params: Vec<&dyn rusqlite::ToSql> = vec![&byte_end_i64, &byte_start_i64];
for kind in preferred_kinds {
params.push(kind);
}
params.push(&byte_start_i64);
params.push(&byte_end_i64);
match conn.query_row(&sql, params.as_slice(), |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, Option<i64>>(1)?,
row.get::<_, String>(2)?,
row.get::<_, u64>(3)?,
row.get::<_, u64>(4)?,
))
}) {
Ok(result) => result,
Err(rusqlite::Error::QueryReturnedNoRows) => {
let fallback_sql = r#"
SELECT id, parent_id, kind, byte_start, byte_end
FROM ast_nodes
WHERE byte_start <= ? AND byte_end >= ?
ORDER BY ABS(byte_start - ?) + ABS(byte_end - ?)
LIMIT 1
"#;
match conn.query_row(
fallback_sql,
[
byte_end as i64,
byte_start as i64,
byte_start as i64,
byte_end as i64,
],
|row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, Option<i64>>(1)?,
row.get::<_, String>(2)?,
row.get::<_, u64>(3)?,
row.get::<_, u64>(4)?,
))
},
) {
Ok(result) => result,
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
Err(e) => return Err(e.into()),
}
}
Err(e) => return Err(e.into()),
}
} else {
let sql = r#"
SELECT id, parent_id, kind, byte_start, byte_end
FROM ast_nodes
WHERE byte_start <= ? AND byte_end >= ?
ORDER BY
CASE WHEN byte_start <= ? AND byte_end >= ? THEN 0 ELSE 1 END ASC,
(byte_end - byte_start) ASC
LIMIT 1
"#;
match conn.query_row(
sql,
[
byte_end as i64,
byte_start as i64,
byte_start as i64,
byte_end as i64,
],
|row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, Option<i64>>(1)?,
row.get::<_, String>(2)?,
row.get::<_, u64>(3)?,
row.get::<_, u64>(4)?,
))
},
) {
Ok(result) => result,
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
Err(e) => return Err(e.into()),
}
};
let mut ctx = AstContext {
ast_id,
kind,
parent_id,
byte_start: ast_byte_start,
byte_end: ast_byte_end,
depth: None,
parent_kind: None,
children_count_by_kind: None,
decision_points: None,
};
if include_enriched {
ctx.depth = Some(calculate_ast_depth(conn, ast_id)?.unwrap_or(0));
ctx.parent_kind = get_parent_kind(conn, parent_id)?;
ctx.children_count_by_kind = Some(count_children_by_kind(conn, ast_id)?);
ctx.decision_points = Some(count_decision_points(conn, ast_id)?);
}
Ok(Some(ctx))
}
#[derive(Debug, Clone)]
pub struct LanguageNodeKinds {
pub language: &'static str,
pub loops: &'static [&'static str],
pub conditionals: &'static [&'static str],
pub functions: &'static [&'static str],
pub declarations: &'static [&'static str],
}
pub static PYTHON_NODE_KINDS: LanguageNodeKinds = LanguageNodeKinds {
language: "python",
loops: &["for_statement", "while_statement"],
conditionals: &["if_statement", "match_statement"],
functions: &["function_definition", "lambda", "async_function_definition"],
declarations: &["class_definition", "type_alias_statement"],
};
pub static JAVASCRIPT_NODE_KINDS: LanguageNodeKinds = LanguageNodeKinds {
language: "javascript",
loops: &[
"for_statement",
"for_in_statement",
"for_of_statement",
"while_statement",
"do_statement",
],
conditionals: &["if_statement", "switch_statement", "catch_clause"],
functions: &[
"function_declaration",
"function_expression",
"arrow_function",
"generator_function_declaration",
"generator_function_expression",
],
declarations: &[
"class_declaration",
"class_expression",
"variable_declaration",
"type_alias_declaration",
],
};
pub static TYPESCRIPT_NODE_KINDS: LanguageNodeKinds = LanguageNodeKinds {
language: "typescript",
loops: &[
"for_statement",
"for_in_statement",
"for_of_statement",
"while_statement",
"do_statement",
],
conditionals: &["if_statement", "switch_statement", "catch_clause"],
functions: &[
"function_declaration",
"function_expression",
"arrow_function",
"generator_function_declaration",
"generator_function_expression",
],
declarations: &[
"class_declaration",
"class_expression",
"variable_declaration",
"type_alias_declaration",
"interface_declaration",
"enum_declaration",
],
};
pub fn get_supported_languages() -> &'static [&'static str] {
&["rust", "python", "javascript", "typescript"]
}
pub fn get_node_kinds_for_language(language: &str, category: &str) -> Option<Vec<String>> {
let kinds = match language.to_lowercase().as_str() {
"python" => {
let mapping = &PYTHON_NODE_KINDS;
match category.to_lowercase().as_str() {
"loops" => mapping.loops,
"conditionals" => mapping.conditionals,
"functions" => mapping.functions,
"declarations" => mapping.declarations,
_ => return None,
}
}
"javascript" => {
let mapping = &JAVASCRIPT_NODE_KINDS;
match category.to_lowercase().as_str() {
"loops" => mapping.loops,
"conditionals" => mapping.conditionals,
"functions" => mapping.functions,
"declarations" => mapping.declarations,
_ => return None,
}
}
"typescript" => {
let mapping = &TYPESCRIPT_NODE_KINDS;
match category.to_lowercase().as_str() {
"loops" => mapping.loops,
"conditionals" => mapping.conditionals,
"functions" => mapping.functions,
"declarations" => mapping.declarations,
_ => return None,
}
}
_ => return None,
};
Some(kinds.iter().map(|s| s.to_string()).collect())
}
pub fn expand_shorthand(input: &str) -> String {
let normalized = input.trim().to_lowercase();
for &(shorthand, expansion) in AST_SHORTHANDS {
if normalized == shorthand {
return expansion.to_string();
}
}
input.to_string()
}
pub fn expand_shorthands(input: &str) -> Vec<String> {
let mut result = std::collections::HashSet::new();
for part in input.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
let expanded = expand_shorthand(part);
for kind in expanded.split(',') {
let kind = kind.trim();
if !kind.is_empty() {
result.insert(kind.to_string());
}
}
}
let mut kinds: Vec<String> = result.into_iter().collect();
kinds.sort();
kinds
}
pub fn expand_shorthand_with_language(shorthand: &str, language: Option<&str>) -> Vec<String> {
let normalized = shorthand.trim().to_lowercase();
if let Some(lang) = language {
let lang_lower = lang.to_lowercase();
if let Some(kinds) = get_node_kinds_for_language(&lang_lower, &normalized) {
return kinds;
}
}
let expanded = expand_shorthand(&normalized);
expanded.split(',').map(|s| s.trim().to_string()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[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).unwrap();
assert!(json.contains("\"ast_id\":123"));
assert!(json.contains("\"kind\":\"function_item\""));
assert!(json.contains("\"parent_id\":122"));
assert!(json.contains("\"byte_start\":100"));
assert!(json.contains("\"byte_end\":200"));
assert!(!json.contains("depth"));
assert!(!json.contains("parent_kind"));
assert!(!json.contains("children_count_by_kind"));
assert!(!json.contains("decision_points"));
}
#[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).unwrap();
assert!(json.contains("\"parent_id\":null"));
}
#[test]
fn test_ast_context_enriched_serialization() {
let mut children = HashMap::new();
children.insert("let_declaration".to_string(), 3);
children.insert("if_expression".to_string(), 2);
let ctx = AstContext {
ast_id: 42,
kind: "function_item".to_string(),
parent_id: None,
byte_start: 1000,
byte_end: 2000,
depth: Some(0),
parent_kind: None,
children_count_by_kind: Some(children),
decision_points: Some(2),
};
let json = serde_json::to_string(&ctx).unwrap();
assert!(json.contains("\"ast_id\":42"));
assert!(json.contains("\"kind\":\"function_item\""));
assert!(json.contains("\"depth\":0"));
assert!(json.contains("\"decision_points\":2"));
assert!(json.contains("\"let_declaration\":3"));
assert!(json.contains("\"if_expression\":2"));
assert!(!json.contains("parent_kind"));
}
#[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_check_ast_table_exists_missing() {
let conn = Connection::open_in_memory().unwrap();
let result = check_ast_table_exists(&conn).unwrap();
assert!(!result, "Should return false when table doesn't exist");
}
#[test]
fn test_check_ast_table_exists_present() {
let conn = Connection::open_in_memory().unwrap();
conn.execute(ast_nodes_table_schema(), []).unwrap();
let result = check_ast_table_exists(&conn).unwrap();
assert!(result, "Should return true when table exists");
}
#[test]
fn test_check_ast_table_exists_with_other_tables() {
let conn = Connection::open_in_memory().unwrap();
conn.execute("CREATE TABLE other_table (id INTEGER PRIMARY KEY)", [])
.unwrap();
let result = check_ast_table_exists(&conn).unwrap();
assert!(!result, "Should return false when only other tables exist");
}
#[test]
fn test_calculate_decision_depth() {
use super::*;
let conn = Connection::open_in_memory().unwrap();
conn.execute(ast_nodes_table_schema(), []).unwrap();
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, 900),
(3, 2, 'if_expression', 150, 800),
(4, 3, 'loop_expression', 200, 700),
(5, 4, 'let_declaration', 250, 600),
(6, 5, 'match_expression', 300, 500)",
[],
)
.unwrap();
assert_eq!(
calculate_decision_depth(&conn, 1).unwrap().unwrap(),
0,
"mod_item at root should have decision depth 0"
);
assert_eq!(
calculate_decision_depth(&conn, 2).unwrap().unwrap(),
0,
"function_item (child of mod) should have decision depth 0"
);
assert_eq!(
calculate_decision_depth(&conn, 3).unwrap().unwrap(),
1,
"if_expression should have decision depth 1"
);
assert_eq!(
calculate_decision_depth(&conn, 4).unwrap().unwrap(),
2,
"loop_expression (child of if) should have decision depth 2"
);
assert_eq!(
calculate_decision_depth(&conn, 5).unwrap().unwrap(),
2,
"let_declaration (child of loop) should have decision depth 2"
);
assert_eq!(
calculate_decision_depth(&conn, 6).unwrap().unwrap(),
3,
"match_expression (child of let) should have decision depth 3"
);
}
}