use rusqlite::{Connection, OptionalExtension, Row, ToSql, params};
use rust_stemmers::{Algorithm, Stemmer};
use std::collections::{HashMap, HashSet};
use thiserror::Error;
use crate::db::local_variable_predicate;
use crate::models::{
BlastRadiusResult, FileFact, ImpactedSymbol, LiteralFact, ReferenceSite, SearchExplain,
StructuralFact, Symbol, SymbolSearchResult, TestTarget, TypeFact, has_generated_lambda_name,
is_generated_lambda_name,
};
#[derive(Debug, Error)]
pub enum QueryError {
#[error("Database query error: {0}")]
Sqlite(#[from] rusqlite::Error),
#[error("Symbol '{name}' not found in {workspace}. {hint}")]
SymbolNotFound {
name: String,
workspace: String,
hint: String,
},
#[error(
"Ambiguous symbol '{0}': found {1} matching candidates. Choose a candidate by its kind and source line, then pass its id as `symbol_id` (CLI: `--symbol-id`) to get_symbol_body, get_symbol_context, find_references, or blast_radius:\n{2}"
)]
AmbiguousSymbol(String, usize, String),
#[error("Invalid direction '{0}': must be 'callers' or 'callees'")]
InvalidDirection(String),
#[error("Result limit must be between 0 and {MAX_RESULT_LIMIT}, got {0}")]
InvalidResultLimit(usize),
}
pub const MAX_RESULT_LIMIT: usize = 200;
pub fn validate_result_limit(limit: usize) -> Result<(), QueryError> {
if limit > MAX_RESULT_LIMIT {
return Err(QueryError::InvalidResultLimit(limit));
}
Ok(())
}
fn map_symbol(row: &Row) -> rusqlite::Result<Symbol> {
Ok(Symbol {
symbol_id: row.get("symbol_id")?,
file_id: row.get("file_id")?,
path: row.get::<_, String>("path")?.replace('\\', "/"),
language: row.get("language")?,
name: row.get("name")?,
kind: row.get("kind")?,
signature: row.get("signature")?,
doc_comment: row.get("doc_comment")?,
visibility: row.get("visibility")?,
parent_symbol_id: row.get("parent_symbol_id")?,
start_line: row.get::<_, i64>("start_line")? as usize,
start_column: row.get::<_, i64>("start_column")? as usize,
end_line: row.get::<_, i64>("end_line")? as usize,
end_column: row.get::<_, i64>("end_column")? as usize,
start_byte: row.get::<_, i64>("start_byte")? as usize,
end_byte: row.get::<_, i64>("end_byte")? as usize,
body_start_line: row
.get::<_, Option<i64>>("body_start_line")?
.map(|v| v as usize),
body_start_column: row
.get::<_, Option<i64>>("body_start_column")?
.map(|v| v as usize),
body_end_line: row
.get::<_, Option<i64>>("body_end_line")?
.map(|v| v as usize),
body_end_column: row
.get::<_, Option<i64>>("body_end_column")?
.map(|v| v as usize),
body_start_byte: row
.get::<_, Option<i64>>("body_start_byte")?
.map(|v| v as usize),
body_end_byte: row
.get::<_, Option<i64>>("body_end_byte")?
.map(|v| v as usize),
body_hash: row.get("body_hash")?,
semantic_group: row.get("semantic_group")?,
is_test: row.get::<_, i64>("is_test")? != 0,
test_container: row.get::<_, i64>("test_container")? != 0,
})
}
pub(crate) fn escape_like(value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('%', "\\%")
.replace('_', "\\_")
}
pub fn load_scoped_files(
conn: &Connection,
path_filter: Option<&str>,
) -> Result<Vec<FileFact>, QueryError> {
let norm = path_filter
.map(|p| p.replace('\\', "/").trim_matches('/').to_string())
.filter(|p| !p.is_empty());
let norm_bs = norm.as_ref().map(|p| p.replace('/', "\\"));
let prefix = norm.as_ref().map(|path| format!("{}/%", escape_like(path)));
let prefix_bs = norm_bs
.as_ref()
.map(|path| format!("{}\\\\%", escape_like(path)));
let sql = "SELECT file_id, path, language, content_hash, content_bytes, line_count, indexed_at
FROM files
WHERE (:path IS NULL
OR path = :path COLLATE NOCASE
OR path = :path_bs COLLATE NOCASE
OR path LIKE :path_prefix ESCAPE '\\'
OR path LIKE :path_prefix_bs ESCAPE '\\')
ORDER BY (:path IS NOT NULL AND (path = :path OR path = :path_bs)) DESC, path ASC";
let mut stmt = conn.prepare(sql)?;
let files = stmt
.query_map(
rusqlite::named_params! {
":path": norm.as_deref(),
":path_bs": norm_bs.as_deref(),
":path_prefix": prefix.as_deref(),
":path_prefix_bs": prefix_bs.as_deref(),
},
|row| {
Ok(FileFact {
file_id: row.get(0)?,
path: row.get::<_, String>(1)?.replace('\\', "/"),
language: row.get(2)?,
content_hash: row.get(3)?,
content_bytes: row.get(4)?,
line_count: row.get(5)?,
indexed_at: row.get(6)?,
})
},
)?
.collect::<Result<Vec<_>, _>>()?;
Ok(files)
}
pub const OUTLINE_FILE_CAP: usize = 1000;
pub struct OutlineScope {
path: Option<String>,
path_bs: Option<String>,
prefix: Option<String>,
prefix_bs: Option<String>,
max_slashes: i64,
}
impl OutlineScope {
pub fn new(path_filter: Option<&str>, depth: usize) -> Self {
let path = path_filter
.map(|p| p.replace('\\', "/").trim_matches('/').to_string())
.filter(|p| !p.is_empty());
let path_bs = path.as_ref().map(|p| p.replace('/', "\\"));
let max_slashes = match &path {
None => depth.saturating_sub(1),
Some(p) => p.matches('/').count() + depth,
} as i64;
Self {
prefix: path.as_ref().map(|p| format!("{}/%", escape_like(p))),
prefix_bs: path_bs.as_ref().map(|p| format!("{}\\\\%", escape_like(p))),
path,
path_bs,
max_slashes,
}
}
pub fn lists(&self, path: &str) -> bool {
path.matches(['/', '\\']).count() as i64 <= self.max_slashes
}
fn params(&self) -> [(&'static str, &dyn ToSql); 5] {
[
(":path", &self.path),
(":path_bs", &self.path_bs),
(":path_prefix", &self.prefix),
(":path_prefix_bs", &self.prefix_bs),
(":max_slashes", &self.max_slashes),
]
}
}
fn outline_file_terms(conn: &Connection) -> String {
let supported = if has_column(conn, "files", "status") {
"COALESCE(status, '') != 'unsupported'"
} else {
"1 = 1"
};
format!(
"(:path IS NULL
OR path = :path COLLATE NOCASE
OR path = :path_bs COLLATE NOCASE
OR path LIKE :path_prefix ESCAPE '\\'
OR path LIKE :path_prefix_bs ESCAPE '\\')
AND {supported}"
)
}
fn outline_files_cte(conn: &Connection) -> String {
format!(
"outline_files AS (
SELECT path FROM files
WHERE {}
AND length(path) - length(replace(replace(path, '/', ''), '\\', '')) <= :max_slashes
ORDER BY path ASC
LIMIT {OUTLINE_FILE_CAP}
)",
outline_file_terms(conn)
)
}
pub fn for_each_outline_path(
conn: &Connection,
scope: &OutlineScope,
mut visit: impl FnMut(String) -> bool,
) -> Result<(), QueryError> {
let sql = format!(
"SELECT path FROM files WHERE {} ORDER BY path ASC",
outline_file_terms(conn)
);
let mut stmt = conn.prepare(&sql)?;
let params = scope.params();
let mut rows = stmt.query(¶ms[..4])?;
while let Some(row) = rows.next()? {
if !visit(row.get(0)?) {
break;
}
}
Ok(())
}
fn outline_level(alias: &str) -> String {
format!(
"({alias}.parent_symbol_id IS NULL OR EXISTS (
SELECT 1 FROM symbols ns WHERE ns.symbol_id = {alias}.parent_symbol_id
AND ns.kind IN ('namespace', 'module', 'package')))"
)
}
const OUTLINE_KINDS: &str =
"('function', 'method', 'struct', 'enum', 'trait', 'class', 'interface', 'type')";
pub fn load_scoped_outline_symbols(
conn: &Connection,
path_filter: Option<&str>,
depth: usize,
limit_per_file: usize,
) -> Result<HashMap<String, Vec<Symbol>>, QueryError> {
let scope = OutlineScope::new(path_filter, depth);
let sql = format!(
"WITH {files},
ranked AS (
SELECT s.symbol_id, s.file_id, s.path, s.language, s.name, s.kind, s.signature, s.doc_comment,
s.visibility, s.parent_symbol_id, s.start_line, s.start_column, s.end_line, s.end_column,
s.start_byte, s.end_byte, s.body_start_line, s.body_start_column, s.body_end_line,
s.body_end_column, s.body_start_byte, s.body_end_byte, s.body_hash, s.semantic_group,
s.is_test, s.test_container,
ROW_NUMBER() OVER (
PARTITION BY s.path ORDER BY (s.is_test = 1 OR s.test_container = 1), s.start_line ASC
) as rn
FROM symbols s
JOIN outline_files bf ON s.path = bf.path
WHERE s.kind IN {OUTLINE_KINDS}
AND {top}
AND NOT EXISTS (SELECT 1 FROM symbols overload
WHERE +overload.path = s.path AND overload.name = s.name
AND overload.kind = s.kind
AND +overload.parent_symbol_id IS s.parent_symbol_id
AND overload.start_line < s.start_line)
)
SELECT symbol_id, file_id, path, language, name, kind, signature, doc_comment,
visibility, parent_symbol_id, start_line, start_column, end_line, end_column,
start_byte, end_byte, body_start_line, body_start_column, body_end_line,
body_end_column, body_start_byte, body_end_byte, body_hash, semantic_group,
is_test, test_container
FROM ranked
WHERE rn <= :limit
ORDER BY path ASC, start_line ASC",
files = outline_files_cte(conn),
top = outline_level("s"),
);
let mut stmt = conn.prepare(&sql)?;
let limit = limit_per_file as i64;
let mut params = scope.params().to_vec();
params.push((":limit", &limit));
let mut rows = stmt.query(¶ms[..])?;
let mut symbols_by_file: HashMap<String, Vec<Symbol>> = HashMap::new();
while let Some(row) = rows.next()? {
let sym = map_symbol(row)?;
symbols_by_file
.entry(sym.path.clone())
.or_default()
.push(sym);
}
Ok(symbols_by_file)
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct OutlineCounts {
pub definitions: usize,
pub tests: usize,
pub fixtures: usize,
}
pub fn load_outline_counts(
conn: &Connection,
path_filter: Option<&str>,
depth: usize,
) -> Result<HashMap<String, OutlineCounts>, QueryError> {
let scope = OutlineScope::new(path_filter, depth);
let lifecycle = if has_column(conn, "symbols", "test_lifecycle") {
"COALESCE(s.test_lifecycle, 0) != 0"
} else {
"0"
};
let mut stmt = conn.prepare(&format!(
"WITH {files}
SELECT replace(s.path, '\\', '/'),
COUNT(DISTINCT CASE WHEN {top} AND s.is_test = 0 AND s.test_container = 0
AND s.kind IN {OUTLINE_KINDS}
THEN s.kind || ' ' || s.name END),
SUM(s.is_test = 1 AND NOT {lifecycle}),
SUM(s.is_test = 1 AND {lifecycle})
FROM symbols s
JOIN outline_files bf ON s.path = bf.path
GROUP BY replace(s.path, '\\', '/')",
files = outline_files_cte(conn),
top = outline_level("s"),
))?;
let rows = stmt.query_map(&scope.params()[..], |row| {
Ok((
row.get::<_, String>(0)?,
OutlineCounts {
definitions: row.get::<_, i64>(1)? as usize,
tests: row.get::<_, i64>(2)? as usize,
fixtures: row.get::<_, i64>(3)? as usize,
},
))
})?;
Ok(rows.collect::<Result<_, _>>()?)
}
pub const BASELINE_PATH_CAP: usize = 20;
pub fn file_sizes_for_paths(conn: &Connection, paths: &[String]) -> usize {
let mut seen = std::collections::HashSet::new();
let distinct: Vec<&String> = paths
.iter()
.filter(|path| seen.insert(path.as_str()))
.take(BASELINE_PATH_CAP)
.collect();
if distinct.is_empty() {
return 0;
}
let placeholders = vec!["?"; distinct.len()].join(", ");
let sql =
format!("SELECT COALESCE(SUM(content_bytes), 0) FROM files WHERE path IN ({placeholders})");
conn.query_row(&sql, rusqlite::params_from_iter(distinct), |row| {
row.get::<_, i64>(0)
})
.map(|bytes| bytes.max(0) as usize)
.unwrap_or(0)
}
pub fn get_file(conn: &Connection, path: &str) -> Result<Option<FileFact>, QueryError> {
let normalized = path.replace('\\', "/");
let backslash = path.replace('/', "\\");
let mut stmt = conn.prepare(
"SELECT file_id, path, language, content_hash, content_bytes, line_count, indexed_at
FROM files
WHERE (path = ?1 COLLATE NOCASE OR path = ?2 COLLATE NOCASE)
ORDER BY (path = ?1 OR path = ?2) DESC
LIMIT 1",
)?;
let mut rows = stmt.query(params![normalized, backslash])?;
if let Some(row) = rows.next()? {
Ok(Some(FileFact {
file_id: row.get(0)?,
path: row.get::<_, String>(1)?.replace('\\', "/"),
language: row.get(2)?,
content_hash: row.get(3)?,
content_bytes: row.get(4)?,
line_count: row.get(5)?,
indexed_at: row.get(6)?,
}))
} else {
Ok(None)
}
}
pub fn count_file_symbols(conn: &Connection, path: &str) -> usize {
let count = |sql: &str| {
conn.query_row(
sql,
params![path.replace('\\', "/"), path.replace('/', "\\")],
|row| row.get::<_, i64>(0),
)
.map(|count| count.max(0) as usize)
.unwrap_or(0)
};
match count("SELECT COUNT(*) FROM symbols WHERE path = ?1 OR path = ?2") {
0 => count(
"SELECT COUNT(*) FROM symbols
WHERE path = ?1 COLLATE NOCASE OR path = ?2 COLLATE NOCASE",
),
exact => exact,
}
}
pub fn count_parse_diagnostics(conn: &Connection, path: &str) -> usize {
conn.query_row(
"SELECT COUNT(*) FROM parse_diagnostics
WHERE path = ?1 COLLATE NOCASE OR path = ?2 COLLATE NOCASE",
params![path.replace('\\', "/"), path.replace('/', "\\")],
|row| row.get::<_, i64>(0),
)
.map(|count| count as usize)
.unwrap_or(0)
}
pub fn for_each_unsupported_path(
conn: &Connection,
scope: &OutlineScope,
mut visit: impl FnMut(&str),
) -> Result<(), QueryError> {
if !has_column(conn, "files", "status") {
return Ok(());
}
let mut stmt = conn.prepare(
"SELECT path FROM files
WHERE status = 'unsupported'
AND (:path IS NULL
OR path = :path COLLATE NOCASE
OR path = :path_bs COLLATE NOCASE
OR path LIKE :path_prefix ESCAPE '\\'
OR path LIKE :path_prefix_bs ESCAPE '\\')",
)?;
let params = scope.params();
let mut rows = stmt.query(¶ms[..4])?;
while let Some(row) = rows.next()? {
visit(&row.get::<_, String>(0)?);
}
Ok(())
}
pub fn load_file_symbols(conn: &Connection, file_path: &str) -> Result<Vec<Symbol>, QueryError> {
let normalized = file_path.replace('\\', "/");
let backslash = file_path.replace('/', "\\");
let mut stmt = conn.prepare(
"SELECT symbol_id, file_id, path, language, name, kind, signature, doc_comment,
visibility, parent_symbol_id, start_line, start_column, end_line, end_column,
start_byte, end_byte, body_start_line, body_start_column, body_end_line,
body_end_column, body_start_byte, body_end_byte, body_hash, semantic_group,
is_test, test_container
FROM symbols
WHERE (path = ?1 OR path = ?2)
ORDER BY start_line ASC, start_column ASC",
)?;
let rows = stmt
.query_map(params![&normalized, &backslash], map_symbol)?
.collect::<Result<Vec<_>, _>>()?;
if !rows.is_empty() {
return Ok(rows);
}
let mut stmt = conn.prepare(
"SELECT symbol_id, file_id, path, language, name, kind, signature, doc_comment,
visibility, parent_symbol_id, start_line, start_column, end_line, end_column,
start_byte, end_byte, body_start_line, body_start_column, body_end_line,
body_end_column, body_start_byte, body_end_byte, body_hash, semantic_group,
is_test, test_container
FROM symbols
WHERE (path = ?1 COLLATE NOCASE OR path = ?2 COLLATE NOCASE)
ORDER BY start_line ASC, start_column ASC",
)?;
let rows = stmt
.query_map(params![normalized, backslash], map_symbol)?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn normalize_kind(kind: &str) -> String {
let lower = kind.trim().to_lowercase();
match lower.as_str() {
"fn" | "func" | "function" => "function".to_string(),
"method" => "method".to_string(),
"struct" => "struct".to_string(),
"class" => "class".to_string(),
"enum" => "enum".to_string(),
"trait" => "trait".to_string(),
"interface" => "interface".to_string(),
"type" | "typedef" => "type".to_string(),
"mod" | "module" => "module".to_string(),
"const" | "constant" => "constant".to_string(),
"var" | "variable" => "variable".to_string(),
_ => lower,
}
}
pub fn search_symbols(
conn: &Connection,
query: &str,
kind_filter: Option<&str>,
include_tests: bool,
limit: usize,
) -> Result<Vec<Symbol>, QueryError> {
search_symbols_scoped(conn, query, kind_filter, None, include_tests, limit)
}
pub(crate) fn inherited_attribute_write(alias: &str) -> String {
let rival_rank = path_proximity("rival.path", "pe.path");
let base_rank = path_proximity("c.path", "pe.path");
format!(
"(CASE WHEN {alias}.kind = 'property' AND {alias}.parent_symbol_id IS NOT NULL
AND ({alias}.signature LIKE 'self.%' OR {alias}.signature LIKE 'this.%' OR {alias}.signature LIKE 'cls.%')
THEN EXISTS (
WITH RECURSIVE base(symbol_id, depth) AS (
SELECT {alias}.parent_symbol_id, 0
UNION
SELECT r.to_symbol_id, base.depth + 1
FROM relationships r JOIN base ON r.from_symbol_id = base.symbol_id
WHERE +r.kind = 'extends' AND base.depth < 8
UNION
SELECT c.symbol_id, base.depth + 1
FROM pending_relationships pe
JOIN base ON pe.from_symbol_id = base.symbol_id
JOIN symbols c ON c.name = pe.target_terminal_name
WHERE +pe.kind = 'extends' AND base.depth < 8
AND c.kind IN ('class', 'interface', 'struct', 'trait', 'protocol')
AND NOT EXISTS (
SELECT 1 FROM symbols rival
WHERE rival.name = c.name AND rival.symbol_id != c.symbol_id
AND rival.kind IN ('class', 'interface', 'struct', 'trait', 'protocol')
AND {rival_rank} >= {base_rank})
)
SELECT 1 FROM base JOIN symbols inherited ON inherited.parent_symbol_id = base.symbol_id
WHERE base.depth > 0 AND inherited.name = {alias}.name
AND (inherited.kind NOT IN ('variable', 'field') OR inherited.signature LIKE '%=%'))
ELSE 0 END)"
)
}
fn inherited_write_exclusion(conn: &Connection) -> String {
if has_table(conn, "relationships") && has_table(conn, "pending_relationships") {
format!(" AND NOT {}", inherited_attribute_write("s"))
} else {
String::new()
}
}
pub fn inherited_writes_among<'a>(
conn: &Connection,
ids: impl Iterator<Item = &'a str>,
) -> Result<HashSet<String>, QueryError> {
if inherited_write_exclusion(conn).is_empty() {
return Ok(HashSet::new());
}
let ids = serde_json::to_string(&ids.collect::<Vec<_>>()).unwrap_or_default();
let mut stmt = conn.prepare(&format!(
"SELECT s.symbol_id FROM symbols s
WHERE s.symbol_id IN (SELECT value FROM json_each(?1)) AND {}",
inherited_attribute_write("s")
))?;
let found = stmt
.query_map(params![ids], |row| row.get(0))?
.collect::<Result<_, _>>()?;
Ok(found)
}
fn search_symbols_sql(variables_wanted: bool, include_tests: bool, limit: usize) -> String {
let mut sql = format!(
"SELECT symbol_id, file_id, path, language, name, kind, signature, doc_comment,
visibility, parent_symbol_id, start_line, start_column, end_line, end_column,
start_byte, end_byte, body_start_line, body_start_column, body_end_line,
body_end_column, body_start_byte, body_end_byte, body_hash, semantic_group,
is_test, test_container
FROM symbols s
WHERE (name = :query OR name LIKE :pattern ESCAPE '\\')
AND {KIND_FILTER}
AND (:path IS NULL OR replace(path, '\\', '/') = :path COLLATE NOCASE OR replace(path, '\\', '/') LIKE :path_like || '/%' ESCAPE '\\' OR replace(path, '\\', '/') LIKE '%/' || :path_like ESCAPE '\\')",
);
sql.push_str(&document_link_exclusion());
if !variables_wanted {
sql.push_str(&format!(" AND NOT {}", local_variable_predicate("s")));
}
if !include_tests {
sql.push_str(&format!(
" AND (+name = :query OR (is_test = 0 AND test_container = 0 AND NOT {}))",
test_path_predicate("s")
));
}
sql.push_str(
" ORDER BY (name = :query) DESC, (name LIKE :prefix ESCAPE '\\') DESC, (kind IN ('function', 'struct', 'class', 'trait', 'method', 'enum', 'interface', 'type')) DESC, (COALESCE(signature, '') LIKE 'self.%') ASC, length(name) ASC, path ASC LIMIT ",
);
sql.push_str(&limit.to_string());
sql
}
pub fn search_symbols_scoped(
conn: &Connection,
query: &str,
kind_filter: Option<&str>,
path_filter: Option<&str>,
include_tests: bool,
limit: usize,
) -> Result<Vec<Symbol>, QueryError> {
validate_result_limit(limit)?;
if limit == 0 {
return Ok(Vec::new());
}
let norm_kind = kind_filter.map(normalize_kind);
if let Some(owner) = query
.strip_suffix("::")
.or_else(|| query.strip_suffix('.'))
.and_then(|owner| owner.rsplit(['.', ':']).next())
.filter(|owner| !owner.is_empty())
{
return owner_members(
conn,
owner,
norm_kind.as_deref(),
path_filter,
include_tests,
limit,
);
}
if (query.contains("::") || query.contains('.'))
&& let Some(sym) = get_symbol_by_name(conn, query, path_filter)?
{
let kind_matches = norm_kind
.as_deref()
.is_none_or(|kind| sym.kind == kind || crate::formatters::display_kind(&sym) == kind);
return Ok(if kind_matches { vec![sym] } else { Vec::new() });
}
let pattern = format!("%{}%", escape_like(query));
let prefix = format!("{}%", escape_like(query));
let normalized_path = path_filter.map(|p| {
p.replace('\\', "/")
.trim_start_matches("./")
.trim_matches('/')
.to_string()
});
let escaped_path = normalized_path.as_deref().map(escape_like);
let path_val = normalized_path.as_deref();
let path_like = escaped_path.as_deref();
let kind_val = norm_kind.as_deref();
let mut fetch = 4 * limit;
loop {
let sql = search_symbols_sql(
norm_kind.as_deref() == Some("variable"),
include_tests,
fetch,
);
let rows = conn
.prepare(&sql)?
.query_map(
rusqlite::named_params! {
":query": query,
":pattern": pattern,
":prefix": prefix,
":kind": kind_val,
":path": path_val,
":path_like": path_like,
},
map_symbol,
)?
.collect::<Result<Vec<Symbol>, _>>()?;
let fetched = rows.len();
let inherited = inherited_writes_among(conn, rows.iter().map(|s| s.symbol_id.as_str()))?;
let mut kept: Vec<Symbol> = rows
.into_iter()
.filter(|s| !inherited.contains(&s.symbol_id))
.collect();
let has_definition = kept.iter().any(|s| s.kind != "import");
let mut counted = 0;
let past_limit = kept.iter().position(|s| {
if !folds_into_import_line(query, s, has_definition) {
counted += 1;
}
counted > limit
});
if let Some(cut) = past_limit {
kept.truncate(cut);
return Ok(kept);
}
if fetched < fetch || fetch >= 4 * MAX_RESULT_LIMIT {
return Ok(kept);
}
fetch *= 2;
}
}
const MEMBER_LIST_OWNER_KINDS: &str = "'class', 'struct', 'interface', 'trait', 'enum', 'record', 'object', \
'protocol', 'union', 'module', 'namespace'";
const KIND_FILTER: &str = "(:kind IS NULL OR s.kind = :kind OR :kind = CASE
WHEN s.language = 'html' AND s.kind = 'class' THEN 'element'
WHEN s.language = 'sql' AND s.kind = 'class' THEN 'table'
WHEN s.language = 'markdown' AND s.kind = 'module' THEN 'section'
WHEN s.language = 'markdown' AND s.kind = 'import' THEN 'link'
WHEN s.kind = 'property' AND s.language <> 'fsharp'
AND (s.signature GLOB 'self.*' OR s.signature GLOB 'this.*' OR s.signature GLOB 'cls.*')
THEN 'attribute'
END)";
fn owner_members(
conn: &Connection,
owner: &str,
kind: Option<&str>,
path_filter: Option<&str>,
include_tests: bool,
limit: usize,
) -> Result<Vec<Symbol>, QueryError> {
let path = path_filter.map(|p| {
p.replace('\\', "/")
.trim_start_matches("./")
.trim_matches('/')
.to_string()
});
let path_like = path.as_deref().map(escape_like);
let tests = if include_tests {
String::new()
} else {
format!(
" AND +s.is_test = 0 AND +s.test_container = 0 AND NOT {}",
test_path_predicate("s")
)
};
let docs = documentation_language_list();
let sql = format!(
"SELECT s.symbol_id, s.file_id, s.path, s.language, s.name, s.kind, s.signature, s.doc_comment,
s.visibility, s.parent_symbol_id, s.start_line, s.start_column, s.end_line, s.end_column,
s.start_byte, s.end_byte, s.body_start_line, s.body_start_column, s.body_end_line,
s.body_end_column, s.body_start_byte, s.body_end_byte, s.body_hash, s.semantic_group,
s.is_test, s.test_container
FROM symbols o JOIN symbols s ON s.parent_symbol_id = o.symbol_id
WHERE o.name = :owner
AND o.kind IN ({MEMBER_LIST_OWNER_KINDS})
AND (o.language NOT IN ({docs})
OR NOT EXISTS (SELECT 1 FROM symbols c WHERE c.name = :owner
AND c.kind IN ({MEMBER_LIST_OWNER_KINDS}) AND c.language NOT IN ({docs})))
AND s.kind NOT IN ('parameter', 'import')
AND {KIND_FILTER}
AND (:path IS NULL OR replace(s.path, '\\', '/') = :path COLLATE NOCASE
OR replace(s.path, '\\', '/') LIKE :path_like || '/%' ESCAPE '\\'
OR replace(s.path, '\\', '/') LIKE '%/' || :path_like ESCAPE '\\'){tests}
ORDER BY s.path, s.start_line"
);
let rows = conn
.prepare(&sql)?
.query_map(
rusqlite::named_params! {
":owner": owner,
":kind": kind,
":path": path.as_deref(),
":path_like": path_like.as_deref(),
},
map_symbol,
)?
.collect::<Result<Vec<Symbol>, _>>()?;
let inherited = inherited_writes_among(conn, rows.iter().map(|s| s.symbol_id.as_str()))?;
Ok(rows
.into_iter()
.filter(|s| !inherited.contains(&s.symbol_id))
.take(limit)
.collect())
}
pub fn folds_into_import_line(query: &str, row: &Symbol, has_definition: bool) -> bool {
has_definition
&& row.kind == "import"
&& (row.name == query
|| row.name.ends_with(&format!(".{query}"))
|| row.name.ends_with(&format!("::{query}")))
}
pub fn sanitize_fts5_query(query: &str) -> (String, String) {
let raw_words = query_words(query);
let split: Vec<(Vec<&str>, Option<&str>)> = raw_words
.iter()
.map(|raw| {
let parts = split_identifier(raw);
let whole = (parts.len() > 1).then_some(*raw);
(parts, whole)
})
.collect();
let any_content = split
.iter()
.any(|(parts, _)| parts.iter().any(|p| !is_stop_word(p)));
let mut and_groups: Vec<String> = Vec::new();
let mut or_terms: Vec<String> = Vec::new();
for (parts, whole) in split {
let parts: Vec<String> = parts
.into_iter()
.filter(|p| !any_content || !is_stop_word(p))
.map(fts5_term)
.collect();
let whole = whole.map(fts5_term);
let group = match (parts.is_empty(), whole.as_deref()) {
(true, None) => continue,
(true, Some(w)) => w.to_string(),
(false, None) => parts.join(" "),
(false, Some(w)) => format!("(({}) OR {w})", parts.join(" ")),
};
and_groups.push(group);
or_terms.extend(parts);
or_terms.extend(whole);
}
if and_groups.is_empty() {
return (String::new(), String::new());
}
let mut and_query = and_groups.join(" AND ");
if (2..=3).contains(&raw_words.len()) {
let all: String = raw_words.concat();
if all.len() <= 64 {
let all = fts5_term(&all);
and_query = format!("({and_query}) OR {all}");
or_terms.push(all);
}
}
(and_query, or_terms.join(" OR "))
}
const STOP_WORDS: &[&str] = &[
"a", "about", "after", "again", "all", "already", "also", "always", "an", "and", "another",
"any", "are", "as", "at", "be", "because", "been", "before", "being", "between", "both", "but",
"by", "can", "could", "did", "do", "does", "each", "either", "else", "ever", "every", "for",
"from", "had", "has", "have", "here", "how", "if", "in", "instead", "is", "it", "its",
"itself", "just", "many", "may", "might", "more", "most", "much", "must", "neither", "never",
"no", "nor", "not", "of", "on", "once", "one", "only", "or", "other", "our", "per", "rather",
"same", "should", "since", "so", "some", "still", "such", "than", "that", "the", "their",
"them", "then", "there", "these", "they", "this", "those", "through", "to", "too", "two",
"until", "very", "via", "was", "we", "were", "what", "when", "where", "whether", "which",
"while", "who", "whom", "why", "will", "with", "within", "without", "would", "yet", "you",
"your",
];
fn is_stop_word(word: &str) -> bool {
STOP_WORDS.contains(&word.to_ascii_lowercase().as_str())
}
fn query_words(query: &str) -> Vec<&str> {
query
.split(|c: char| !c.is_alphanumeric() && c != '_')
.filter(|s| !s.is_empty())
.collect()
}
fn trigram_name_terms(query: &str) -> Vec<String> {
let mut terms: Vec<String> = Vec::new();
for word in query_words(query) {
for term in std::iter::once(word).chain(split_identifier(word)) {
let lower = term.to_lowercase();
if lower.chars().count() >= 3 && !terms.contains(&lower) {
terms.push(lower);
}
}
}
let any_content = terms.iter().any(|t| !is_stop_word(t));
terms.retain(|t| !any_content || !is_stop_word(t));
terms
}
fn fts5_term(token: &str) -> String {
if token.chars().count() < 3 {
format!("\"{token}\"")
} else {
format!("\"{token}\"*")
}
}
fn name_prefix_query(name: &str) -> String {
name.split(|c: char| !c.is_alphanumeric() && c != '_')
.filter(|s| !s.is_empty())
.map(|s| format!("\"{s}\"*"))
.collect::<Vec<_>>()
.join(" ")
}
fn split_identifier(word: &str) -> Vec<&str> {
let mut out = Vec::new();
split_identifier_into(word, &mut out);
out
}
fn split_identifier_into<'a>(word: &'a str, out: &mut Vec<&'a str>) {
let mut chars = word.char_indices().peekable();
let Some((_, first)) = chars.next() else {
return;
};
let mut prev = char_class(first);
let mut start = 0;
while let Some((idx, c)) = chars.next() {
let cur = char_class(c);
let next = chars.peek().map_or(OTHER, |(_, n)| char_class(*n));
if identifier_boundary(prev, cur, next) {
push_piece(out, &word[start..idx]);
start = idx;
}
prev = cur;
}
push_piece(out, &word[start..]);
}
const OTHER: u8 = 0;
const UNDERSCORE: u8 = 1;
const UPPER: u8 = 2;
const LOWER: u8 = 3;
const DIGIT: u8 = 4;
fn char_class(c: char) -> u8 {
if c == '_' {
UNDERSCORE
} else if c.is_uppercase() {
UPPER
} else if c.is_lowercase() {
LOWER
} else if c.is_ascii_digit() {
DIGIT
} else {
OTHER
}
}
fn byte_class(b: u8) -> u8 {
match b {
b'_' => UNDERSCORE,
b'A'..=b'Z' => UPPER,
b'a'..=b'z' => LOWER,
b'0'..=b'9' => DIGIT,
_ => OTHER,
}
}
fn identifier_boundary(prev: u8, cur: u8, next: u8) -> bool {
cur == UNDERSCORE
|| prev == UNDERSCORE
|| (cur == UPPER && (prev == LOWER || prev == DIGIT))
|| (cur == UPPER && prev == UPPER && next == LOWER)
|| ((cur == DIGIT) != (prev == DIGIT))
}
fn push_piece<'a>(out: &mut Vec<&'a str>, piece: &'a str) {
if !piece.is_empty() && piece != "_" {
out.push(piece);
}
}
fn text_tokens_into<'a>(text: &'a str, out: &mut Vec<&'a str>) {
if !text.is_ascii() {
for word in text.split(|c: char| !c.is_alphanumeric() && c != '_') {
split_identifier_into(word, out);
}
return;
}
let bytes = text.as_bytes();
let mut start: Option<usize> = None;
let mut prev = OTHER;
for (i, &b) in bytes.iter().enumerate() {
let cur = byte_class(b);
if cur == OTHER {
if let Some(s) = start.take() {
push_piece(out, &text[s..i]);
}
continue;
}
match start {
None => start = Some(i),
Some(s) => {
let next = bytes.get(i + 1).map_or(OTHER, |n| byte_class(*n));
if identifier_boundary(prev, cur, next) {
push_piece(out, &text[s..i]);
start = Some(i);
}
}
}
prev = cur;
}
if let Some(s) = start {
push_piece(out, &text[s..]);
}
}
#[derive(Clone)]
pub(crate) struct Candidate {
pub result: SymbolSearchResult,
pub bm25: Option<f64>,
pub exact_name: bool,
pub word_match: bool,
pub name_match: bool,
pub owner_match: bool,
pub name_terms: Vec<String>,
pub documentation: bool,
pub nested: bool,
pub owner: Option<String>,
owner_context: Option<String>,
}
const OWNER_KINDS: &str =
"('class', 'struct', 'interface', 'trait', 'enum', 'record', 'object', 'protocol', 'union')";
fn candidate_columns(conn: &Connection) -> String {
format!(
"s.rowid AS row_id, s.symbol_id, s.file_id, s.path, s.language, s.name, s.kind,
s.signature, s.doc_comment, s.visibility, s.parent_symbol_id, s.start_line,
s.start_column, s.end_line, s.end_column, s.start_byte, s.end_byte,
s.body_start_line, s.body_start_column, s.body_end_line, s.body_end_column,
s.body_start_byte, s.body_end_byte, s.body_hash, s.semantic_group, s.is_test,
s.test_container,
(s.language IN ({doc_langs}) OR NOT ({not_doc})) AS documentation,
(s.kind IN ('function', 'method')
AND EXISTS (SELECT 1 FROM symbols nest WHERE nest.symbol_id = s.parent_symbol_id
AND nest.kind IN ('function', 'method', 'constructor')
AND s.start_byte > nest.start_byte AND s.end_byte <= nest.end_byte)) AS nested,
(SELECT o.name FROM symbols o WHERE o.symbol_id = s.parent_symbol_id
AND o.kind IN {OWNER_KINDS}) AS owner_name",
doc_langs = documentation_language_list(),
not_doc = not_documentation(conn, "s")
)
}
fn document_link_exclusion() -> String {
format!(
" AND NOT (s.kind = 'import' AND s.language IN ({}))",
documentation_language_list()
)
}
fn candidate_filters(searching_variables: bool, include_tests: bool) -> String {
let mut sql = format!(
" AND {KIND_FILTER}
AND (:path IS NULL OR replace(s.path, '\\', '/') = :path COLLATE NOCASE OR replace(s.path, '\\', '/') LIKE :path_like || '/%' ESCAPE '\\' OR replace(s.path, '\\', '/') LIKE '%/' || :path_like ESCAPE '\\')",
);
sql.push_str(&document_link_exclusion());
if !searching_variables {
sql.push_str(&format!(" AND NOT {}", local_variable_predicate("s")));
}
if !include_tests {
sql.push_str(" AND +s.is_test = 0 AND +s.test_container = 0");
sql.push_str(&format!(" AND NOT {}", test_path_predicate("s")));
}
sql
}
fn map_search_candidate(row: &Row, terms: &[String]) -> rusqlite::Result<(i64, Candidate)> {
let symbol = map_symbol(row)?;
let lower_name = symbol.name.to_lowercase();
let name_terms = terms
.iter()
.filter(|t| lower_name.contains(t.as_str()))
.cloned()
.collect();
let candidate = Candidate {
result: SymbolSearchResult {
symbol,
score: 0.0,
snippet: None,
explain: None,
},
bm25: None,
exact_name: false,
word_match: false,
name_match: false,
owner_match: false,
name_terms,
documentation: row.get::<_, Option<i64>>("documentation")? == Some(1),
nested: row.get::<_, Option<i64>>("nested")? == Some(1),
owner: row.get("owner_name")?,
owner_context: None,
};
Ok((row.get("row_id")?, candidate))
}
pub(crate) fn collect_search_candidates(
conn: &Connection,
query: &str,
kind_filter: Option<&str>,
path_filter: Option<&str>,
include_tests: bool,
limit: usize,
) -> Result<Vec<Candidate>, QueryError> {
let (and_q, or_q) = sanitize_fts5_query(query);
let terms = trigram_name_terms(query);
let normalized_path = path_filter.map(|p| {
p.replace('\\', "/")
.trim_start_matches("./")
.trim_matches('/')
.to_string()
});
let escaped_path = normalized_path.as_deref().map(escape_like);
let norm_kind = kind_filter.map(normalize_kind);
let searching_variables = norm_kind.as_deref() == Some("variable");
let path_val = normalized_path.as_deref();
let path_like = escaped_path.as_deref();
let kind_val = norm_kind.as_deref();
let columns = candidate_columns(conn);
let filters = candidate_filters(searching_variables, include_tests);
let word_cap = (limit * 4).clamp(40, 160);
let name_cap = (limit * 2).clamp(20, 40);
let new_candidate = |row: &Row| map_search_candidate(row, &terms);
let mut candidates: Vec<Candidate> = Vec::new();
let mut by_rowid: HashMap<i64, usize> = HashMap::new();
let mut admit = |rowid: i64, incoming: Candidate| match by_rowid.get(&rowid).copied() {
Some(i) => {
let existing = &mut candidates[i];
existing.exact_name |= incoming.exact_name;
existing.word_match |= incoming.word_match;
existing.name_match |= incoming.name_match;
existing.owner_match |= incoming.owner_match;
if incoming.bm25.is_some() && existing.bm25.is_none() {
existing.bm25 = incoming.bm25;
existing.result = incoming.result;
}
}
None => {
by_rowid.insert(rowid, candidates.len());
candidates.push(incoming);
}
};
let has_trigram = has_table(conn, "symbol_names_tri");
let exact_query = query.trim();
let exact_phrase = format!("\"{}\"", exact_query.replace('"', "\"\""));
let exact_via_trigram = has_trigram && exact_query.chars().count() >= 3;
let exact_sql = if exact_via_trigram {
format!(
"SELECT {columns} FROM symbol_names_tri
CROSS JOIN symbols s ON s.rowid = symbol_names_tri.rowid
WHERE symbol_names_tri MATCH :exact AND length(s.name) = length(:query) {filters}
ORDER BY s.path ASC, s.start_line ASC LIMIT {MAX_RESULT_LIMIT}"
)
} else {
format!(
"SELECT {columns} FROM symbols s WHERE s.name = :query {filters}
ORDER BY s.path ASC, s.start_line ASC LIMIT {MAX_RESULT_LIMIT}"
)
};
let mut exact_params: Vec<(&str, &dyn ToSql)> = vec![
(":query", &exact_query),
(":kind", &kind_val),
(":path", &path_val),
(":path_like", &path_like),
];
if exact_via_trigram {
exact_params.push((":exact", &exact_phrase));
}
let exact_rows = conn
.prepare(&exact_sql)?
.query_map(exact_params.as_slice(), new_candidate)?
.collect::<Result<Vec<_>, _>>()?;
for (rowid, mut candidate) in exact_rows {
candidate.exact_name = true;
admit(rowid, candidate);
}
let owner_sql = if has_trigram {
format!(
"SELECT {columns} FROM symbol_names_tri
CROSS JOIN symbols o ON o.rowid = symbol_names_tri.rowid
CROSS JOIN symbols s ON s.parent_symbol_id = o.symbol_id
WHERE symbol_names_tri MATCH :phrase AND length(o.name) = length(:word)
AND o.kind IN {OWNER_KINDS} {filters}
ORDER BY s.path ASC, s.start_line ASC LIMIT {name_cap}"
)
} else {
format!(
"SELECT {columns} FROM symbols o
CROSS JOIN symbols s ON s.parent_symbol_id = o.symbol_id
WHERE o.name = :word COLLATE NOCASE AND :phrase IS NOT NULL
AND o.kind IN {OWNER_KINDS} {filters}
ORDER BY s.path ASC, s.start_line ASC LIMIT {name_cap}"
)
};
let mut owner_stmt = conn.prepare(&owner_sql)?;
for word in rerank_words(query).iter().filter(|w| w.len() >= 3) {
let phrase = format!("\"{word}\"");
let owner_rows = owner_stmt
.query_map(
rusqlite::named_params! {
":word": word,
":phrase": phrase,
":kind": kind_val,
":path": path_val,
":path_like": path_like,
},
new_candidate,
)?
.collect::<Result<Vec<_>, _>>()?;
for (rowid, mut candidate) in owner_rows {
candidate.owner_match = true;
admit(rowid, candidate);
}
}
if searching_variables {
let pattern = format!("%{}%", escape_like(exact_query));
let local_sql = format!(
"SELECT {columns} FROM symbols s
WHERE {local} AND (s.name = :query OR s.name LIKE :pattern ESCAPE '\\') {filters}
ORDER BY (s.name = :query) DESC, length(s.name) ASC, s.path ASC LIMIT {limit}",
local = local_variable_predicate("s")
);
let local_rows = conn
.prepare(&local_sql)?
.query_map(
rusqlite::named_params! {
":query": exact_query,
":pattern": pattern,
":kind": kind_val,
":path": path_val,
":path_like": path_like,
},
new_candidate,
)?
.collect::<Result<Vec<_>, _>>()?;
for (rowid, mut candidate) in local_rows {
candidate.exact_name = candidate.result.symbol.name == exact_query;
candidate.name_match = true;
admit(rowid, candidate);
}
}
let word_sql = format!(
"SELECT {columns},
bm25(symbols_fts, 10.0, 5.0, 1.0) AS rank_score,
snippet(symbols_fts, 2, '[', ']', '...', 12) AS doc_snippet,
snippet(symbols_fts, 1, '[', ']', '...', 12) AS sig_snippet,
snippet(symbols_fts, 0, '[', ']', '...', 12) AS name_snippet
FROM symbols_fts
CROSS JOIN symbols s ON s.rowid = symbols_fts.rowid
WHERE symbols_fts MATCH :match {filters}
ORDER BY (s.kind = 'import') ASC, (s.language IN ({doc_langs})) ASC, {not_doc} DESC, (s.name = :query COLLATE NOCASE) DESC, rank_score ASC LIMIT {word_cap}",
doc_langs = documentation_language_list(),
not_doc = not_documentation(conn, "s")
);
let word_rows = |match_clause: &str| -> Result<Vec<(i64, Candidate)>, QueryError> {
let map_fn = |row: &Row| -> rusqlite::Result<(i64, Candidate)> {
let (rowid, mut candidate) = new_candidate(row)?;
let score: f64 = row.get("rank_score")?;
let doc_snip: Option<String> = row.get("doc_snippet").ok();
let sig_snip: Option<String> = row.get("sig_snippet").ok();
let name_snip: Option<String> = row.get("name_snippet").ok();
let highlighted = |s: &Option<String>| s.as_ref().is_some_and(|s| s.contains('['));
candidate.result.snippet = if highlighted(&doc_snip) {
doc_snip
} else if highlighted(&sig_snip) {
sig_snip
} else if highlighted(&name_snip) {
name_snip
} else {
doc_snip.or(sig_snip).or(name_snip)
};
candidate.result.score = score;
candidate.bm25 = Some(score);
candidate.word_match = true;
Ok((rowid, candidate))
};
Ok(conn
.prepare(&word_sql)?
.query_map(
rusqlite::named_params! {
":match": match_clause,
":query": query.trim(),
":kind": kind_val,
":path": path_val,
":path_like": path_like,
},
map_fn,
)?
.collect::<Result<Vec<_>, _>>()?)
};
if !and_q.is_empty() {
let and_rows = word_rows(&and_q)?;
let mut word_admitted: HashSet<i64> = and_rows.iter().map(|(rowid, _)| *rowid).collect();
for (rowid, candidate) in and_rows {
admit(rowid, candidate);
}
if and_q != or_q {
for (rowid, candidate) in word_rows(&or_q)? {
if word_admitted.len() >= word_cap && !word_admitted.contains(&rowid) {
break;
}
word_admitted.insert(rowid);
admit(rowid, candidate);
}
}
}
if !terms.is_empty() && has_trigram {
let match_clause = terms
.iter()
.map(|t| format!("\"{t}\""))
.collect::<Vec<_>>()
.join(" OR ");
let name_sql = format!(
"SELECT {columns} FROM symbol_names_tri
CROSS JOIN symbols s ON s.rowid = symbol_names_tri.rowid
WHERE symbol_names_tri MATCH :match {filters}
ORDER BY bm25(symbol_names_tri) ASC, length(s.name) ASC, s.path ASC LIMIT {name_cap}"
);
let name_rows = conn
.prepare(&name_sql)?
.query_map(
rusqlite::named_params! {
":match": match_clause,
":kind": kind_val,
":path": path_val,
":path_like": path_like,
},
new_candidate,
)?
.collect::<Result<Vec<_>, _>>()?;
for (rowid, mut candidate) in name_rows {
candidate.name_match = true;
admit(rowid, candidate);
}
}
let inherited = inherited_writes_among(
conn,
candidates
.iter()
.map(|c| c.result.symbol.symbol_id.as_str()),
)?;
candidates.retain(|c| !inherited.contains(&c.result.symbol.symbol_id));
Ok(candidates)
}
fn expand_ranked_owner_members(
conn: &Connection,
candidates: &mut Vec<Candidate>,
ranked: &[(SymbolSearchResult, SearchExplain)],
query: &str,
kind_filter: Option<&str>,
path_filter: Option<&str>,
include_tests: bool,
) -> Result<bool, QueryError> {
let owners: Vec<_> = ranked
.iter()
.take(20)
.filter(|(result, explain)| {
explain.documentation == 0.0
&& OWNER_KINDS.contains(&format!("'{}'", result.symbol.kind))
})
.take(3)
.map(|(result, _)| &result.symbol)
.collect();
if owners.is_empty() {
return Ok(false);
}
let normalized_path = path_filter.map(|p| {
p.replace('\\', "/")
.trim_start_matches("./")
.trim_matches('/')
.to_string()
});
let escaped_path = normalized_path.as_deref().map(escape_like);
let norm_kind = kind_filter.map(normalize_kind);
let columns = candidate_columns(conn);
let filters = candidate_filters(norm_kind.as_deref() == Some("variable"), include_tests);
let mut stmt = conn.prepare(&format!(
"SELECT {columns} FROM symbols s WHERE s.parent_symbol_id = :owner {filters}
ORDER BY s.start_line, s.start_column, s.symbol_id LIMIT 40"
))?;
let terms = trigram_name_terms(query);
let stemmer = Stemmer::create(Algorithm::English);
let words = stemmed_query_words(query, &stemmer);
let mut members = Vec::new();
for owner in owners {
let context = format!(
"{} {}",
owner.name,
head_bytes(owner.doc_comment.as_deref().unwrap_or(""), TEXT_HEAD_BYTES)
);
let rows = stmt.query_map(
rusqlite::named_params! {
":owner": owner.symbol_id,
":kind": norm_kind.as_deref(),
":path": normalized_path.as_deref(),
":path_like": escaped_path.as_deref(),
},
|row| map_search_candidate(row, &terms),
)?;
for row in rows {
let (_, mut member) = row?;
let symbol = &member.result.symbol;
let mut tokens = Vec::new();
let named = name_hits(&symbol.name, &words, &stemmer)
.iter()
.any(|hit| *hit > 0);
let text_match = [symbol.signature.as_deref(), symbol.doc_comment.as_deref()]
.into_iter()
.any(|text| {
text_hits(
text.map(|s| head_bytes(s, TEXT_HEAD_BYTES)),
&words,
&mut tokens,
)
.into_iter()
.any(|hit| hit)
});
if named || text_match {
member.owner_context = Some(context.clone());
members.push(member);
}
}
}
let inherited = inherited_writes_among(
conn,
members.iter().map(|c| c.result.symbol.symbol_id.as_str()),
)?;
let mut changed = false;
for member in members {
let id = &member.result.symbol.symbol_id;
if inherited.contains(id) {
continue;
}
if let Some(existing) = candidates
.iter_mut()
.find(|c| c.result.symbol.symbol_id == *id)
{
existing.owner_context = member.owner_context;
} else {
candidates.push(member);
}
changed = true;
}
Ok(changed)
}
pub fn fts_search_symbols_scoped(
conn: &Connection,
query: &str,
kind_filter: Option<&str>,
path_filter: Option<&str>,
include_tests: bool,
limit: usize,
) -> Result<Vec<SymbolSearchResult>, QueryError> {
fts_search_symbols_explained(
conn,
query,
kind_filter,
path_filter,
include_tests,
limit,
false,
)
}
pub fn fts_search_symbols_explained(
conn: &Connection,
query: &str,
kind_filter: Option<&str>,
path_filter: Option<&str>,
include_tests: bool,
limit: usize,
explain: bool,
) -> Result<Vec<SymbolSearchResult>, QueryError> {
validate_result_limit(limit)?;
if limit == 0 {
return Ok(Vec::new());
}
let (and_q, _) = sanitize_fts5_query(query);
if and_q.is_empty() {
return Ok(Vec::new());
}
let normalized_path = path_filter.map(|p| {
p.replace('\\', "/")
.trim_start_matches("./")
.trim_matches('/')
.to_string()
});
let norm_kind = kind_filter.map(normalize_kind);
let escaped_path = normalized_path.as_deref().map(escape_like);
let searching_variables = norm_kind.as_deref() == Some("variable");
let name_search = |local_clause: &str| -> Result<Vec<SymbolSearchResult>, QueryError> {
let pattern = format!("%{}%", escape_like(query));
let mut sql = format!(
"SELECT symbol_id, file_id, path, language, name, kind, signature, doc_comment,
visibility, parent_symbol_id, start_line, start_column, end_line, end_column,
start_byte, end_byte, body_start_line, body_start_column, body_end_line,
body_end_column, body_start_byte, body_end_byte, body_hash, semantic_group,
is_test, test_container
FROM symbols s
WHERE (name = :query OR name LIKE :pattern ESCAPE '\\')
AND {KIND_FILTER}
AND (:path IS NULL OR replace(path, '\\', '/') = :path COLLATE NOCASE OR replace(path, '\\', '/') LIKE :path_like || '/%' ESCAPE '\\' OR replace(path, '\\', '/') LIKE '%/' || :path_like ESCAPE '\\')",
);
sql.push_str(&document_link_exclusion());
sql.push_str(local_clause);
if !include_tests {
sql.push_str(" AND is_test = 0 AND test_container = 0");
sql.push_str(&format!(" AND NOT {}", test_path_predicate("s")));
}
sql.push_str(
" ORDER BY (name = :query) DESC, (kind IN ('function', 'struct', 'class', 'trait', 'method', 'enum', 'interface', 'type')) DESC, length(name) ASC, path ASC LIMIT ",
);
sql.push_str(&limit.to_string());
let mut stmt = conn.prepare(&sql)?;
let path_val = normalized_path.as_deref();
let path_like = escaped_path.as_deref();
let kind_val = norm_kind.as_deref();
let rows = stmt
.query_map(
rusqlite::named_params! {
":query": query,
":pattern": pattern,
":kind": kind_val,
":path": path_val,
":path_like": path_like,
},
map_symbol,
)?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows
.into_iter()
.map(|s| SymbolSearchResult {
symbol: s,
score: 0.0,
snippet: None,
explain: None,
})
.collect())
};
if !has_table(conn, "symbols_fts") {
let local_clause = if searching_variables {
String::new()
} else {
format!(" AND NOT {}", local_variable_predicate("s"))
};
return name_search(&local_clause);
}
let mut candidates =
collect_search_candidates(conn, query, kind_filter, path_filter, include_tests, limit)?;
let started = std::time::Instant::now();
let idf = idf_weights(conn, &rerank_words(query));
let mut ranked = rerank_with(candidates.clone(), query, include_tests, Some(&idf));
let mut rerank_us = started.elapsed().as_micros();
let mut candidate_count = candidates.len();
if expand_ranked_owner_members(
conn,
&mut candidates,
&ranked,
query,
kind_filter,
path_filter,
include_tests,
)? {
candidate_count = candidates.len();
let started = std::time::Instant::now();
ranked = rerank_with(candidates, query, include_tests, Some(&idf));
rerank_us += started.elapsed().as_micros();
}
let unique_query_terms: HashSet<String> = rerank_words(query).into_iter().collect();
let min_multi_term_results = ((limit * 3).div_ceil(4)).max(15).min(limit);
let multi_term_results = ranked
.iter()
.filter(|(_, breakdown)| covered_query_term_count(&breakdown.terms) >= 2)
.count();
if unique_query_terms.len() > 1 && multi_term_results >= min_multi_term_results {
let mut strong_results_seen = 0;
ranked.retain(|(_, breakdown)| {
if covered_query_term_count(&breakdown.terms) >= 2 {
strong_results_seen += 1;
true
} else {
strong_results_seen < min_multi_term_results || has_distinctive_name_term(breakdown)
}
});
}
Ok(ranked
.into_iter()
.take(limit)
.map(|(mut result, mut breakdown)| {
if explain {
breakdown.candidates = candidate_count;
breakdown.rerank_us = rerank_us;
result.explain = Some(breakdown);
}
result
})
.collect())
}
const W_NAME_WHOLE: f64 = 100.0;
const W_NAME_ALL_WORDS: f64 = 60.0;
const W_KIND_DEFINITION: f64 = 4.0;
const W_KIND_MEMBER: f64 = 0.0;
const W_KIND_IMPORT: f64 = -50.0;
const W_NESTED: f64 = -2.0;
const W_PATH_ROLE: f64 = -10.0;
const W_DOCUMENTATION_ROW: f64 = -200.0;
const W_TEST_INTENT: f64 = 5.0;
const W_TERMS: f64 = 52.0;
const MAX_TERM_CREDIT: f64 = 3.0;
const TEXT_CREDIT: f64 = 1.0;
const FIELD_DIVERSITY_BONUS: f64 = 6.0;
const DISTINCTIVE_NAME_IDF_FACTOR: f64 = 1.1;
const OWNER_CREDIT: f64 = 3.0;
const OWNER_CONTEXT_CREDIT: f64 = 0.5;
const TEXT_HEAD_BYTES: usize = 400;
const DOC_HEAD_BYTES: usize = 640;
pub const DEFINITION_KINDS: &[&str] = &[
"function",
"method",
"class",
"struct",
"trait",
"interface",
"enum",
"type",
];
const MEMBER_KINDS: &[&str] = &["enum_member", "field", "property", "constant", "variable"];
const DEMOTED_PATH_SEGMENTS: &[&str] = &["scripts", "examples", "benchmarks", "fixtures", "vendor"];
const TEST_INTENT_WORDS: &[&str] = &["test", "tests", "spec", "specs"];
struct QueryWord {
word: String,
stem: String,
}
fn stemmed_query_words(query: &str, stemmer: &Stemmer) -> Vec<QueryWord> {
rerank_words(query)
.into_iter()
.map(|word| QueryWord {
stem: stemmer.stem(&word).into_owned(),
word,
})
.collect()
}
struct Hits {
name: Vec<u8>,
owner: Vec<bool>,
signature: Vec<bool>,
doc: Vec<bool>,
owner_context: Vec<bool>,
}
const DF_CAP: usize = 20_000;
fn idf_weights(conn: &Connection, words: &[String]) -> Vec<f64> {
let n = conn
.query_row("SELECT max(rowid) FROM symbols", [], |r| {
r.get::<_, Option<i64>>(0)
})
.ok()
.flatten()
.unwrap_or(0) as f64;
let mut count = conn
.prepare(&format!(
"SELECT count(*) FROM (SELECT rowid FROM symbols_fts WHERE symbols_fts MATCH ?1 LIMIT {DF_CAP})"
))
.ok();
words
.iter()
.map(|word| {
let df = count
.as_mut()
.and_then(|stmt| document_frequency(stmt, &format!("\"{word}\"")))
.unwrap_or(0);
(1.0 + n / (df as f64 + 1.0)).ln()
})
.collect()
}
fn document_frequency(stmt: &mut rusqlite::Statement<'_>, term: &str) -> Option<i64> {
stmt.query_row([term], |r| r.get(0)).ok()
}
fn term_credits(hits: &Hits, words: &[QueryWord]) -> Vec<(String, String, f64)> {
words
.iter()
.enumerate()
.map(|(i, w)| {
let (field, credit) = match hits.name[i] {
3 => ("name", 3.0),
2 => ("name", 2.0),
_ if hits.owner[i] => ("owner", OWNER_CREDIT),
_ if hits.signature[i] => ("signature", TEXT_CREDIT),
_ if hits.doc[i] => ("doc", TEXT_CREDIT),
1 => ("name", 1.0),
_ if hits.owner_context[i] => ("owner_context", OWNER_CONTEXT_CREDIT),
_ => ("none", 0.0),
};
(w.word.clone(), field.to_string(), credit)
})
.collect()
}
fn credited_field_bonus(terms: &[(String, String, f64)]) -> f64 {
let query_terms: HashSet<&str> = terms.iter().map(|(word, _, _)| word.as_str()).collect();
let covered_terms: HashSet<&str> = terms
.iter()
.filter(|(_, _, credit)| *credit >= TEXT_CREDIT)
.map(|(word, _, _)| word.as_str())
.collect();
let adequate_query_coverage =
!query_terms.is_empty() && covered_terms.len() * 2 >= query_terms.len();
let mut document_terms = HashSet::new();
let has_exact_name_term = terms
.iter()
.any(|(_, field, credit)| field == "name" && *credit == 3.0);
let fields = terms.iter().filter(|(_, _, credit)| *credit > 0.0).fold(
0u8,
|fields, (word, field, _)| {
if field == "doc" {
document_terms.insert(word.as_str());
}
fields
| match field.as_str() {
"name" => 1,
"owner" => 2,
"signature" => 4,
"doc" => 8,
"owner_context" => 16,
_ => 0,
}
},
);
if has_exact_name_term
&& fields & 4 != 0
&& fields & 8 != 0
&& document_terms.len() >= 2
&& adequate_query_coverage
{
FIELD_DIVERSITY_BONUS
} else {
0.0
}
}
fn covered_query_term_count(terms: &[(String, String, f64)]) -> usize {
terms
.iter()
.filter(|(_, _, credit)| *credit > 0.0)
.map(|(word, _, _)| word.as_str())
.collect::<HashSet<_>>()
.len()
}
fn has_distinctive_name_term(explain: &SearchExplain) -> bool {
let weights = &explain.word_weights;
let Some(mean_weight) = (!weights.is_empty())
.then(|| weights.iter().map(|(_, weight)| weight).sum::<f64>() / weights.len() as f64)
else {
return false;
};
explain
.terms
.iter()
.zip(weights)
.any(|((term, field, credit), (weighted_term, weight))| {
term == weighted_term
&& field == "name"
&& *credit >= 2.0
&& *weight >= mean_weight * DISTINCTIVE_NAME_IDF_FACTOR
})
}
fn term_score(terms: &[(String, String, f64)], weights: &[f64]) -> f64 {
let total: f64 = weights.iter().sum();
if total == 0.0 {
return 0.0;
}
let credited: f64 = terms
.iter()
.zip(weights)
.map(|((_, _, credit), weight)| credit * weight)
.sum();
W_TERMS * credited / (MAX_TERM_CREDIT * total)
}
fn rerank_words(query: &str) -> Vec<String> {
let words: Vec<String> = query_words(query)
.into_iter()
.flat_map(split_identifier)
.map(str::to_lowercase)
.collect();
let any_content = words.iter().any(|w| !is_stop_word(w));
words
.into_iter()
.filter(|w| !any_content || !is_stop_word(w))
.collect()
}
fn collapse(text: &str) -> String {
text.chars()
.filter(|c| c.is_alphanumeric())
.flat_map(char::to_lowercase)
.collect()
}
fn head_bytes(text: &str, bytes: usize) -> &str {
let mut end = bytes.min(text.len());
while !text.is_char_boundary(end) {
end -= 1;
}
&text[..end]
}
fn token_run_equals(tokens: &[String], word: &str) -> bool {
(0..tokens.len()).any(|start| {
let mut joined = String::new();
for token in &tokens[start..] {
joined.push_str(token);
if joined.len() >= word.len() {
return joined == word;
}
}
false
})
}
fn name_hits(name: &str, words: &[QueryWord], stemmer: &Stemmer) -> Vec<u8> {
let tokens: Vec<String> = split_identifier(name)
.into_iter()
.map(str::to_lowercase)
.collect();
let stems: Vec<String> = tokens
.iter()
.map(|t| stemmer.stem(t).into_owned())
.collect();
let collapsed = collapse(name);
words
.iter()
.map(|w| {
if token_run_equals(&tokens, &w.word) {
3
} else if stems.contains(&w.stem) {
2
} else if w.word.chars().count() >= 3 && collapsed.contains(&w.word) {
1
} else {
0
}
})
.collect()
}
fn lowercase_prefix_match(token: &str, prefix: &str, exact: bool) -> bool {
if token.is_ascii() && prefix.is_ascii() {
let Some(head) = token.as_bytes().get(..prefix.len()) else {
return false;
};
return head.eq_ignore_ascii_case(prefix.as_bytes())
&& (!exact || token.len() == prefix.len());
}
let mut lower = token.chars().flat_map(char::to_lowercase);
for expected in prefix.chars() {
if lower.next() != Some(expected) {
return false;
}
}
!exact || lower.next().is_none()
}
fn text_hits<'a>(
text: Option<&'a str>,
words: &[QueryWord],
tokens: &mut Vec<&'a str>,
) -> Vec<bool> {
tokens.clear();
text_tokens_into(text.unwrap_or(""), tokens);
words
.iter()
.map(|w| {
let stem_prefix = w.stem.chars().count() >= 3;
let exact_word = w.word.chars().count() < 3;
tokens.iter().any(|t| {
lowercase_prefix_match(t, &w.word, exact_word)
|| (stem_prefix && lowercase_prefix_match(t, &w.stem, false))
})
})
.collect()
}
fn kind_prior(kind: &str) -> f64 {
let kind = normalize_kind(kind);
match kind.as_str() {
"import" => W_KIND_IMPORT,
k if DEFINITION_KINDS.contains(&k) => W_KIND_DEFINITION,
k if MEMBER_KINDS.contains(&k) => W_KIND_MEMBER,
_ => 0.0,
}
}
fn path_role(path: &str, words: &[QueryWord], stemmer: &Stemmer) -> f64 {
let Some(segment) = path.split(['/', '\\']).find(|seg| {
DEMOTED_PATH_SEGMENTS
.iter()
.any(|d| d.eq_ignore_ascii_case(seg))
}) else {
return 0.0;
};
let segment = segment.to_lowercase();
let segment_stem = stemmer.stem(&segment);
let named = words.iter().any(|w| {
w.word == segment || w.word == segment_stem || w.stem == segment || w.stem == segment_stem
});
if named { 0.0 } else { W_PATH_ROLE }
}
fn bracket_longest_term(name: &str, terms: &[String]) -> String {
let lower = name.to_lowercase();
if lower.len() != name.len() {
return name.to_string();
}
let mut best: Option<(usize, usize)> = None;
for term in terms {
if let Some(start) = lower.find(term.as_str()) {
let end = start + term.len();
let longer = best.is_none_or(|(s, e)| end - start > e - s);
if longer && name.is_char_boundary(start) && name.is_char_boundary(end) {
best = Some((start, end));
}
}
}
match best {
Some((start, end)) => {
format!("{}[{}]{}", &name[..start], &name[start..end], &name[end..])
}
None => name.to_string(),
}
}
fn branch_snippet(candidate: &Candidate) -> Option<String> {
let name = &candidate.result.symbol.name;
if candidate.word_match {
candidate.result.snippet.clone()
} else if candidate.exact_name {
Some(name.clone())
} else {
Some(bracket_longest_term(name, &candidate.name_terms))
}
}
fn rerank_with(
candidates: Vec<Candidate>,
query: &str,
include_tests: bool,
idf: Option<&[f64]>,
) -> Vec<(SymbolSearchResult, SearchExplain)> {
let stemmer = Stemmer::create(Algorithm::English);
let words = stemmed_query_words(query, &stemmer);
let collapsed_query = collapse(query);
let test_intent = include_tests
&& words
.iter()
.any(|w| TEST_INTENT_WORDS.contains(&w.word.as_str()));
let mut tokens: Vec<&str> = Vec::new();
let hits: Vec<Hits> = candidates
.iter()
.map(|candidate| {
let symbol = &candidate.result.symbol;
let name = name_hits(&symbol.name, &words, &stemmer);
let named = name.iter().any(|strength| *strength > 0);
Hits {
owner: match candidate.owner.as_deref().map(collapse) {
Some(owner) if named => {
let owner_stem = stemmer.stem(&owner);
words
.iter()
.map(|w| owner == w.word || owner_stem == w.stem)
.collect()
}
_ => vec![false; words.len()],
},
name,
signature: text_hits(
symbol
.signature
.as_deref()
.map(|signature| head_bytes(signature, TEXT_HEAD_BYTES)),
&words,
&mut tokens,
),
doc: text_hits(
symbol
.doc_comment
.as_deref()
.map(|doc| head_bytes(doc, DOC_HEAD_BYTES)),
&words,
&mut tokens,
),
owner_context: text_hits(candidate.owner_context.as_deref(), &words, &mut tokens),
}
})
.collect();
let term_weights: Vec<f64> = match idf {
Some(weights) if weights.len() == words.len() => weights.to_vec(),
_ => vec![1.0; words.len()],
};
let word_weights: Vec<(String, f64)> = words
.iter()
.zip(&term_weights)
.map(|(w, weight)| (w.word.clone(), *weight))
.collect();
let mut scored: Vec<(SymbolSearchResult, SearchExplain)> = candidates
.into_iter()
.zip(hits)
.map(|(candidate, hits)| {
let symbol = &candidate.result.symbol;
let name_strength: u32 = hits.name.iter().map(|s| u32::from(*s)).sum();
let tier = if !collapsed_query.is_empty() && collapse(&symbol.name) == collapsed_query {
"whole"
} else if !hits.name.is_empty() && hits.name.iter().all(|s| *s >= 2) {
"all"
} else if hits.name.iter().any(|s| *s > 0) {
"partial"
} else {
"none"
};
let terms = term_credits(&hits, &words);
let field_diversity_bonus = if matches!(symbol.kind.as_str(), "function" | "method") {
credited_field_bonus(&terms)
} else {
0.0
};
let explain = SearchExplain {
bm25: candidate.bm25,
branches: [
(candidate.exact_name, "exact"),
(candidate.word_match, "word"),
(candidate.name_match, "name"),
(candidate.owner_match, "owner"),
(candidate.owner_context.is_some(), "owner_context"),
]
.into_iter()
.filter(|(hit, _)| *hit)
.map(|(_, branch)| branch.to_string())
.collect(),
name_tier: tier.to_string(),
name_strength,
term_score: term_score(&terms, &term_weights),
field_diversity_bonus,
name_bonus: match tier {
"whole"
if DEFINITION_KINDS.contains(&normalize_kind(&symbol.kind).as_str()) =>
{
W_NAME_WHOLE
}
"whole" | "all" => W_NAME_ALL_WORDS,
_ => 0.0,
},
kind_prior: kind_prior(&symbol.kind),
path_role: path_role(&symbol.path, &words, &stemmer),
documentation: if candidate.documentation {
W_DOCUMENTATION_ROW
} else {
0.0
},
test_intent: if test_intent && (symbol.is_test || symbol.test_container) {
W_TEST_INTENT
} else {
0.0
},
nested: if candidate.nested { W_NESTED } else { 0.0 },
terms,
word_weights: word_weights.clone(),
candidates: 0,
rerank_us: 0,
};
let score = explain.term_score
+ explain.field_diversity_bonus
+ explain.name_bonus
+ explain.kind_prior
+ explain.path_role
+ explain.documentation
+ explain.test_intent
+ explain.nested;
let snippet = branch_snippet(&candidate);
let mut result = candidate.result;
result.score = score;
result.snippet = snippet;
(result, explain)
})
.collect();
scored.sort_by(|(a, ea), (b, eb)| {
b.score
.total_cmp(&a.score)
.then_with(|| eb.name_strength.cmp(&ea.name_strength))
.then_with(|| {
a.symbol
.name
.starts_with('_')
.cmp(&b.symbol.name.starts_with('_'))
})
.then_with(|| ea.bm25.is_none().cmp(&eb.bm25.is_none()))
.then_with(|| ea.bm25.unwrap_or(0.0).total_cmp(&eb.bm25.unwrap_or(0.0)))
.then_with(|| a.symbol.name.len().cmp(&b.symbol.name.len()))
.then_with(|| a.symbol.path.cmp(&b.symbol.path))
.then_with(|| a.symbol.name.cmp(&b.symbol.name))
});
scored
}
const LOW_SIGNAL_KINDS_SQL: &str =
"'import','variable','parameter','field','property','module','namespace'";
pub fn find_related_tests(
conn: &Connection,
target_symbol: &Symbol,
limit: usize,
) -> Result<Vec<Symbol>, QueryError> {
if limit == 0 || DOCUMENTATION_LANGUAGES.contains(&target_symbol.language.as_str()) {
return Ok(Vec::new());
}
const COLUMNS: &str = "s.symbol_id, s.file_id, s.path, s.language, s.name, s.kind, s.signature, s.doc_comment,
s.visibility, s.parent_symbol_id, s.start_line, s.start_column, s.end_line, s.end_column,
s.start_byte, s.end_byte, s.body_start_line, s.body_start_column, s.body_end_line,
s.body_end_column, s.body_start_byte, s.body_end_byte, s.body_hash, s.semantic_group,
s.is_test, s.test_container";
const TEST_ORDER: &str = "s.is_test DESC, s.path, s.start_line, s.symbol_id";
let not_setup = if has_column(conn, "symbols", "test_lifecycle") {
"COALESCE(s.test_lifecycle, 0) = 0"
} else {
"1 = 1"
};
let is_test = format!(
"((s.is_test = 1 OR s.test_container = 1 OR ({} AND s.kind IN ('function', 'method'))) AND {not_setup})",
test_path_predicate("s")
);
let not_documentation = not_documentation(conn, "s");
let mut tests = Vec::new();
let mut seen_ids = std::collections::HashSet::new();
let callers_sql = format!(
"SELECT {COLUMNS}
FROM symbols s
JOIN relationships r ON r.from_symbol_id = s.symbol_id
WHERE r.to_symbol_id = ?1 AND {is_test} AND {not_documentation}
ORDER BY {TEST_ORDER}
LIMIT ?2"
);
if let Ok(mut stmt) = conn.prepare(&callers_sql)
&& let Ok(rows) = stmt.query_map(params![target_symbol.symbol_id, limit as i64], map_symbol)
{
for row in rows.flatten() {
if seen_ids.insert(row.symbol_id.clone()) {
tests.push(row);
if tests.len() >= limit {
return Ok(tests);
}
}
}
}
let remaining = limit - tests.len();
if remaining > 0 && has_pending_namespace_column(conn) {
let pending_sql = format!(
"SELECT DISTINCT {COLUMNS}
FROM pending_relationships p
JOIN symbols s ON p.from_symbol_id = s.symbol_id
JOIN symbols s_from ON s_from.symbol_id = s.symbol_id
JOIN symbols s_target ON s_target.symbol_id = ?1
LEFT JOIN symbols s_target_parent ON s_target.parent_symbol_id = s_target_parent.symbol_id
WHERE p.target_terminal_name = s_target.name
AND {is_test}
AND {not_documentation}
AND {pred}
ORDER BY {TEST_ORDER}
LIMIT ?2",
pred = pending_target_predicate(conn, "s_target", "s_target_parent")
);
if let Ok(mut stmt) = conn.prepare(&pending_sql)
&& let Ok(rows) = stmt.query_map(
params![target_symbol.symbol_id, remaining as i64],
map_symbol,
)
{
for row in rows.flatten() {
if seen_ids.insert(row.symbol_id.clone()) {
tests.push(row);
if tests.len() >= limit {
return Ok(tests);
}
}
}
}
}
let mut test_by_id = conn.prepare(&format!(
"SELECT {COLUMNS} FROM symbols s WHERE s.symbol_id = ?1 AND {is_test} AND {not_documentation}"
))?;
for site in find_references_for_symbol(
conn,
&target_symbol.name,
"callers",
MAX_RESULT_LIMIT,
&target_symbol.symbol_id,
)
.unwrap_or_default()
{
if site.kind != "member_access (candidate)"
&& !seen_ids.contains(&site.from_symbol_id)
&& let Some(test) = test_by_id
.query_row(params![site.from_symbol_id], map_symbol)
.optional()?
{
seen_ids.insert(test.symbol_id.clone());
tests.push(test);
if tests.len() >= limit {
return Ok(tests);
}
}
}
if (target_symbol.kind == "constructor" || target_symbol.name == "__init__")
&& tests.len() < limit
&& let Some(class) = target_symbol
.parent_symbol_id
.as_deref()
.map(|id| get_symbol_by_id(conn, id))
.transpose()?
.flatten()
.filter(|class| class.kind == "class" || class.kind == "struct")
{
for test in find_related_tests(conn, &class, limit - tests.len())? {
if seen_ids.insert(test.symbol_id.clone()) {
tests.push(test);
}
}
return Ok(tests);
}
let name = &target_symbol.name;
let dunder = name.len() > 4 && name.starts_with("__") && name.ends_with("__");
let definitions: i64 = conn.query_row(
"SELECT COUNT(*) FROM symbols WHERE name = ?1 AND kind NOT IN ('import', 'export')",
params![name],
|row| row.get(0),
)?;
if dunder || definitions > 1 {
return Ok(tests);
}
let remaining = limit - tests.len();
let name_sql = format!(
"SELECT {COLUMNS}
FROM symbols s
WHERE {is_test}
AND {not_documentation}
AND (s.name LIKE ?1 ESCAPE '\\' OR s.signature LIKE ?1 ESCAPE '\\')
ORDER BY (s.name LIKE ?1 ESCAPE '\\') DESC, {TEST_ORDER}
LIMIT ?2"
);
if let Ok(mut stmt) = conn.prepare(&name_sql)
&& let Ok(rows) = stmt.query_map(
params![format!("%{}%", escape_like(name)), (remaining * 2) as i64],
map_symbol,
)
{
for row in rows.flatten() {
if seen_ids.insert(row.symbol_id.clone()) {
tests.push(row);
if tests.len() >= limit {
return Ok(tests);
}
}
}
}
let remaining = limit - tests.len();
let fts_exists: bool = conn
.query_row(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='symbols_fts'",
[],
|_| Ok(true),
)
.unwrap_or(false);
if remaining > 0 && fts_exists {
let fts_sql = format!(
"SELECT {COLUMNS}
FROM symbols_fts
CROSS JOIN symbols s ON s.rowid = symbols_fts.rowid
WHERE symbols_fts MATCH ?1 AND {is_test} AND {not_documentation}
ORDER BY {TEST_ORDER}
LIMIT ?2"
);
let and_q = name_prefix_query(&target_symbol.name);
if !and_q.is_empty()
&& let Ok(mut stmt) = conn.prepare(&fts_sql)
&& let Ok(rows) = stmt.query_map(params![and_q, (remaining * 2) as i64], map_symbol)
{
for row in rows.flatten() {
if seen_ids.insert(row.symbol_id.clone()) {
tests.push(row);
if tests.len() >= limit {
break;
}
}
}
}
}
Ok(tests)
}
pub fn qualify_members<'a>(
conn: &Connection,
symbols: impl IntoIterator<Item = &'a mut Symbol>,
) -> Result<(), QueryError> {
let mut stmt = conn.prepare(&format!(
"SELECT name FROM symbols
WHERE symbol_id = ?1
AND kind IN ('class', 'struct', 'interface', 'trait', 'enum', 'record', 'object', 'protocol', 'union')
AND language NOT IN ({})",
documentation_language_list()
))?;
for symbol in symbols {
let Some(parent) = symbol.parent_symbol_id.as_deref() else {
continue;
};
if let Some(owner) = stmt
.query_row(params![parent], |row| row.get::<_, String>(0))
.optional()?
&& !symbol.name.starts_with(&format!("{owner}."))
{
symbol.name = format!("{owner}.{}", symbol.name);
}
}
Ok(())
}
pub fn get_symbol_by_name(
conn: &Connection,
name: &str,
path_filter: Option<&str>,
) -> Result<Option<Symbol>, QueryError> {
get_symbol_by_name_internal(conn, name, path_filter, false)
}
pub fn get_symbol_by_name_exact(
conn: &Connection,
name: &str,
exact_path: &str,
) -> Result<Option<Symbol>, QueryError> {
get_symbol_by_name_internal(conn, name, Some(exact_path), true)
}
pub fn get_symbol_by_id(conn: &Connection, symbol_id: &str) -> Result<Option<Symbol>, QueryError> {
conn.query_row(
"SELECT symbol_id, file_id, path, language, name, kind, signature, doc_comment,
visibility, parent_symbol_id, start_line, start_column, end_line, end_column,
start_byte, end_byte, body_start_line, body_start_column, body_end_line,
body_end_column, body_start_byte, body_end_byte, body_hash, semantic_group,
is_test, test_container
FROM symbols WHERE symbol_id = ?1",
[symbol_id],
map_symbol,
)
.optional()
.map_err(QueryError::from)
}
fn split_qualified_name(name: &str) -> (Vec<&str>, &str) {
let separator = if name.contains("::") {
"::"
} else if name.contains('.') {
"."
} else {
return (Vec::new(), name);
};
let mut segments: Vec<&str> = name.split(separator).collect();
let terminal = segments.pop().unwrap_or(name);
(segments, terminal)
}
fn ancestor_names(conn: &Connection, symbol_id: &str) -> Result<Vec<String>, QueryError> {
let mut stmt = conn.prepare(
"SELECT p.symbol_id, p.name FROM symbols s
JOIN symbols p ON s.parent_symbol_id = p.symbol_id
WHERE s.symbol_id = ?1",
)?;
let mut names = Vec::new();
let mut current = symbol_id.to_string();
for _ in 0..32 {
let mut rows = stmt.query(params![current])?;
let Some(row) = rows.next()? else { break };
let parent_id: String = row.get(0)?;
let parent_name: String = row.get(1)?;
names.push(parent_name);
current = parent_id;
}
Ok(names)
}
const ANCESTOR_WALK_ROW_CAP: i64 = 2000;
fn chain_contains(chain: &[String], wanted: &[&str]) -> bool {
let mut remaining = chain.iter();
wanted
.iter()
.all(|segment| remaining.any(|name| name == segment))
}
fn get_symbol_by_name_internal(
conn: &Connection,
name: &str,
path_filter: Option<&str>,
exact_path: bool,
) -> Result<Option<Symbol>, QueryError> {
let (ancestor_segments, terminal_name) = split_qualified_name(name);
let parent_name = ancestor_segments.last().copied();
let sql = "SELECT s.symbol_id, s.file_id, s.path, s.language, s.name, s.kind, s.signature, s.doc_comment,
s.visibility, s.parent_symbol_id, s.start_line, s.start_column, s.end_line, s.end_column,
s.start_byte, s.end_byte, s.body_start_line, s.body_start_column, s.body_end_line,
s.body_end_column, s.body_start_byte, s.body_end_byte, s.body_hash, s.semantic_group,
s.is_test, s.test_container
FROM symbols s
LEFT JOIN symbols p ON s.parent_symbol_id = p.symbol_id
WHERE (s.name = :name OR (s.name = :term AND (:parent IS NULL OR p.name = :parent)))
AND (:path IS NULL OR s.path = :path COLLATE NOCASE OR s.path = :path_bs COLLATE NOCASE OR (:exact = 0 AND (s.path LIKE '%/' || :path_like ESCAPE '\\' OR s.path LIKE '%\\\\' || :path_like_bs ESCAPE '\\' OR s.path LIKE :path_like || '/%' ESCAPE '\\' OR s.path LIKE :path_like_bs || '\\\\%' ESCAPE '\\')))
ORDER BY (s.kind != 'import') DESC,
(s.kind IN ('function', 'struct', 'class', 'trait', 'method', 'enum', 'interface', 'type')) DESC,
(s.name = :name) DESC,
(:path IS NOT NULL AND (s.path = :path COLLATE NOCASE OR s.path = :path_bs COLLATE NOCASE)) DESC,
s.is_test ASC
LIMIT :limit";
let row_cap: i64 = if ancestor_segments.len() > 1 {
ANCESTOR_WALK_ROW_CAP
} else {
25
};
let mut stmt = conn.prepare(sql)?;
let normalized_path = path_filter.map(|p| p.replace('\\', "/").trim_matches('/').to_string());
let backslash_path = normalized_path.as_deref().map(|p| p.replace('/', "\\"));
let path_like = normalized_path.as_deref().map(escape_like);
let path_like_bs = backslash_path.as_deref().map(escape_like);
let mut rows = stmt.query(rusqlite::named_params! {
":name": name,
":term": terminal_name,
":parent": parent_name,
":path": normalized_path.as_deref(),
":path_bs": backslash_path.as_deref(),
":path_like": path_like.as_deref(),
":path_like_bs": path_like_bs.as_deref(),
":exact": if exact_path { 1 } else { 0 },
":limit": row_cap,
})?;
let mut matches: Vec<Symbol> = Vec::new();
while let Some(row) = rows.next()? {
matches.push(map_symbol(row)?);
}
drop(rows);
if ancestor_segments.len() > 1 {
let required: Vec<&str> = ancestor_segments.iter().rev().copied().collect();
let mut kept = Vec::with_capacity(matches.len());
for symbol in matches {
if symbol.name == name
|| chain_contains(&ancestor_names(conn, &symbol.symbol_id)?, &required)
{
kept.push(symbol);
}
}
matches = kept;
}
if matches.is_empty() {
return Ok(None);
}
if matches.len() == 1 {
return Ok(Some(matches.remove(0)));
}
let candidates: Vec<Symbol> = if matches.iter().any(|s| s.kind != "import") {
matches.into_iter().filter(|s| s.kind != "import").collect()
} else {
matches
};
if candidates.len() == 1 {
return Ok(Some(candidates.into_iter().next().unwrap()));
}
let exact_name_matches: Vec<_> = candidates
.iter()
.filter(|s| s.name == name)
.cloned()
.collect();
if exact_name_matches.len() == 1 {
return Ok(Some(exact_name_matches.into_iter().next().unwrap()));
}
let definition_candidates = if exact_name_matches.is_empty() {
&candidates
} else {
&exact_name_matches
};
let def_matches: Vec<_> = definition_candidates
.iter()
.filter(|s| {
matches!(
s.kind.as_str(),
"function"
| "struct"
| "class"
| "trait"
| "method"
| "enum"
| "interface"
| "type"
)
})
.cloned()
.collect();
if def_matches.len() == 1 {
return Ok(Some(def_matches.into_iter().next().unwrap()));
}
let active_pool = if !def_matches.is_empty() {
def_matches
} else if !exact_name_matches.is_empty() {
exact_name_matches
} else {
candidates
};
if let Some(ref p) = normalized_path {
let exact_path_matches: Vec<_> = active_pool
.iter()
.filter(|s| s.path == *p)
.cloned()
.collect();
if exact_path_matches.len() == 1 {
return Ok(Some(exact_path_matches.into_iter().next().unwrap()));
}
}
if active_pool.len() == 1 {
return Ok(Some(active_pool.into_iter().next().unwrap()));
}
let mut outside_tests = active_pool.iter().filter(|s| !is_test_path(&s.path));
if let (Some(only), None) = (outside_tests.next(), outside_tests.next()) {
return Ok(Some(only.clone()));
}
let mut outer = outer_definitions(conn, &active_pool)?;
if outer.len() == 1 {
return Ok(Some(outer.remove(0)));
}
let mut candidate_list = String::new();
for s in &active_pool {
candidate_list.push_str(&format!(
"- {} `{}` in {}:{} (id={})\n",
crate::formatters::display_kind(s),
s.name,
s.path,
s.start_line,
s.symbol_id
));
}
Err(QueryError::AmbiguousSymbol(
name.to_string(),
active_pool.len(),
candidate_list,
))
}
fn outer_definitions(conn: &Connection, pool: &[Symbol]) -> Result<Vec<Symbol>, QueryError> {
let definitions: Vec<&Symbol> = pool
.iter()
.filter(|s| {
s.kind != "export"
|| !pool.iter().any(|other| {
other.kind != "export"
&& other.path == s.path
&& other.start_line == s.start_line
})
})
.collect();
let mut parent_kind = conn.prepare("SELECT kind FROM symbols WHERE symbol_id = ?1")?;
let mut outer = Vec::new();
for symbol in &definitions {
let kind = match &symbol.parent_symbol_id {
Some(parent) => parent_kind
.query_row([parent], |row| row.get::<_, String>(0))
.optional()?,
None => None,
};
if !matches!(kind.as_deref(), Some("function" | "method" | "constructor")) {
outer.push((*symbol).clone());
}
}
Ok(if outer.is_empty() {
definitions.into_iter().cloned().collect()
} else {
outer
})
}
pub fn workspace_name(conn: &Connection) -> String {
conn.query_row(
"SELECT value FROM artifact_metadata WHERE key = 'root_path'",
[],
|row| row.get::<_, String>(0),
)
.ok()
.map(|root| root.replace('\\', "/"))
.and_then(|root| {
root.trim_end_matches('/')
.rsplit('/')
.next()
.filter(|name| !name.is_empty())
.map(str::to_string)
})
.unwrap_or_else(|| "this workspace".to_string())
}
fn edit_distance(left: &str, right: &str) -> usize {
let left: Vec<char> = left.chars().collect();
let right: Vec<char> = right.chars().collect();
let mut previous: Vec<usize> = (0..=right.len()).collect();
let mut current = vec![0usize; right.len() + 1];
for (i, a) in left.iter().enumerate() {
current[0] = i + 1;
for (j, b) in right.iter().enumerate() {
let substitution = previous[j] + usize::from(a != b);
current[j + 1] = substitution.min(previous[j + 1] + 1).min(current[j] + 1);
}
std::mem::swap(&mut previous, &mut current);
}
previous[right.len()]
}
fn near_names_by_trigram(
conn: &Connection,
name: &str,
path_filter: Option<&str>,
) -> Result<Vec<Symbol>, QueryError> {
if !has_table(conn, "symbol_names_tri") {
return Ok(Vec::new());
}
let lower = name.to_lowercase();
let chars: Vec<char> = lower.chars().collect();
let chunks: Vec<String> = chars
.windows(3)
.map(|window| window.iter().collect::<String>())
.filter(|chunk| chunk.chars().all(|c| c.is_alphanumeric() || c == '_'))
.collect();
if chunks.is_empty() {
return Ok(Vec::new());
}
let match_clause = chunks
.iter()
.map(|chunk| format!("\"{chunk}\""))
.collect::<Vec<_>>()
.join(" OR ");
let normalized_path = path_filter.map(|p| {
p.replace('\\', "/")
.trim_start_matches("./")
.trim_matches('/')
.to_string()
});
let escaped_path = normalized_path.as_deref().map(escape_like);
let kind_val: Option<&str> = None;
let columns = candidate_columns(conn);
let filters = candidate_filters(false, false);
let sql = format!(
"SELECT {columns} FROM symbol_names_tri
CROSS JOIN symbols s ON s.rowid = symbol_names_tri.rowid
WHERE symbol_names_tri MATCH :match {filters}
ORDER BY bm25(symbol_names_tri) ASC, length(s.name) ASC, s.path ASC LIMIT 20"
);
let rows = conn
.prepare(&sql)?
.query_map(
rusqlite::named_params! {
":match": match_clause,
":kind": kind_val,
":path": normalized_path.as_deref(),
":path_like": escaped_path.as_deref(),
},
map_symbol,
)?
.collect::<Result<Vec<_>, _>>()?;
let budget = (chars.len() / 4).max(2);
let mut scored: Vec<(usize, Symbol)> = rows
.into_iter()
.map(|symbol| (edit_distance(&lower, &symbol.name.to_lowercase()), symbol))
.filter(|(distance, _)| *distance <= budget)
.collect();
scored.sort_by_key(|(distance, symbol)| (*distance, symbol.name.chars().count()));
Ok(scored
.into_iter()
.map(|(_, symbol)| symbol)
.take(3)
.collect())
}
pub fn suggest_symbol_names(
conn: &Connection,
name: &str,
path_filter: Option<&str>,
) -> Vec<Symbol> {
let (ancestors, terminal) = split_qualified_name(name);
let mut found =
search_symbols_scoped(conn, terminal, None, path_filter, false, 3).unwrap_or_default();
if found.is_empty() && terminal.chars().count() >= 4 {
found = near_names_by_trigram(conn, terminal, path_filter).unwrap_or_default();
}
if let Some(parent) = ancestors.last().copied() {
let mut ranked: Vec<(bool, Symbol)> = found
.into_iter()
.map(|symbol| {
let shares_parent = ancestor_names(conn, &symbol.symbol_id)
.unwrap_or_default()
.first()
.is_some_and(|found_parent| found_parent == parent);
(!shares_parent, symbol)
})
.collect();
ranked.sort_by_key(|(demoted, _)| *demoted);
found = ranked.into_iter().map(|(_, symbol)| symbol).collect();
}
found.truncate(3);
found
}
pub fn suggest_file_paths(conn: &Connection, rel_path: &str) -> Vec<String> {
let wanted = rel_path.replace('\\', "/");
let wanted = wanted.trim_start_matches("./").trim_matches('/');
let basename = wanted.rsplit('/').next().unwrap_or(wanted).to_lowercase();
if basename.is_empty() {
return Vec::new();
}
let stem = basename.split('.').next().unwrap_or(&basename).to_string();
let segments: Vec<&str> = wanted.split('/').collect();
let tail = if segments.len() >= 2 {
segments[segments.len() - 2..].join("/").to_lowercase()
} else {
basename.clone()
};
let path_expr = "replace(files.path, '\\', '/')";
let file_name = format!(
"lower(replace({path_expr}, rtrim({path_expr}, replace({path_expr}, '/', '')), ''))"
);
let rules = [
(format!("{file_name} = :value"), basename.clone()),
(
format!("{file_name} LIKE :value ESCAPE '\\'"),
format!("{}%", escape_like(&stem)),
),
(
format!("lower({path_expr}) LIKE :value ESCAPE '\\'"),
format!("%{}", escape_like(&tail)),
),
];
for (predicate, value) in rules {
let sql = format!(
"SELECT {path_expr} FROM files WHERE {predicate}
ORDER BY length(files.path) ASC, files.path ASC LIMIT 3"
);
let found: Vec<String> = conn
.prepare(&sql)
.and_then(|mut stmt| {
stmt.query_map(rusqlite::named_params! { ":value": value }, |row| {
row.get::<_, String>(0)
})?
.collect()
})
.unwrap_or_default();
if !found.is_empty() {
return found;
}
}
let max_edits = (basename.chars().count() / 4).clamp(1, 2);
let mut near: Vec<(usize, String)> = Vec::new();
let _ = conn
.prepare(&format!("SELECT {path_expr} FROM files"))
.and_then(|mut stmt| {
for path in stmt.query_map([], |row| row.get::<_, String>(0))?.flatten() {
let name = path.rsplit('/').next().unwrap_or(&path).to_lowercase();
let edits = edit_distance(&name, &basename);
if edits <= max_edits {
near.push((edits, path));
}
}
Ok(())
});
near.sort();
near.into_iter().take(3).map(|(_, path)| path).collect()
}
pub fn symbol_not_found_parts(
conn: &Connection,
name: &str,
path_filter: Option<&str>,
) -> (String, String) {
let candidates = suggest_symbol_names(conn, name, path_filter);
let hint = if candidates.is_empty() {
"No similar name is indexed; check the workspace and spelling.".to_string()
} else {
let list = candidates
.iter()
.map(|s| format!(" - {} `{}` ({}:{})", s.kind, s.name, s.path, s.start_line))
.collect::<Vec<_>>()
.join("\n");
format!("Did you mean one of:\n{list}")
};
(workspace_name(conn), hint)
}
pub fn file_not_found_parts(conn: &Connection, rel_path: &str) -> (String, String) {
let candidates = suggest_file_paths(conn, rel_path);
let hint = if candidates.is_empty() {
"No similar path is indexed; check the workspace and spelling.".to_string()
} else {
let list = candidates
.iter()
.map(|path| format!(" - {path}"))
.collect::<Vec<_>>()
.join("\n");
format!("Did you mean one of:\n{list}")
};
(workspace_name(conn), hint)
}
pub fn find_references(
conn: &Connection,
symbol_name: &str,
direction: &str,
limit: usize,
) -> Result<Vec<ReferenceSite>, QueryError> {
find_references_ext(conn, symbol_name, direction, limit, false)
}
pub fn find_references_ext(
conn: &Connection,
symbol_name: &str,
direction: &str,
limit: usize,
include_external: bool,
) -> Result<Vec<ReferenceSite>, QueryError> {
find_references_scoped(conn, symbol_name, direction, limit, include_external, None)
}
pub fn find_references_scoped(
conn: &Connection,
symbol_name: &str,
direction: &str,
limit: usize,
include_external: bool,
path_filter: Option<&str>,
) -> Result<Vec<ReferenceSite>, QueryError> {
validate_result_limit(limit)?;
if direction != "callers" && direction != "callees" {
return Err(QueryError::InvalidDirection(direction.to_string()));
}
match get_symbol_by_name(conn, symbol_name, path_filter)? {
Some(target) => find_references_internal(
conn,
&target.name,
direction,
limit,
Some(&target.symbol_id),
include_external,
),
None => {
let (workspace, hint) = symbol_not_found_parts(conn, symbol_name, path_filter);
Err(QueryError::SymbolNotFound {
name: symbol_name.to_string(),
workspace,
hint,
})
}
}
}
pub fn find_references_for_symbol(
conn: &Connection,
symbol_name: &str,
direction: &str,
limit: usize,
symbol_id: &str,
) -> Result<Vec<ReferenceSite>, QueryError> {
find_references_for_symbol_ext(conn, symbol_name, direction, limit, symbol_id, false)
}
pub fn find_references_for_symbol_ext(
conn: &Connection,
symbol_name: &str,
direction: &str,
limit: usize,
symbol_id: &str,
include_external: bool,
) -> Result<Vec<ReferenceSite>, QueryError> {
find_references_internal(
conn,
symbol_name,
direction,
limit,
Some(symbol_id),
include_external,
)
}
fn call_site_proximity(candidate_path: &str) -> String {
path_proximity(candidate_path, "p.path")
}
fn path_proximity(candidate_path: &str, site_path: &str) -> String {
let normalized = format!("replace({candidate_path}, '\\', '/')");
let call_site = format!("replace({site_path}, '\\', '/')");
format!(
"CASE WHEN {normalized} = {call_site} THEN 2
WHEN rtrim({normalized}, replace({normalized}, '/', '')) = rtrim({call_site}, replace({call_site}, '/', '')) THEN 1
ELSE 0 END"
)
}
fn is_relative_import_path(value: &str) -> String {
format!("({value} LIKE '.%' AND instr({value}, '/') > 0)")
}
fn relative_import_matches(value: &str, target: &str) -> String {
let stem = format!(
"CASE WHEN {value} GLOB '*.[jt]s' THEN substr({value}, 1, length({value}) - 3)
WHEN {value} GLOB '*.[mc][jt]s' OR {value} GLOB '*.[jt]sx'
THEN substr({value}, 1, length({value}) - 4)
ELSE {value} END"
);
let caller = "replace(p.path, '\\', '/')";
let parent_dir = "rtrim(substr(hop.dir, 1, length(hop.dir) - 1), replace(substr(hop.dir, 1, length(hop.dir) - 1), '/', ''))";
let files = ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts", "d.ts"]
.iter()
.flat_map(|ext| {
[
format!("hop.dir || hop.rest || '.{ext}'"),
format!("hop.dir || hop.rest || '/index.{ext}'"),
]
})
.collect::<Vec<_>>()
.join(", ");
format!(
"EXISTS (
WITH RECURSIVE hop(dir, rest) AS (
SELECT rtrim({caller}, replace({caller}, '/', '')),
CASE WHEN ({stem}) LIKE './%' THEN substr(({stem}), 3) ELSE ({stem}) END
UNION ALL
SELECT {parent_dir}, substr(hop.rest, 4) FROM hop WHERE hop.rest LIKE '../%'
)
SELECT 1 FROM hop
WHERE hop.rest NOT LIKE '../%' AND hop.rest != ''
AND replace({target}.path, '\\', '/') IN ({files})
)"
)
}
const JS_FAMILY_LANGUAGES: &str = "'javascript', 'typescript', 'tsx', 'jsx', 'vue'";
fn identifier_language_matches(identifier: &str, target: &str) -> String {
format!(
"({target}.language = {identifier}.language
OR ({identifier}.language IN ({JS_FAMILY_LANGUAGES})
AND {target}.language IN ({JS_FAMILY_LANGUAGES}))
OR ({identifier}.language IN ('razor', 'csharp')
AND {target}.language IN ('razor', 'csharp'))
OR ({identifier}.language = 'qml' AND {target}.language = 'cpp'))"
)
}
const NESTED_TYPE_KINDS: &str =
"'class', 'struct', 'interface', 'enum', 'record', 'trait', 'protocol'";
const IMPLICIT_RECEIVER_LANGUAGES: &[&str] = &[
"java", "csharp", "kotlin", "swift", "cpp", "scala", "dart", "ruby", "vbnet",
];
fn receiver_builder_calls(conn: &Connection) -> String {
let resolved_calls = if has_column(conn, "relationships", "from_symbol_id")
&& has_column(conn, "relationships", "to_symbol_id")
&& has_column(conn, "relationships", "kind")
&& has_column(conn, "relationships", "path")
&& has_column(conn, "relationships", "start_line")
{
"UNION
SELECT resolved_builder.name, assign_call.path, resolved_builder.symbol_id
FROM symbols assigned
JOIN relationships assign_call
ON assign_call.from_symbol_id = p.from_symbol_id
AND assign_call.start_line = assigned.start_line
AND +assign_call.kind = 'calls'
JOIN symbols resolved_builder ON resolved_builder.symbol_id = assign_call.to_symbol_id
WHERE assigned.parent_symbol_id = p.from_symbol_id
AND +assigned.name = p.target_receiver
AND +assigned.kind = 'variable'
AND assigned.start_line <= p.start_line"
} else {
""
};
let fixture_calls = if has_column(conn, "symbols", "test_lifecycle") {
"UNION
SELECT fixture_call.target_terminal_name, fixture.path, NULL
FROM symbols fixture
JOIN pending_relationships fixture_call ON fixture_call.from_symbol_id = fixture.symbol_id
WHERE fixture.name = p.target_receiver
AND fixture.kind IN ('function', 'method')
AND +fixture.test_lifecycle = 1
AND EXISTS (
SELECT 1 FROM symbols parameter
WHERE parameter.parent_symbol_id = p.from_symbol_id
AND +parameter.name = p.target_receiver
AND (parameter.kind = 'parameter'
OR json_valid(parameter.metadata_json)
AND json_extract(parameter.metadata_json, '$.role') = 'parameter')
)
AND (fixture.path = p.path
OR fixture.path LIKE '%conftest.py'
AND substr(p.path, 1, length(fixture.path) - 11)
= substr(fixture.path, 1, length(fixture.path) - 11))"
} else {
""
};
format!(
"SELECT assign_call.target_terminal_name, assign_call.path, NULL
FROM symbols assigned
JOIN pending_relationships assign_call
ON assign_call.from_symbol_id = p.from_symbol_id
AND assign_call.start_line = assigned.start_line
AND +assign_call.kind = 'calls'
WHERE assigned.parent_symbol_id = p.from_symbol_id
AND +assigned.name = p.target_receiver
AND +assigned.kind = 'variable'
AND assigned.start_line <= p.start_line
{resolved_calls}
{fixture_calls}"
)
}
fn builder_return_type_sql(builder: &str) -> String {
format!(
"CASE WHEN json_valid({builder}.metadata_json)
THEN COALESCE(NULLIF(json_extract({builder}.metadata_json, '$.returnType'), ''),
json_extract({builder}.metadata_json, '$.inferredReturnType')) END"
)
}
fn receiver_definition_scope(target: &str, origin: &str) -> String {
let source = "json_extract(binding.metadata_json, '$.source')";
let relative = is_relative_import_path(source);
let relative_match = relative_import_matches(source, target).replace("p.path", origin);
let caller = format!("replace({origin}, '\\', '/')");
let parent_dir = "rtrim(substr(hop.dir, 1, length(hop.dir) - 1), replace(substr(hop.dir, 1, length(hop.dir) - 1), '/', ''))";
format!(
"({target}.path = {origin} OR NOT EXISTS (
SELECT 1 FROM symbols local
WHERE local.name = {target}.name AND local.path = {origin}
AND local.kind = {target}.kind
)) AND NOT EXISTS (
SELECT 1 FROM symbols binding
WHERE binding.name = {target}.name AND binding.path = {origin}
AND binding.kind = 'import' AND json_valid(binding.metadata_json)
AND NOT ({target}.path = {origin} AND binding.start_line < {target}.start_line)
AND COALESCE({source}, '') != ''
AND NOT CASE WHEN {relative} THEN {relative_match}
WHEN {source} LIKE '.%' THEN EXISTS (
WITH RECURSIVE hop(dir, rest) AS (
SELECT rtrim({caller}, replace({caller}, '/', '')), substr({source}, 2)
UNION ALL
SELECT {parent_dir}, substr(hop.rest, 2) FROM hop WHERE hop.rest LIKE '.%'
)
SELECT 1 FROM hop WHERE hop.rest NOT LIKE '.%'
AND (replace({target}.path, '\\', '/') = hop.dir || replace(hop.rest, '.', '/') || '.py'
OR replace({target}.path, '\\', '/') LIKE
rtrim(hop.dir || replace(hop.rest, '.', '/'), '/') || '/%')
)
ELSE instr('/' || replace({target}.path, '\\', '/'), '/' || replace({source}, '.', '/') || '.') > 0
OR instr('/' || replace({target}.path, '\\', '/'), '/' || replace({source}, '.', '/') || '/') > 0
END
)"
)
}
fn member_receiver_match(conn: &Connection) -> String {
let callable_member = "p.kind = 'member_access'
AND CASE WHEN json_valid(p.metadata_json)
THEN json_extract(p.metadata_json, '$.role') END IS NOT 'signal_handler'
AND EXISTS (
SELECT 1 FROM symbols selected
JOIN symbols owner ON owner.symbol_id = selected.parent_symbol_id
WHERE selected.symbol_id = ?3
AND selected.kind IN ('method', 'function', 'constructor')
AND owner.kind IN ('class', 'struct', 'interface', 'trait', 'enum', 'protocol')
)";
let unproven_member = "p.kind = 'member_access' AND NOT p.has_metadata";
let documentation_member = format!(
"{unproven_member} AND p.language IN ({})",
documentation_language_list()
);
if !has_pending_namespace_column(conn) || !has_column(conn, "symbols", "metadata_json") {
return format!(
"CASE WHEN {documentation_member} THEN -1 WHEN {callable_member} THEN 0
WHEN {unproven_member} THEN -1 ELSE 1 END"
);
}
let matched = pending_target_predicate(conn, "candidate", "candidate_parent");
let builders = receiver_builder_calls(conn);
let builder_return_type = builder_return_type_sql("builder");
let class_scope = receiver_definition_scope("receiver_class", "builder_call.path");
let builder_scope = receiver_definition_scope("builder", "builder_call.path");
let returned_scope = receiver_definition_scope("returned_class", "builder.path");
format!(
"CASE WHEN {documentation_member} THEN -1
WHEN NOT ({callable_member}) THEN CASE WHEN {unproven_member} THEN -1 ELSE 1 END
WHEN {unproven_member} THEN 0
WHEN (SELECT COUNT(*) FROM symbols binding
WHERE binding.parent_symbol_id = p.from_symbol_id
AND +binding.name = p.target_receiver
AND +binding.kind IN ('variable', 'parameter')
AND binding.start_line <= p.start_line) > 1 THEN 0
WHEN EXISTS (
WITH builder_call(name, path, resolved_symbol_id) AS ({builders})
SELECT 1 FROM builder_call
WHERE (SELECT COUNT(*) FROM builder_call) > 1
AND builder_call.resolved_symbol_id IS NULL
AND NOT EXISTS (
SELECT 1 FROM symbols receiver_class
WHERE receiver_class.name = builder_call.name AND receiver_class.kind = 'class'
AND (receiver_class.symbol_id = builder_call.resolved_symbol_id
OR builder_call.resolved_symbol_id IS NULL AND {class_scope})
)
AND NOT EXISTS (
SELECT 1 FROM symbols builder
JOIN symbols returned_class ON returned_class.name =
{builder_return_type}
WHERE builder.name = builder_call.name AND builder.kind IN ('function', 'method')
AND (builder.symbol_id = builder_call.resolved_symbol_id
OR CASE WHEN builder_call.resolved_symbol_id IS NULL
AND EXISTS (
SELECT 1 FROM symbols returned_class_probe
WHERE returned_class_probe.name = {builder_return_type}
AND returned_class_probe.kind = 'class'
)
THEN {builder_scope} ELSE 0 END)
AND returned_class.kind = 'class'
AND {returned_scope}
)
) THEN 0
ELSE (
SELECT CASE WHEN COUNT(*) > 1 THEN 0
WHEN MAX(candidate.symbol_id = ?3) = 1 THEN 1
WHEN COUNT(*) = 1 THEN -1
WHEN EXISTS (
WITH builder_call(name, path, resolved_symbol_id) AS ({builders})
SELECT 1 FROM builder_call
JOIN symbols imported ON imported.name = builder_call.name
AND imported.path = builder_call.path AND imported.kind = 'import'
JOIN symbols selected ON selected.symbol_id = ?3
JOIN pending_relationships base ON base.from_symbol_id = selected.parent_symbol_id
AND base.kind = 'extends' AND base.target_terminal_name = builder_call.name
JOIN symbols base_import ON base_import.name = base.target_terminal_name
AND base_import.path = base.path AND base_import.kind = 'import'
WHERE json_valid(imported.metadata_json) AND json_valid(base_import.metadata_json)
AND json_extract(imported.metadata_json, '$.source')
= json_extract(base_import.metadata_json, '$.source')
) THEN -1
ELSE 0 END
FROM symbols candidate
JOIN symbols candidate_parent ON candidate_parent.symbol_id = candidate.parent_symbol_id
WHERE candidate.name = p.name
AND candidate.kind IN ('method', 'function', 'constructor')
AND candidate_parent.kind IN ('class', 'struct', 'interface', 'trait', 'enum', 'protocol')
AND {matched}
) END"
)
}
fn pending_target_predicate(conn: &Connection, target: &str, parent: &str) -> String {
let ns = "json_each(CASE WHEN json_valid(p.target_namespace_json) THEN p.target_namespace_json ELSE '[]' END)";
let target_path = format!("('/' || replace({target}.path, '\\', '/'))");
let like_value = "replace(replace(replace(value, '\\', '\\\\'), '%', '\\%'), '_', '\\_')";
let closer_rank = call_site_proximity("closer.path");
let target_rank = call_site_proximity(&format!("{target}.path"));
let dotted = "replace(module_path.value, '/', '.')";
let segment = format!("replace({dotted}, rtrim({dotted}, replace({dotted}, '.', '')), '')");
let relative = is_relative_import_path("import_source.value");
let relative_match = relative_import_matches("import_source.value", target);
let receiver_import_elsewhere = if has_column(conn, "symbols", "metadata_json") {
format!(
"AND NOT EXISTS (
SELECT 1 FROM symbols receiver_import
CROSS JOIN json_each(json_array(
json_extract(receiver_import.metadata_json, '$.source')
)) import_source
WHERE receiver_import.kind = 'import'
AND receiver_import.path = p.path
AND receiver_import.name = p.target_receiver
AND json_valid(receiver_import.metadata_json)
AND {relative}
AND NOT {relative_match}
)"
)
} else {
String::new()
};
let import_alias_receiver = if has_column(conn, "symbols", "metadata_json") {
format!("OR EXISTS (
SELECT 1 FROM symbols alias_import
WHERE alias_import.kind = 'import'
AND alias_import.path = p.path
AND json_valid(alias_import.metadata_json)
AND (json_extract(alias_import.metadata_json, '$.alias') = p.target_receiver
OR json_extract(alias_import.metadata_json, '$.local_name') = p.target_receiver)
AND COALESCE(json_extract(alias_import.metadata_json, '$.source'), '') NOT LIKE 'Qt%'
)
OR EXISTS (
SELECT 1 FROM symbols module_import
CROSS JOIN json_each(json_array(
json_extract(module_import.metadata_json, '$.source')
)) import_source
CROSS JOIN json_each(json_array(
json_extract(module_import.metadata_json, '$.importedName'),
import_source.value
)) module_path
WHERE module_import.kind = 'import'
AND module_import.path = p.path
AND module_import.name = p.target_receiver
AND json_valid(module_import.metadata_json)
AND CASE WHEN {relative} THEN {relative_match}
ELSE {segment} != ''
AND ({target_path} LIKE '%/' || {segment} || '/%'
OR {target_path} LIKE '%/' || {segment} || '.%')
END
)"
)
} else {
String::new()
};
let builder_calls = receiver_builder_calls(conn);
let builder_return_type = builder_return_type_sql("builder");
let receiver_type_scope = if has_column(conn, "symbols", "metadata_json") {
receiver_definition_scope(parent, "p.path")
} else {
"1 = 1".to_string()
};
let builder_scope = receiver_definition_scope("builder", "builder_call.path");
let returned_class_scope = receiver_definition_scope("returned_class", "builder.path");
let built_scope = receiver_definition_scope("built_class", "built_name.path");
let fixture_receiver = if has_column(conn, "symbols", "metadata_json") {
format!(
"OR CASE WHEN EXISTS (
SELECT 1 FROM symbols taken
WHERE taken.parent_symbol_id = p.from_symbol_id
AND taken.name = p.target_receiver
AND taken.kind IN ('parameter', 'variable')
) THEN EXISTS (
WITH RECURSIVE builder_call(name, path, resolved_symbol_id) AS ({builder_calls}),
built_name(name, path, resolved_symbol_id) AS (
SELECT name, path, resolved_symbol_id FROM builder_call
UNION
SELECT returned_class.name, builder.path, returned_class.symbol_id
FROM builder_call
JOIN symbols builder ON builder.name = builder_call.name
AND builder.kind IN ('function', 'method')
AND (builder.symbol_id = builder_call.resolved_symbol_id
OR CASE WHEN builder_call.resolved_symbol_id IS NULL
AND EXISTS (
SELECT 1 FROM symbols returned_class_probe
WHERE returned_class_probe.name = {builder_return_type}
AND returned_class_probe.kind = 'class'
)
THEN {builder_scope} ELSE 0 END)
JOIN symbols returned_class ON returned_class.name = {builder_return_type}
AND returned_class.kind = 'class' AND {returned_class_scope}
),
built(symbol_id, depth) AS (
SELECT built_class.symbol_id, 0
FROM built_name
CROSS JOIN symbols built_class ON built_class.name = built_name.name
WHERE built_class.kind = 'class' AND {built_scope}
AND (built_name.resolved_symbol_id IS NULL
OR built_class.symbol_id = built_name.resolved_symbol_id)
UNION
SELECT r.to_symbol_id, built.depth + 1
FROM relationships r JOIN built ON r.from_symbol_id = built.symbol_id
WHERE +r.kind = 'extends' AND built.depth < 8
UNION
SELECT c.symbol_id, built.depth + 1
FROM pending_relationships pe
JOIN built ON pe.from_symbol_id = built.symbol_id
JOIN symbols c ON c.name = pe.target_terminal_name
WHERE +pe.kind = 'extends' AND built.depth < 8
AND c.kind IN ('class', 'interface', 'struct', 'trait', 'protocol')
)
SELECT 1 FROM built WHERE built.symbol_id = {target}.parent_symbol_id
AND NOT EXISTS (
SELECT 1 FROM built closer
JOIN symbols nearer ON nearer.parent_symbol_id = closer.symbol_id
WHERE nearer.name = {target}.name
AND nearer.kind IN ('method', 'function', 'constructor', 'property')
AND closer.depth < built.depth
)
) ELSE 0 END"
)
} else {
String::new()
};
let rival_rank = path_proximity("rival.path", "pe.path");
let base_rank = path_proximity("c.path", "pe.path");
let class_chain = |own_class_rule: &str| {
format!(
"EXISTS (
WITH RECURSIVE base(symbol_id, depth) AS (
SELECT s_from.parent_symbol_id, 0
UNION
SELECT r.to_symbol_id, base.depth + 1
FROM relationships r JOIN base ON r.from_symbol_id = base.symbol_id
WHERE +r.kind = 'extends' AND base.depth < 8
UNION
SELECT c.symbol_id, base.depth + 1
FROM pending_relationships pe
JOIN base ON pe.from_symbol_id = base.symbol_id
JOIN symbols c ON c.name = pe.target_terminal_name
WHERE +pe.kind = 'extends' AND base.depth < 8
AND c.kind IN ('class', 'interface', 'struct', 'trait', 'protocol')
AND NOT EXISTS (
SELECT 1 FROM symbols rival
WHERE rival.name = c.name
AND rival.kind IN ('class', 'interface', 'struct', 'trait', 'protocol')
AND {rival_rank} > {base_rank})
)
SELECT 1 FROM base
WHERE base.symbol_id = {target}.parent_symbol_id
AND (base.depth > 0 OR {own_class_rule})
AND NOT EXISTS (
SELECT 1 FROM base closer
JOIN symbols nearer ON nearer.parent_symbol_id = closer.symbol_id
WHERE nearer.name = {target}.name
AND nearer.kind IN ('method', 'function', 'constructor', 'property')
AND closer.depth < base.depth
AND (closer.depth > 0 OR p.target_receiver IS NOT 'super')
)
)"
)
};
let inherited = class_chain(&format!("{target}.kind IN ('variable', 'field')"));
let receiver_chain = class_chain("p.target_receiver IS NOT 'super'");
let implicit_receiver_languages = IMPLICIT_RECEIVER_LANGUAGES
.iter()
.map(|language| format!("'{language}'"))
.collect::<Vec<_>>()
.join(", ");
let enclosing = format!(
"EXISTS (
WITH RECURSIVE lexical(symbol_id, depth) AS (
SELECT s_from.parent_symbol_id, 1
UNION ALL
SELECT outer_scope.parent_symbol_id, lexical.depth + 1
FROM symbols outer_scope JOIN lexical ON outer_scope.symbol_id = lexical.symbol_id
WHERE outer_scope.parent_symbol_id IS NOT NULL AND lexical.depth < 32
)
SELECT 1 FROM lexical JOIN symbols scope ON scope.symbol_id = lexical.symbol_id
WHERE lexical.symbol_id = {target}.parent_symbol_id
AND lexical.depth > 1
AND (scope.kind NOT IN ({MEMBER_LIST_OWNER_KINDS})
OR scope.kind IN ('module', 'namespace')
OR {target}.kind IN ({NESTED_TYPE_KINDS})
AND s_from.language IN ({implicit_receiver_languages}))
AND NOT EXISTS (
SELECT 1 FROM lexical closer
JOIN symbols nearer ON nearer.parent_symbol_id = closer.symbol_id
WHERE closer.depth < lexical.depth
AND nearer.name = {target}.name
AND nearer.kind IN ({target}.kind, 'import')
)
)"
);
format!(
"(
NOT (p.kind IS 'extends' AND p.from_symbol_id = {target}.symbol_id)
AND (
(
{target}.parent_symbol_id IS NOT NULL
AND {parent}.name IS NOT NULL
AND (
EXISTS (SELECT 1 FROM {ns} WHERE value = {parent}.name)
OR (EXISTS (SELECT 1 FROM {ns} WHERE value = 'Self')
AND s_from.parent_symbol_id = {target}.parent_symbol_id)
OR (p.target_receiver IS NOT NULL AND p.target_receiver != '' AND {parent}.name = p.target_receiver
{receiver_import_elsewhere})
OR ((p.target_receiver IN ('self', 'this', 'cls', 'Self', 'super')
OR EXISTS (SELECT 1 FROM {ns} WHERE value = 'Self'))
AND {receiver_chain})
OR EXISTS (
SELECT 1 FROM symbols receiver
JOIN type_facts receiver_type ON receiver_type.symbol_id = receiver.symbol_id
WHERE receiver.name = p.target_receiver
AND receiver.path = p.path
AND receiver_type.resolved_type = {parent}.name
AND NOT EXISTS (
SELECT 1 FROM symbols shadow
WHERE shadow.name = receiver.name
AND shadow.path = receiver.path
AND shadow.parent_symbol_id = p.from_symbol_id
AND shadow.kind IN ('variable', 'parameter')
AND shadow.symbol_id != receiver.symbol_id
)
) AND {receiver_type_scope}
{fixture_receiver}
)
AND NOT EXISTS (
SELECT 1 FROM {ns}
WHERE value NOT IN ('std', 'core', 'alloc', 'crate', 'super', 'self', 'Self', {parent}.name)
AND NOT EXISTS (
WITH RECURSIVE ancestor(symbol_id, depth) AS (
SELECT {target}.parent_symbol_id, 0
UNION ALL
SELECT s.parent_symbol_id, ancestor.depth + 1
FROM symbols s JOIN ancestor ON s.symbol_id = ancestor.symbol_id
WHERE s.parent_symbol_id IS NOT NULL AND ancestor.depth < 32
)
SELECT 1 FROM ancestor JOIN symbols a ON a.symbol_id = ancestor.symbol_id
WHERE a.name = value
)
AND {target_path} NOT LIKE '%/' || {like_value} || '.%' ESCAPE '\\'
AND {target_path} NOT LIKE '%/' || {like_value} || '/%' ESCAPE '\\'
)
)
OR (
(p.target_namespace_json IS NULL OR p.target_namespace_json = '[]')
AND (
p.target_receiver IS NULL
OR p.target_receiver = ''
{import_alias_receiver}
)
AND ({target}.parent_symbol_id IS NULL
OR s_from.parent_symbol_id = {target}.parent_symbol_id
OR {target}.parent_symbol_id = p.from_symbol_id
OR CASE WHEN p.target_receiver IS NULL OR p.target_receiver = ''
THEN {enclosing}
ELSE 0 END
OR CASE WHEN (p.target_receiver IS NULL OR p.target_receiver = '')
AND s_from.language IN ({implicit_receiver_languages})
THEN {parent}.path = s_from.path
AND {parent}.name = (
SELECT caller_owner.name FROM symbols caller_owner
WHERE caller_owner.symbol_id = s_from.parent_symbol_id
AND caller_owner.kind = {parent}.kind
)
OR {inherited}
ELSE 0 END)
AND (p.target_receiver IS NOT NULL AND p.target_receiver != '' OR {target}.path = p.path OR NOT EXISTS (
SELECT 1 FROM symbols shadow
WHERE shadow.name = {target}.name
AND shadow.path = p.path
AND shadow.symbol_id != p.from_symbol_id
AND shadow.kind NOT IN ('import', 'export', 'module', 'namespace')
AND (shadow.parent_symbol_id IS NULL OR shadow.parent_symbol_id = p.from_symbol_id)
))
AND ({target}.parent_symbol_id IS NOT NULL OR NOT EXISTS (
SELECT 1 FROM symbols closer
WHERE closer.name = {target}.name
AND closer.symbol_id != {target}.symbol_id
AND closer.parent_symbol_id IS NULL
AND closer.kind = {target}.kind
AND NOT (p.kind IS 'extends' AND closer.symbol_id = p.from_symbol_id)
AND {closer_rank} > {target_rank}
))
)
OR (
{target}.parent_symbol_id IS NULL
AND EXISTS (
SELECT 1 FROM {ns}
WHERE value NOT IN ('std', 'core', 'alloc', 'crate', 'super')
AND {target_path} LIKE '%/' || {like_value} || '.%' ESCAPE '\\'
)
)
)
)"
)
}
pub(crate) const DOCUMENTATION_LANGUAGES: &[&str] = &[
"markdown", "yaml", "toml", "json", "html", "css", "xml", "ini", "text",
];
fn documentation_language_list() -> String {
DOCUMENTATION_LANGUAGES
.iter()
.map(|l| format!("'{l}'"))
.collect::<Vec<_>>()
.join(", ")
}
fn not_documentation(conn: &Connection, alias: &str) -> String {
let has_content_type: bool = conn
.query_row(
"SELECT 1 FROM pragma_table_info('symbols') WHERE name = 'content_type'",
[],
|_| Ok(true),
)
.unwrap_or(false);
if has_content_type {
format!("({alias}.content_type IS NULL OR {alias}.content_type != 'documentation')")
} else {
"1 = 1".to_string()
}
}
pub(crate) fn has_column(conn: &Connection, table: &str, column: &str) -> bool {
conn.query_row(
"SELECT 1 FROM pragma_table_info(?1) WHERE name = ?2",
[table, column],
|_| Ok(true),
)
.unwrap_or(false)
}
pub(crate) fn has_table(conn: &Connection, name: &str) -> bool {
conn.query_row(
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1",
[name],
|_| Ok(true),
)
.unwrap_or(false)
}
fn has_pending_namespace_column(conn: &Connection) -> bool {
let has_ns: bool = conn
.query_row(
"SELECT 1 FROM pragma_table_info('pending_relationships') WHERE name = 'target_namespace_json'",
[],
|_| Ok(true),
)
.unwrap_or(false);
let has_display: bool = conn
.query_row(
"SELECT 1 FROM pragma_table_info('pending_relationships') WHERE name = 'target_display_name'",
[],
|_| Ok(true),
)
.unwrap_or(false);
let has_receiver: bool = conn
.query_row(
"SELECT 1 FROM pragma_table_info('pending_relationships') WHERE name = 'target_receiver'",
[],
|_| Ok(true),
)
.unwrap_or(false);
has_ns && has_display && has_receiver
}
fn find_references_internal(
conn: &Connection,
symbol_name: &str,
direction: &str,
limit: usize,
symbol_id: Option<&str>,
include_external: bool,
) -> Result<Vec<ReferenceSite>, QueryError> {
let mut sites = find_direct_references(
conn,
symbol_name,
direction,
limit,
symbol_id,
include_external,
)?;
let class = match symbol_id.filter(|_| direction == "callers") {
Some(id) => constructed_class(conn, id)?,
None => None,
};
if let Some((class_id, class_name)) = class.filter(|_| sites.len() < limit) {
let mut builds = find_direct_references(
conn,
&class_name,
"callers",
limit - sites.len(),
Some(&class_id),
include_external,
)?;
builds.retain(|site| site.kind == "calls");
sites.extend(builds);
sites.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.start_line.cmp(&b.start_line))
.then(a.start_column.cmp(&b.start_column))
});
}
if direction == "callers" {
populate_lambda_enclosing_names(conn, &mut sites)?;
}
Ok(sites)
}
fn populate_lambda_enclosing_names(
conn: &Connection,
sites: &mut [ReferenceSite],
) -> Result<(), QueryError> {
if !sites
.iter()
.any(|site| has_generated_lambda_name(&site.from_symbol_name))
{
return Ok(());
}
let mut stmt = conn.prepare(
"SELECT caller.language, caller.kind, caller.signature, parent.name
FROM symbols caller
LEFT JOIN symbols parent ON parent.symbol_id = caller.parent_symbol_id
WHERE caller.symbol_id = ?1",
)?;
for site in sites
.iter_mut()
.filter(|site| has_generated_lambda_name(&site.from_symbol_name))
{
let details = stmt
.query_row([&site.from_symbol_id], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, Option<String>>(2)?,
row.get::<_, Option<String>>(3)?,
))
})
.optional()?;
if let Some((language, kind, signature, enclosing_symbol_name)) = details
&& is_generated_lambda_name(
&site.from_symbol_name,
&language,
&kind,
signature.as_deref(),
)
{
site.enclosing_symbol_name = enclosing_symbol_name;
}
}
Ok(())
}
fn constructed_class(
conn: &Connection,
symbol_id: &str,
) -> Result<Option<(String, String)>, QueryError> {
Ok(conn
.query_row(
"SELECT class.symbol_id, class.name
FROM symbols ctor JOIN symbols class ON class.symbol_id = ctor.parent_symbol_id
WHERE ctor.symbol_id = ?1 AND ctor.kind = 'constructor'
AND class.kind IN ('class', 'struct', 'record')",
params![symbol_id],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.optional()?)
}
fn find_direct_references(
conn: &Connection,
symbol_name: &str,
direction: &str,
limit: usize,
symbol_id: Option<&str>,
include_external: bool,
) -> Result<Vec<ReferenceSite>, QueryError> {
let mut results = Vec::new();
if direction == "callers" {
let mut stmt = conn.prepare(
"SELECT s_from.name AS from_name,
r.from_symbol_id,
s_to.name AS to_name,
r.kind,
r.path,
r.start_line,
r.start_column
FROM relationships r
JOIN symbols s_from ON r.from_symbol_id = s_from.symbol_id
JOIN symbols s_to ON r.to_symbol_id = s_to.symbol_id
WHERE s_to.name = ?1 AND (?3 IS NULL OR r.to_symbol_id = ?3)
LIMIT ?2",
)?;
let rows = stmt.query_map(params![symbol_name, limit as i64, symbol_id], |row| {
Ok(ReferenceSite {
from_symbol_name: row.get(0)?,
from_symbol_id: row.get(1)?,
to_symbol_name: row.get(2)?,
kind: row.get(3)?,
path: row.get::<_, String>(4)?.replace('\\', "/"),
start_line: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
occurrences: None,
target: None,
enclosing_symbol_name: None,
})
})?;
for r in rows {
results.push(r?);
}
if results.len() < limit {
let remaining = limit - results.len();
if has_pending_namespace_column(conn) {
if let Some(sid) = symbol_id {
let mut pending_stmt = conn.prepare(
&format!("SELECT s_from.name AS from_name,
p.from_symbol_id,
p.target_terminal_name AS to_name,
p.kind,
p.path,
p.start_line,
p.start_column
FROM pending_relationships p
JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
JOIN symbols s_target ON s_target.symbol_id = ?3
LEFT JOIN symbols s_target_parent ON s_target.parent_symbol_id = s_target_parent.symbol_id
WHERE p.target_terminal_name = ?1
AND {pred}
LIMIT ?2", pred = pending_target_predicate(conn, "s_target", "s_target_parent")),
)?;
let p_rows = pending_stmt.query_map(
params![symbol_name, remaining as i64, sid],
|row| {
Ok(ReferenceSite {
from_symbol_name: row.get(0)?,
from_symbol_id: row.get(1)?,
to_symbol_name: row.get(2)?,
kind: row.get(3)?,
path: row.get::<_, String>(4)?.replace('\\', "/"),
start_line: Some(row.get::<_, i64>(5)? as usize),
start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
occurrences: None,
target: None,
enclosing_symbol_name: None,
})
},
)?;
for r in p_rows {
results.push(r?);
}
} else {
let mut pending_stmt = conn.prepare(
"SELECT s_from.name AS from_name,
p.from_symbol_id,
p.target_terminal_name AS to_name,
p.kind,
p.path,
p.start_line,
p.start_column
FROM pending_relationships p
JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
WHERE p.target_terminal_name = ?1
AND (
(p.target_namespace_json IS NULL OR p.target_namespace_json = '[]')
OR EXISTS (
SELECT 1 FROM symbols s_any
JOIN symbols s_any_parent ON s_any.parent_symbol_id = s_any_parent.symbol_id
WHERE s_any.name = p.target_terminal_name
AND EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid(p.target_namespace_json) THEN p.target_namespace_json ELSE '[]' END) WHERE value = s_any_parent.name)
)
)
LIMIT ?2",
)?;
let p_rows =
pending_stmt.query_map(params![symbol_name, remaining as i64], |row| {
Ok(ReferenceSite {
from_symbol_name: row.get(0)?,
from_symbol_id: row.get(1)?,
to_symbol_name: row.get(2)?,
kind: row.get(3)?,
path: row.get::<_, String>(4)?.replace('\\', "/"),
start_line: Some(row.get::<_, i64>(5)? as usize),
start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
occurrences: None,
target: None,
enclosing_symbol_name: None,
})
})?;
for r in p_rows {
results.push(r?);
}
}
} else {
let is_nested = if let Some(sid) = symbol_id {
conn.query_row(
"SELECT 1 FROM symbols WHERE symbol_id = ?1 AND parent_symbol_id IS NOT NULL",
params![sid],
|_| Ok(true),
)
.unwrap_or(false)
} else {
false
};
if !is_nested {
let mut pending_stmt = conn.prepare(
"SELECT s_from.name AS from_name,
p.from_symbol_id,
p.target_terminal_name AS to_name,
p.kind,
p.path,
p.start_line,
p.start_column
FROM pending_relationships p
JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
WHERE p.target_terminal_name = ?1
LIMIT ?2",
)?;
let p_rows =
pending_stmt.query_map(params![symbol_name, remaining as i64], |row| {
Ok(ReferenceSite {
from_symbol_name: row.get(0)?,
from_symbol_id: row.get(1)?,
to_symbol_name: row.get(2)?,
kind: row.get(3)?,
path: row.get::<_, String>(4)?.replace('\\', "/"),
start_line: Some(row.get::<_, i64>(5)? as usize),
start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
occurrences: None,
target: None,
enclosing_symbol_name: None,
})
})?;
for r in p_rows {
results.push(r?);
}
}
}
}
if results.len() < limit && has_table(conn, "identifiers") {
let remaining = limit - results.len();
let receiver_match = member_receiver_match(conn);
let type_usage_shadow = format!(
"i.kind = 'type_usage'
AND ?2 IS NOT NULL
AND selected_target.kind IN ({NESTED_TYPE_KINDS})
AND EXISTS (
WITH RECURSIVE lexical(symbol_id, depth) AS (
SELECT s_from.symbol_id, 0
UNION ALL
SELECT scope.parent_symbol_id, lexical.depth + 1
FROM symbols scope JOIN lexical ON scope.symbol_id = lexical.symbol_id
WHERE scope.parent_symbol_id IS NOT NULL AND lexical.depth < 32
), visible_types(symbol_id, depth) AS (
SELECT shadow.symbol_id, lexical.depth
FROM lexical
JOIN symbols shadow ON shadow.parent_symbol_id = lexical.symbol_id
WHERE shadow.name = selected_target.name
AND shadow.kind IN ({NESTED_TYPE_KINDS})
AND shadow.language = i.language
UNION ALL
SELECT shadow.symbol_id, 100
FROM symbols shadow
WHERE shadow.name = selected_target.name
AND shadow.kind IN ({NESTED_TYPE_KINDS})
AND shadow.language = i.language
AND shadow.parent_symbol_id IS NULL
AND replace(shadow.path, '\\', '/') = replace(i.path, '\\', '/')
)
SELECT 1 FROM visible_types nearest
WHERE nearest.symbol_id != selected_target.symbol_id
AND nearest.depth = (SELECT MIN(depth) FROM visible_types)
)"
);
let receiver_match = receiver_match.replace("?3", "?1");
let language_match = identifier_language_matches("i", "selected_target");
let mut receiver_stmt = conn.prepare(&format!(
"SELECT {receiver_match}
FROM (SELECT name, kind, path, start_line, start_column,
containing_symbol_id AS from_symbol_id,
CASE WHEN json_valid(metadata_json) THEN metadata_json ELSE '{{}}' END AS metadata_json,
COALESCE(json_valid(metadata_json), 0) AS has_metadata,
language,
CASE WHEN json_valid(metadata_json) THEN json_extract(metadata_json, '$.receiver') END AS target_receiver,
'[]' AS target_namespace_json
FROM identifiers WHERE identifier_id = ?2) p
LEFT JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id"
))?;
let mut ident_stmt = conn.prepare(&format!(
"WITH raw AS MATERIALIZED (
SELECT identifier_id, name, kind, path, start_line, start_column,
containing_symbol_id, language,
CASE WHEN json_valid(metadata_json) THEN metadata_json ELSE '{{}}' END AS metadata_json
FROM identifiers WHERE name = ?1 AND kind IN ('type_usage', 'member_access')
)
SELECT i.identifier_id,
COALESCE(s_from.name, ''),
COALESCE(i.containing_symbol_id, ''),
i.name,
CASE WHEN json_valid(i.metadata_json)
AND json_extract(i.metadata_json, '$.role') = 'signal_handler'
THEN CASE WHEN json_extract(i.metadata_json, '$.receiver') = (
SELECT CASE WHEN t.kind IN ('class', 'struct', 'enum', 'interface', 'trait', 'module', 'namespace')
THEN t.name ELSE tp.name END
FROM symbols t
LEFT JOIN symbols tp ON tp.symbol_id = t.parent_symbol_id
WHERE t.symbol_id = ?2)
THEN 'handler' ELSE 'handler (candidate)' END
ELSE i.kind END,
i.path,
i.start_line,
i.start_column
FROM raw i
LEFT JOIN symbols s_from ON i.containing_symbol_id = s_from.symbol_id
LEFT JOIN symbols selected_target ON selected_target.symbol_id = ?2
WHERE i.name = ?1 AND i.kind IN ('type_usage', 'member_access')
AND COALESCE(s_from.kind, '') != 'import'
AND (?2 IS NULL OR {language_match})
AND NOT ({type_usage_shadow})
AND NOT EXISTS (
SELECT 1 FROM relationships covered
JOIN symbols covered_to ON covered.to_symbol_id = covered_to.symbol_id
WHERE covered_to.name = i.name
AND covered.path = i.path
AND covered.start_line = i.start_line
AND (?2 IS NULL OR covered.to_symbol_id = ?2)
)
AND NOT EXISTS (
SELECT 1 FROM pending_relationships covered
WHERE covered.target_terminal_name = i.name
AND covered.path = i.path
AND covered.start_line = i.start_line
)
AND (?2 IS NULL OR NOT EXISTS (
SELECT 1 FROM symbols owner
JOIN symbols member ON member.parent_symbol_id = owner.symbol_id
WHERE owner.name = CASE WHEN json_valid(i.metadata_json) THEN json_extract(i.metadata_json, '$.receiver') END
AND member.name = i.name
AND owner.name IS NOT (SELECT parent.name FROM symbols target
JOIN symbols parent ON parent.symbol_id = target.parent_symbol_id
WHERE target.symbol_id = ?2)
))
AND NOT (i.kind = 'member_access' AND EXISTS (
SELECT 1 FROM symbols target
LEFT JOIN symbols scope ON scope.symbol_id = target.parent_symbol_id
WHERE target.symbol_id = ?2
AND (scope.kind IN ('function', 'method', 'constructor')
OR (scope.symbol_id IS NULL
AND target.path != i.path
AND target.language IN ('javascript', 'typescript', 'tsx')
AND COALESCE(target.visibility, 'private') = 'private'))
))
AND NOT COALESCE((i.kind = 'member_access'
AND json_valid(i.metadata_json)
AND json_extract(i.metadata_json, '$.role') IS NOT 'signal_handler'
AND json_extract(i.metadata_json, '$.receiver') GLOB '[A-Z]*'
AND NOT EXISTS (SELECT 1 FROM symbols known
WHERE known.name = json_extract(i.metadata_json, '$.receiver')
AND known.kind NOT IN ('variable', 'parameter', 'method'))), 0)
ORDER BY i.path, i.start_line, i.start_column, i.identifier_id"
))?;
let mut supported = Vec::new();
let mut candidates = Vec::new();
let mut supported_keys = HashSet::new();
let mut candidate_keys = HashSet::new();
let site_key = |site: &ReferenceSite| {
(
site.from_symbol_id.clone(),
site.to_symbol_name.clone(),
site.kind.clone(),
site.path.clone(),
site.start_line,
)
};
let mut ident_rows = ident_stmt.query(params![symbol_name, symbol_id])?;
while let Some(row) = ident_rows.next()? {
let identifier_id: String = row.get(0)?;
let mut site = ReferenceSite {
from_symbol_name: row.get(1)?,
from_symbol_id: row.get(2)?,
to_symbol_name: row.get(3)?,
kind: row.get(4)?,
path: row.get::<_, String>(5)?.replace('\\', "/"),
start_line: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
start_column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
occurrences: None,
target: None,
enclosing_symbol_name: None,
};
let receiver_match: i64 =
receiver_stmt.query_row(params![symbol_id, identifier_id], |row| row.get(0))?;
if receiver_match < 0 {
continue;
}
if receiver_match == 0 && site.kind == "member_access" {
site.kind = "member_access (candidate)".to_string();
}
if site.kind == "member_access (candidate)" {
if candidates.len() < remaining && candidate_keys.insert(site_key(&site)) {
candidates.push(site);
}
} else if supported_keys.insert(site_key(&site)) {
supported.push(site);
if supported.len() >= remaining {
break;
}
}
}
let site_order = |a: &ReferenceSite, b: &ReferenceSite| {
a.path
.cmp(&b.path)
.then(a.start_line.cmp(&b.start_line))
.then(a.start_column.cmp(&b.start_column))
};
supported.sort_by(site_order);
supported.truncate(remaining);
candidates.sort_by(site_order);
candidates.truncate(remaining - supported.len());
results.extend(supported);
results.extend(candidates);
if let Some(sid) = symbol_id.filter(|_| results.len() < limit) {
let remaining = limit - results.len();
let language_match = identifier_language_matches("i", "target");
let mut receiver_stmt = conn.prepare(&format!(
"SELECT COALESCE(s.name, ''),
COALESCE(i.containing_symbol_id, ''),
i.name,
i.kind,
i.path,
MIN(i.start_line),
i.start_column,
COUNT(*)
FROM identifiers i
LEFT JOIN symbols s ON i.containing_symbol_id = s.symbol_id
JOIN symbols target ON target.symbol_id = ?2
LEFT JOIN symbols target_parent ON target_parent.symbol_id = target.parent_symbol_id
WHERE i.kind = 'member_access'
AND i.name != target.name
AND COALESCE(s.kind, '') != 'import'
AND target.kind IN ('class', 'struct', 'enum', 'interface', 'trait', 'module', 'namespace')
AND json_valid(i.metadata_json)
AND json_extract(i.metadata_json, '$.receiver') = target.name
AND (json_extract(i.metadata_json, '$.receiver_qualifier') IS NULL
OR json_extract(i.metadata_json, '$.receiver_qualifier') = target_parent.name)
AND {language_match}
AND NOT EXISTS (
SELECT 1 FROM symbols nearer
WHERE nearer.name = target.name
AND nearer.path = i.path
AND nearer.symbol_id != target.symbol_id
AND nearer.kind IN ({NESTED_TYPE_KINDS}, 'module', 'namespace')
)
GROUP BY i.path
ORDER BY i.path, i.start_line
LIMIT ?1"
))?;
let rows = receiver_stmt.query_map(params![remaining as i64, sid], |row| {
Ok(ReferenceSite {
from_symbol_name: row.get(0)?,
from_symbol_id: row.get(1)?,
to_symbol_name: row.get(2)?,
kind: row.get(3)?,
path: row.get::<_, String>(4)?.replace('\\', "/"),
start_line: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
occurrences: Some(row.get::<_, i64>(7)? as usize),
target: None,
enclosing_symbol_name: None,
})
})?;
for r in rows {
results.push(r?);
}
}
}
} else {
let mut stmt = conn.prepare(
"SELECT s_from.name AS from_name,
r.from_symbol_id,
s_to.name AS to_name,
r.kind,
r.path,
r.start_line,
r.start_column
FROM relationships r
JOIN symbols s_from ON r.from_symbol_id = s_from.symbol_id
JOIN symbols s_to ON r.to_symbol_id = s_to.symbol_id
WHERE s_from.name = ?1 AND (?3 IS NULL OR r.from_symbol_id = ?3)
LIMIT ?2",
)?;
let rows = stmt.query_map(params![symbol_name, limit as i64, symbol_id], |row| {
Ok(ReferenceSite {
from_symbol_name: row.get(0)?,
from_symbol_id: row.get(1)?,
to_symbol_name: row.get(2)?,
kind: row.get(3)?,
path: row.get::<_, String>(4)?.replace('\\', "/"),
start_line: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
occurrences: None,
target: None,
enclosing_symbol_name: None,
})
})?;
for r in rows {
results.push(r?);
}
if results.len() < limit {
let remaining = limit - results.len();
let p_rows: Vec<ReferenceSite> = if has_pending_namespace_column(conn) {
let sql = if include_external {
String::from("SELECT DISTINCT s_from.name AS from_name,
p.from_symbol_id,
COALESCE(NULLIF(p.target_display_name, ''), p.target_terminal_name) AS to_name,
p.kind,
p.path,
p.start_line,
p.start_column
FROM pending_relationships p
JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
WHERE s_from.name = ?1 AND (?3 IS NULL OR p.from_symbol_id = ?3)
LIMIT ?2")
} else {
format!("SELECT DISTINCT s_from.name AS from_name,
p.from_symbol_id,
p.target_terminal_name AS to_name,
p.kind,
p.path,
p.start_line,
p.start_column
FROM pending_relationships p
JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
WHERE s_from.name = ?1 AND (?3 IS NULL OR p.from_symbol_id = ?3)
AND EXISTS (
SELECT 1 FROM symbols s_to
LEFT JOIN symbols s_to_parent ON s_to.parent_symbol_id = s_to_parent.symbol_id
WHERE s_to.name = p.target_terminal_name
AND {kind}
AND {pred}
)
LIMIT ?2", kind = callee_target_kind("s_to_parent"), pred = pending_target_predicate(conn, "s_to", "s_to_parent"))
};
let mut pending_stmt = conn.prepare(&sql)?;
let rows = pending_stmt.query_map(
params![symbol_name, remaining as i64, symbol_id],
|row| {
Ok(ReferenceSite {
from_symbol_name: row.get(0)?,
from_symbol_id: row.get(1)?,
to_symbol_name: row.get(2)?,
kind: row.get(3)?,
path: row.get::<_, String>(4)?.replace('\\', "/"),
start_line: Some(row.get::<_, i64>(5)? as usize),
start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
occurrences: None,
target: None,
enclosing_symbol_name: None,
})
},
)?;
let mut out = Vec::new();
for r in rows {
out.push(r?);
}
out
} else {
let sql = if include_external {
"SELECT DISTINCT s_from.name AS from_name,
p.from_symbol_id,
p.target_terminal_name AS to_name,
p.kind,
p.path,
p.start_line,
p.start_column
FROM pending_relationships p
JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
WHERE s_from.name = ?1 AND (?3 IS NULL OR p.from_symbol_id = ?3)
LIMIT ?2"
} else {
"SELECT DISTINCT s_from.name AS from_name,
p.from_symbol_id,
p.target_terminal_name AS to_name,
p.kind,
p.path,
p.start_line,
p.start_column
FROM pending_relationships p
JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
WHERE s_from.name = ?1 AND (?3 IS NULL OR p.from_symbol_id = ?3)
AND EXISTS (SELECT 1 FROM symbols s_to WHERE s_to.name = p.target_terminal_name)
LIMIT ?2"
};
let mut pending_stmt = conn.prepare(sql)?;
let rows = pending_stmt.query_map(
params![symbol_name, remaining as i64, symbol_id],
|row| {
Ok(ReferenceSite {
from_symbol_name: row.get(0)?,
from_symbol_id: row.get(1)?,
to_symbol_name: row.get(2)?,
kind: row.get(3)?,
path: row.get::<_, String>(4)?.replace('\\', "/"),
start_line: Some(row.get::<_, i64>(5)? as usize),
start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
occurrences: None,
target: None,
enclosing_symbol_name: None,
})
},
)?;
let mut out = Vec::new();
for r in rows {
out.push(r?);
}
out
};
for r in p_rows {
results.push(r);
}
}
results.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.start_line.cmp(&b.start_line))
.then(a.start_column.cmp(&b.start_column))
});
let mut sites = merge_same_site(results);
describe_callee_targets(conn, &mut sites)?;
return Ok(sites);
}
Ok(merge_same_site(results))
}
fn callee_target_kind(parent: &str) -> String {
format!(
"(s_to.kind NOT IN ('import', 'variable', 'parameter', 'field', 'property', 'module', 'namespace')
OR p.kind = 'calls' AND s_to.kind IN ('variable', 'field')
AND {parent}.kind IN ('class', 'struct', 'interface', 'trait', 'record', 'object', 'protocol'))"
)
}
fn describe_callee_targets(
conn: &Connection,
sites: &mut [ReferenceSite],
) -> Result<(), QueryError> {
let owner = "CASE WHEN par.kind IN ('class', 'struct', 'interface', 'trait', 'enum', 'record', 'object', 'protocol', 'union')
THEN par.name || '.' ELSE '' END";
let pending = if has_pending_namespace_column(conn) {
format!(
"UNION
SELECT {owner} || s_to.name, replace(s_to.path, '\\', '/'), s_to.start_line
FROM pending_relationships p
JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
JOIN symbols s_to ON s_to.name = p.target_terminal_name
LEFT JOIN symbols par ON s_to.parent_symbol_id = par.symbol_id
LEFT JOIN symbols s_to_parent ON s_to.parent_symbol_id = s_to_parent.symbol_id
WHERE p.from_symbol_id = ?1 AND p.start_line = ?2
AND (p.target_terminal_name = ?3 OR p.target_display_name = ?3)
AND {kind}
AND {pred}",
kind = callee_target_kind("s_to_parent"),
pred = pending_target_predicate(conn, "s_to", "s_to_parent")
)
} else {
String::new()
};
let mut stmt = conn.prepare(&format!(
"SELECT {owner} || s_to.name, replace(s_to.path, '\\', '/'), s_to.start_line
FROM relationships r
JOIN symbols s_to ON r.to_symbol_id = s_to.symbol_id
LEFT JOIN symbols par ON s_to.parent_symbol_id = par.symbol_id
WHERE r.from_symbol_id = ?1 AND r.start_line = ?2 AND s_to.name = ?3
{pending}
ORDER BY 2, 3
LIMIT 4"
))?;
for site in sites.iter_mut() {
let Some(line) = site.start_line else {
continue;
};
let targets = stmt
.query_map(
params![site.from_symbol_id, line as i64, site.to_symbol_name],
|row| {
Ok(format!(
"`{}` ({}:{})",
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, i64>(2)?
))
},
)?
.collect::<Result<Vec<_>, _>>()?;
site.target = match targets.len() {
0 => None,
1 => targets.into_iter().next(),
2 | 3 => Some(format!("one of {}", targets.join(", "))),
_ => Some(format!(
"more than 3 definitions named `{}`",
site.to_symbol_name
)),
};
}
Ok(())
}
pub fn import_sites(
conn: &Connection,
name: &str,
path_filter: Option<&str>,
) -> Result<Vec<(String, usize)>, QueryError> {
if !has_column(conn, "symbols", "metadata_json") {
return Ok(Vec::new());
}
let dotted = "replace(module_path.value, '/', '.')";
let segment = format!("replace({dotted}, rtrim({dotted}, replace({dotted}, '.', '')), '')");
let target_path = "('/' || replace(target.path, '\\', '/'))";
let dir = |path: &str| {
format!("rtrim(replace({path}, '\\', '/'), replace(replace({path}, '\\', '/'), '/', ''))")
};
let (import_dir, target_dir) = (dir("i.path"), dir("target.path"));
let mut stmt = conn.prepare(&format!(
"SELECT DISTINCT replace(i.path, '\\', '/'), i.start_line
FROM symbols target
JOIN symbols i ON i.name = target.name AND i.kind = 'import'
CROSS JOIN json_each(json_array(json_extract(i.metadata_json, '$.source'))) module_path
WHERE target.name = ?1
AND target.parent_symbol_id IS NULL
AND target.kind NOT IN ({LOW_SIGNAL_KINDS_SQL})
AND (?2 IS NULL OR replace(target.path, '\\', '/') = ?2
OR replace(target.path, '\\', '/') LIKE ?3 ESCAPE '\\')
AND json_valid(i.metadata_json)
AND i.language NOT IN ({docs})
AND (
{segment} != ''
AND ({target_path} GLOB '*/' || {segment} || '/*'
OR {target_path} GLOB '*/' || {segment} || '.*')
OR module_path.value != '' AND replace(module_path.value, '.', '') = ''
AND substr({import_dir}, 1, length({target_dir})) = {target_dir}
)
ORDER BY 1, 2",
docs = documentation_language_list()
))?;
let norm = path_filter.map(|p| p.replace('\\', "/").trim_matches('/').to_string());
let prefix = norm.as_ref().map(|p| format!("{}/%", escape_like(p)));
let rows = stmt.query_map(params![name, norm, prefix], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize))
})?;
Ok(rows.collect::<Result<_, _>>()?)
}
fn merge_same_site(sites: Vec<ReferenceSite>) -> Vec<ReferenceSite> {
let mut merged: Vec<ReferenceSite> = Vec::with_capacity(sites.len());
let mut index = HashMap::new();
for site in sites {
let key = (
site.from_symbol_id.clone(),
site.to_symbol_name.clone(),
site.kind.clone(),
site.path.clone(),
site.start_line,
);
match index.get(&key) {
Some(&at) => {
let kept: &mut ReferenceSite = &mut merged[at];
kept.start_column = kept.start_column.min(site.start_column);
}
None => {
index.insert(key, merged.len());
merged.push(site);
}
}
}
merged
}
pub fn find_callee_signatures(
conn: &Connection,
symbol_name: &str,
symbol_id: &str,
limit: usize,
include_external: bool,
) -> Result<Vec<String>, QueryError> {
let mut stmt = conn.prepare(
"SELECT DISTINCT s_to.name, s_to.signature, s_to.path, s_to.start_line, s_to.kind
FROM relationships r
JOIN symbols s_from ON r.from_symbol_id = s_from.symbol_id
JOIN symbols s_to ON r.to_symbol_id = s_to.symbol_id
WHERE s_from.name = ?1 AND r.from_symbol_id = ?2
LIMIT ?3",
)?;
let rows = stmt.query_map(params![symbol_name, symbol_id, (limit * 2) as i64], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, Option<String>>(1)?,
row.get::<_, String>(2)?.replace('\\', "/"),
row.get::<_, Option<i64>>(3)?.unwrap_or(1) as usize,
row.get::<_, String>(4)?,
))
})?;
let mut signatures = Vec::new();
let mut variants = Vec::new();
for r in rows.flatten() {
let (name, sig_opt, path, line, kind) = r;
let sig = sig_opt.unwrap_or(name);
let entry = format!("{sig} ({path}:{line})");
if kind == "variant" {
if !variants.contains(&entry) {
variants.push(entry);
}
} else if !signatures.contains(&entry) {
signatures.push(entry);
}
}
if signatures.len() < limit {
let remaining = (limit - signatures.len()) * 2;
let p_rows: Vec<(String, Option<String>, String, usize, String)> =
if has_pending_namespace_column(conn) {
let mut p_stmt = conn.prepare(
&format!("SELECT DISTINCT s_to.name, s_to.signature, s_to.path, s_to.start_line, s_to.kind
FROM pending_relationships p
JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
JOIN symbols s_to ON s_to.name = p.target_terminal_name
LEFT JOIN symbols s_parent ON s_to.parent_symbol_id = s_parent.symbol_id
WHERE s_from.name = ?1 AND p.from_symbol_id = ?2
AND {kind}
AND {pred}
LIMIT ?3", kind = callee_target_kind("s_parent"), pred = pending_target_predicate(conn, "s_to", "s_parent")),
)?;
let rows =
p_stmt.query_map(params![symbol_name, symbol_id, remaining as i64], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, Option<String>>(1)?,
row.get::<_, String>(2)?.replace('\\', "/"),
row.get::<_, Option<i64>>(3)?.unwrap_or(1) as usize,
row.get::<_, String>(4)?,
))
})?;
rows.flatten().collect()
} else {
let mut p_stmt = conn.prepare(
"SELECT DISTINCT s_to.name, s_to.signature, s_to.path, s_to.start_line, s_to.kind
FROM pending_relationships p
JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
JOIN symbols s_to ON s_to.name = p.target_terminal_name
WHERE s_from.name = ?1 AND p.from_symbol_id = ?2
AND s_to.kind NOT IN ('import', 'variable', 'parameter', 'field', 'property', 'module', 'namespace')
LIMIT ?3",
)?;
let rows =
p_stmt.query_map(params![symbol_name, symbol_id, remaining as i64], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, Option<String>>(1)?,
row.get::<_, String>(2)?.replace('\\', "/"),
row.get::<_, Option<i64>>(3)?.unwrap_or(1) as usize,
row.get::<_, String>(4)?,
))
})?;
rows.flatten().collect()
};
for r in p_rows {
let (name, sig_opt, path, line, kind) = r;
let sig = sig_opt.unwrap_or(name);
let entry = format!("{sig} ({path}:{line})");
if kind == "variant" {
if !variants.contains(&entry) {
variants.push(entry);
}
} else if !signatures.contains(&entry) {
signatures.push(entry);
}
}
}
if include_external && signatures.len() < limit {
let remaining = (limit - signatures.len()) * 2;
let ext_rows: Vec<(String, String, usize)> = if has_pending_namespace_column(conn) {
let mut ext_stmt = conn.prepare(
&format!("SELECT DISTINCT COALESCE(NULLIF(p.target_display_name, ''), p.target_terminal_name), p.path, p.start_line
FROM pending_relationships p
JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
WHERE s_from.name = ?1 AND p.from_symbol_id = ?2
AND NOT EXISTS (
SELECT 1 FROM symbols s_to
LEFT JOIN symbols s_parent ON s_to.parent_symbol_id = s_parent.symbol_id
WHERE s_to.name = p.target_terminal_name
AND {kind}
AND {pred}
)
LIMIT ?3", kind = callee_target_kind("s_parent"), pred = pending_target_predicate(conn, "s_to", "s_parent")),
)?;
let rows =
ext_stmt.query_map(params![symbol_name, symbol_id, remaining as i64], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?.replace('\\', "/"),
row.get::<_, Option<i64>>(2)?.unwrap_or(1) as usize,
))
})?;
rows.flatten().collect()
} else {
let mut ext_stmt = conn.prepare(
"SELECT DISTINCT p.target_terminal_name, p.path, p.start_line
FROM pending_relationships p
JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
WHERE s_from.name = ?1 AND p.from_symbol_id = ?2
AND NOT EXISTS (
SELECT 1 FROM symbols s_to
WHERE s_to.name = p.target_terminal_name
AND s_to.kind NOT IN ('import', 'variable', 'parameter', 'field', 'property', 'module', 'namespace')
)
LIMIT ?3",
)?;
let rows =
ext_stmt.query_map(params![symbol_name, symbol_id, remaining as i64], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?.replace('\\', "/"),
row.get::<_, Option<i64>>(2)?.unwrap_or(1) as usize,
))
})?;
rows.flatten().collect()
};
for r in ext_rows {
let (name, path, line) = r;
let entry = format!("{name} ({path}:{line})");
if !signatures.contains(&entry) {
signatures.push(entry);
}
}
}
for v in variants {
if signatures.len() >= limit {
break;
}
if !signatures.contains(&v) {
signatures.push(v);
}
}
signatures.truncate(limit);
Ok(signatures)
}
const SQL_FAMILIES: &[&str] = &["sql."];
const ROUTE_FAMILIES: &[&str] = &[
".route.",
".attribute_route.",
".scope_route.",
".file_route.",
".route_handler.",
".route_reference.",
".route_group.",
".route_prefix.",
".resource_route.",
".router_mount.",
".include_router.",
".server_route.",
".route_definition.",
];
const CONFIG_FAMILIES: &[&str] = &["toml.", "yaml.", "json.", "env."];
const MODEL_FAMILIES: &[&str] = &[
"sql.table_definition.v1",
"sql.column_definition.v1",
"sql.constraint.v1",
"sql.foreign_key.v1",
"sql.index_definition.v1",
"json.schema.v1",
];
const SIGNAL_FAMILIES: &[&str] = &[".signal_declaration."];
const IMPORT_FAMILIES: &[&str] = &[".import_statement.", ".import."];
const BINDING_FAMILIES: &[&str] = &[".binding."];
const COMPONENT_FAMILIES: &[&str] = &[".object_instantiation.", ".object_type."];
const MODULE_FAMILIES: &[&str] = &[".module."];
const PRAGMA_FAMILIES: &[&str] = &[".pragma.", "javascript.qml_directive.v1"];
const PROPERTY_FAMILIES: &[&str] = &[".property_declaration.", ".qt_property."];
pub const CATEGORY_ALIASES: &[(&str, &[&str])] = &[
("sql", SQL_FAMILIES),
("query", SQL_FAMILIES),
("queries", SQL_FAMILIES),
("route", ROUTE_FAMILIES),
("routes", ROUTE_FAMILIES),
("config", CONFIG_FAMILIES),
("model", MODEL_FAMILIES),
("models", MODEL_FAMILIES),
("signal", SIGNAL_FAMILIES),
("signals", SIGNAL_FAMILIES),
("import", IMPORT_FAMILIES),
("imports", IMPORT_FAMILIES),
("binding", BINDING_FAMILIES),
("bindings", BINDING_FAMILIES),
("component", COMPONENT_FAMILIES),
("components", COMPONENT_FAMILIES),
("module", MODULE_FAMILIES),
("modules", MODULE_FAMILIES),
("pragma", PRAGMA_FAMILIES),
("property", PROPERTY_FAMILIES),
("properties", PROPERTY_FAMILIES),
];
fn category_families(category: &str) -> Option<&'static [&'static str]> {
let wanted = category.trim().to_ascii_lowercase();
CATEGORY_ALIASES
.iter()
.find(|(alias, _)| *alias == wanted)
.map(|(_, families)| *families)
}
fn matches_family(pattern_id: &str, rule: &str) -> bool {
if rule.starts_with('.') {
pattern_id.contains(rule)
} else if rule.ends_with('.') {
pattern_id.starts_with(rule)
} else {
pattern_id == rule
}
}
fn family_clause(column: &str, families: &[&str]) -> String {
let alternatives: Vec<String> = families
.iter()
.map(|rule| {
let escaped = escape_like(rule);
if rule.starts_with('.') {
format!("{column} LIKE '%{escaped}%' ESCAPE '\\'")
} else if rule.ends_with('.') {
format!("{column} LIKE '{escaped}%' ESCAPE '\\'")
} else {
format!("{column} = '{rule}'")
}
})
.collect();
format!("({})", alternatives.join(" OR "))
}
pub fn is_category_alias(category: &str) -> bool {
category_families(category).is_some()
}
pub fn alias_fact_counts(categories: &[(String, usize)]) -> Vec<(&'static str, usize, usize)> {
let mut reported: Vec<&[&str]> = Vec::new();
let mut counts = Vec::new();
for (alias, families) in CATEGORY_ALIASES {
if reported.contains(families) {
continue;
}
reported.push(families);
let mut patterns = 0;
let mut facts = 0;
for (pattern_id, count) in categories {
if families.iter().any(|rule| matches_family(pattern_id, rule)) {
patterns += 1;
facts += count;
}
}
if facts > 0 {
counts.push((*alias, patterns, facts));
}
}
counts
}
pub fn find_structural_facts_scoped(
conn: &Connection,
category: &str,
path_filter: Option<&str>,
limit: usize,
) -> Result<Vec<StructuralFact>, QueryError> {
validate_result_limit(limit)?;
let norm_path = path_filter
.map(|p| {
p.replace('\\', "/")
.trim_start_matches("./")
.trim_matches('/')
.to_string()
})
.filter(|p| !p.is_empty());
let dir_prefix = norm_path
.as_deref()
.map(|p| format!("{}/%", escape_like(p)));
let cat_pattern = format!("%{}%", escape_like(category));
let cat_clause = match category_families(category) {
Some(families) => family_clause("sf.pattern_id", families),
None => {
"(sf.pattern_id LIKE :cat ESCAPE '\\' OR sf.capture_name LIKE :cat ESCAPE '\\' OR sf.node_kind LIKE :cat ESCAPE '\\')"
.to_string()
}
};
let sql = format!(
"SELECT sf.structural_fact_id, sf.path, sf.language, sf.pattern_id,
sf.capture_name, sf.node_kind, s.name AS containing_symbol_name,
sf.start_line, sf.end_line, sf.confidence,
COALESCE(
CASE WHEN json_extract(sf.metadata_json, '$.key_path') LIKE '$.%'
THEN substr(json_extract(sf.metadata_json, '$.key_path'), 3)
ELSE json_extract(sf.metadata_json, '$.key_path') END,
json_extract(sf.metadata_json, '$.key'),
CASE WHEN json_extract(sf.metadata_json, '$.normalized_route_template') IS NOT NULL
THEN COALESCE(json_extract(sf.metadata_json, '$.verb') || ' ', '')
|| json_extract(sf.metadata_json, '$.normalized_route_template') END,
CASE WHEN sf.pattern_id = 'cpp.qt_property.v1'
THEN json_extract(sf.metadata_json, '$.name') END
) AS display_key,
sf.metadata_json
FROM structural_facts sf
LEFT JOIN symbols s ON sf.containing_symbol_id = s.symbol_id
WHERE (:cat IS NOT NULL AND {cat_clause})
AND (:path IS NULL OR replace(sf.path, '\\', '/') = :path COLLATE NOCASE OR replace(sf.path, '\\', '/') LIKE :dir_prefix ESCAPE '\\')
ORDER BY sf.path ASC, sf.start_line ASC,
COALESCE(
json_extract(sf.metadata_json, '$.effective_route_template'),
json_extract(sf.metadata_json, '$.route_template'),
json_extract(sf.metadata_json, '$.normalized_route_template'),
''
) ASC,
COALESCE(json_extract(sf.metadata_json, '$.verb'), '') ASC,
sf.structural_fact_id ASC
LIMIT :limit"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map(
rusqlite::named_params! {
":cat": cat_pattern,
":path": norm_path.as_deref(),
":dir_prefix": dir_prefix.as_deref(),
":limit": limit as i64,
},
|row| {
Ok(StructuralFact {
structural_fact_id: row.get(0)?,
path: row.get::<_, String>(1)?.replace('\\', "/"),
language: row.get(2)?,
pattern_id: row.get(3)?,
capture_name: row.get(4)?,
node_kind: row.get(5)?,
key: row.get(10)?,
metadata: row
.get::<_, Option<String>>(11)?
.map(|json| serde_json::from_str(&json))
.transpose()
.map_err(|err| {
rusqlite::Error::FromSqlConversionFailure(
11,
rusqlite::types::Type::Text,
Box::new(err),
)
})?,
containing_symbol_name: row.get(6)?,
start_line: row.get::<_, i64>(7)? as usize,
end_line: row.get::<_, i64>(8)? as usize,
confidence: row.get(9)?,
})
},
)?;
let mut results = Vec::new();
for r in rows {
let mut fact = r?;
let display = fact.metadata.as_ref().and_then(|metadata| {
route_display(metadata).or_else(|| mount_display(&fact.capture_name, metadata))
});
if let Some(display) = display {
fact.key = Some(display);
}
results.push(fact);
}
Ok(results)
}
fn route_display(metadata: &serde_json::Value) -> Option<String> {
let text = |key: &str| metadata.get(key).and_then(|value| value.as_str());
let normalized = text("normalized_route_template")?;
let template = match (text("effective_route_template"), text("route_template")) {
(Some(effective), _) => effective,
(None, Some(raw)) if route_shape(raw) == route_shape(normalized) => raw,
_ => normalized,
};
Some(match text("verb") {
Some(verb) => format!("{verb} {template}"),
None => template.to_string(),
})
}
fn mount_display(capture: &str, metadata: &serde_json::Value) -> Option<String> {
let text = |key: &str| metadata.get(key).and_then(|value| value.as_str());
if text("query_family") != Some("framework") {
return None;
}
let target = text("mount_target")?;
Some(
match text("normalized_mount_path").or_else(|| text("mount_path")) {
Some(path) => format!("{capture} `{target}` at {path}"),
None => format!("{capture} `{target}`"),
},
)
}
fn route_shape(template: &str) -> String {
let mut shape = String::new();
let mut chars = template.chars().peekable();
while let Some(c) = chars.next() {
match c {
'<' | '{' => {
let close = if c == '<' { '>' } else { '}' };
for inner in chars.by_ref() {
if inner == close {
break;
}
}
shape.push('*');
}
':' => {
while chars.peek().is_some_and(|next| *next != '/') {
chars.next();
}
shape.push('*');
}
_ => shape.push(c),
}
}
shape
}
pub fn find_structural_facts(
conn: &Connection,
category: &str,
limit: usize,
) -> Result<Vec<StructuralFact>, QueryError> {
find_structural_facts_scoped(conn, category, None, limit)
}
pub fn find_literals_scoped(
conn: &Connection,
category: &str,
path_filter: Option<&str>,
limit: usize,
) -> Result<Vec<LiteralFact>, QueryError> {
validate_result_limit(limit)?;
let norm_path = path_filter
.map(|p| {
p.replace('\\', "/")
.trim_start_matches("./")
.trim_matches('/')
.to_string()
})
.filter(|p| !p.is_empty());
let dir_prefix = norm_path
.as_deref()
.map(|p| format!("{}/%", escape_like(p)));
let cat_pattern = format!("%{}%", escape_like(category));
let cat_lower = category.trim().to_ascii_lowercase();
let cat_clause = match cat_lower.as_str() {
"config" => {
"(l.kind LIKE '%config%' OR l.kind LIKE '%toml%' OR l.kind LIKE '%json%' OR l.kind LIKE '%yaml%')"
}
"route" | "routes" => "l.kind LIKE '%route%'",
"query" | "queries" | "sql" => "(l.kind LIKE '%sql%' OR l.kind LIKE '%query%')",
"model" | "models" => "l.kind LIKE '%model%'",
_ => "(l.kind LIKE :cat ESCAPE '\\' OR l.literal_text LIKE :cat ESCAPE '\\')",
};
let sql = format!(
"SELECT l.literal_id, l.path, l.literal_text, l.kind, l.carrier,
l.start_line, s.name AS containing_symbol_name
FROM literals l
LEFT JOIN symbols s ON l.containing_symbol_id = s.symbol_id
WHERE (:cat IS NOT NULL AND {cat_clause})
AND (:path IS NULL OR replace(l.path, '\\', '/') = :path COLLATE NOCASE OR replace(l.path, '\\', '/') LIKE :dir_prefix ESCAPE '\\')
ORDER BY l.path ASC, l.start_line ASC
LIMIT :limit"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map(
rusqlite::named_params! {
":cat": cat_pattern,
":path": norm_path.as_deref(),
":dir_prefix": dir_prefix.as_deref(),
":limit": limit as i64,
},
|row| {
Ok(LiteralFact {
literal_id: row.get(0)?,
path: row.get::<_, String>(1)?.replace('\\', "/"),
literal_text: row.get(2)?,
kind: row.get(3)?,
carrier: row.get(4)?,
start_line: row.get::<_, i64>(5)? as usize,
containing_symbol_name: row.get(6)?,
})
},
)?;
let mut results = Vec::new();
for r in rows {
results.push(r?);
}
Ok(results)
}
pub fn find_literals(
conn: &Connection,
category: &str,
limit: usize,
) -> Result<Vec<LiteralFact>, QueryError> {
find_literals_scoped(conn, category, None, limit)
}
pub fn list_structural_fact_categories_scoped(
conn: &Connection,
path_filter: Option<&str>,
) -> Result<Vec<(String, usize)>, QueryError> {
let norm_path = path_filter
.map(|p| {
p.replace('\\', "/")
.trim_start_matches("./")
.trim_matches('/')
.to_string()
})
.filter(|p| !p.is_empty());
let dir_prefix = norm_path
.as_deref()
.map(|p| format!("{}/%", escape_like(p)));
let mut categories = Vec::new();
let sql = "SELECT pattern_id, COUNT(*) AS cnt FROM structural_facts
WHERE (:path IS NULL OR replace(path, '\\', '/') = :path COLLATE NOCASE OR replace(path, '\\', '/') LIKE :dir_prefix ESCAPE '\\')
GROUP BY pattern_id ORDER BY cnt DESC";
let mut stmt = conn.prepare(sql)?;
let rows = stmt.query_map(
rusqlite::named_params! {
":path": norm_path.as_deref(),
":dir_prefix": dir_prefix.as_deref(),
},
|row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize)),
)?;
for r in rows {
categories.push(r?);
}
let lit_sql = "SELECT kind, COUNT(*) AS cnt FROM literals
WHERE (:path IS NULL OR replace(path, '\\', '/') = :path COLLATE NOCASE OR replace(path, '\\', '/') LIKE :dir_prefix ESCAPE '\\')
GROUP BY kind ORDER BY cnt DESC";
let mut lit_stmt = conn.prepare(lit_sql)?;
let lit_rows = lit_stmt.query_map(
rusqlite::named_params! {
":path": norm_path.as_deref(),
":dir_prefix": dir_prefix.as_deref(),
},
|row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize)),
)?;
for r in lit_rows {
categories.push(r?);
}
Ok(categories)
}
pub fn list_structural_fact_categories(
conn: &Connection,
) -> Result<Vec<(String, usize)>, QueryError> {
list_structural_fact_categories_scoped(conn, None)
}
pub fn find_type_facts(conn: &Connection, symbol_id: &str) -> Result<Vec<TypeFact>, QueryError> {
let has_table: bool = conn
.query_row(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='type_facts'",
[],
|_| Ok(true),
)
.unwrap_or(false);
if !has_table {
return Ok(Vec::new());
}
let mut stmt = conn.prepare(
"SELECT type_fact_id, symbol_id, language, resolved_type, generic_params_json
FROM type_facts
WHERE symbol_id = ?1",
)?;
let rows = stmt.query_map(params![symbol_id], |row| {
Ok(TypeFact {
type_fact_id: row.get(0)?,
symbol_id: row.get(1)?,
language: row.get(2)?,
resolved_type: row.get(3)?,
generic_params: row.get(4)?,
})
})?;
let mut results = Vec::new();
for r in rows {
results.push(r?);
}
Ok(results)
}
pub fn find_related_types(
conn: &Connection,
target: &Symbol,
limit: usize,
) -> Result<Vec<String>, QueryError> {
if !has_table(conn, "type_facts") {
return Ok(Vec::new());
}
let mut stmt = conn.prepare(
"SELECT d.name, d.kind, d.signature, d.path, d.start_line
FROM symbols d
WHERE d.kind IN ('struct', 'enum', 'trait', 'class', 'interface', 'type', 'union')
AND d.name IN (
SELECT t.resolved_type FROM type_facts t
JOIN symbols s ON s.symbol_id = t.symbol_id
WHERE s.symbol_id = ?1 OR s.parent_symbol_id = ?1
)
ORDER BY d.path = ?2 DESC, length(d.path), d.path, d.start_line",
)?;
let rows = stmt.query_map(params![target.symbol_id, target.path], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, Option<String>>(2)?,
row.get::<_, String>(3)?,
row.get::<_, i64>(4)?,
))
})?;
let mut seen = std::collections::HashSet::new();
let mut types = Vec::new();
for row in rows {
let (name, kind, signature, path, line) = row?;
if types.len() >= limit {
break;
}
if !seen.insert(name.clone()) {
continue;
}
let signature = signature.unwrap_or_else(|| format!("{kind} {name}"));
types.push(format!(
"{} ({path}:{line})",
type_declaration_line(&signature)
));
}
Ok(types)
}
fn type_declaration_line(signature: &str) -> String {
let mut declaration = signature;
while declaration.starts_with("#[")
&& let Some((_, rest)) = declaration.split_once("] ")
{
declaration = rest;
}
let line = declaration.split_whitespace().collect::<Vec<_>>().join(" ");
if line.chars().count() <= 120 {
return line;
}
line.chars().take(119).collect::<String>() + "…"
}
fn names_a_test(name: &str) -> bool {
let lowered = name.to_ascii_lowercase();
lowered.starts_with("test")
|| ["test", "tests", "spec", "specs"]
.iter()
.any(|suffix| lowered.ends_with(suffix))
}
pub fn is_test_path(path: &str) -> bool {
let p = path.replace('\\', "/");
let cut = p.rfind('/').map_or(0, |i| i + 1);
let directories = format!("/{}/", p[..cut].to_lowercase());
let file_name = &p[cut..];
let lower_name = file_name.to_lowercase();
directories.contains("/test/")
|| directories.contains("/tests/")
|| directories.contains("/autotests/")
|| directories.contains("/__tests__/")
|| lower_name.starts_with("tst_")
|| (lower_name.starts_with("test_")
&& (lower_name.ends_with(".py") || lower_name.ends_with(".rb")))
|| lower_name.contains("_test.")
|| lower_name.contains(".test.")
|| lower_name.contains(".spec.")
|| lower_name == "test.rs"
|| lower_name == "tests.rs"
|| file_name.ends_with("Tests.cs")
}
pub(crate) fn test_path_predicate(alias: &str) -> String {
let guard = format!(
"(lower({alias}.path) LIKE '%test%' OR lower({alias}.path) LIKE '%spec%' OR lower({alias}.path) LIKE '%tst\\_%' ESCAPE '\\')"
);
let p = format!("replace({alias}.path, '\\', '/')");
let directories = format!("'/' || lower(rtrim({p}, replace({p}, '/', ''))) || '/'");
let file_name = format!("replace({p}, rtrim({p}, replace({p}, '/', '')), '')");
let lower_name = format!("lower({file_name})");
let like = |subject: &String, pattern: &str| format!("{subject} LIKE '{pattern}' ESCAPE '\\'");
let clauses = [
like(&directories, "%/test/%"),
like(&directories, "%/tests/%"),
like(&directories, "%/autotests/%"),
like(&directories, "%/\\_\\_tests\\_\\_/%"),
like(&lower_name, "tst\\_%"),
like(&lower_name, "test\\_%.py"),
like(&lower_name, "test\\_%.rb"),
like(&lower_name, "%\\_test.%"),
like(&lower_name, "%.test.%"),
like(&lower_name, "%.spec.%"),
format!("{lower_name} = 'test.rs'"),
format!("{lower_name} = 'tests.rs'"),
format!("{file_name} GLOB '*Tests.cs'"),
]
.join(" OR ");
format!("({guard} AND ({clauses}))")
}
pub fn compute_blast_radius_scoped(
conn: &Connection,
seed_symbols: &[&str],
symbol_path_filter: Option<&str>,
seed_paths: &[&str],
max_depth: usize,
limit: usize,
) -> Result<BlastRadiusResult, QueryError> {
compute_blast_radius_scoped_with_ids(
conn,
seed_symbols,
&[],
symbol_path_filter,
seed_paths,
max_depth,
limit,
)
}
pub fn compute_blast_radius_scoped_with_ids(
conn: &Connection,
seed_symbols: &[&str],
seed_ids: &[&str],
symbol_path_filter: Option<&str>,
seed_paths: &[&str],
max_depth: usize,
limit: usize,
) -> Result<BlastRadiusResult, QueryError> {
validate_result_limit(limit)?;
let max_depth = max_depth.min(5);
let mut resolved_seed_symbols = seed_symbols
.iter()
.map(|name| {
get_symbol_by_name(conn, name, symbol_path_filter)?.ok_or_else(|| {
let (workspace, hint) = symbol_not_found_parts(conn, name, symbol_path_filter);
QueryError::SymbolNotFound {
name: (*name).to_string(),
workspace,
hint,
}
})
})
.collect::<Result<Vec<_>, _>>()?;
for id in seed_ids {
let symbol = get_symbol_by_id(conn, id)?.ok_or_else(|| {
let workspace = workspace_name(conn);
QueryError::SymbolNotFound {
name: format!("symbol id {id}"),
workspace,
hint: "Run lookup_symbol or search_symbols again and select a current id."
.to_string(),
}
})?;
resolved_seed_symbols.push(symbol);
}
let mut seeds = Vec::new();
let seed_type = if (!seed_symbols.is_empty() || !seed_ids.is_empty()) && !seed_paths.is_empty()
{
for s in seed_symbols {
seeds.push(s.to_string());
}
for id in seed_ids {
seeds.push(id.to_string());
}
for p in seed_paths {
seeds.push(p.to_string());
}
"mixed".to_string()
} else if !seed_symbols.is_empty() || !seed_ids.is_empty() {
for s in seed_symbols {
seeds.push(s.to_string());
}
for id in seed_ids {
seeds.push(id.to_string());
}
"symbol".to_string()
} else if !seed_paths.is_empty() {
for p in seed_paths {
seeds.push(p.to_string());
}
"file".to_string()
} else {
return Ok(BlastRadiusResult {
seed_type: "none".to_string(),
seeds: Vec::new(),
likely_tests: Vec::new(),
impacted_symbols: Vec::new(),
likely_tests_truncated: false,
impacted_symbols_truncated: false,
traversal_ceiling_reached: false,
test_file_ceiling_reached: false,
limit_at_maximum: false,
likely_tests_found: 0,
impacted_symbols_found: 0,
});
};
let mut where_clauses = Vec::new();
let mut params_vec: Vec<rusqlite::types::Value> = Vec::new();
if !resolved_seed_symbols.is_empty() {
let placeholders: Vec<String> = (1..=resolved_seed_symbols.len())
.map(|i| format!("?{}", i))
.collect();
where_clauses.push(format!("symbol_id IN ({})", placeholders.join(", ")));
for symbol in &resolved_seed_symbols {
params_vec.push(rusqlite::types::Value::Text(symbol.symbol_id.clone()));
}
}
if !seed_paths.is_empty() {
let mut path_conds = Vec::new();
for p in seed_paths.iter() {
let raw = p
.replace('\\', "/")
.trim_start_matches("./")
.trim_matches('/')
.to_string();
let exact_idx = params_vec.len() + 1;
params_vec.push(rusqlite::types::Value::Text(raw.clone()));
let dir_pattern = format!("{}/%", escape_like(&raw));
let like_idx = params_vec.len() + 1;
params_vec.push(rusqlite::types::Value::Text(dir_pattern));
path_conds.push(format!(
"replace(path, '\\', '/') = ?{exact_idx} COLLATE NOCASE OR replace(path, '\\', '/') LIKE ?{like_idx} ESCAPE '\\'"
));
}
where_clauses.push(format!("({})", path_conds.join(" OR ")));
}
let seed_condition = where_clauses.join(" OR ");
let max_depth_idx = params_vec.len() + 1;
params_vec.push(rusqlite::types::Value::Integer(max_depth as i64));
let mut traversal_ceiling_reached = false;
let mut test_file_ceiling_reached = false;
let has_relationships: bool = conn
.query_row(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='relationships'",
[],
|_| Ok(true),
)
.unwrap_or(false);
let has_pending: bool = conn
.query_row(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='pending_relationships'",
[],
|_| Ok(true),
)
.unwrap_or(false);
let mut likely_tests = Vec::new();
let mut impacted_symbols = Vec::new();
let mut seen_test_keys = HashSet::new();
let mut recursive_branches = Vec::new();
if has_relationships {
recursive_branches.push(format!(
"SELECT r.from_symbol_id, iw.depth + 1, iw.via
FROM relationships r
JOIN impact_walk iw ON r.to_symbol_id = iw.symbol_id
JOIN symbols s_from ON r.from_symbol_id = s_from.symbol_id
WHERE iw.depth < ?{max_depth_idx}
AND s_from.kind NOT IN ({LOW_SIGNAL_KINDS_SQL})"
));
}
if has_pending {
let (parent_join, ns_condition) = if conn
.query_row(
"SELECT 1 FROM pragma_table_info('pending_relationships') WHERE name='target_namespace_json'",
[],
|_| Ok(true),
)
.unwrap_or(false)
{
(
"LEFT JOIN symbols s_target_parent ON s_target.parent_symbol_id = s_target_parent.symbol_id
LEFT JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id",
format!("AND {pred}", pred = pending_target_predicate(conn, "s_target", "s_target_parent")),
)
} else {
("", String::new())
};
recursive_branches.push(format!(
"SELECT p.from_symbol_id, iw.depth + 1, iw.via
FROM pending_relationships p
JOIN symbols s_target ON p.target_terminal_name = s_target.name
JOIN impact_walk iw ON s_target.symbol_id = iw.symbol_id
{parent_join}
WHERE iw.depth < ?{max_depth_idx}
AND s_target.kind NOT IN ({LOW_SIGNAL_KINDS_SQL})
{ns_condition}"
));
}
if has_relationships {
recursive_branches.push(format!(
"SELECT r.from_symbol_id, iw.depth + 1, 1
FROM impact_walk iw
CROSS JOIN symbols ctor ON ctor.symbol_id = iw.symbol_id
JOIN relationships r ON r.to_symbol_id = ctor.parent_symbol_id
WHERE iw.depth < ?{max_depth_idx}
AND ctor.kind = 'constructor'
AND +r.kind = 'calls'"
));
}
if has_pending && has_pending_namespace_column(conn) {
recursive_branches.push(format!(
"SELECT p.from_symbol_id, iw.depth + 1, 1
FROM impact_walk iw
CROSS JOIN symbols ctor ON ctor.symbol_id = iw.symbol_id
CROSS JOIN symbols s_target ON s_target.symbol_id = ctor.parent_symbol_id
JOIN pending_relationships p ON p.target_terminal_name = s_target.name
LEFT JOIN symbols s_target_parent ON s_target.parent_symbol_id = s_target_parent.symbol_id
LEFT JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
WHERE iw.depth < ?{max_depth_idx}
AND ctor.kind = 'constructor'
AND s_target.kind IN ('class', 'struct', 'record')
AND +p.kind = 'calls'
AND {pred}",
pred = pending_target_predicate(conn, "s_target", "s_target_parent")
));
}
let lifecycle = if has_column(conn, "symbols", "test_lifecycle") {
"COALESCE(s.test_lifecycle, 0) != 0"
} else {
"0"
};
let mut fixtures = Vec::new();
let mut setups = Vec::new();
let mut entry_classes = Vec::new();
let mut constructor_only_test_keys = HashSet::new();
let mut non_constructor_test_keys = HashSet::new();
let mut walked: Vec<String> = resolved_seed_symbols
.iter()
.map(|symbol| symbol.symbol_id.clone())
.collect();
if !recursive_branches.is_empty() {
let recursive_sql = recursive_branches.join("\n UNION \n");
let not_documentation = not_documentation(conn, "s");
let sql = format!(
"WITH RECURSIVE impact_walk(symbol_id, depth, via) AS (
SELECT symbol_id, 0, 0
FROM symbols
WHERE ({seed_condition})
AND kind NOT IN ({LOW_SIGNAL_KINDS_SQL})
UNION
{recursive_sql}
)
SELECT s.symbol_id, s.name, s.kind, s.path, s.start_line, s.is_test, s.test_container,
MIN(iw.depth) as min_depth, MIN(iw.via) as min_via,
{lifecycle} AS is_fixture, s.parent_symbol_id,
EXISTS (SELECT 1 FROM symbols owner
WHERE owner.symbol_id = s.parent_symbol_id
AND COALESCE(owner.test_container, 0) != 0) AS in_test_class,
s.language, s.signature
FROM impact_walk iw
CROSS JOIN symbols s ON iw.symbol_id = s.symbol_id
WHERE s.kind NOT IN ({LOW_SIGNAL_KINDS_SQL})
AND {not_documentation}
GROUP BY s.symbol_id, s.name, s.kind, s.path, s.start_line, s.is_test, s.test_container
HAVING MIN(iw.depth) > 0
ORDER BY MIN(iw.via) ASC, min_depth ASC, s.path ASC, s.name ASC
LIMIT 201"
);
let mut stmt = conn.prepare(&sql)?;
let param_refs: Vec<&dyn rusqlite::ToSql> = params_vec
.iter()
.map(|v| v as &dyn rusqlite::ToSql)
.collect();
let rows = stmt.query_map(param_refs.as_slice(), |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
row.get::<_, i64>(4)? as usize,
row.get::<_, bool>(5)?,
row.get::<_, bool>(6)?,
row.get::<_, i64>(7)? as usize,
row.get::<_, i64>(8)? as usize,
row.get::<_, bool>(9)?,
row.get::<_, Option<String>>(10)?,
row.get::<_, bool>(11)?,
row.get::<_, String>(12)?,
row.get::<_, Option<String>>(13)?,
))
})?;
for (index, r) in rows.enumerate() {
if index == 200 {
traversal_ceiling_reached = true;
break;
}
let (
sym_id,
name,
kind,
raw_path,
line,
is_test,
test_container,
depth,
min_via,
is_fixture,
parent,
in_test_class,
language,
signature,
) = r?;
walked.push(sym_id);
let path = raw_path.replace('\\', "/");
let is_test_target = is_test
|| test_container
|| is_fixture
|| (is_test_path(&path) && (in_test_class || names_a_test(&name)));
let constructor_only = min_via > 0;
if name == "__call__"
&& let Some(parent) = &parent
{
entry_classes.push(parent.clone());
}
if is_test_target {
let key = format!("{}:{}", path, line);
let test_key = (path.clone(), line);
if constructor_only {
constructor_only_test_keys.insert(test_key);
} else {
non_constructor_test_keys.insert(test_key);
}
if seen_test_keys.insert(key) {
let lowered = name.to_ascii_lowercase();
let pytest_fixture = path.ends_with(".py")
&& !["setup", "teardown", "asyncsetup", "asyncteardown"]
.iter()
.any(|prefix| lowered.starts_with(prefix));
let caller = if depth == 1 {
"direct caller".to_string()
} else {
format!("indirect caller [depth {depth}]")
};
let reason = if is_fixture && pytest_fixture {
fixtures.push((name.clone(), path.clone(), line, constructor_only));
format!("fixture ({caller})")
} else if is_fixture {
if let Some(class_id) = parent {
setups.push((
name.clone(),
class_id,
path.clone(),
line,
constructor_only,
));
}
format!("setup ({caller})")
} else {
caller
};
likely_tests.push(TestTarget {
name,
path,
line,
reason,
});
}
} else {
let enclosing_symbol_name =
if is_generated_lambda_name(&name, &language, &kind, signature.as_deref()) {
match parent.as_deref() {
Some(parent_id) => {
get_symbol_by_id(conn, parent_id)?.map(|parent| parent.name)
}
None => None,
}
} else {
None
};
impacted_symbols.push(ImpactedSymbol {
name,
kind,
path,
line,
depth,
enclosing_symbol_name,
});
}
}
}
let mut replaced = HashSet::new();
for (fixture, fixture_path, line, constructor_only) in &fixtures {
let shown = fixture_name(conn, fixture, fixture_path);
let users = fixture_users(conn, fixture, fixture_path)?;
if !users.is_empty() {
replaced.insert((fixture_path.clone(), *line));
}
for test in users {
let test_key = (test.path.clone(), test.line);
if *constructor_only {
constructor_only_test_keys.insert(test_key);
} else {
non_constructor_test_keys.insert(test_key);
}
if seen_test_keys.insert(format!("{}:{}", test.path, test.line)) {
likely_tests.push(TestTarget {
reason: format!("uses fixture `{shown}`"),
..test
});
}
}
}
for (setup, class_id, setup_path, line, constructor_only) in &setups {
let tests = tests_in_class(conn, class_id)?;
if !tests.is_empty() {
replaced.insert((setup_path.clone(), *line));
}
for test in tests {
let test_key = (test.path.clone(), test.line);
if *constructor_only {
constructor_only_test_keys.insert(test_key);
} else {
non_constructor_test_keys.insert(test_key);
}
if seen_test_keys.insert(format!("{}:{}", test.path, test.line)) {
likely_tests.push(TestTarget {
reason: format!("setup `{setup}` runs before it"),
..test
});
}
}
}
likely_tests.retain(|test| !replaced.contains(&(test.path.clone(), test.line)));
constructor_only_test_keys.retain(|key| !non_constructor_test_keys.contains(key));
let mut test_name_terms = Vec::new();
let mut module_terms = Vec::new();
for path in seed_paths.iter().copied().chain(
resolved_seed_symbols
.iter()
.map(|symbol| symbol.path.as_str()),
) {
let mut parts = path.rsplit(['/', '\\']);
let file = parts.next().unwrap_or(path);
if let Some(stem) = std::path::Path::new(file)
.file_stem()
.and_then(|s| s.to_str())
&& stem.len() >= 3
&& !test_name_terms.iter().any(|(name, _)| name == stem)
{
test_name_terms.push((stem.to_string(), "stem-matched test file"));
}
let module = parts.next();
if parts.next() == Some("src")
&& let Some(module) = module
&& module.len() >= 3
&& !["test", "tests", "spec", "specs", "bin", "lib"]
.iter()
.any(|generic| module.eq_ignore_ascii_case(generic))
&& !test_name_terms.iter().any(|(name, _)| name == module)
&& !module_terms.iter().any(|name| name == module)
{
module_terms.push(module.to_string());
}
}
test_name_terms.extend(
module_terms
.into_iter()
.map(|module| (module, "module-matched test file")),
);
let has_files: bool = conn
.query_row(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='files'",
[],
|_| Ok(true),
)
.unwrap_or(false);
if has_files {
let doc_file = format!(
"EXISTS (SELECT 1 FROM symbols d WHERE d.path = files.path AND NOT {})",
not_documentation(conn, "d")
);
let slashed = "replace(path, '\\', '/')";
let file_name =
format!("replace({slashed}, rtrim({slashed}, replace({slashed}, '/', '')), '')");
let name_tokens =
format!("'_' || lower(replace(replace({file_name}, '-', '_'), '.', '_')) || '_'");
let names_or_holds_tests = format!(
"({name_tokens} LIKE '%\\_test\\_%' ESCAPE '\\' OR {name_tokens} LIKE '%\\_tests\\_%' ESCAPE '\\'
OR {name_tokens} LIKE '%\\_spec\\_%' ESCAPE '\\' OR {name_tokens} LIKE '%\\_specs\\_%' ESCAPE '\\'
OR {name_tokens} LIKE '%\\_tst\\_%' ESCAPE '\\'
OR {file_name} GLOB '*Test.*' OR {file_name} GLOB '*Tests.*'
OR {file_name} GLOB '*Spec.*' OR {file_name} GLOB '*Specs.*'
OR EXISTS (SELECT 1 FROM symbols t WHERE t.path = files.path AND t.is_test = 1))"
);
let mut test_files_stmt = conn.prepare(&format!(
"SELECT DISTINCT path FROM files
WHERE (path LIKE '%test%' OR path LIKE '%spec%') AND {file_name} LIKE ?1 ESCAPE '\\'
AND {names_or_holds_tests}
AND NOT {doc_file}
ORDER BY path ASC
LIMIT 201"
))?;
let mut module_test_files_stmt = conn.prepare(&format!(
"SELECT DISTINCT path FROM files
WHERE (path LIKE '%test%' OR path LIKE '%spec%')
AND (path LIKE ?1 ESCAPE '\\' OR path LIKE ?2 ESCAPE '\\')
AND {names_or_holds_tests}
AND NOT {doc_file}
ORDER BY path ASC
LIMIT 201"
))?;
for (term, reason) in test_name_terms {
let module_match = reason == "module-matched test file";
let escaped_term = escape_like(&term);
let mut rows = if module_match {
module_test_files_stmt.query(rusqlite::params![
format!("%/{escaped_term}%"),
format!("%/test\\_{escaped_term}%")
])?
} else {
test_files_stmt.query([format!("%{escaped_term}%")])?
};
let mut candidates = 0;
let mut matches = 0;
while let Some(row) = rows.next()? {
candidates += 1;
if candidates == 201 {
test_file_ceiling_reached = true;
break;
}
let p: String = row.get(0)?;
let basename = p.rsplit(['/', '\\']).next().unwrap_or_default();
if !module_match && !has_word_run(&file_words(basename), &file_words(&term)) {
continue;
}
if module_match {
let basename = p
.rsplit(['/', '\\'])
.next()
.unwrap_or_default()
.to_lowercase();
let term = term.to_lowercase();
if !basename.starts_with(&term)
&& !basename.starts_with(&format!("test_{term}"))
{
continue;
}
}
if matches == 10 {
test_file_ceiling_reached = true;
break;
}
matches += 1;
let p = p.replace('\\', "/");
let key = format!("{}:1", p);
if seen_test_keys.insert(key) {
likely_tests.push(TestTarget {
name: p.clone(),
path: p,
line: 1,
reason: reason.to_string(),
});
}
}
}
}
if has_table(conn, "relationships") {
let mut entry_callers = conn.prepare(
"SELECT DISTINCT caller.parent_symbol_id
FROM relationships r
JOIN symbols caller ON caller.symbol_id = r.from_symbol_id
WHERE caller.name = '__call__' AND caller.parent_symbol_id IS NOT NULL
AND r.to_symbol_id IN (SELECT value FROM json_each(?1))",
)?;
let walked_json = serde_json::to_string(&walked).unwrap_or_else(|_| "[]".into());
for class_id in entry_callers.query_map([walked_json], |row| row.get::<_, String>(0))? {
let class_id = class_id?;
if !entry_classes.contains(&class_id) {
entry_classes.push(class_id);
}
}
}
let mut seed_words: Vec<String> = Vec::new();
for symbol in &resolved_seed_symbols {
let callees =
find_references_for_symbol(conn, &symbol.name, "callees", 50, &symbol.symbol_id)
.unwrap_or_default();
for name in std::iter::once(symbol.name.as_str())
.chain(callees.iter().map(|site| site.to_symbol_name.as_str()))
{
for word in name_words(name) {
if !seed_words.contains(&word) && !GENERIC_NAME_WORDS.contains(&word.as_str()) {
seed_words.push(word);
}
}
}
}
let mut possible_tests = Vec::new();
for class_id in entry_classes {
for test in implicit_entry_tests(conn, &class_id, &seed_words)? {
if seen_test_keys.insert(format!("{}:{}", test.test.path, test.test.line)) {
possible_tests.push(test);
}
}
}
possible_tests.sort_by(|a, b| {
b.handler_evidence
.cmp(&a.handler_evidence)
.then_with(|| b.shared_word_score.cmp(&a.shared_word_score))
.then_with(|| a.test.path.cmp(&b.test.path))
.then_with(|| a.test.line.cmp(&b.test.line))
});
likely_tests.extend(possible_tests.into_iter().map(|test| test.test));
qualify_test_methods(conn, &mut likely_tests)?;
if seed_type == "file" {
let mut constructor_tests_by_file =
std::collections::BTreeMap::<String, Vec<TestTarget>>::new();
let mut other_tests = Vec::with_capacity(likely_tests.len());
for test in likely_tests.drain(..) {
if constructor_only_test_keys.contains(&(test.path.clone(), test.line)) {
constructor_tests_by_file
.entry(test.path.clone())
.or_default()
.push(test);
} else {
other_tests.push(test);
}
}
for (path, mut tests) in constructor_tests_by_file {
if tests.len() > 1 {
let count = tests.len();
other_tests.push(TestTarget {
name: path.clone(),
path,
line: 1,
reason: format!("constructor-only callers ({count} targets)"),
});
} else if let Some(test) = tests.pop() {
other_tests.push(test);
}
}
likely_tests = other_tests;
}
let whole_files: HashSet<String> = likely_tests
.iter()
.filter(|test| test.reason.ends_with("matched test file"))
.map(|test| test.path.clone())
.collect();
likely_tests.retain(|test| {
test.reason.ends_with("matched test file") || !whole_files.contains(&test.path)
});
likely_tests.sort_by_key(|test| {
if test.reason.ends_with("matched test file") {
0
} else if test.reason.starts_with("possible:") {
2
} else {
1
}
});
let likely_tests_found = likely_tests.len();
let impacted_symbols_found = impacted_symbols.len();
let likely_tests_truncated = likely_tests.len() > limit;
let impacted_symbols_truncated = impacted_symbols.len() > limit;
if likely_tests.len() > limit {
likely_tests.truncate(limit);
}
if impacted_symbols.len() > limit {
impacted_symbols.truncate(limit);
}
Ok(BlastRadiusResult {
seed_type,
seeds,
likely_tests,
impacted_symbols,
likely_tests_truncated,
impacted_symbols_truncated,
traversal_ceiling_reached,
test_file_ceiling_reached,
limit_at_maximum: limit >= MAX_RESULT_LIMIT,
likely_tests_found,
impacted_symbols_found,
})
}
fn qualify_test_methods(conn: &Connection, tests: &mut [TestTarget]) -> Result<(), QueryError> {
let mut stmt = conn.prepare(
"SELECT class.name FROM symbols t
JOIN symbols class ON class.symbol_id = t.parent_symbol_id
WHERE replace(t.path, '\\', '/') = ?1 AND t.start_line = ?2 AND t.name = ?3
AND class.kind = 'class'
LIMIT 1",
)?;
for test in tests.iter_mut().filter(|test| test.name != test.path) {
if let Some(class) = stmt
.query_row(params![test.path, test.line as i64, test.name], |row| {
row.get::<_, String>(0)
})
.optional()?
{
test.name = format!("{class}::{}", test.name);
}
}
Ok(())
}
fn fixture_name(conn: &Connection, function: &str, path: &str) -> String {
let signature: Option<String> = conn
.query_row(
"SELECT signature FROM symbols
WHERE name = ?1 AND replace(path, '\\', '/') = ?2 AND signature LIKE '%fixture(%'
LIMIT 1",
params![function, path],
|row| row.get(0),
)
.optional()
.ok()
.flatten();
signature
.as_deref()
.and_then(declared_fixture_name)
.unwrap_or_else(|| function.to_string())
}
fn declared_fixture_name(signature: &str) -> Option<String> {
let args = &signature[signature.find("fixture(")? + "fixture(".len()..];
let args = &args[..args.find(')').unwrap_or(args.len())];
let value = args[args.find("name=")? + "name=".len()..].trim_start();
let quote = value.chars().next().filter(|c| *c == '"' || *c == '\'')?;
let end = value[1..].find(quote)?;
Some(value[1..1 + end].to_string())
}
fn fixture_users(
conn: &Connection,
name: &str,
fixture_path: &str,
) -> Result<Vec<TestTarget>, QueryError> {
let lifecycle = if has_column(conn, "symbols", "test_lifecycle") {
"COALESCE(t.test_lifecycle, 0) != 0"
} else {
"0"
};
let mut stmt = conn.prepare(&format!(
"SELECT DISTINCT t.name, t.path, t.start_line, {lifecycle}
FROM symbols parameter
JOIN symbols t ON t.symbol_id = parameter.parent_symbol_id
WHERE parameter.name = ?1
AND (parameter.kind = 'parameter'
OR (parameter.kind = 'variable' AND parameter.start_byte < t.body_start_byte))
AND t.is_test = 1
AND replace(t.path, '\\', '/') LIKE ?2 ESCAPE '\\'
ORDER BY t.path, t.start_line
LIMIT 2000"
))?;
let mut users = Vec::new();
let mut listed = HashSet::new();
let mut visited = HashSet::new();
let mut pending = vec![(name.to_string(), fixture_path.to_string())];
while let Some((function, fixture_path)) = pending.pop() {
if !visited.insert((function.clone(), fixture_path.clone())) {
continue;
}
let name = fixture_name(conn, &function, &fixture_path);
let scope = match fixture_path.rsplit_once('/') {
Some((dir, "conftest.py")) => format!("{}/%", escape_like(dir)),
None if fixture_path == "conftest.py" => "%".to_string(),
_ => escape_like(&fixture_path),
};
let rows = stmt.query_map(params![name, scope], |row| {
Ok((
TestTarget {
name: row.get(0)?,
path: row.get::<_, String>(1)?.replace('\\', "/"),
line: row.get::<_, i64>(2)? as usize,
reason: String::new(),
},
row.get::<_, bool>(3)?,
))
})?;
for row in rows {
let (user, is_fixture) = row?;
if is_fixture {
pending.push((user.name, user.path));
} else if listed.insert((user.path.clone(), user.line)) {
users.push(user);
}
}
}
Ok(users)
}
fn tests_in_class(conn: &Connection, class_id: &str) -> Result<Vec<TestTarget>, QueryError> {
let not_lifecycle = if has_column(conn, "symbols", "test_lifecycle") {
"COALESCE(test_lifecycle, 0) = 0"
} else {
"1"
};
let mut stmt = conn.prepare(&format!(
"SELECT name, path, start_line FROM symbols
WHERE parent_symbol_id = ?1 AND is_test = 1 AND {not_lifecycle}
ORDER BY start_line"
))?;
let rows = stmt.query_map([class_id], |row| {
Ok(TestTarget {
name: row.get(0)?,
path: row.get::<_, String>(1)?.replace('\\', "/"),
line: row.get::<_, i64>(2)? as usize,
reason: String::new(),
})
})?;
Ok(rows.collect::<Result<_, _>>()?)
}
fn file_words(name: &str) -> Vec<String> {
let mut words = Vec::new();
let mut word = String::new();
let mut previous_lower = false;
for c in name.chars() {
if (!c.is_alphanumeric() || (c.is_uppercase() && previous_lower)) && !word.is_empty() {
words.push(std::mem::take(&mut word).to_lowercase());
}
if c.is_alphanumeric() {
word.push(c);
}
previous_lower = c.is_lowercase() || c.is_ascii_digit();
}
if !word.is_empty() {
words.push(word.to_lowercase());
}
words
}
fn has_word_run(words: &[String], run: &[String]) -> bool {
!run.is_empty() && words.windows(run.len()).any(|window| window == run)
}
fn name_words(name: &str) -> Vec<String> {
let mut words = Vec::new();
let mut word = String::new();
let mut previous_lower = false;
for c in name.chars() {
if !c.is_alphanumeric() || (c.is_uppercase() && previous_lower) {
if word.len() >= 3 {
words.push(word.to_lowercase());
}
word.clear();
}
if c.is_alphanumeric() {
word.push(c);
}
previous_lower = c.is_lowercase();
}
if word.len() >= 3 {
words.push(word.to_lowercase());
}
words
}
const GENERIC_NAME_WORDS: &[&str] = &[
"get", "set", "new", "the", "and", "for", "self", "init", "ensure", "sync", "find", "make",
"create", "load", "call", "add",
];
fn common_prefix_len(a: &str, b: &str) -> usize {
a.chars().zip(b.chars()).take_while(|(x, y)| x == y).count()
}
fn shared_words(seed_words: &[String], test: &TestTarget) -> (usize, Vec<String>) {
let mut found: Vec<String> = Vec::new();
let mut score = 0;
let file = test.path.rsplit('/').next().unwrap_or(&test.path);
for text in [test.name.as_str(), file] {
let words = name_words(text);
for seed in seed_words {
if words.iter().any(|word| {
word.starts_with(seed.as_str())
|| seed.starts_with(word.as_str())
|| common_prefix_len(word, seed) >= 5
}) {
score += 1;
if !found.contains(seed) {
found.push(seed.clone());
}
}
}
}
(score, found)
}
#[derive(Default)]
struct ImplicitHandlerEvidence {
registers_error_handler: bool,
calls_abort: bool,
}
impl ImplicitHandlerEvidence {
fn is_relevant(&self) -> bool {
self.registers_error_handler || self.calls_abort
}
}
struct RankedImplicitTest {
handler_evidence: bool,
shared_word_score: usize,
test: TestTarget,
}
fn implicit_handler_evidence(
conn: &Connection,
test: &TestTarget,
) -> Result<ImplicitHandlerEvidence, QueryError> {
let mut call_sources = Vec::new();
if has_table(conn, "pending_relationships") {
call_sources.push(
"SELECT lower(p.target_terminal_name) FROM candidate_scope d\n\
CROSS JOIN pending_relationships p ON p.from_symbol_id = d.symbol_id\n\
WHERE p.kind = 'calls'"
.to_string(),
);
}
if has_table(conn, "relationships") {
call_sources.push(
"SELECT lower(target.name) FROM candidate_scope d\n\
CROSS JOIN relationships r ON r.from_symbol_id = d.symbol_id\n\
JOIN symbols target ON target.symbol_id = r.to_symbol_id\n\
WHERE r.kind = 'calls'"
.to_string(),
);
}
if call_sources.is_empty() {
return Ok(ImplicitHandlerEvidence::default());
}
let sql = format!(
r#"WITH RECURSIVE candidate_scope(symbol_id) AS (
SELECT symbol_id FROM symbols
WHERE replace(path, char(92), '/') = ?1 AND start_line = ?2 AND name = ?3
UNION
SELECT child.symbol_id FROM symbols child
JOIN candidate_scope parent ON child.parent_symbol_id = parent.symbol_id
), candidate_calls(name) AS (
{}
)
SELECT
EXISTS (SELECT 1 FROM candidate_calls
WHERE name IN ('errorhandler', 'register_error_handler')),
EXISTS (SELECT 1 FROM candidate_calls WHERE name = 'abort')"#,
call_sources.join("\nUNION ALL\n")
);
conn.query_row(
&sql,
params![test.path, test.line as i64, test.name],
|row| {
Ok(ImplicitHandlerEvidence {
registers_error_handler: row.get(0)?,
calls_abort: row.get(1)?,
})
},
)
.map_err(QueryError::from)
}
fn implicit_entry_tests(
conn: &Connection,
class_id: &str,
seed_words: &[String],
) -> Result<Vec<RankedImplicitTest>, QueryError> {
let Some(class) = get_symbol_by_id(conn, class_id)? else {
return Ok(Vec::new());
};
let lifecycle = if has_column(conn, "symbols", "test_lifecycle") {
"COALESCE(test_lifecycle, 0) != 0"
} else {
"0"
};
let mut classes = vec![class];
let mut sites_by_class = Vec::new();
let mut next = 0;
while next < classes.len() && classes.len() <= 50 {
let built = classes[next].clone();
next += 1;
let sites =
find_references_for_symbol(conn, &built.name, "callers", 200, &built.symbol_id)?;
for site in sites.iter().filter(|site| site.kind == "extends") {
if !classes.iter().any(|c| c.symbol_id == site.from_symbol_id)
&& let Some(subclass) = get_symbol_by_id(conn, &site.from_symbol_id)?
{
classes.push(subclass);
}
}
sites_by_class.push((built, sites));
}
let mut builders = Vec::new();
for (built, sites) in &sites_by_class {
for site in sites {
if site.kind != "calls" {
continue;
}
let Some(builder) = get_symbol_by_id(conn, &site.from_symbol_id)? else {
continue;
};
if !(builder.is_test || is_test_path(&builder.path)) {
continue;
}
let is_fixture: bool = conn
.query_row(
&format!("SELECT {lifecycle} FROM symbols WHERE symbol_id = ?1"),
[&builder.symbol_id],
|row| row.get(0),
)
.unwrap_or(false);
if is_fixture {
let via = format!(
"builds `{}` through fixture `{}`",
built.name,
fixture_name(conn, &builder.name, &builder.path)
);
builders.extend(
fixture_users(conn, &builder.name, &builder.path)?
.into_iter()
.map(|test| TestTarget {
reason: via.clone(),
..test
}),
);
} else if builder.is_test {
builders.push(TestTarget {
name: builder.name,
path: builder.path,
line: builder.start_line,
reason: format!("builds `{}`", built.name),
});
}
}
}
let mut ranked = Vec::new();
for test in builders
.into_iter()
.filter(|test| uses_a_client(conn, test))
{
let evidence = implicit_handler_evidence(conn, &test)?;
let (score, words) = shared_words(seed_words, &test);
if score == 0 && !evidence.is_relevant() {
continue;
}
let mut reasons = Vec::new();
if evidence.registers_error_handler {
reasons.push("registers an error handler".to_string());
}
if evidence.calls_abort {
reasons.push("calls `abort`".to_string());
}
if score > 0 {
let shared = words
.iter()
.map(|word| format!("`{word}`"))
.collect::<Vec<_>>()
.join(", ");
reasons.push(format!("shares {shared}"));
}
let reason = format!(
"possible: {}, and a test client calls its `__call__`, which can reach the target; {}",
test.reason,
reasons.join("; ")
);
ranked.push(RankedImplicitTest {
handler_evidence: evidence.is_relevant(),
shared_word_score: score,
test: TestTarget { reason, ..test },
});
}
Ok(ranked)
}
fn uses_a_client(conn: &Connection, test: &TestTarget) -> bool {
conn.query_row(
"SELECT EXISTS (
SELECT 1 FROM symbols t
WHERE t.name = ?3 AND t.start_line = ?2 AND replace(t.path, '\\', '/') = ?1
AND (EXISTS (SELECT 1 FROM symbols p
WHERE p.parent_symbol_id = t.symbol_id
AND p.kind IN ('parameter', 'variable')
AND lower(p.name) LIKE '%client%')
OR EXISTS (SELECT 1 FROM pending_relationships pr
WHERE pr.from_symbol_id = t.symbol_id
AND (lower(pr.target_terminal_name) LIKE '%client%'
OR lower(COALESCE(pr.target_receiver, '')) LIKE '%client%'))
OR EXISTS (SELECT 1 FROM relationships r
JOIN symbols callee ON callee.symbol_id = r.to_symbol_id
WHERE r.from_symbol_id = t.symbol_id
AND lower(callee.name) LIKE '%client%')))",
params![test.path, test.line as i64, test.name],
|row| row.get(0),
)
.unwrap_or(false)
}
pub fn compute_blast_radius(
conn: &Connection,
seed_symbols: &[&str],
seed_paths: &[&str],
max_depth: usize,
limit: usize,
) -> Result<BlastRadiusResult, QueryError> {
compute_blast_radius_scoped(conn, seed_symbols, None, seed_paths, max_depth, limit)
}
#[cfg(test)]
mod tests {
#[test]
fn a_type_declaration_drops_attributes_and_fits_on_one_short_line() {
assert_eq!(
super::type_declaration_line(
"#[derive(Debug)] #[serde(tag = \"t\")] pub struct Workspace"
),
"pub struct Workspace"
);
let record = format!(
"public sealed record Node(\n{})",
" string Id,\n".repeat(20)
);
let line = super::type_declaration_line(&record);
assert!(
line.starts_with("public sealed record Node( string Id, string Id,"),
"{line}"
);
assert_eq!(line.chars().count(), 120);
assert!(line.ends_with('…'));
}
#[test]
fn a_mount_names_what_it_mounts_and_where() {
let mount = |capture: &str, json: &str| {
super::mount_display(capture, &serde_json::from_str(json).unwrap())
};
assert_eq!(
mount(
"blueprint_registration",
r#"{"framework":"flask","mount_target":"auth.bp","query_family":"framework"}"#
)
.as_deref(),
Some("blueprint_registration `auth.bp`")
);
assert_eq!(
mount(
"mount",
r#"{"mount_path":"/jobs","mount_target":"Sidekiq::Web","normalized_mount_path":"/admin/jobs","query_family":"framework"}"#
)
.as_deref(),
Some("mount `Sidekiq::Web` at /admin/jobs")
);
assert_eq!(
mount(
"property",
r#"{"mount_target":"x","query_family":"metadata"}"#
),
None
);
}
#[test]
fn a_route_shows_its_source_template_and_keeps_a_joined_prefix() {
let route = |json: &str| super::route_display(&serde_json::from_str(json).unwrap());
assert_eq!(
route(
r#"{"verb":"GET","route_template":"/<int:id>/update","normalized_route_template":"/:id/update"}"#
),
Some("GET /<int:id>/update".into())
);
assert_eq!(
route(
r#"{"verb":"POST","route_template":"/register","effective_route_template":"/auth/register","normalized_route_template":"/auth/register"}"#
),
Some("POST /auth/register".into())
);
assert_eq!(
route(
r#"{"verb":"GET","route_template":"/users/{id}","normalized_route_template":"/api/v1/users/:id"}"#
),
Some("GET /api/v1/users/:id".into())
);
assert_eq!(route(r#"{"key":"x"}"#), None);
}
#[test]
fn a_fixture_declared_with_a_name_is_used_by_that_name() {
assert_eq!(
super::declared_fixture_name("@pytest.fixture(name=\"async_app\") def _async_app()"),
Some("async_app".into())
);
assert_eq!(
super::declared_fixture_name(
"@pytest.fixture(scope='session', name='db') def make_db()"
),
Some("db".into())
);
assert_eq!(
super::declared_fixture_name("@pytest.fixture def app()"),
None
);
assert_eq!(
super::declared_fixture_name("@pytest.fixture(params=[1]) def f(name='x')"),
None
);
}
#[test]
fn a_stem_matches_whole_words_of_a_test_file_name() {
let matches = |file: &str, stem: &str| {
super::has_word_run(&super::file_words(file), &super::file_words(stem))
};
assert!(matches("test_app.py", "app"));
assert!(matches("AppTest.java", "App"));
assert!(matches("test_user_error_handler.py", "user_error_handler"));
assert!(matches("widget_test_3.rs", "widget"));
assert!(!matches("test_appctx.py", "app"));
assert!(!matches("test_mapper.py", "app"));
}
#[test]
fn a_test_with_a_client_is_found_under_a_backslash_path() {
let conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE symbols (symbol_id TEXT, name TEXT, path TEXT, start_line INTEGER,
parent_symbol_id TEXT, kind TEXT);
CREATE TABLE pending_relationships (from_symbol_id TEXT, target_terminal_name TEXT,
target_receiver TEXT);
CREATE TABLE relationships (from_symbol_id TEXT, to_symbol_id TEXT);
INSERT INTO symbols VALUES ('t', 'test_user', 'tests\\test_user.py', 4, NULL, 'function');
INSERT INTO symbols VALUES ('c', 'client', 'tests\\test_user.py', 4, 't', 'parameter');",
)
.unwrap();
let test = crate::models::TestTarget {
name: "test_user".into(),
path: "tests/test_user.py".into(),
line: 4,
reason: String::new(),
};
assert!(super::uses_a_client(&conn, &test));
}
#[test]
fn result_limit_rejects_values_above_the_shared_ceiling() {
assert!(validate_result_limit(MAX_RESULT_LIMIT).is_ok());
assert!(matches!(
validate_result_limit(usize::MAX),
Err(QueryError::InvalidResultLimit(usize::MAX))
));
}
#[test]
fn find_references_rejects_an_unbounded_limit_before_sql_execution() {
let conn = Connection::open_in_memory().unwrap();
assert!(matches!(
find_references_scoped(&conn, "target", "callers", usize::MAX, false, None),
Err(QueryError::InvalidResultLimit(usize::MAX))
));
}
use super::*;
use crate::db::{ensure_fts_index, open_read_write};
#[test]
fn count_parse_diagnostics_counts_rows_for_one_file() {
let dir = crate::safe_tempdir();
let conn = open_read_write(&dir.path().join("parse_diagnostics.db")).unwrap();
assert_eq!(count_parse_diagnostics(&conn, "src/lib.rs"), 0);
conn.execute_batch(
"CREATE TABLE parse_diagnostics (
diagnostic_id TEXT, file_id TEXT, path TEXT, language TEXT, kind TEXT
);
INSERT INTO parse_diagnostics VALUES ('d1', 'f1', 'src/lib.rs', 'rust', 'error');
INSERT INTO parse_diagnostics VALUES ('d2', 'f1', 'src/lib.rs', 'rust', 'error');
INSERT INTO parse_diagnostics VALUES ('d3', 'f2', 'src/other.rs', 'rust', 'error');",
)
.unwrap();
assert_eq!(count_parse_diagnostics(&conn, "src/lib.rs"), 2);
assert_eq!(count_parse_diagnostics(&conn, "src\\lib.rs"), 2);
assert_eq!(count_parse_diagnostics(&conn, "src/clean.rs"), 0);
}
#[test]
fn test_sanitize_fts5_query() {
let (and_q, or_q) = sanitize_fts5_query("parse tokens");
assert_eq!(and_q, "(\"parse\"* AND \"tokens\"*) OR \"parsetokens\"*");
assert_eq!(or_q, "\"parse\"* OR \"tokens\"* OR \"parsetokens\"*");
let (and_q, or_q) = sanitize_fts5_query(" Option<T> ");
assert_eq!(and_q, "(\"Option\"* AND \"T\") OR \"OptionT\"*");
assert_eq!(or_q, "\"Option\"* OR \"T\" OR \"OptionT\"*");
let (and_q, or_q) = sanitize_fts5_query(" ");
assert!(and_q.is_empty());
assert!(or_q.is_empty());
}
#[test]
fn sanitize_splits_case_boundaries_and_drops_stop_words() {
let (and_q, or_q) = sanitize_fts5_query("ValidateSyntax");
assert_eq!(
and_q,
"((\"Validate\"* \"Syntax\"*) OR \"ValidateSyntax\"*)"
);
assert_eq!(or_q, "\"Validate\"* OR \"Syntax\"* OR \"ValidateSyntax\"*");
let (and_q, _) = sanitize_fts5_query("find tests related to a symbol");
assert_eq!(
and_q,
"\"find\"* AND \"tests\"* AND \"related\"* AND \"symbol\"*"
);
let (and_q, or_q) = sanitize_fts5_query("parseHTTPResponse2");
assert_eq!(
and_q,
"((\"parse\"* \"HTTP\"* \"Response\"* \"2\") OR \"parseHTTPResponse2\"*)"
);
assert!(or_q.ends_with("OR \"parseHTTPResponse2\"*"));
let (and_q, _) = sanitize_fts5_query("validate_syntax");
assert_eq!(
and_q,
"((\"validate\"* \"syntax\"*) OR \"validate_syntax\"*)"
);
let (and_q, _) = sanitize_fts5_query("isReady");
assert_eq!(and_q, "((\"Ready\"*) OR \"isReady\"*)");
let (and_q, _) = sanitize_fts5_query("before");
assert_eq!(and_q, "\"before\"*");
let (and_q, _) = sanitize_fts5_query("fooBar quux");
assert_eq!(
and_q,
"(((\"foo\"* \"Bar\"*) OR \"fooBar\"*) AND \"quux\"*) OR \"fooBarquux\"*"
);
let (and_q, _) = sanitize_fts5_query("the for a");
assert_eq!(and_q, "(\"the\"* AND \"for\"* AND \"a\") OR \"thefora\"*");
}
fn search_fixture(rows: &str) -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(&format!(
"CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT,
kind TEXT, signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER, content_type TEXT
);
INSERT INTO symbols VALUES {rows};"
))
.unwrap();
ensure_fts_index(&conn).unwrap();
conn
}
fn code_row(id: &str, path: &str, language: &str, name: &str, doc: &str) -> String {
format!(
"('{id}', 'f_{id}', '{path}', '{language}', '{name}', 'function', 'fn {name}()', '{doc}', 'pub', NULL,
10, 0, 20, 1, 100, 250, 12, 4, 19, 1, 120, 240, 'h_{id}', NULL, 0, 0, 'code')"
)
}
fn doc_row(id: &str, name: &str, doc: &str) -> String {
format!(
"('{id}', 'f_{id}', 'docs/{id}.md', 'markdown', '{name}', 'module', '{name}', '{doc}', NULL, NULL,
3, 0, 3, 1, 10, 40, NULL, NULL, NULL, NULL, NULL, NULL, 'h_{id}', NULL, 0, 0, 'documentation')"
)
}
#[test]
fn ranked_owner_context_recovers_members_and_enriches_existing_rows() {
let mut rows = vec![
code_row(
"planner",
"src/planner.rs",
"rust",
"EditPlanner",
"Plans edit operations for a function body",
),
code_row(
"target",
"src/planner.rs",
"rust",
"ReplaceBodyImplementation",
"",
),
code_row("existing", "src/planner.rs", "rust", "Body", ""),
code_row(
"unrelated",
"src/planner.rs",
"rust",
"SaveConfiguration",
"",
),
];
rows.extend((0..200).map(|i| {
code_row(
&format!("noise{i}"),
"src/noise.rs",
"rust",
&format!("body{i:03}"),
"edit planner body",
)
}));
let conn = search_fixture(&rows.join(","));
conn.execute_batch(
"UPDATE symbols SET kind = 'class' WHERE symbol_id = 'planner';
UPDATE symbols SET kind = 'method', parent_symbol_id = 'planner'
WHERE symbol_id IN ('target', 'existing', 'unrelated');",
)
.unwrap();
let ordinary =
collect_search_candidates(&conn, "edit planner body", None, None, false, 200).unwrap();
assert!(
!ordinary
.iter()
.any(|c| c.result.symbol.symbol_id == "target")
);
assert!(
ordinary
.iter()
.any(|c| c.result.symbol.symbol_id == "existing")
);
let found =
fts_search_symbols_explained(&conn, "edit planner body", None, None, false, 200, true)
.unwrap();
for id in ["target", "existing"] {
let members: Vec<_> = found.iter().filter(|r| r.symbol.symbol_id == id).collect();
assert_eq!(members.len(), 1);
let explain = members[0].explain.as_ref().unwrap();
assert!(explain.branches.iter().any(|b| b == "owner_context"));
for word in ["edit", "planner"] {
assert!(
explain
.terms
.contains(&(word.into(), "owner_context".into(), 0.5))
);
}
assert!(explain.terms.contains(&("body".into(), "name".into(), 3.0)));
}
assert!(!found.iter().any(|r| r.symbol.symbol_id == "unrelated"));
for exact in ["EditPlanner", "Body"] {
assert_eq!(search_names(&conn, exact)[0], exact);
}
let scoped = fts_search_symbols_scoped(
&conn,
"edit planner body",
None,
Some("src/noise.rs"),
false,
200,
)
.unwrap();
assert!(scoped.iter().all(|r| r.symbol.path == "src/noise.rs"));
let methods =
fts_search_symbols_scoped(&conn, "edit planner body", Some("method"), None, false, 200)
.unwrap();
assert!(methods.iter().all(|r| r.symbol.kind == "method"));
assert!(methods.iter().all(|r| r.explain.is_none()));
}
#[test]
fn ranked_owner_expansion_is_bounded_and_preserves_member_filters() {
let mut rows = Vec::new();
for owner in 0..4 {
rows.push(code_row(
&format!("o{owner}"),
"src/lib.rs",
"rust",
&format!("Planner{owner}"),
"body planner",
));
for member in 0..41 {
rows.push(code_row(
&format!("o{owner}m{member:02}"),
"src/lib.rs",
"rust",
&format!("Body{member:02}"),
"",
));
}
}
rows.push(doc_row("docowner", "PlannerDocs", "body planner"));
rows.push(code_row(
"docchild",
"src/lib.rs",
"rust",
"BodyFromDocs",
"",
));
rows.push(code_row(
"testchild",
"tests/lib.rs",
"rust",
"BodyTest",
"",
));
rows.push(code_row(
"linkchild",
"docs/link.md",
"markdown",
"BodyLink",
"",
));
rows.push(code_row(
"nestedchild",
"src/lib.rs",
"rust",
"BodyNested",
"",
));
let conn = search_fixture(&rows.join(","));
for owner in 0..4 {
conn.execute(
"UPDATE symbols SET kind = 'class' WHERE symbol_id = ?1",
[format!("o{owner}")],
)
.unwrap();
conn.execute(
"UPDATE symbols SET kind = 'method', parent_symbol_id = ?1 WHERE symbol_id LIKE ?2",
params![format!("o{owner}"), format!("o{owner}m%")],
)
.unwrap();
}
conn.execute_batch(
"UPDATE symbols SET kind = 'class' WHERE symbol_id = 'docowner';
UPDATE symbols SET parent_symbol_id = 'docowner' WHERE symbol_id = 'docchild';
UPDATE symbols SET parent_symbol_id = 'o0' WHERE symbol_id IN ('testchild', 'linkchild');
UPDATE symbols SET kind = 'import' WHERE symbol_id = 'linkchild';
UPDATE symbols SET parent_symbol_id = 'o0m00' WHERE symbol_id = 'nestedchild';",
).unwrap();
let mut candidates =
collect_search_candidates(&conn, "planner body", None, None, false, 200).unwrap();
candidates.retain(|c| c.result.symbol.parent_symbol_id.is_none());
let ranked = rerank_with(candidates.clone(), "planner body", false, None);
assert!(
expand_ranked_owner_members(
&conn,
&mut candidates,
&ranked,
"planner body",
None,
None,
false
)
.unwrap()
);
let expanded: Vec<_> = candidates
.iter()
.filter(|c| c.owner_context.is_some())
.collect();
assert_eq!(expanded.len(), 120);
for owner in 0..3 {
assert_eq!(
expanded
.iter()
.filter(|c| c.result.symbol.parent_symbol_id.as_deref()
== Some(&format!("o{owner}")))
.count(),
40
);
assert!(
!expanded
.iter()
.any(|c| c.result.symbol.symbol_id == format!("o{owner}m40"))
);
}
for excluded in ["docchild", "testchild", "linkchild", "nestedchild"] {
assert!(
!expanded
.iter()
.any(|c| c.result.symbol.symbol_id == excluded)
);
}
let mut late_ranked = rerank_with(
(0..20).map(|i| function(&format!("body{i}"))).collect(),
"body",
false,
None,
);
late_ranked.push(ranked[0].clone());
assert!(
!expand_ranked_owner_members(
&conn,
&mut Vec::new(),
&late_ranked,
"planner body",
None,
None,
false
)
.unwrap()
);
}
#[test]
fn lookup_and_search_skip_documentation_links_recorded_as_imports() {
let conn = search_fixture(
&[
"('i1', 'f_i1', 'src/app.py', 'python', 'Flask', 'import', 'from flask import Flask', NULL, NULL, NULL, 1, 0, 1, 20, 0, 20, NULL, NULL, NULL, NULL, NULL, NULL, 'h_i1', NULL, 0, 0, 'code')".to_string(),
"('i2', 'f_i2', 'README.md', 'markdown', 'Flask', 'import', '[Flask](https://flask.dev)', NULL, NULL, NULL, 1, 0, 1, 20, 0, 20, NULL, NULL, NULL, NULL, NULL, NULL, 'h_i2', NULL, 0, 0, 'documentation')".to_string(),
]
.join(","),
);
let lookup: Vec<String> = search_symbols(&conn, "Flask", Some("import"), true, 10)
.unwrap()
.into_iter()
.map(|s| s.path)
.collect();
assert_eq!(lookup, vec!["src/app.py"]);
let search: Vec<String> =
fts_search_symbols_scoped(&conn, "Flask", Some("import"), None, true, 10)
.unwrap()
.into_iter()
.map(|r| r.symbol.path)
.collect();
assert_eq!(search, vec!["src/app.py"]);
}
#[test]
fn lookup_ranks_a_prefix_match_above_a_substring_match() {
let conn = search_fixture(
&[
code_row("c1", "tests/a.rs", "rust", "test_test_client", ""),
code_row("c2", "tests/b.rs", "rust", "test_client_open_environ", ""),
]
.join(","),
);
let names: Vec<String> = search_symbols(&conn, "test_cli", None, true, 10)
.unwrap()
.into_iter()
.map(|s| s.name)
.collect();
assert_eq!(names, vec!["test_client_open_environ", "test_test_client"]);
}
#[test]
fn lookup_ranks_a_declared_property_above_a_self_assignment() {
let property = |id: &str, path: &str, signature: &str| {
format!(
"('{id}', 'f_{id}', '{path}', 'python', 'debug', 'property', '{signature}', NULL, NULL, NULL, 1, 0, 1, 20, 0, 20, NULL, NULL, NULL, NULL, NULL, NULL, 'h_{id}', NULL, 0, 0, 'code')"
)
};
let conn = search_fixture(
&[
property("p1", "src/flask/app.py", "self.debug = get_debug_flag()"),
property(
"p2",
"src/flask/sansio/app.py",
"@property def debug(self) -> bool",
),
]
.join(","),
);
let paths: Vec<String> = search_symbols(&conn, "debug", None, false, 10)
.unwrap()
.into_iter()
.map(|s| s.path)
.collect();
assert_eq!(paths, vec!["src/flask/sansio/app.py", "src/flask/app.py"]);
}
fn search_names(conn: &Connection, query: &str) -> Vec<String> {
fts_search_symbols_scoped(conn, query, None, None, false, 10)
.unwrap()
.into_iter()
.map(|r| r.symbol.name)
.collect()
}
const TEST_PATH_CASES: &[(&str, bool)] = &[
("tests/foo.py", true),
("tests/x.py", true),
("tests/tools/test_web.py", true),
("src/tests/x.rs", true),
("__tests__/a.ts", true),
("a/__tests__/b.ts", true),
("test/x.java", true),
("src/test/Helper.java", true),
("src/test_utils.py", true),
("lib/test_helper.rb", true),
("test_config.py", true),
("pkg/test_data/x.json", false),
("src/test_detection.rs", false),
("autotests/tst_pagerow.qml", true),
("autotests/helper.qml", true),
("src/autotests/columnview.cpp", true),
("tst_foo.qml", true),
("src/tst_columnview.qml", true),
("autotests\\tst_bar.qml", true),
("autotests_helper/x.rs", false),
("src/autotest.rs", false),
("src/tstamp.rs", false),
("src/tst.rs", false),
("crates/julie-index/src/analysis/test_quality.rs", false),
("x/foo_test.go", true),
("x/foo.test.ts", true),
("x/foo.spec.js", true),
("src/lib_test.rs", true),
("src/test.rs", true),
("tests.rs", true),
("src/tests.rs", true),
("Foo.Tests.cs", true),
("x/FooTests.cs", true),
("src/FooTests.cs", true),
("src/Foo.Tests.cs", true),
("tests/Foo.cs", true),
("x/parser.spec.ts", true),
("test_x.py", true),
("test.rs", true),
("tests\\x.py", true),
("src/protocol.spec.v1/parser.rs", false),
("pkg/test_support/runtime.py", false),
("src/Contests.cs", false),
("spec/x.rb", false),
("crates/x/src/impact/likely_tests.rs", false),
("x/foo_tests.rs", false),
("src/latest.rs", false),
("x/latest.go", false),
("x/manifest.rs", false),
("src/attest.rs", false),
("contest/x.py", false),
("src/testing.rs", false),
("src/main.rs", false),
("pkg/service.go", false),
];
#[test]
fn test_path_rule_and_its_sql_mirror_agree_on_every_path() {
let conn = Connection::open_in_memory().unwrap();
let sql = format!(
"SELECT {} FROM (SELECT :path AS path) s",
test_path_predicate("s")
);
let mut stmt = conn.prepare(&sql).unwrap();
for (path, expected) in TEST_PATH_CASES {
assert_eq!(is_test_path(path), *expected, "rust rule: {path}");
let from_sql: bool = stmt
.query_row(rusqlite::named_params! { ":path": path }, |row| row.get(0))
.unwrap();
assert_eq!(from_sql, *expected, "sql mirror: {path}");
}
}
#[test]
fn unflagged_test_file_rows_are_hidden_unless_tests_are_included() {
let conn = search_fixture(
&[
code_row(
"a",
"src/parser.rs",
"rust",
"parse_sidecar",
"Parse a sidecar.",
),
code_row(
"b",
"src/tests/helpers.py",
"python",
"parse_sidecar_fixture",
"Parse a sidecar.",
),
]
.join(", "),
);
let default_search: Vec<String> =
fts_search_symbols_scoped(&conn, "parse sidecar", None, None, false, 10)
.unwrap()
.into_iter()
.map(|r| r.symbol.name)
.collect();
assert_eq!(default_search, vec!["parse_sidecar".to_string()]);
let with_tests: Vec<String> =
fts_search_symbols_scoped(&conn, "parse sidecar", None, None, true, 10)
.unwrap()
.into_iter()
.map(|r| r.symbol.name)
.collect();
assert!(with_tests.contains(&"parse_sidecar_fixture".to_string()));
let default_lookup: Vec<String> =
search_symbols_scoped(&conn, "parse_sidecar", None, None, false, 10)
.unwrap()
.into_iter()
.map(|s| s.name)
.collect();
assert_eq!(default_lookup, vec!["parse_sidecar".to_string()]);
let lookup_with_tests: Vec<String> =
search_symbols_scoped(&conn, "parse_sidecar", None, None, true, 10)
.unwrap()
.into_iter()
.map(|s| s.name)
.collect();
assert!(lookup_with_tests.contains(&"parse_sidecar_fixture".to_string()));
}
#[test]
fn count_file_symbols_prefers_the_exact_case_path_like_the_loader() {
let conn = search_fixture(
&[
code_row("a", "src/Foo.rs", "rust", "one", ""),
code_row("b", "src/foo.rs", "rust", "two", ""),
code_row("c", "src/foo.rs", "rust", "three", ""),
]
.join(", "),
);
for path in ["src/Foo.rs", "src/foo.rs", "src/FOO.rs", "src\\foo.rs"] {
assert_eq!(
count_file_symbols(&conn, path),
load_file_symbols(&conn, path).unwrap().len(),
"{path}"
);
}
assert_eq!(count_file_symbols(&conn, "src/Foo.rs"), 1);
assert_eq!(count_file_symbols(&conn, "src/FOO.rs"), 3);
}
#[test]
fn lookup_statement_is_not_planned_as_a_multi_index_or() {
let conn = search_fixture(&code_row("a", "src/lib.rs", "rust", "needle", ""));
conn.execute_batch(
"CREATE INDEX idx_symbols_name_kind ON symbols(name, kind);
CREATE INDEX idx_symbols_test_container ON symbols(test_container);
CREATE INDEX idx_symbols_is_test ON symbols(is_test);",
)
.unwrap();
let sql = format!(
"EXPLAIN QUERY PLAN {}",
search_symbols_sql(false, false, 20)
);
let plan: Vec<String> = conn
.prepare(&sql)
.unwrap()
.query_map(
rusqlite::named_params! {
":query": "needle",
":pattern": "%needle%",
":kind": None::<&str>,
":path": None::<&str>,
":path_like": None::<&str>,
},
|row| row.get::<_, String>(3),
)
.unwrap()
.collect::<Result<_, _>>()
.unwrap();
assert!(
!plan.iter().any(|step| step.contains("MULTI-INDEX OR")),
"{plan:?}"
);
}
#[test]
fn qualified_lookup_in_a_test_file_returns_the_named_row() {
let conn = search_fixture(
&[
"('c', 'f_c', 'src/tests/helpers.py', 'python', 'Helpers', 'class', 'class Helpers', '', 'pub', NULL,
1, 0, 9, 1, 0, 90, 1, 0, 9, 1, 5, 88, 'h_c', NULL, 0, 0, 'code')".to_string(),
"('d', 'f_d', 'src/tests/helpers.py', 'python', 'load_fixture', 'method', 'def load_fixture()', '', 'pub', 'c',
10, 0, 20, 1, 100, 250, 12, 4, 19, 1, 120, 240, 'h_d', NULL, 0, 0, 'code')".to_string(),
]
.join(", "),
);
assert_eq!(
search_symbols_scoped(&conn, "Helpers.load_fixture", None, None, false, 10)
.unwrap()
.len(),
1
);
assert_eq!(
search_symbols_scoped(&conn, "Helpers.load_fixture", None, None, true, 10)
.unwrap()
.len(),
1
);
assert!(
search_symbols_scoped(&conn, "load_fix", None, None, false, 10)
.unwrap()
.is_empty()
);
}
#[test]
fn concept_query_prefers_partial_code_match_over_full_doc_match() {
let conn = search_fixture(
&[
doc_row(
"d1",
"Safety guarantees",
"Pre-flight syntax validation runs before the edit touches disk",
),
doc_row(
"d2",
"Audit",
"The syntax validation before an edit is the invariant",
),
code_row(
"c1",
"src/syntax.rs",
"rust",
"validate_syntax",
"Validate the syntax of a file",
),
code_row(
"c2",
"src/edit.rs",
"rust",
"replace_symbol_body",
"Atomic edit with validation",
),
]
.join(","),
);
let names = search_names(&conn, "syntax validation before edit");
assert_eq!(names[0], "validate_syntax");
assert!(names.contains(&"replace_symbol_body".to_string()));
assert!(names.contains(&"Safety guarantees".to_string()));
}
#[test]
fn camel_case_query_finds_snake_case_symbol_and_vice_versa() {
let conn = search_fixture(
&[
code_row("c1", "src/syntax.rs", "rust", "validate_syntax", ""),
code_row("c2", "src/syntax.ts", "typescript", "validateSyntax", ""),
]
.join(","),
);
let mut camel = search_names(&conn, "ValidateSyntax");
camel.sort();
assert_eq!(camel, vec!["validateSyntax", "validate_syntax"]);
let mut words = search_names(&conn, "validate syntax");
words.sort();
assert_eq!(words, vec!["validateSyntax", "validate_syntax"]);
}
#[test]
fn stop_word_prefixed_camel_case_symbol_is_still_found() {
let conn = search_fixture(
&[
code_row("c1", "src/state.ts", "typescript", "isReady", ""),
code_row("c2", "src/hooks.rs", "rust", "before", ""),
code_row(
"c3",
"src/x.rs",
"rust",
"fooBar",
"has fooBar but not the other word",
),
]
.join(","),
);
assert_eq!(search_names(&conn, "isReady"), vec!["isReady"]);
assert_eq!(search_names(&conn, "before"), vec!["before"]);
}
#[test]
fn related_tests_use_the_name_as_typed_without_splitting() {
let conn = search_fixture(
&[
code_row("c1", "src/state.ts", "typescript", "isReady", ""),
"('t1', 'f_t1', 'tests/ready.rs', 'rust', 'test_ready', 'function', 'fn test_ready()', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_t1', NULL, 1, 0, 'code')".to_string(),
"('t2', 'f_t2', 'tests/state.rs', 'rust', 'isReady_reports_true', 'function', 'fn isReady_reports_true()', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_t2', NULL, 1, 0, 'code')".to_string(),
]
.join(","),
);
let target = get_symbol_by_name(&conn, "isReady", None).unwrap().unwrap();
let names: Vec<String> = find_related_tests(&conn, &target, 5)
.unwrap()
.into_iter()
.map(|t| t.name)
.collect();
assert_eq!(names, vec!["isReady_reports_true"]);
}
#[test]
fn related_tests_match_underscores_literally_and_skip_dunder_names() {
let conn = search_fixture(
&[
code_row("c1", "src/app.py", "python", "wsgi_app", ""),
code_row("c2", "src/app.py", "python", "__init__", ""),
"('t1', 'f_t1', 'tests/test_app.py', 'python', 'test_wsgi_app', 'function', 'def test_wsgi_app()', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_t1', NULL, 1, 0, 'code')".to_string(),
"('t2', 'f_t2', 'tests/test_app.py', 'python', 'test_wsgiXapp', 'function', 'def test_wsgiXapp()', NULL, NULL, NULL, 6, 0, 9, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_t2', NULL, 1, 0, 'code')".to_string(),
"('t3', 'f_t3', 'tests/test_views.py', 'python', 'test_init_once', 'function', 'def test_init_once()', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_t3', NULL, 1, 0, 'code')".to_string(),
]
.join(","),
);
let names = |name: &str| -> Vec<String> {
let target = get_symbol_by_name(&conn, name, None).unwrap().unwrap();
find_related_tests(&conn, &target, 5)
.unwrap()
.into_iter()
.map(|t| t.name)
.collect()
};
assert_eq!(names("wsgi_app"), vec!["test_wsgi_app"]);
assert!(names("__init__").is_empty());
}
#[test]
fn related_tests_admit_recognized_unflagged_paths_without_low_signal_rows() {
let conn = search_fixture(
"('target', 'f_target', 'src/core.rs', 'rust', 'calculate', 'function', 'fn calculate()', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_target', NULL, 0, 0, 'code'),
('direct', 'f_direct', 'tests/direct.rs', 'rust', 'direct_case', 'function', 'fn direct_case()', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_direct', NULL, 0, 0, 'code'),
('pending', 'f_pending', 'autotests/tst_pending.qml', 'qml', 'pending_case', 'function', 'function pending_case() {}', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_pending', NULL, 0, 0, 'code'),
('name', 'f_name', 'tests/name.rs', 'rust', 'calculate_named_case', 'function', 'fn calculate_named_case()', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_name', NULL, 0, 0, 'code'),
('fts', 'f_fts', 'tests/fts.rs', 'rust', 'fts_case', 'function', 'fn fts_case()', 'calculate behavior', NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_fts', NULL, 0, 0, 'code'),
('local', 'f_local', 'tests/name.rs', 'rust', 'calculate_local', 'variable', 'let calculate_local = 1;', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_local', NULL, 0, 0, 'code'),
('import', 'f_import', 'tests/name.rs', 'rust', 'calculate_import', 'import', 'use calculate_import;', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_import', NULL, 0, 0, 'code'),
('production', 'f_production', 'src/testing.rs', 'rust', 'calculate_production', 'function', 'fn calculate_production()', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_production', NULL, 0, 0, 'code')",
);
conn.execute_batch(
"CREATE TABLE relationships (from_symbol_id TEXT, to_symbol_id TEXT, kind TEXT, path TEXT, start_line INTEGER, start_column INTEGER);
CREATE TABLE pending_relationships (from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT, path TEXT, start_line INTEGER, start_column INTEGER, target_receiver TEXT, target_namespace_json TEXT, target_display_name TEXT);
CREATE TABLE type_facts (type_fact_id TEXT, symbol_id TEXT, language TEXT, resolved_type TEXT, generic_params_json TEXT);
INSERT INTO relationships VALUES ('direct', 'target', 'calls', 'tests/direct.rs', 1, 0);
INSERT INTO pending_relationships VALUES ('pending', 'calculate', 'calls', 'autotests/tst_pending.qml', 1, 0, NULL, '[]', 'calculate');",
)
.unwrap();
let target = get_symbol_by_name(&conn, "calculate", None)
.unwrap()
.unwrap();
let names: Vec<String> = find_related_tests(&conn, &target, 5)
.unwrap()
.into_iter()
.map(|test| test.name)
.collect();
assert_eq!(
names,
vec![
"direct_case",
"pending_case",
"calculate_named_case",
"fts_case"
]
);
}
#[test]
fn exact_name_ranks_before_longer_names_with_the_same_tokens() {
let conn = search_fixture(
&[
code_row(
"c1",
"src/queries.rs",
"rust",
"fts_search_symbols_scoped",
"search symbols scoped with fts",
),
code_row("c2", "src/queries.rs", "rust", "search_symbols_scoped", ""),
]
.join(","),
);
assert_eq!(
search_names(&conn, "search_symbols_scoped")[0],
"search_symbols_scoped"
);
}
fn sidecar_fixture() -> Connection {
search_fixture(
&[
code_row("c1", "src/sidecar.rs", "rust", "parseSha256Sidecar", ""),
code_row(
"c2",
"src/sidecar.rs",
"rust",
"parse_sidecar_file",
"parse the sha256 sidecar file",
),
]
.join(","),
)
}
fn candidate<'a>(candidates: &'a [Candidate], name: &str) -> &'a Candidate {
candidates
.iter()
.find(|c| c.result.symbol.name == name)
.unwrap_or_else(|| panic!("{name} is not a candidate"))
}
#[test]
fn name_substring_admits_a_symbol_the_word_branch_cannot_reach() {
let conn = sidecar_fixture();
let candidates = collect_search_candidates(&conn, "sha256", None, None, false, 10).unwrap();
let target = candidate(&candidates, "parseSha256Sidecar");
assert!(target.name_match);
assert!(!target.word_match);
assert!(!target.exact_name);
assert!(target.name_terms.contains(&"sha256".to_string()));
}
#[test]
fn a_row_matching_every_word_does_not_hide_a_row_matching_some() {
let conn = search_fixture(
&[
code_row(
"c1",
"examples/demo.rs",
"rust",
"demo",
"restore offline state",
),
code_row(
"c2",
"src/replay.rs",
"rust",
"replay",
"restore offline records",
),
]
.join(","),
);
let candidates =
collect_search_candidates(&conn, "restore offline state", None, None, false, 10)
.unwrap();
assert!(candidate(&candidates, "demo").word_match);
assert!(candidate(&candidates, "replay").word_match);
assert_eq!(search_names(&conn, "restore offline state")[0], "replay");
}
#[test]
fn name_branch_admits_the_target_when_word_matches_exceed_the_cap() {
let mut rows: Vec<String> = (1..=170)
.map(|i| {
code_row(
&format!("h{i:03}"),
"src/sidecar.rs",
"rust",
&format!("sidecar_helper_{i:03}"),
"parse sidecar file",
)
})
.collect();
rows.push(code_row(
"c1",
"src/sidecar.rs",
"rust",
"parseSha256Sidecar",
"",
));
let conn = search_fixture(&rows.join(","));
let candidates = collect_search_candidates(
&conn,
"parse the sha256 sidecar file",
None,
None,
false,
40,
)
.unwrap();
assert!(candidate(&candidates, "parseSha256Sidecar").name_match);
assert_eq!(candidates.iter().filter(|c| c.word_match).count(), 160);
}
#[test]
fn the_or_pass_fills_the_word_cap_but_never_exceeds_it() {
let mut rows: Vec<String> = (1..=20)
.map(|i| {
code_row(
&format!("a{i:02}"),
"src/a.rs",
"rust",
&format!("both_{i:02}"),
"restore offline",
)
})
.collect();
rows.extend((1..=50).map(|i| {
code_row(
&format!("p{i:02}"),
"src/p.rs",
"rust",
&format!("partial_{i:02}"),
"restore records",
)
}));
let conn = search_fixture(&rows.join(","));
let candidates =
collect_search_candidates(&conn, "restore offline", None, None, false, 10).unwrap();
let word_rows: Vec<&Candidate> = candidates.iter().filter(|c| c.word_match).collect();
assert_eq!(word_rows.len(), 40);
assert_eq!(
word_rows
.iter()
.filter(|c| c.result.symbol.name.starts_with("both_"))
.count(),
20
);
}
#[test]
fn exact_name_is_admitted_regardless_of_case() {
let conn = search_fixture(&code_row("c1", "src/q.rs", "rust", "xyzzy_q", ""));
let candidates =
collect_search_candidates(&conn, "XYZZY_Q", None, None, false, 10).unwrap();
assert!(candidate(&candidates, "xyzzy_q").exact_name);
conn.execute_batch("DROP TABLE symbol_names_tri").unwrap();
let candidates =
collect_search_candidates(&conn, "xyzzy_q", None, None, false, 10).unwrap();
assert!(candidate(&candidates, "xyzzy_q").exact_name);
}
#[test]
fn exact_name_with_a_quote_is_admitted_through_the_trigram_index() {
let conn = search_fixture(&code_row(
"c1",
"src/say.js",
"javascript",
"say \"hi\"",
"",
));
let candidates =
collect_search_candidates(&conn, "say \"hi\"", None, None, false, 10).unwrap();
let target = candidate(&candidates, "say \"hi\"");
assert!(target.exact_name && target.name_match);
}
#[test]
fn a_row_matched_by_every_branch_is_one_candidate_with_all_flags() {
let conn = search_fixture(
&[
code_row("c1", "src/a.rs", "rust", "sidecar", ""),
code_row("c2", "src/b.rs", "rust", "sidecar_helper", ""),
]
.join(","),
);
let candidates =
collect_search_candidates(&conn, "sidecar", None, None, false, 10).unwrap();
assert_eq!(candidates.len(), 2);
let target = candidate(&candidates, "sidecar");
assert!(target.exact_name && target.word_match && target.name_match);
assert!(target.bm25.is_some());
let helper = candidate(&candidates, "sidecar_helper");
assert!(!helper.exact_name && helper.word_match && helper.name_match);
}
#[test]
fn an_index_without_the_trigram_table_returns_word_rows_only() {
let conn = sidecar_fixture();
conn.execute_batch("DROP TABLE symbol_names_tri").unwrap();
let candidates = collect_search_candidates(&conn, "sha256", None, None, false, 10).unwrap();
let names: Vec<&str> = candidates
.iter()
.map(|c| c.result.symbol.name.as_str())
.collect();
assert_eq!(names, vec!["parse_sidecar_file"]);
assert!(candidates.iter().all(|c| c.word_match && !c.name_match));
assert_eq!(search_names(&conn, "sha256"), vec!["parse_sidecar_file"]);
}
#[test]
fn words_under_three_characters_skip_the_name_branch() {
let conn = search_fixture(
&[
code_row("c1", "src/a.rs", "rust", "ab", ""),
code_row("c2", "src/b.rs", "rust", "cab", ""),
]
.join(","),
);
let candidates = collect_search_candidates(&conn, "ab", None, None, false, 10).unwrap();
assert!(candidates.iter().all(|c| !c.name_match));
assert!(candidate(&candidates, "ab").exact_name);
}
#[test]
fn trigram_terms_include_the_identifier_parts_of_each_word() {
assert_eq!(
trigram_name_terms("collapse_name"),
vec!["collapse_name", "collapse", "name"]
);
assert_eq!(
trigram_name_terms("parse the sha256 sidecar"),
vec!["parse", "sha256", "sha", "256", "sidecar"]
);
assert_eq!(trigram_name_terms("isReady"), vec!["isready", "ready"]);
assert_eq!(trigram_name_terms("the before"), vec!["the", "before"]);
assert!(trigram_name_terms("ab").is_empty());
}
#[test]
fn snake_case_query_admits_a_pascal_case_name_through_the_name_branch() {
let conn = search_fixture(
&[
code_row("c1", "src/collapse.rs", "rust", "CollapseName", ""),
code_row("c2", "src/other.rs", "rust", "name_collapsed", ""),
]
.join(","),
);
let candidates =
collect_search_candidates(&conn, "collapse_name", None, None, false, 10).unwrap();
let target = candidate(&candidates, "CollapseName");
assert!(target.name_match);
assert_eq!(target.name_terms, vec!["collapse", "name"]);
assert_eq!(search_names(&conn, "collapse_name")[0], "CollapseName");
}
fn plain_candidate(name: &str, kind: &str, path: &str) -> Candidate {
Candidate {
result: SymbolSearchResult {
symbol: Symbol {
symbol_id: format!("{path}:{name}"),
file_id: "f".into(),
path: path.into(),
language: "rust".into(),
name: name.into(),
kind: kind.into(),
signature: None,
doc_comment: None,
visibility: None,
parent_symbol_id: None,
start_line: 1,
start_column: 0,
end_line: 1,
end_column: 0,
start_byte: 0,
end_byte: 0,
body_start_line: None,
body_start_column: None,
body_end_line: None,
body_end_column: None,
body_start_byte: None,
body_end_byte: None,
body_hash: None,
semantic_group: None,
is_test: false,
test_container: false,
},
score: 0.0,
snippet: None,
explain: None,
},
bm25: None,
exact_name: false,
word_match: false,
name_match: false,
owner_match: false,
name_terms: Vec::new(),
documentation: false,
nested: false,
owner: None,
owner_context: None,
}
}
fn function(name: &str) -> Candidate {
plain_candidate(name, "function", "src/lib.rs")
}
fn ranked(candidates: Vec<Candidate>, query: &str) -> Vec<(SymbolSearchResult, SearchExplain)> {
rerank_with(candidates, query, false, None)
}
fn ranked_names(candidates: Vec<Candidate>, query: &str) -> Vec<String> {
ranked(candidates, query)
.into_iter()
.map(|(r, _)| r.symbol.name)
.collect()
}
fn documented(name: &str, signature: Option<&str>, doc: &str) -> Candidate {
let mut candidate = function(name);
candidate.result.symbol.signature = signature.map(str::to_string);
candidate.result.symbol.doc_comment = Some(doc.into());
candidate
}
fn strip_ansi_case() -> Vec<Candidate> {
vec![
documented("strip_ansi", None, "Remove ANSI escape sequences"),
documented(
"_strip_code_fences",
Some("def _strip_code_fences(text: str)"),
"The first fenced code block's body, or the stripped text",
),
]
}
#[test]
fn rerank_words_split_identifiers_and_drop_stop_words_only_beside_content_words() {
assert_eq!(
rerank_words("parse the sha256 sidecar file"),
vec!["parse", "sha", "256", "sidecar", "file"]
);
assert_eq!(
rerank_words("parse_sha256_sidecar"),
vec!["parse", "sha", "256", "sidecar"]
);
assert_eq!(
rerank_words("ParseHTTPResponse"),
vec!["parse", "http", "response"]
);
assert_eq!(rerank_words("is_ok"), vec!["ok"]);
assert_eq!(rerank_words("the before"), vec!["the", "before"]);
}
#[test]
fn stop_words_cover_english_function_words_but_not_identifier_directions() {
for word in ["was", "whether", "another"] {
assert!(is_stop_word(word), "{word} must be a stop word");
}
for word in ["down", "into", "run"] {
assert!(!is_stop_word(word), "{word} must stay a content word");
}
assert_eq!(rerank_words("what was the file"), vec!["file"]);
}
#[test]
fn a_public_name_sorts_before_its_private_twin_at_an_equal_score() {
let mut private = function("_create_skill");
private.bm25 = Some(-9.0);
let mut public = function("create_skill");
public.bm25 = Some(-1.0);
let rows = ranked(vec![private, public], "create skill");
assert_eq!(rows[0].0.score, rows[1].0.score);
assert_eq!(rows[0].1.name_strength, rows[1].1.name_strength);
assert_eq!(
rows.iter()
.map(|(r, _)| r.symbol.name.as_str())
.collect::<Vec<_>>(),
vec!["create_skill", "_create_skill"]
);
}
#[test]
fn a_whole_name_constant_yields_to_a_function_that_holds_the_word_with_context() {
let rows = ranked(
vec![
plain_candidate("Glob", "constant", "src/glob.rs"),
function("matches_glob_pattern"),
],
"glob",
);
assert_eq!(rows[0].0.symbol.name, "matches_glob_pattern");
assert_eq!(rows[1].1.name_tier, "whole");
assert_eq!(rows[1].1.name_bonus, W_NAME_ALL_WORDS);
}
#[test]
fn name_tiers_are_whole_then_all_words_then_partial_then_none() {
let rows = ranked(
vec![
function("validate_everything"),
function("validate_syntax_now"),
function("validate_syntax"),
function("unrelated"),
],
"validate syntax",
);
let tiers: Vec<(&str, &str, f64)> = rows
.iter()
.map(|(r, e)| (r.symbol.name.as_str(), e.name_tier.as_str(), e.name_bonus))
.collect();
assert_eq!(
tiers,
vec![
("validate_syntax", "whole", W_NAME_WHOLE),
("validate_syntax_now", "all", W_NAME_ALL_WORDS),
("validate_everything", "partial", 0.0),
("unrelated", "none", 0.0),
]
);
assert_eq!(rows[0].0.score, W_NAME_WHOLE + W_TERMS + W_KIND_DEFINITION);
assert_eq!(
rows[1].0.score,
W_NAME_ALL_WORDS + W_TERMS + W_KIND_DEFINITION
);
assert_eq!(rows[2].0.score, W_TERMS / 2.0 + W_KIND_DEFINITION);
assert_eq!(rows[3].0.score, W_KIND_DEFINITION);
}
#[test]
fn distinct_scoring_prefers_three_terms_covered_once_over_two_terms_repeated() {
assert_eq!(
ranked_names(strip_ansi_case(), "strip ansi escape codes"),
vec!["strip_ansi", "_strip_code_fences"]
);
}
#[test]
fn distinct_scoring_denies_the_all_words_bonus_to_a_substring_only_name() {
let candidates = vec![
function("execute_julie_extract"),
documented("slice_bytes", None, "cut a byte range"),
];
let rows = ranked(candidates, "cut");
let bonuses: Vec<(&str, &str, f64)> = rows
.iter()
.map(|(r, e)| (r.symbol.name.as_str(), e.name_tier.as_str(), e.name_bonus))
.collect();
assert_eq!(
bonuses,
vec![
("execute_julie_extract", "partial", 0.0),
("slice_bytes", "none", 0.0),
]
);
assert_eq!(rows[0].0.score, rows[1].0.score);
}
#[test]
fn distinct_scoring_keeps_the_whole_name_and_all_words_tiers_in_order() {
let candidates = vec![
function("validate_everything"),
function("validate_syntax_now"),
function("validate_syntax"),
function("unrelated"),
];
assert_eq!(
ranked_names(candidates, "validate syntax"),
vec![
"validate_syntax",
"validate_syntax_now",
"validate_everything",
"unrelated"
]
);
}
#[test]
fn idf_weights_rank_a_term_in_one_row_above_a_term_in_most_rows() {
let mut rows: Vec<String> = (0..10)
.map(|i| {
code_row(
&format!("s{i}"),
&format!("src/f{i}.rs"),
"rust",
&format!("search_{i}"),
"searches the index",
)
})
.collect();
rows.push(code_row(
"rare",
"src/rare.rs",
"rust",
"sanitize_input",
"sanitize the input",
));
let conn = search_fixture(&rows.join(", "));
let terms = vec!["sanitize".to_string(), "search".to_string()];
let weights = idf_weights(&conn, &terms);
assert!(weights[0] > weights[1]);
}
#[test]
fn idf_weights_count_a_term_the_way_the_index_tokenizer_stems_it() {
let conn = search_fixture(&code_row(
"n",
"src/news.rs",
"rust",
"fetch_news",
"fetch the news feed",
));
let terms = vec!["news".to_string(), "unseen".to_string()];
let weights = idf_weights(&conn, &terms);
assert!(weights[0] < weights[1]);
}
#[test]
fn a_signature_hit_past_the_head_byte_cap_does_not_credit_its_term() {
let crediting_field = |padding: usize| {
let mut row = function("handler");
row.result.symbol.signature = Some(format!(
"fn handler({}sidecar: u8)",
"a: u8, ".repeat(padding)
));
ranked(vec![row], "sidecar")[0].1.terms[0].1.clone()
};
assert_eq!(crediting_field(4), "signature");
assert_eq!(crediting_field(80), "none");
}
#[test]
fn explain_terms_name_the_crediting_field_of_every_query_term() {
let rows = ranked(strip_ansi_case(), "strip ansi escape codes");
let terms: Vec<(&str, &str, f64)> = rows[0]
.1
.terms
.iter()
.map(|(term, field, credit)| (term.as_str(), field.as_str(), *credit))
.collect();
assert_eq!(rows[0].0.symbol.name, "strip_ansi");
assert_eq!(
terms,
vec![
("strip", "name", 3.0),
("ansi", "name", 3.0),
("escape", "doc", TEXT_CREDIT),
("codes", "none", 0.0),
]
);
}
#[test]
fn credited_fields_require_whole_name_multi_field_evidence_and_query_coverage() {
let terms = vec![
("wsgi".into(), "name".into(), 3.0),
("response".into(), "signature".into(), TEXT_CREDIT),
("request".into(), "doc".into(), TEXT_CREDIT),
("error".into(), "doc".into(), TEXT_CREDIT),
];
assert_eq!(credited_field_bonus(&terms), FIELD_DIVERSITY_BONUS);
assert_eq!(credited_field_bonus(&terms[..2]), 0.0);
assert_eq!(
credited_field_bonus(&[
("wsgi".into(), "name".into(), 2.0),
("response".into(), "signature".into(), TEXT_CREDIT),
("request".into(), "doc".into(), TEXT_CREDIT),
("error".into(), "doc".into(), TEXT_CREDIT),
]),
0.0
);
let sparse_terms = [
("terminal".into(), "name".into(), 3.0),
("docker".into(), "signature".into(), TEXT_CREDIT),
("local".into(), "doc".into(), TEXT_CREDIT),
("sandbox".into(), "doc".into(), TEXT_CREDIT),
("seven".into(), "none".into(), 0.0),
("ssh".into(), "none".into(), 0.0),
("singularity".into(), "none".into(), 0.0),
("modal".into(), "none".into(), 0.0),
("daytona".into(), "none".into(), 0.0),
("vercel".into(), "none".into(), 0.0),
];
assert_eq!(credited_field_bonus(&sparse_terms), 0.0);
assert_eq!(
credited_field_bonus(&[
("response".into(), "name".into(), 3.0),
("request".into(), "name".into(), 3.0),
]),
0.0
);
}
#[test]
fn credited_field_bonus_requires_two_distinct_document_terms() {
let terms = [
("strip".into(), "name".into(), 3.0),
("path".into(), "signature".into(), TEXT_CREDIT),
("windows".into(), "doc".into(), TEXT_CREDIT),
];
assert_eq!(credited_field_bonus(&terms), 0.0);
}
#[test]
fn name_coverage_accepts_token_runs_substrings_and_stems() {
let strengths = |name: &str, query: &str| {
let stemmer = Stemmer::create(Algorithm::English);
let words = stemmed_query_words(query, &stemmer);
name_hits(name, &words, &stemmer)
};
assert_eq!(strengths("parseSha256Sidecar", "sha 256"), vec![3, 3]);
assert_eq!(strengths("parseSha256Sidecar", "sha256"), vec![3, 3]);
assert_eq!(strengths("parseSha256Sidecar", "esha"), vec![1]);
assert_eq!(strengths("validate_syntax", "validation"), vec![2]);
assert_eq!(strengths("is_ok", "ok"), vec![3]);
assert_eq!(strengths("isReady", "is"), vec![3]);
assert_eq!(strengths("größe_berechnen", "größe"), vec![3]);
assert_eq!(
strengths("parseSha256Sidecar", "sidecar checksum"),
vec![3, 0]
);
assert_eq!(
strengths("parseSha256Sidecar", "checksum digest"),
vec![0, 0]
);
}
#[test]
fn a_rarer_term_moves_the_score_more_than_a_common_one() {
let weights = [4.0, 1.0];
let rows = rerank_with(
vec![function("rare_helper"), function("common_helper")],
"rare common",
false,
Some(&weights),
);
assert_eq!(
rows[0].1.word_weights,
vec![("rare".to_string(), 4.0), ("common".to_string(), 1.0)]
);
assert_eq!(rows[0].0.symbol.name, "rare_helper");
assert_eq!(rows[0].0.score, W_TERMS * 4.0 / 5.0 + W_KIND_DEFINITION);
assert_eq!(rows[1].0.score, W_TERMS * 1.0 / 5.0 + W_KIND_DEFINITION);
}
#[test]
fn any_name_hit_outranks_a_zero_coverage_definition_for_long_queries() {
let rows = ranked(
vec![
function("render_mode"),
plain_candidate("retry_count", "constant", "src/scan.rs"),
],
"how many times a failed download is tried again retry limit",
);
assert_eq!(rows[0].0.symbol.name, "retry_count");
assert_eq!(rows[0].1.name_tier, "partial");
assert!(rows[0].0.score > rows[1].0.score);
assert_eq!(rows[1].0.score, W_KIND_DEFINITION);
}
#[test]
fn a_doc_hit_past_the_head_byte_cap_does_not_credit_its_term() {
let mut row = function("load");
row.result.symbol.signature = Some("fn load(config: &Config) -> Loaded".into());
row.result.symbol.doc_comment = Some(format!("{}settings", "é".repeat(321)));
let (result, explain) = ranked(vec![row], "config settings").remove(0);
assert_eq!(
explain.terms,
vec![
("config".into(), "signature".into(), TEXT_CREDIT),
("settings".into(), "none".into(), 0.0),
]
);
assert_eq!(result.score, explain.term_score + W_KIND_DEFINITION);
}
#[test]
fn text_coverage_matches_whole_tokens_by_word_or_stem_prefix() {
let crediting_field =
|row: Candidate, query: &str| ranked(vec![row], query).remove(0).1.terms[0].1.clone();
let doc_field = |doc: &str, query: &str| {
let mut row = function("row");
row.result.symbol.doc_comment = Some(doc.into());
crediting_field(row, query)
};
let sig_field = |signature: &str, query: &str| {
let mut row = function("row");
row.result.symbol.signature = Some(signature.into());
crediting_field(row, query)
};
assert_eq!(doc_field("The system runs.", "stemming"), "none");
assert_eq!(doc_field("The stemmer runs.", "stemming"), "doc");
assert_eq!(doc_field("Compares stems.", "stemming"), "doc");
assert_eq!(doc_field("An important port.", "porter"), "none");
assert_eq!(
sig_field("fn sha256sum(data: &[u8]) -> String", "sha256"),
"signature"
);
assert_eq!(sig_field("fn is_ok()", "ok"), "signature");
assert_eq!(sig_field("fn okay()", "ok"), "none");
assert_eq!(
sig_field("fn parseSha256Sidecar(text)", "sidecar"),
"signature"
);
}
#[test]
fn text_tokens_split_like_query_words_then_identifiers() {
fn two_pass(text: &str) -> Vec<&str> {
query_words(text)
.into_iter()
.flat_map(split_identifier)
.collect()
}
fn one_pass(text: &str) -> Vec<&str> {
let mut out = Vec::new();
text_tokens_into(text, &mut out);
out
}
let ascii = "fn parseHTTPResponse2(raw: &str, _id: u8) -> Vec<&str> // sha256_sum";
let unicode = "Berechnet die Größe: größe_berechnen(pfad) -> ÜberGroß2x";
assert_eq!(one_pass(ascii), two_pass(ascii));
assert_eq!(
one_pass(ascii),
vec![
"fn", "parse", "HTTP", "Response", "2", "raw", "str", "id", "u", "8", "Vec", "str",
"sha", "256", "sum",
]
);
assert_eq!(one_pass(unicode), two_pass(unicode));
assert!(one_pass("").is_empty());
assert!(one_pass("_ __ ...").is_empty());
}
#[test]
fn a_doc_credits_a_term_by_its_stem() {
let mut row = function("check");
row.result.symbol.doc_comment = Some("Validates the input.".into());
let explain = ranked(vec![row], "validation").remove(0).1;
assert_eq!(
explain.terms,
vec![("validation".into(), "doc".into(), TEXT_CREDIT)]
);
}
#[test]
fn kind_prior_orders_definitions_over_members_over_imports() {
let rows = ranked(
vec![
plain_candidate("Scan", "import", "src/a.rs"),
plain_candidate("Scan", "enum_member", "src/b.rs"),
plain_candidate("Scan", "function", "src/c.rs"),
],
"scan",
);
let order: Vec<(&str, f64)> = rows
.iter()
.map(|(r, e)| (r.symbol.path.as_str(), e.kind_prior))
.collect();
assert_eq!(
order,
vec![
("src/c.rs", W_KIND_DEFINITION),
("src/b.rs", W_KIND_MEMBER),
("src/a.rs", W_KIND_IMPORT),
]
);
}
#[test]
fn a_partial_name_match_on_a_member_beats_the_kind_prior_of_a_function() {
let names = ranked_names(
vec![
function("RenderMode"),
plain_candidate("MaxRetryCount", "constant", "pkg/scan.go"),
],
"retry download limit timeout",
);
assert_eq!(names[0], "MaxRetryCount");
}
#[test]
fn path_role_demotes_role_directories_unless_the_query_names_them() {
let rows = |query: &str| {
ranked(
vec![
plain_candidate("verifyChecksum", "function", "scripts/launcher.ts"),
plain_candidate("verify_checksum", "function", "src/archive.rs"),
],
query,
)
};
let plain = rows("verify checksum");
assert_eq!(plain[0].0.symbol.path, "src/archive.rs");
assert_eq!(plain[1].1.path_role, W_PATH_ROLE);
let named = rows("launcher script verify checksum");
assert!(named.iter().all(|(_, e)| e.path_role == 0.0));
let only_launcher = rows("launcher verify checksum");
assert_eq!(only_launcher[0].0.symbol.path, "src/archive.rs");
assert_eq!(only_launcher[1].1.path_role, W_PATH_ROLE);
let windows = ranked(
vec![plain_candidate(
"verifyChecksum",
"function",
"scripts\\launcher.ts",
)],
"verify checksum",
);
assert_eq!(windows[0].1.path_role, W_PATH_ROLE);
}
#[test]
fn documentation_rows_sort_after_every_code_row() {
let mut heading = plain_candidate("Verify checksum", "heading", "README.md");
heading.documentation = true;
heading.result.symbol.language = "markdown".into();
heading.result.symbol.signature = Some("Verify checksum".into());
heading.result.symbol.doc_comment = Some("Verify the checksum of the archive.".into());
let rows = ranked(
vec![
heading,
plain_candidate("unrelated", "variable", "src/a.rs"),
],
"verify checksum",
);
assert_eq!(rows[0].0.symbol.name, "unrelated");
assert_eq!(rows[1].1.documentation, W_DOCUMENTATION_ROW);
assert_eq!(rows[1].1.name_tier, "whole");
assert!(rows[1].0.score < 0.0);
}
#[test]
fn a_helper_nested_in_a_function_ranks_below_an_equal_module_function() {
let mut helper = plain_candidate("run_test", "function", "tests/test_basic.py");
helper.nested = true;
let module_function = plain_candidate("run_dotenv", "function", "tests/test_basic.py");
let rows = rerank_with(vec![helper, module_function], "run", true, None);
assert_eq!(rows[0].0.symbol.name, "run_dotenv");
assert_eq!(rows[1].1.nested, W_NESTED);
}
#[test]
fn test_intent_boosts_test_rows_only_when_tests_are_included_and_named() {
let rows = |query: &str, include_tests: bool| {
let mut test_row = plain_candidate("payment_flow", "function", "tests/payment.rs");
test_row.result.symbol.is_test = true;
let plain_row = plain_candidate("payment_flow", "function", "src/payment.rs");
rerank_with(vec![plain_row, test_row], query, include_tests, None)
};
let boosted = rows("payment flow tests", true);
assert_eq!(boosted[0].0.symbol.path, "tests/payment.rs");
assert_eq!(boosted[0].1.test_intent, W_TEST_INTENT);
assert_eq!(boosted[1].1.test_intent, 0.0);
assert!(
rows("payment flow tests", false)
.iter()
.all(|(_, e)| e.test_intent == 0.0)
);
assert!(
rows("payment flow", true)
.iter()
.all(|(_, e)| e.test_intent == 0.0)
);
}
#[test]
fn ties_break_by_bm25_then_name_length_then_path() {
let mut word_row = plain_candidate("payment", "function", "src/z.rs");
word_row.word_match = true;
word_row.bm25 = Some(-4.0);
let mut weaker_word_row = plain_candidate("payment", "function", "src/a.rs");
weaker_word_row.word_match = true;
weaker_word_row.bm25 = Some(-2.0);
let mut name_only = plain_candidate("payment", "function", "src/b.rs");
name_only.name_match = true;
let rows = ranked(
vec![
plain_candidate("payment", "function", "src/y.rs"),
name_only,
weaker_word_row,
word_row,
],
"payment",
);
let paths: Vec<&str> = rows.iter().map(|(r, _)| r.symbol.path.as_str()).collect();
assert_eq!(paths, vec!["src/z.rs", "src/a.rs", "src/b.rs", "src/y.rs"]);
let by_length = ranked_names(
vec![
function("payment_gateway_client"),
function("payment_gateway"),
],
"gateway",
);
assert_eq!(by_length, vec!["payment_gateway", "payment_gateway_client"]);
}
#[test]
fn a_whole_token_name_outranks_a_substring_name_with_better_bm25() {
let mut token = function("csr");
token.bm25 = Some(-1.0);
let mut substring = function("action_csrf_token");
substring.bm25 = Some(-5.0);
let rows = ranked(vec![substring, token], "csr adjacency");
assert!(rows[0].0.score > rows[1].0.score);
assert_eq!(rows[0].0.symbol.name, "csr");
}
#[test]
fn an_acronym_token_outranks_a_name_that_only_contains_it() {
let mut token = function("http_client");
token.bm25 = Some(-1.0);
let mut substring = function("shttpd_config");
substring.bm25 = Some(-5.0);
let rows = ranked(vec![substring, token], "http");
assert!(rows[0].0.score > rows[1].0.score);
assert_eq!(rows[0].0.symbol.name, "http_client");
}
#[test]
fn explain_reports_the_sum_of_the_name_strengths() {
let rows = ranked(vec![function("action_csrf_token")], "csr token");
assert_eq!(rows[0].1.name_strength, 4);
}
#[test]
fn snippets_follow_the_admitting_branch() {
let mut word_row = function("parse_sidecar_file");
word_row.word_match = true;
word_row.result.snippet = Some("parse the [sha256] sidecar file".into());
let mut name_row = function("parseSha256Sidecar");
name_row.name_match = true;
name_row.name_terms = vec!["sha".into(), "sha256".into(), "256".into()];
let mut exact_row = function("sha256");
exact_row.exact_name = true;
let rows = ranked(vec![word_row, name_row, exact_row], "sha256");
let snippets: Vec<(&str, &str)> = rows
.iter()
.map(|(r, _)| (r.symbol.name.as_str(), r.snippet.as_deref().unwrap()))
.collect();
assert_eq!(
snippets,
vec![
("sha256", "sha256"),
("parseSha256Sidecar", "parse[Sha256]Sidecar"),
("parse_sidecar_file", "parse the [sha256] sidecar file"),
]
);
assert_eq!(rows[1].1.branches, vec!["name"]);
assert_eq!(rows[0].1.branches, vec!["exact"]);
}
#[test]
fn explain_is_attached_only_when_requested() {
let conn = sidecar_fixture();
let query = "sha256";
let silent = fts_search_symbols_scoped(&conn, query, None, None, false, 10).unwrap();
assert!(silent.iter().all(|r| r.explain.is_none()));
assert!(silent[0].score > 0.0);
assert_eq!(
serde_json::to_value(&silent[0]).unwrap().get("explain"),
None
);
let explained =
fts_search_symbols_explained(&conn, query, None, None, false, 10, true).unwrap();
let by_name = |name: &str| {
explained
.iter()
.find(|r| r.symbol.name == name)
.and_then(|r| r.explain.as_ref())
.unwrap()
};
let name_only = by_name("parseSha256Sidecar");
assert_eq!(name_only.candidates, 2);
assert_eq!(name_only.branches, vec!["name"]);
assert_eq!(name_only.bm25, None);
let word_row = by_name("parse_sidecar_file");
assert!(word_row.bm25.unwrap() < 0.0);
assert_eq!(word_row.candidates, 2);
assert!(
serde_json::to_value(&explained[0])
.unwrap()
.get("explain")
.is_some()
);
}
#[test]
fn search_symbols_treats_like_wildcards_as_literals() {
let dir = crate::safe_tempdir();
let db_path = dir.path().join("search_symbols_treats_like_wildcards.db");
let conn = open_read_write(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE symbols (
symbol_id TEXT, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER
);
INSERT INTO symbols VALUES (
's', 'f', 'src/lib.rs', 'rust', 'ordinary', 'function', NULL, NULL, NULL, NULL,
1, 0, 1, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0
);
INSERT INTO symbols VALUES (
'p', 'f', 'src/lib.rs', 'rust', 'literal%name', 'function', NULL, NULL, NULL, NULL,
1, 0, 1, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0
);
INSERT INTO symbols VALUES (
'u', 'f', 'src/lib.rs', 'rust', 'literal_name', 'function', NULL, NULL, NULL, NULL,
1, 0, 1, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0
);
CREATE TABLE files (
file_id TEXT, path TEXT, language TEXT, content_hash TEXT,
content_bytes INTEGER, line_count INTEGER, indexed_at TEXT
);
INSERT INTO files VALUES ('f1', 'src/literal_path/lib.rs', 'rust', 'hash', 0, 0, 'now');
INSERT INTO files VALUES ('f2', 'src/literalXpath/lib.rs', 'rust', 'hash', 0, 0, 'now'
);",
)
.unwrap();
assert_eq!(
search_symbols(&conn, "%", None, false, 10).unwrap()[0].name,
"literal%name"
);
assert_eq!(
search_symbols(&conn, "_", None, false, 10).unwrap()[0].name,
"literal_name"
);
assert_eq!(
load_scoped_files(&conn, Some("src/literal_path"))
.unwrap()
.len(),
1
);
}
#[test]
fn find_references_for_symbol_limits_callees_by_symbol_id() {
let dir = crate::safe_tempdir();
let db_path = dir.path().join("find_references_for_symbol.db");
let conn = open_read_write(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE symbols (
symbol_id TEXT, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER
);
CREATE TABLE relationships (
from_symbol_id TEXT, to_symbol_id TEXT, kind TEXT, path TEXT,
start_line INTEGER, start_column INTEGER
);
CREATE TABLE pending_relationships (
from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT, path TEXT,
start_line INTEGER, start_column INTEGER
);
INSERT INTO symbols VALUES
('wanted', 'f', 'a.rs', 'rust', 'new', 'method', NULL, NULL, NULL, NULL, 1, 0, 1, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0),
('other', 'f', 'b.rs', 'rust', 'new', 'method', NULL, NULL, NULL, NULL, 1, 0, 1, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0),
('wanted-callee', 'f', 'a.rs', 'rust', 'wanted_dep', 'function', NULL, NULL, NULL, NULL, 1, 0, 1, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0),
('other-callee', 'f', 'b.rs', 'rust', 'other_dep', 'function', NULL, NULL, NULL, NULL, 1, 0, 1, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0);
INSERT INTO relationships VALUES
('other', 'other-callee', 'calls', 'b.rs', 1, 0),
('wanted', 'wanted-callee', 'calls', 'a.rs', 1, 0);",
)
.unwrap();
let references = find_references_for_symbol(&conn, "new", "callees", 1, "wanted").unwrap();
assert_eq!(references.len(), 1);
assert_eq!(references[0].to_symbol_name, "wanted_dep");
}
#[test]
fn test_fts_search_symbols_and_porter_stemming() {
let dir = crate::safe_tempdir();
let db_path = dir.path().join("fts_search_symbols.db");
let conn = open_read_write(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY,
file_id TEXT,
path TEXT,
language TEXT,
name TEXT,
kind TEXT,
signature TEXT,
doc_comment TEXT,
visibility TEXT,
parent_symbol_id TEXT,
start_line INTEGER,
start_column INTEGER,
end_line INTEGER,
end_column INTEGER,
start_byte INTEGER,
end_byte INTEGER,
body_start_line INTEGER,
body_start_column INTEGER,
body_end_line INTEGER,
body_end_column INTEGER,
body_start_byte INTEGER,
body_end_byte INTEGER,
body_hash TEXT,
semantic_group TEXT,
is_test INTEGER,
test_container INTEGER
);
INSERT INTO symbols VALUES (
's1', 'f1', 'src/payment.rs', 'rust', 'PaymentGateway', 'trait',
'pub trait PaymentGateway', 'Core payment provider interface for transactions',
'pub', NULL, 10, 0, 20, 1, 100, 250, 12, 4, 19, 1, 120, 240, 'hash1', 'type', 0, 0
);
INSERT INTO symbols VALUES (
's2', 'f1', 'src/payment.rs', 'rust', 'StripeClient', 'struct',
'pub struct StripeClient', 'Handles HTTP requests to stripe payment API',
'pub', NULL, 25, 0, 35, 1, 300, 450, 27, 4, 34, 1, 320, 440, 'hash2', 'type', 0, 0
);
INSERT INTO symbols VALUES (
's3', 'f2', 'src/parser.rs', 'rust', 'parse_tokens', 'function',
'pub fn parse_tokens(stream: &TokenStream) -> Result<Vec<Token>>', 'Parses syntax tokens from stream',
'pub', NULL, 5, 0, 15, 1, 50, 200, 7, 4, 14, 1, 70, 190, 'hash3', 'function', 0, 0
);
INSERT INTO symbols VALUES (
's4', 'f3', 'tests/payment_test.rs', 'rust', 'test_payment_flow', 'function',
'fn test_payment_flow()', 'Tests payment charge workflow',
NULL, NULL, 5, 0, 15, 1, 50, 200, 7, 4, 14, 1, 70, 190, 'hash4', 'function', 1, 0
);",
)
.unwrap();
ensure_fts_index(&conn).unwrap();
let results =
fts_search_symbols_scoped(&conn, "parsing tokens", None, None, false, 10).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].symbol.name, "parse_tokens");
assert!(results[0].snippet.is_some());
let results =
fts_search_symbols_scoped(&conn, "transactions", None, None, false, 10).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].symbol.name, "PaymentGateway");
let results = fts_search_symbols_scoped(&conn, "payment", None, None, false, 10).unwrap();
assert_eq!(results.len(), 2);
assert!(results.iter().all(|r| !r.symbol.is_test));
let results = fts_search_symbols_scoped(&conn, "payment", None, None, true, 10).unwrap();
assert_eq!(results.len(), 3);
let results =
fts_search_symbols_scoped(&conn, "stripe kafka redis", None, None, false, 10).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].symbol.name, "StripeClient");
}
#[test]
fn find_related_tests_returns_each_test_once_under_the_limit() {
let dir = crate::safe_tempdir();
let conn = open_read_write(&dir.path().join("related_tests_limit.db")).unwrap();
conn.execute_batch(
"CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT,
name TEXT, kind TEXT, signature TEXT, doc_comment TEXT,
visibility TEXT, parent_symbol_id TEXT, start_line INTEGER,
start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER
);
CREATE TABLE relationships (
from_symbol_id TEXT, to_symbol_id TEXT, kind TEXT, path TEXT,
start_line INTEGER, start_column INTEGER
);
CREATE TABLE pending_relationships (
from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT, path TEXT,
start_line INTEGER, start_column INTEGER,
target_receiver TEXT, target_namespace_json TEXT, target_display_name TEXT
);
CREATE TABLE type_facts (
type_fact_id TEXT, symbol_id TEXT, language TEXT, resolved_type TEXT, generic_params_json TEXT
);
INSERT INTO symbols VALUES
('s_target', 'f1', 'src/lib.rs', 'rust', 'compute', 'function', 'pub fn compute()', NULL, 'pub', NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0),
('t_a', 'f2', 'tests/a.rs', 'rust', 'first_case', 'function', 'fn first_case()', NULL, NULL, NULL, 1, 0, 20, 1, 0, 300, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, 0),
('t_b', 'f3', 'tests/b.rs', 'rust', 'second_case', 'function', 'fn second_case()', NULL, NULL, NULL, 1, 0, 10, 1, 0, 100, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, 0);
INSERT INTO pending_relationships (from_symbol_id, target_terminal_name, kind, path, start_line, start_column, target_receiver, target_namespace_json, target_display_name) VALUES
('t_a', 'compute', 'calls', 'tests/a.rs', 3, 4, NULL, NULL, 'compute'),
('t_a', 'compute', 'calls', 'tests/a.rs', 5, 4, NULL, NULL, 'compute'),
('t_a', 'compute', 'calls', 'tests/a.rs', 7, 4, NULL, NULL, 'compute'),
('t_a', 'compute', 'calls', 'tests/a.rs', 9, 4, NULL, NULL, 'compute'),
('t_a', 'compute', 'calls', 'tests/a.rs', 11, 4, NULL, NULL, 'compute'),
('t_b', 'compute', 'calls', 'tests/b.rs', 3, 4, NULL, NULL, 'compute');",
)
.unwrap();
let target = get_symbol_by_name(&conn, "compute", None).unwrap().unwrap();
let tests = find_related_tests(&conn, &target, 5).unwrap();
let mut names: Vec<&str> = tests.iter().map(|t| t.name.as_str()).collect();
names.sort();
assert_eq!(names, vec!["first_case", "second_case"]);
}
#[test]
fn documentation_rows_rank_after_code_in_search() {
let dir = crate::safe_tempdir();
let conn = open_read_write(&dir.path().join("doc_rank.db")).unwrap();
conn.execute_batch(
"CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT,
kind TEXT, signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER, content_type TEXT
);
INSERT INTO symbols VALUES
('s_doc', 'f1', 'docs/plans/018.adoc', 'asciidoc', 'Reconcile offline edits',
'heading', 'Reconcile offline edits', NULL, NULL, NULL,
3, 0, 3, 1, 10, 40, 3, 0, 3, 1, 10, 40, 'hash_doc', NULL, 0, 0, 'documentation'),
('s_code', 'f2', 'src/sync.rs', 'rust', 'reconcile_offline_edits', 'function',
'fn reconcile_offline_edits()', 'Reconcile offline edits at startup', 'pub', NULL,
10, 0, 20, 1, 100, 250, 12, 4, 19, 1, 120, 240, 'hash_code', NULL, 0, 0, 'code');",
)
.unwrap();
ensure_fts_index(&conn).unwrap();
let results =
fts_search_symbols_scoped(&conn, "reconcile offline edits", None, None, false, 10)
.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].symbol.name, "reconcile_offline_edits");
assert_eq!(results[1].symbol.name, "Reconcile offline edits");
}
#[test]
fn test_queries_nocase_and_path_normalization() {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE files (
file_id TEXT PRIMARY KEY,
path TEXT NOT NULL,
language TEXT,
content_hash TEXT,
content_bytes INTEGER,
line_count INTEGER,
indexed_at INTEGER
);
CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY,
file_id TEXT,
path TEXT NOT NULL,
language TEXT,
name TEXT,
kind TEXT,
signature TEXT,
doc_comment TEXT,
visibility TEXT,
parent_symbol_id TEXT,
start_line INTEGER,
start_column INTEGER,
end_line INTEGER,
end_column INTEGER,
start_byte INTEGER,
end_byte INTEGER,
body_start_line INTEGER,
body_start_column INTEGER,
body_end_line INTEGER,
body_end_column INTEGER,
body_start_byte INTEGER,
body_end_byte INTEGER,
body_hash TEXT,
semantic_group TEXT,
is_test INTEGER,
test_container INTEGER
);
-- Insert with backslashes and mixed casing to verify defensive normalization and COLLATE NOCASE
INSERT INTO files VALUES ('f1', 'src\\Payment.rs', 'rust', 'hash1', 100, 10, '2026-09-14T00:00:00Z');
INSERT INTO symbols VALUES (
's1', 'f1', 'src\\Payment.rs', 'rust', 'ProcessPayment', 'function',
'pub fn ProcessPayment()', NULL, 'pub', NULL, 1, 0, 5, 0, 0, 50,
2, 4, 4, 1, 10, 45, 'bhash', 'function', 0, 0
);",
)
.unwrap();
let file = get_file(&conn, "SRC/PAYMENT.RS")
.unwrap()
.expect("File should be found");
assert_eq!(
file.path, "src/Payment.rs",
"Path should be normalized to forward slashes"
);
let file2 = get_file(&conn, "src/payment.rs")
.unwrap()
.expect("File should be found");
assert_eq!(file2.path, "src/Payment.rs");
let syms = load_file_symbols(&conn, "SRC/PAYMENT.RS").unwrap();
assert_eq!(syms.len(), 1);
assert_eq!(
syms[0].path, "src/Payment.rs",
"Symbol path should be normalized to forward slashes"
);
let sym = get_symbol_by_name(&conn, "ProcessPayment", Some("SRC/PAYMENT.RS"))
.unwrap()
.expect("Symbol should be found with case-insensitive path filter");
assert_eq!(sym.path, "src/Payment.rs");
}
#[test]
fn test_exact_case_prioritized_over_nocase() {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE files (
file_id TEXT PRIMARY KEY,
path TEXT NOT NULL,
language TEXT,
content_hash TEXT,
content_bytes INTEGER,
line_count INTEGER,
indexed_at TEXT
);
CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY,
file_id TEXT,
path TEXT NOT NULL,
language TEXT,
name TEXT NOT NULL,
kind TEXT NOT NULL,
signature TEXT,
doc_comment TEXT,
visibility TEXT,
parent_symbol_id TEXT,
start_line INTEGER,
start_column INTEGER,
end_line INTEGER,
end_column INTEGER,
start_byte INTEGER,
end_byte INTEGER,
body_start_line INTEGER,
body_start_column INTEGER,
body_end_line INTEGER,
body_end_column INTEGER,
body_start_byte INTEGER,
body_end_byte INTEGER,
body_hash TEXT,
semantic_group TEXT,
is_test INTEGER,
test_container INTEGER
);
INSERT INTO files VALUES ('f1', 'src/Payment.rs', 'rust', 'h1', 100, 10, '2026-09-14T00:00:00Z');
INSERT INTO files VALUES ('f2', 'src/payment.rs', 'rust', 'h2', 100, 10, '2026-09-14T00:00:00Z');
INSERT INTO symbols VALUES (
's1', 'f1', 'src/Payment.rs', 'rust', 'pay', 'function',
'pub fn pay()', NULL, 'pub', NULL, 1, 0, 5, 0, 0, 50,
2, 4, 4, 1, 10, 45, 'b1', 'function', 0, 0
);
INSERT INTO symbols VALUES (
's2', 'f2', 'src/payment.rs', 'rust', 'pay', 'function',
'pub fn pay()', NULL, 'pub', NULL, 1, 0, 5, 0, 0, 50,
2, 4, 4, 1, 10, 45, 'b2', 'function', 0, 0
);",
)
.unwrap();
let f_lower = get_file(&conn, "src/payment.rs").unwrap().unwrap();
assert_eq!(f_lower.path, "src/payment.rs");
assert_eq!(f_lower.file_id, "f2");
let f_upper = get_file(&conn, "src/Payment.rs").unwrap().unwrap();
assert_eq!(f_upper.path, "src/Payment.rs");
assert_eq!(f_upper.file_id, "f1");
let syms_lower = load_file_symbols(&conn, "src/payment.rs").unwrap();
assert_eq!(syms_lower.len(), 1);
assert_eq!(syms_lower[0].file_id, "f2");
let syms_upper = load_file_symbols(&conn, "src/Payment.rs").unwrap();
assert_eq!(syms_upper.len(), 1);
assert_eq!(syms_upper[0].file_id, "f1");
}
#[test]
fn test_conservative_pending_resolution_ignores_unmatched_namespace() {
let dir = crate::safe_tempdir();
let db_path = dir.path().join("conservative_resolution.db");
let conn = open_read_write(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT,
name TEXT, kind TEXT, signature TEXT, doc_comment TEXT,
visibility TEXT, parent_symbol_id TEXT, start_line INTEGER,
start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER
);
CREATE TABLE relationships (
from_symbol_id TEXT, to_symbol_id TEXT, kind TEXT, path TEXT,
start_line INTEGER, start_column INTEGER
);
CREATE TABLE pending_relationships (
from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT, path TEXT,
start_line INTEGER, start_column INTEGER,
target_receiver TEXT, target_namespace_json TEXT, target_display_name TEXT
);
CREATE TABLE type_facts (
type_fact_id TEXT, symbol_id TEXT, language TEXT, resolved_type TEXT, generic_params_json TEXT
);
-- Workspace struct Workspace and method Workspace::new
INSERT INTO symbols VALUES
('s_ws', 'f1', 'src/workspace.rs', 'rust', 'Workspace', 'struct', 'pub struct Workspace', NULL, 'pub', NULL, 1, 0, 10, 0, 0, 100, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'struct', 0, 0),
('s_ws_new', 'f1', 'src/workspace.rs', 'rust', 'new', 'method', 'pub fn new() -> Workspace', NULL, 'pub', 's_ws', 2, 4, 4, 5, 20, 50, 2, 4, 4, 5, 20, 50, 'h1', 'method', 0, 0),
('s_caller', 'f2', 'src/caller.rs', 'rust', 'my_func', 'function', 'pub fn my_func()', NULL, 'pub', NULL, 1, 0, 10, 0, 0, 100, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'function', 0, 0);
-- my_func calls Vec::new() (external namespace 'Vec')
INSERT INTO pending_relationships (from_symbol_id, target_terminal_name, kind, path, start_line, start_column, target_receiver, target_namespace_json, target_display_name) VALUES
('s_caller', 'new', 'calls', 'src/caller.rs', 3, 8, NULL, '[\"Vec\"]', 'Vec::new');",
)
.unwrap();
let sigs = find_callee_signatures(&conn, "my_func", "s_caller", 10, false).unwrap();
assert!(sigs.is_empty(), "Expected 0 signatures, got: {:?}", sigs);
let refs = find_references_for_symbol(&conn, "my_func", "callees", 10, "s_caller").unwrap();
assert!(refs.is_empty(), "Expected 0 references, got: {:?}", refs);
let callers = find_references_for_symbol(&conn, "new", "callers", 10, "s_ws_new").unwrap();
assert!(
callers.is_empty(),
"Expected 0 callers for Workspace::new, got: {:?}",
callers
);
let blast = compute_blast_radius(&conn, &["new"], &["src/workspace.rs"], 2, 20).unwrap();
assert!(
!blast.impacted_symbols.iter().any(|s| s.name == "my_func"),
"my_func should not be impacted before calling Workspace::new: {:?}",
blast.impacted_symbols
);
conn.execute(
"INSERT INTO pending_relationships (from_symbol_id, target_terminal_name, kind, path, start_line, start_column, target_receiver, target_namespace_json, target_display_name) VALUES ('s_caller', 'new', 'calls', 'src/caller.rs', 5, 8, NULL, '[\"Workspace\"]', 'Workspace::new')",
[],
)
.unwrap();
let sigs2 = find_callee_signatures(&conn, "my_func", "s_caller", 10, false).unwrap();
assert_eq!(
sigs2.len(),
1,
"Expected 1 signature for Workspace::new, got: {:?}",
sigs2
);
assert!(sigs2[0].contains("pub fn new() -> Workspace"));
let blast2 = compute_blast_radius(&conn, &["new"], &["src/workspace.rs"], 2, 20).unwrap();
assert!(
blast2.impacted_symbols.iter().any(|s| s.name == "my_func"),
"my_func should be impacted after calling Workspace::new: {:?}",
blast2.impacted_symbols
);
conn.execute(
"INSERT INTO symbols VALUES
('s_other', 'f3', 'src/other.rs', 'rust', 'other_func', 'function', 'pub fn other_func()', NULL, 'pub', NULL, 1, 0, 10, 0, 0, 100, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'function', 0, 0);",
[],
)
.unwrap();
conn.execute(
"INSERT INTO pending_relationships (from_symbol_id, target_terminal_name, kind, path, start_line, start_column, target_receiver, target_namespace_json, target_display_name) VALUES ('s_other', 'new', 'calls', 'src/other.rs', 2, 8, NULL, NULL, 'new')",
[],
)
.unwrap();
let sigs_other = find_callee_signatures(&conn, "other_func", "s_other", 10, false).unwrap();
assert!(
sigs_other.is_empty(),
"Bare call to new() from outside Workspace should not resolve to Workspace::new: {:?}",
sigs_other
);
conn.execute(
"INSERT INTO symbols VALUES
('s_ws_helper', 'f1', 'src/workspace.rs', 'rust', 'helper', 'method', 'pub fn helper()', NULL, 'pub', 's_ws', 5, 4, 7, 5, 60, 90, 5, 4, 7, 5, 60, 90, 'h2', 'method', 0, 0);",
[],
)
.unwrap();
conn.execute(
"INSERT INTO pending_relationships (from_symbol_id, target_terminal_name, kind, path, start_line, start_column, target_receiver, target_namespace_json, target_display_name) VALUES ('s_ws_helper', 'new', 'calls', 'src/workspace.rs', 6, 8, NULL, NULL, 'new')",
[],
)
.unwrap();
let sigs_sibling =
find_callee_signatures(&conn, "helper", "s_ws_helper", 10, false).unwrap();
assert_eq!(
sigs_sibling.len(),
1,
"Sibling method calling bare new() should resolve to Workspace::new: {:?}",
sigs_sibling
);
let ext_sigs = find_callee_signatures(&conn, "my_func", "s_caller", 10, true).unwrap();
assert!(
ext_sigs.iter().any(|s| s.contains("Vec")),
"include_external: true should include external Vec::new: {:?}",
ext_sigs
);
}
#[test]
fn test_find_structural_facts_and_literals_scoped() {
let dir = crate::safe_tempdir();
let db_path = dir.path().join("facts_test.db");
let conn = open_read_write(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
semantic_group TEXT, is_test INTEGER, test_container INTEGER
);
CREATE TABLE structural_facts (
structural_fact_id TEXT PRIMARY KEY, file_id TEXT, path TEXT NOT NULL, language TEXT,
pattern_id TEXT, capture_name TEXT, node_kind TEXT, containing_symbol_id TEXT,
start_line INTEGER, end_line INTEGER, confidence REAL, metadata_json TEXT
);
CREATE TABLE literals (
literal_id TEXT PRIMARY KEY, file_id TEXT, path TEXT NOT NULL, language TEXT,
kind TEXT, literal_text TEXT, carrier TEXT, containing_symbol_id TEXT,
start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
start_byte INTEGER, end_byte INTEGER
);
INSERT INTO structural_facts VALUES
('sf_toml', 'f1', 'Cargo.toml', 'toml', 'toml.key_value.v1', 'key_value', 'table', NULL, 1, 2, 1.0, '{\"key\":\"command\",\"key_path\":\"mcp_servers.code-kb.command\"}'),
('sf_yaml', 'f6', '.github/workflows/ci.yml', 'yaml', 'yaml.key_value.v1', 'key_value', 'block_mapping_pair', NULL, 3, 3, 1.0, '{\"key\":\"name\",\"key_path\":\"$.on.name\"}'),
('sf_route', 'f2', 'src/routes/api.rs', 'rust', 'axum.route.v1', 'get_users', 'function', NULL, 10, 20, 1.0, '{\"verb\":\"GET\",\"route_template\":\"/api/v1/users/{id}\",\"normalized_route_template\":\"/api/v1/users/:id\"}'),
('sf_sql', 'f3', 'src/db/queries.rs', 'rust', 'sql.select_query.v1', 'select_users', 'function', NULL, 30, 40, 1.0, NULL),
('sf_model', 'f4', 'src/models/user.rs', 'rust', 'sql.table_definition.v1', 'User', 'struct', NULL, 50, 60, 1.0, NULL),
('sf_css', 'f7', 'web/site.css', 'css', 'css.media_query.v1', 'media', 'media_statement', NULL, 1, 1, 1.0, NULL),
('sf_custom', 'f5', 'src/custom.rs', 'rust', 'my_custom_pattern', 'custom_name', 'item', NULL, 70, 80, 1.0, NULL);
INSERT INTO literals VALUES
('lit_toml', 'f1', 'Cargo.toml', 'toml', 'toml_key', '\"version\"', 'key', NULL, 3, 0, 3, 9, 20, 29),
('lit_route', 'f2', 'src/routes/api.rs', 'rust', 'http_route', '\"/api/v1/users\"', 'string', NULL, 12, 0, 12, 15, 100, 115),
('lit_sql', 'f3', 'src/db/queries.rs', 'rust', 'sql_query', '\"SELECT * FROM users\"', 'string', NULL, 32, 0, 32, 21, 200, 221),
('lit_model', 'f4', 'src/models/user.rs', 'rust', 'model_table', '\"users_table\"', 'string', NULL, 52, 0, 52, 13, 300, 313);",
)
.unwrap();
let facts_config = find_structural_facts_scoped(&conn, "config", None, 10).unwrap();
assert_eq!(facts_config.len(), 2);
assert_eq!(facts_config[0].pattern_id, "yaml.key_value.v1");
assert_eq!(facts_config[0].key.as_deref(), Some("on.name"));
assert_eq!(facts_config[1].pattern_id, "toml.key_value.v1");
assert_eq!(
facts_config[1].key.as_deref(),
Some("mcp_servers.code-kb.command")
);
let lits_config = find_literals_scoped(&conn, "config", None, 10).unwrap();
assert_eq!(lits_config.len(), 1);
assert_eq!(lits_config[0].kind, "toml_key");
let facts_route = find_structural_facts_scoped(&conn, "route", None, 10).unwrap();
assert_eq!(facts_route.len(), 1);
assert_eq!(facts_route[0].pattern_id, "axum.route.v1");
assert_eq!(
facts_route[0].key.as_deref(),
Some("GET /api/v1/users/{id}")
);
let facts_routes = find_structural_facts_scoped(&conn, "routes", None, 10).unwrap();
assert_eq!(facts_routes.len(), 1);
let lits_route = find_literals_scoped(&conn, "route", None, 10).unwrap();
assert_eq!(lits_route.len(), 1);
assert_eq!(lits_route[0].kind, "http_route");
for q in &["query", "queries", "sql"] {
let facts = find_structural_facts_scoped(&conn, q, None, 10).unwrap();
assert_eq!(facts.len(), 2, "Failed for {}", q);
assert!(facts.iter().all(|f| f.pattern_id.starts_with("sql.")));
let lits = find_literals_scoped(&conn, q, None, 10).unwrap();
assert_eq!(lits.len(), 1, "Failed for {}", q);
assert_eq!(lits[0].kind, "sql_query");
}
for m in &["model", "models"] {
let facts = find_structural_facts_scoped(&conn, m, None, 10).unwrap();
assert_eq!(facts.len(), 1, "Failed for {}", m);
assert_eq!(facts[0].pattern_id, "sql.table_definition.v1");
let lits = find_literals_scoped(&conn, m, None, 10).unwrap();
assert_eq!(lits.len(), 1, "Failed for {}", m);
assert_eq!(lits[0].kind, "model_table");
}
let facts_custom = find_structural_facts_scoped(&conn, "custom_pattern", None, 10).unwrap();
assert_eq!(facts_custom.len(), 1);
assert_eq!(facts_custom[0].pattern_id, "my_custom_pattern");
assert_eq!(facts_custom[0].key, None);
let facts_exact =
find_structural_facts_scoped(&conn, "config", Some("Cargo.toml"), 10).unwrap();
assert_eq!(facts_exact.len(), 1);
let facts_miss =
find_structural_facts_scoped(&conn, "config", Some("src/routes/api.rs"), 10).unwrap();
assert_eq!(facts_miss.len(), 0);
let facts_dir =
find_structural_facts_scoped(&conn, "route", Some("src/routes"), 10).unwrap();
assert_eq!(facts_dir.len(), 1);
let facts_dir_miss =
find_structural_facts_scoped(&conn, "route", Some("src/db"), 10).unwrap();
assert_eq!(facts_dir_miss.len(), 0);
let f_del = find_structural_facts(&conn, "config", 10).unwrap();
assert_eq!(f_del.len(), 2);
let l_del = find_literals(&conn, "config", 10).unwrap();
assert_eq!(l_del.len(), 1);
}
fn local_variable_fixture() -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT,
kind TEXT, signature TEXT, doc_comment TEXT, visibility TEXT,
parent_symbol_id TEXT, start_line INTEGER, start_column INTEGER,
end_line INTEGER, end_column INTEGER, start_byte INTEGER, end_byte INTEGER,
body_start_line INTEGER, body_start_column INTEGER, body_end_line INTEGER,
body_end_column INTEGER, body_start_byte INTEGER, body_end_byte INTEGER,
body_hash TEXT, semantic_group TEXT, is_test INTEGER, test_container INTEGER
);
INSERT INTO symbols (symbol_id, file_id, path, language, name, kind, signature,
parent_symbol_id, start_line, start_column, end_line, end_column,
start_byte, end_byte, is_test, test_container)
VALUES
('func', 'f1', 'src/db.rs', 'rust', 'open_conn', 'function',
'fn open_conn() -> sqlite Connection', NULL, 1, 0, 9, 1, 0, 100, 0, 0),
('local', 'f1', 'src/db.rs', 'rust', 'conn', 'variable',
'let conn: sqlite Connection', 'func', 2, 4, 2, 30, 10, 40, 0, 0),
('pool', 'f1', 'src/db.rs', 'rust', 'Pool', 'struct',
'struct Pool sqlite', NULL, 12, 0, 16, 1, 120, 200, 0, 0),
('field', 'f1', 'src/db.rs', 'rust', 'conn', 'variable',
'conn: sqlite Connection', 'pool', 13, 4, 13, 28, 130, 160, 0, 0),
('global', 'f1', 'src/db.rs', 'rust', 'conn', 'variable',
'static conn: sqlite Connection', NULL, 20, 0, 20, 30, 210, 240, 0, 0),
('closure', 'f1', 'src/db.rs', 'rust', 'with_conn', 'variable',
'let with_conn = |c: sqlite Connection|', 'func', 4, 4, 6, 5, 50, 90, 0, 0),
('nested', 'f1', 'src/db.rs', 'rust', 'conn', 'variable',
'let conn = c sqlite', 'closure', 5, 8, 5, 24, 60, 80, 0, 0);",
)
.unwrap();
conn
}
fn matched_symbol_ids(conn: &Connection, query: &str) -> Vec<String> {
let mut stmt = conn
.prepare(
"SELECT s.symbol_id FROM symbols_fts f
JOIN symbols s ON s.rowid = f.rowid
WHERE f.symbols_fts MATCH ?1 ORDER BY s.symbol_id",
)
.unwrap();
let mut ids = stmt
.query_map(params![query], |row| row.get::<_, String>(0))
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
ids.sort();
ids
}
#[test]
fn fts_index_excludes_locals_and_rebuilds_a_stale_index() {
let conn = local_variable_fixture();
conn.execute_batch(
"CREATE VIRTUAL TABLE symbols_fts USING fts5(
name, signature, doc_comment,
content='symbols', content_rowid='rowid', tokenize='porter unicode61'
);
INSERT INTO symbols_fts(rowid, name, signature, doc_comment)
SELECT rowid, name, signature, doc_comment FROM symbols;",
)
.unwrap();
ensure_fts_index(&conn).unwrap();
assert_eq!(
matched_symbol_ids(&conn, "sqlite"),
vec!["field", "func", "global", "pool"]
);
}
#[test]
fn lookup_excludes_locals_and_parameters() {
let conn = local_variable_fixture();
let ids: Vec<String> = search_symbols_scoped(&conn, "conn", None, None, false, 10)
.unwrap()
.into_iter()
.map(|s| s.symbol_id)
.collect();
assert!(!ids.contains(&"local".to_string()));
assert!(!ids.contains(&"nested".to_string()));
assert!(ids.contains(&"field".to_string()));
assert!(ids.contains(&"global".to_string()));
}
#[test]
fn search_excludes_locals_and_parameters() {
let conn = local_variable_fixture();
ensure_fts_index(&conn).unwrap();
let ids: Vec<String> = fts_search_symbols_scoped(&conn, "sqlite", None, None, false, 10)
.unwrap()
.into_iter()
.map(|r| r.symbol.symbol_id)
.collect();
assert!(!ids.contains(&"local".to_string()));
assert!(ids.contains(&"func".to_string()));
}
#[test]
fn variable_kind_search_keeps_full_text_matching() {
let conn = local_variable_fixture();
ensure_fts_index(&conn).unwrap();
let ids: Vec<String> = fts_search_symbols_scoped(
&conn,
"sqlite connection",
Some("variable"),
None,
false,
10,
)
.unwrap()
.into_iter()
.map(|r| r.symbol.symbol_id)
.collect();
assert!(ids.contains(&"global".to_string()));
assert!(ids.contains(&"field".to_string()));
}
#[test]
fn qualified_lookup_returns_the_named_local_variable() {
let conn = local_variable_fixture();
let ids: Vec<String> =
search_symbols_scoped(&conn, "open_conn::conn", None, None, false, 10)
.unwrap()
.into_iter()
.map(|s| s.symbol_id)
.collect();
assert_eq!(ids, vec!["local".to_string()]);
}
#[test]
fn exact_local_variable_outranks_a_partial_global_match_within_the_limit() {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE symbols (
symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT,
kind TEXT, signature TEXT, doc_comment TEXT, visibility TEXT,
parent_symbol_id TEXT, start_line INTEGER, start_column INTEGER,
end_line INTEGER, end_column INTEGER, start_byte INTEGER, end_byte INTEGER,
body_start_line INTEGER, body_start_column INTEGER, body_end_line INTEGER,
body_end_column INTEGER, body_start_byte INTEGER, body_end_byte INTEGER,
body_hash TEXT, semantic_group TEXT, is_test INTEGER, test_container INTEGER
);
INSERT INTO symbols (symbol_id, file_id, path, language, name, kind, signature,
parent_symbol_id, start_line, start_column, end_line, end_column,
start_byte, end_byte, is_test, test_container)
VALUES
('func', 'f1', 'src/sum.rs', 'rust', 'digest', 'function',
'fn digest()', NULL, 1, 0, 9, 1, 0, 100, 0, 0),
('local', 'f1', 'src/sum.rs', 'rust', 'checksum', 'variable',
'let checksum', 'func', 2, 4, 2, 30, 10, 40, 0, 0),
('global', 'f1', 'src/sum.rs', 'rust', 'getChecksum', 'variable',
'const getChecksum', NULL, 20, 0, 20, 30, 210, 240, 0, 0);",
)
.unwrap();
ensure_fts_index(&conn).unwrap();
let rows =
fts_search_symbols_explained(&conn, "checksum", Some("variable"), None, false, 1, true)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].symbol.symbol_id, "local");
let explain = rows[0].explain.as_ref().unwrap();
assert_eq!(explain.name_tier, "whole");
assert_eq!(explain.branches, vec!["exact", "name"]);
assert_eq!(explain.candidates, 2);
}
#[test]
fn variable_kind_filter_returns_locals_and_parameters() {
let conn = local_variable_fixture();
ensure_fts_index(&conn).unwrap();
let lookup_ids: Vec<String> =
search_symbols_scoped(&conn, "conn", Some("variable"), None, false, 10)
.unwrap()
.into_iter()
.map(|s| s.symbol_id)
.collect();
assert!(lookup_ids.contains(&"local".to_string()));
assert!(lookup_ids.contains(&"nested".to_string()));
let search_ids: Vec<String> =
fts_search_symbols_scoped(&conn, "conn", Some("variable"), None, false, 10)
.unwrap()
.into_iter()
.map(|r| r.symbol.symbol_id)
.collect();
assert!(search_ids.contains(&"local".to_string()));
}
}