1use rusqlite::{Connection, OptionalExtension, Row, ToSql, params};
2use rust_stemmers::{Algorithm, Stemmer};
3use std::collections::{HashMap, HashSet};
4use thiserror::Error;
5
6use crate::db::local_variable_predicate;
7use crate::models::{
8 BlastRadiusResult, FileFact, ImpactedSymbol, LiteralFact, ReferenceSite, SearchExplain,
9 StructuralFact, Symbol, SymbolSearchResult, TestTarget, TypeFact, has_generated_lambda_name,
10 is_generated_lambda_name,
11};
12
13#[derive(Debug, Error)]
14pub enum QueryError {
15 #[error("Database query error: {0}")]
16 Sqlite(#[from] rusqlite::Error),
17 #[error("Symbol '{name}' not found in {workspace}. {hint}")]
18 SymbolNotFound {
19 name: String,
20 workspace: String,
21 hint: String,
22 },
23 #[error(
24 "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}"
25 )]
26 AmbiguousSymbol(String, usize, String),
27 #[error("Invalid direction '{0}': must be 'callers' or 'callees'")]
28 InvalidDirection(String),
29 #[error("Result limit must be between 0 and {MAX_RESULT_LIMIT}, got {0}")]
30 InvalidResultLimit(usize),
31}
32
33pub const MAX_RESULT_LIMIT: usize = 200;
34
35pub fn validate_result_limit(limit: usize) -> Result<(), QueryError> {
36 if limit > MAX_RESULT_LIMIT {
37 return Err(QueryError::InvalidResultLimit(limit));
38 }
39 Ok(())
40}
41
42fn map_symbol(row: &Row) -> rusqlite::Result<Symbol> {
43 Ok(Symbol {
44 symbol_id: row.get("symbol_id")?,
45 file_id: row.get("file_id")?,
46 path: row.get::<_, String>("path")?.replace('\\', "/"),
47 language: row.get("language")?,
48 name: row.get("name")?,
49 kind: row.get("kind")?,
50 signature: row.get("signature")?,
51 doc_comment: row.get("doc_comment")?,
52 visibility: row.get("visibility")?,
53 parent_symbol_id: row.get("parent_symbol_id")?,
54 start_line: row.get::<_, i64>("start_line")? as usize,
55 start_column: row.get::<_, i64>("start_column")? as usize,
56 end_line: row.get::<_, i64>("end_line")? as usize,
57 end_column: row.get::<_, i64>("end_column")? as usize,
58 start_byte: row.get::<_, i64>("start_byte")? as usize,
59 end_byte: row.get::<_, i64>("end_byte")? as usize,
60 body_start_line: row
61 .get::<_, Option<i64>>("body_start_line")?
62 .map(|v| v as usize),
63 body_start_column: row
64 .get::<_, Option<i64>>("body_start_column")?
65 .map(|v| v as usize),
66 body_end_line: row
67 .get::<_, Option<i64>>("body_end_line")?
68 .map(|v| v as usize),
69 body_end_column: row
70 .get::<_, Option<i64>>("body_end_column")?
71 .map(|v| v as usize),
72 body_start_byte: row
73 .get::<_, Option<i64>>("body_start_byte")?
74 .map(|v| v as usize),
75 body_end_byte: row
76 .get::<_, Option<i64>>("body_end_byte")?
77 .map(|v| v as usize),
78 body_hash: row.get("body_hash")?,
79 semantic_group: row.get("semantic_group")?,
80 is_test: row.get::<_, i64>("is_test")? != 0,
81 test_container: row.get::<_, i64>("test_container")? != 0,
82 })
83}
84
85pub(crate) fn escape_like(value: &str) -> String {
86 value
87 .replace('\\', "\\\\")
88 .replace('%', "\\%")
89 .replace('_', "\\_")
90}
91
92pub fn load_scoped_files(
94 conn: &Connection,
95 path_filter: Option<&str>,
96) -> Result<Vec<FileFact>, QueryError> {
97 let norm = path_filter
98 .map(|p| p.replace('\\', "/").trim_matches('/').to_string())
99 .filter(|p| !p.is_empty());
100 let norm_bs = norm.as_ref().map(|p| p.replace('/', "\\"));
101 let prefix = norm.as_ref().map(|path| format!("{}/%", escape_like(path)));
102 let prefix_bs = norm_bs
103 .as_ref()
104 .map(|path| format!("{}\\\\%", escape_like(path)));
105
106 let sql = "SELECT file_id, path, language, content_hash, content_bytes, line_count, indexed_at
107 FROM files
108 WHERE (:path IS NULL
109 OR path = :path COLLATE NOCASE
110 OR path = :path_bs COLLATE NOCASE
111 OR path LIKE :path_prefix ESCAPE '\\'
112 OR path LIKE :path_prefix_bs ESCAPE '\\')
113 ORDER BY (:path IS NOT NULL AND (path = :path OR path = :path_bs)) DESC, path ASC";
114
115 let mut stmt = conn.prepare(sql)?;
116 let files = stmt
117 .query_map(
118 rusqlite::named_params! {
119 ":path": norm.as_deref(),
120 ":path_bs": norm_bs.as_deref(),
121 ":path_prefix": prefix.as_deref(),
122 ":path_prefix_bs": prefix_bs.as_deref(),
123 },
124 |row| {
125 Ok(FileFact {
126 file_id: row.get(0)?,
127 path: row.get::<_, String>(1)?.replace('\\', "/"),
128 language: row.get(2)?,
129 content_hash: row.get(3)?,
130 content_bytes: row.get(4)?,
131 line_count: row.get(5)?,
132 indexed_at: row.get(6)?,
133 })
134 },
135 )?
136 .collect::<Result<Vec<_>, _>>()?;
137
138 Ok(files)
139}
140
141pub const OUTLINE_FILE_CAP: usize = 1000;
143
144pub struct OutlineScope {
146 path: Option<String>,
147 path_bs: Option<String>,
148 prefix: Option<String>,
149 prefix_bs: Option<String>,
150 max_slashes: i64,
152}
153
154impl OutlineScope {
155 pub fn new(path_filter: Option<&str>, depth: usize) -> Self {
156 let path = path_filter
157 .map(|p| p.replace('\\', "/").trim_matches('/').to_string())
158 .filter(|p| !p.is_empty());
159 let path_bs = path.as_ref().map(|p| p.replace('/', "\\"));
160 let max_slashes = match &path {
161 None => depth.saturating_sub(1),
162 Some(p) => p.matches('/').count() + depth,
163 } as i64;
164 Self {
165 prefix: path.as_ref().map(|p| format!("{}/%", escape_like(p))),
166 prefix_bs: path_bs.as_ref().map(|p| format!("{}\\\\%", escape_like(p))),
167 path,
168 path_bs,
169 max_slashes,
170 }
171 }
172
173 pub fn lists(&self, path: &str) -> bool {
175 path.matches(['/', '\\']).count() as i64 <= self.max_slashes
176 }
177
178 fn params(&self) -> [(&'static str, &dyn ToSql); 5] {
179 [
180 (":path", &self.path),
181 (":path_bs", &self.path_bs),
182 (":path_prefix", &self.prefix),
183 (":path_prefix_bs", &self.prefix_bs),
184 (":max_slashes", &self.max_slashes),
185 ]
186 }
187}
188
189fn outline_file_terms(conn: &Connection) -> String {
191 let supported = if has_column(conn, "files", "status") {
192 "COALESCE(status, '') != 'unsupported'"
193 } else {
194 "1 = 1"
195 };
196 format!(
197 "(:path IS NULL
198 OR path = :path COLLATE NOCASE
199 OR path = :path_bs COLLATE NOCASE
200 OR path LIKE :path_prefix ESCAPE '\\'
201 OR path LIKE :path_prefix_bs ESCAPE '\\')
202 AND {supported}"
203 )
204}
205
206fn outline_files_cte(conn: &Connection) -> String {
208 format!(
209 "outline_files AS (
210 SELECT path FROM files
211 WHERE {}
212 AND length(path) - length(replace(replace(path, '/', ''), '\\', '')) <= :max_slashes
213 ORDER BY path ASC
214 LIMIT {OUTLINE_FILE_CAP}
215 )",
216 outline_file_terms(conn)
217 )
218}
219
220pub fn for_each_outline_path(
222 conn: &Connection,
223 scope: &OutlineScope,
224 mut visit: impl FnMut(String) -> bool,
225) -> Result<(), QueryError> {
226 let sql = format!(
227 "SELECT path FROM files WHERE {} ORDER BY path ASC",
228 outline_file_terms(conn)
229 );
230 let mut stmt = conn.prepare(&sql)?;
231 let params = scope.params();
232 let mut rows = stmt.query(¶ms[..4])?;
233 while let Some(row) = rows.next()? {
234 if !visit(row.get(0)?) {
235 break;
236 }
237 }
238 Ok(())
239}
240
241fn outline_level(alias: &str) -> String {
244 format!(
245 "({alias}.parent_symbol_id IS NULL OR EXISTS (
246 SELECT 1 FROM symbols ns WHERE ns.symbol_id = {alias}.parent_symbol_id
247 AND ns.kind IN ('namespace', 'module', 'package')))"
248 )
249}
250
251const OUTLINE_KINDS: &str =
252 "('function', 'method', 'struct', 'enum', 'trait', 'class', 'interface', 'type')";
253
254pub fn load_scoped_outline_symbols(
259 conn: &Connection,
260 path_filter: Option<&str>,
261 depth: usize,
262 limit_per_file: usize,
263) -> Result<HashMap<String, Vec<Symbol>>, QueryError> {
264 let scope = OutlineScope::new(path_filter, depth);
265 let sql = format!(
266 "WITH {files},
267 ranked AS (
268 SELECT s.symbol_id, s.file_id, s.path, s.language, s.name, s.kind, s.signature, s.doc_comment,
269 s.visibility, s.parent_symbol_id, s.start_line, s.start_column, s.end_line, s.end_column,
270 s.start_byte, s.end_byte, s.body_start_line, s.body_start_column, s.body_end_line,
271 s.body_end_column, s.body_start_byte, s.body_end_byte, s.body_hash, s.semantic_group,
272 s.is_test, s.test_container,
273 ROW_NUMBER() OVER (
274 PARTITION BY s.path ORDER BY (s.is_test = 1 OR s.test_container = 1), s.start_line ASC
275 ) as rn
276 FROM symbols s
277 JOIN outline_files bf ON s.path = bf.path
278 WHERE s.kind IN {OUTLINE_KINDS}
279 AND {top}
280 AND NOT EXISTS (SELECT 1 FROM symbols overload
281 WHERE +overload.path = s.path AND overload.name = s.name
282 AND overload.kind = s.kind
283 AND +overload.parent_symbol_id IS s.parent_symbol_id
284 AND overload.start_line < s.start_line)
285 )
286 SELECT symbol_id, file_id, path, language, name, kind, signature, doc_comment,
287 visibility, parent_symbol_id, start_line, start_column, end_line, end_column,
288 start_byte, end_byte, body_start_line, body_start_column, body_end_line,
289 body_end_column, body_start_byte, body_end_byte, body_hash, semantic_group,
290 is_test, test_container
291 FROM ranked
292 WHERE rn <= :limit
293 ORDER BY path ASC, start_line ASC",
294 files = outline_files_cte(conn),
295 top = outline_level("s"),
296 );
297
298 let mut stmt = conn.prepare(&sql)?;
299 let limit = limit_per_file as i64;
300 let mut params = scope.params().to_vec();
301 params.push((":limit", &limit));
302 let mut rows = stmt.query(¶ms[..])?;
303
304 let mut symbols_by_file: HashMap<String, Vec<Symbol>> = HashMap::new();
305 while let Some(row) = rows.next()? {
306 let sym = map_symbol(row)?;
307 symbols_by_file
308 .entry(sym.path.clone())
309 .or_default()
310 .push(sym);
311 }
312
313 Ok(symbols_by_file)
314}
315
316#[derive(Debug, Clone, Copy, Default, PartialEq)]
318pub struct OutlineCounts {
319 pub definitions: usize,
321 pub tests: usize,
323 pub fixtures: usize,
325}
326
327pub fn load_outline_counts(
330 conn: &Connection,
331 path_filter: Option<&str>,
332 depth: usize,
333) -> Result<HashMap<String, OutlineCounts>, QueryError> {
334 let scope = OutlineScope::new(path_filter, depth);
335 let lifecycle = if has_column(conn, "symbols", "test_lifecycle") {
336 "COALESCE(s.test_lifecycle, 0) != 0"
337 } else {
338 "0"
339 };
340 let mut stmt = conn.prepare(&format!(
341 "WITH {files}
342 SELECT replace(s.path, '\\', '/'),
343 COUNT(DISTINCT CASE WHEN {top} AND s.is_test = 0 AND s.test_container = 0
344 AND s.kind IN {OUTLINE_KINDS}
345 THEN s.kind || ' ' || s.name END),
346 SUM(s.is_test = 1 AND NOT {lifecycle}),
347 SUM(s.is_test = 1 AND {lifecycle})
348 FROM symbols s
349 JOIN outline_files bf ON s.path = bf.path
350 GROUP BY replace(s.path, '\\', '/')",
351 files = outline_files_cte(conn),
352 top = outline_level("s"),
353 ))?;
354 let rows = stmt.query_map(&scope.params()[..], |row| {
355 Ok((
356 row.get::<_, String>(0)?,
357 OutlineCounts {
358 definitions: row.get::<_, i64>(1)? as usize,
359 tests: row.get::<_, i64>(2)? as usize,
360 fixtures: row.get::<_, i64>(3)? as usize,
361 },
362 ))
363 })?;
364 Ok(rows.collect::<Result<_, _>>()?)
365}
366
367pub const BASELINE_PATH_CAP: usize = 20;
369
370pub fn file_sizes_for_paths(conn: &Connection, paths: &[String]) -> usize {
373 let mut seen = std::collections::HashSet::new();
374 let distinct: Vec<&String> = paths
375 .iter()
376 .filter(|path| seen.insert(path.as_str()))
377 .take(BASELINE_PATH_CAP)
378 .collect();
379 if distinct.is_empty() {
380 return 0;
381 }
382
383 let placeholders = vec!["?"; distinct.len()].join(", ");
384 let sql =
385 format!("SELECT COALESCE(SUM(content_bytes), 0) FROM files WHERE path IN ({placeholders})");
386 conn.query_row(&sql, rusqlite::params_from_iter(distinct), |row| {
387 row.get::<_, i64>(0)
388 })
389 .map(|bytes| bytes.max(0) as usize)
390 .unwrap_or(0)
391}
392
393pub fn get_file(conn: &Connection, path: &str) -> Result<Option<FileFact>, QueryError> {
395 let normalized = path.replace('\\', "/");
396 let backslash = path.replace('/', "\\");
397
398 let mut stmt = conn.prepare(
400 "SELECT file_id, path, language, content_hash, content_bytes, line_count, indexed_at
401 FROM files
402 WHERE (path = ?1 COLLATE NOCASE OR path = ?2 COLLATE NOCASE)
403 ORDER BY (path = ?1 OR path = ?2) DESC
404 LIMIT 1",
405 )?;
406
407 let mut rows = stmt.query(params![normalized, backslash])?;
408 if let Some(row) = rows.next()? {
409 Ok(Some(FileFact {
410 file_id: row.get(0)?,
411 path: row.get::<_, String>(1)?.replace('\\', "/"),
412 language: row.get(2)?,
413 content_hash: row.get(3)?,
414 content_bytes: row.get(4)?,
415 line_count: row.get(5)?,
416 indexed_at: row.get(6)?,
417 }))
418 } else {
419 Ok(None)
420 }
421}
422
423pub fn count_file_symbols(conn: &Connection, path: &str) -> usize {
427 let count = |sql: &str| {
428 conn.query_row(
429 sql,
430 params![path.replace('\\', "/"), path.replace('/', "\\")],
431 |row| row.get::<_, i64>(0),
432 )
433 .map(|count| count.max(0) as usize)
434 .unwrap_or(0)
435 };
436 match count("SELECT COUNT(*) FROM symbols WHERE path = ?1 OR path = ?2") {
437 0 => count(
438 "SELECT COUNT(*) FROM symbols
439 WHERE path = ?1 COLLATE NOCASE OR path = ?2 COLLATE NOCASE",
440 ),
441 exact => exact,
442 }
443}
444
445pub fn count_parse_diagnostics(conn: &Connection, path: &str) -> usize {
446 conn.query_row(
447 "SELECT COUNT(*) FROM parse_diagnostics
448 WHERE path = ?1 COLLATE NOCASE OR path = ?2 COLLATE NOCASE",
449 params![path.replace('\\', "/"), path.replace('/', "\\")],
450 |row| row.get::<_, i64>(0),
451 )
452 .map(|count| count as usize)
453 .unwrap_or(0)
454}
455
456pub fn for_each_unsupported_path(
458 conn: &Connection,
459 scope: &OutlineScope,
460 mut visit: impl FnMut(&str),
461) -> Result<(), QueryError> {
462 if !has_column(conn, "files", "status") {
463 return Ok(());
464 }
465 let mut stmt = conn.prepare(
466 "SELECT path FROM files
467 WHERE status = 'unsupported'
468 AND (:path IS NULL
469 OR path = :path COLLATE NOCASE
470 OR path = :path_bs COLLATE NOCASE
471 OR path LIKE :path_prefix ESCAPE '\\'
472 OR path LIKE :path_prefix_bs ESCAPE '\\')",
473 )?;
474 let params = scope.params();
475 let mut rows = stmt.query(¶ms[..4])?;
476 while let Some(row) = rows.next()? {
477 visit(&row.get::<_, String>(0)?);
478 }
479 Ok(())
480}
481
482pub fn load_file_symbols(conn: &Connection, file_path: &str) -> Result<Vec<Symbol>, QueryError> {
484 let normalized = file_path.replace('\\', "/");
486 let backslash = file_path.replace('/', "\\");
487
488 let mut stmt = conn.prepare(
490 "SELECT symbol_id, file_id, path, language, name, kind, signature, doc_comment,
491 visibility, parent_symbol_id, start_line, start_column, end_line, end_column,
492 start_byte, end_byte, body_start_line, body_start_column, body_end_line,
493 body_end_column, body_start_byte, body_end_byte, body_hash, semantic_group,
494 is_test, test_container
495 FROM symbols
496 WHERE (path = ?1 OR path = ?2)
497 ORDER BY start_line ASC, start_column ASC",
498 )?;
499
500 let rows = stmt
501 .query_map(params![&normalized, &backslash], map_symbol)?
502 .collect::<Result<Vec<_>, _>>()?;
503
504 if !rows.is_empty() {
505 return Ok(rows);
506 }
507
508 let mut stmt = conn.prepare(
510 "SELECT symbol_id, file_id, path, language, name, kind, signature, doc_comment,
511 visibility, parent_symbol_id, start_line, start_column, end_line, end_column,
512 start_byte, end_byte, body_start_line, body_start_column, body_end_line,
513 body_end_column, body_start_byte, body_end_byte, body_hash, semantic_group,
514 is_test, test_container
515 FROM symbols
516 WHERE (path = ?1 COLLATE NOCASE OR path = ?2 COLLATE NOCASE)
517 ORDER BY start_line ASC, start_column ASC",
518 )?;
519
520 let rows = stmt
521 .query_map(params![normalized, backslash], map_symbol)?
522 .collect::<Result<Vec<_>, _>>()?;
523
524 Ok(rows)
525}
526
527pub fn normalize_kind(kind: &str) -> String {
529 let lower = kind.trim().to_lowercase();
530 match lower.as_str() {
531 "fn" | "func" | "function" => "function".to_string(),
532 "method" => "method".to_string(),
533 "struct" => "struct".to_string(),
534 "class" => "class".to_string(),
535 "enum" => "enum".to_string(),
536 "trait" => "trait".to_string(),
537 "interface" => "interface".to_string(),
538 "type" | "typedef" => "type".to_string(),
539 "mod" | "module" => "module".to_string(),
540 "const" | "constant" => "constant".to_string(),
541 "var" | "variable" => "variable".to_string(),
542 _ => lower,
543 }
544}
545
546pub fn search_symbols(
548 conn: &Connection,
549 query: &str,
550 kind_filter: Option<&str>,
551 include_tests: bool,
552 limit: usize,
553) -> Result<Vec<Symbol>, QueryError> {
554 search_symbols_scoped(conn, query, kind_filter, None, include_tests, limit)
555}
556
557pub(crate) fn inherited_attribute_write(alias: &str) -> String {
571 let rival_rank = path_proximity("rival.path", "pe.path");
572 let base_rank = path_proximity("c.path", "pe.path");
573 format!(
574 "(CASE WHEN {alias}.kind = 'property' AND {alias}.parent_symbol_id IS NOT NULL
575 AND ({alias}.signature LIKE 'self.%' OR {alias}.signature LIKE 'this.%' OR {alias}.signature LIKE 'cls.%')
576 THEN EXISTS (
577 WITH RECURSIVE base(symbol_id, depth) AS (
578 SELECT {alias}.parent_symbol_id, 0
579 UNION
580 SELECT r.to_symbol_id, base.depth + 1
581 FROM relationships r JOIN base ON r.from_symbol_id = base.symbol_id
582 WHERE +r.kind = 'extends' AND base.depth < 8
583 UNION
584 SELECT c.symbol_id, base.depth + 1
585 FROM pending_relationships pe
586 JOIN base ON pe.from_symbol_id = base.symbol_id
587 JOIN symbols c ON c.name = pe.target_terminal_name
588 WHERE +pe.kind = 'extends' AND base.depth < 8
589 AND c.kind IN ('class', 'interface', 'struct', 'trait', 'protocol')
590 AND NOT EXISTS (
591 SELECT 1 FROM symbols rival
592 WHERE rival.name = c.name AND rival.symbol_id != c.symbol_id
593 AND rival.kind IN ('class', 'interface', 'struct', 'trait', 'protocol')
594 AND {rival_rank} >= {base_rank})
595 )
596 SELECT 1 FROM base JOIN symbols inherited ON inherited.parent_symbol_id = base.symbol_id
597 WHERE inherited.name = {alias}.name
598 AND (base.depth > 0 OR NOT (COALESCE(inherited.signature, '') LIKE 'self.%'
599 OR inherited.signature LIKE 'this.%' OR inherited.signature LIKE 'cls.%'))
600 AND (inherited.kind NOT IN ('variable', 'field') OR inherited.signature LIKE '%=%'))
601 ELSE 0 END)"
602 )
603}
604
605fn inherited_write_exclusion(conn: &Connection) -> String {
608 if has_table(conn, "relationships") && has_table(conn, "pending_relationships") {
609 format!(" AND NOT {}", inherited_attribute_write("s"))
610 } else {
611 String::new()
612 }
613}
614
615pub fn inherited_writes_among<'a>(
618 conn: &Connection,
619 ids: impl Iterator<Item = &'a str>,
620) -> Result<HashSet<String>, QueryError> {
621 if inherited_write_exclusion(conn).is_empty() {
622 return Ok(HashSet::new());
623 }
624 let ids = serde_json::to_string(&ids.collect::<Vec<_>>()).unwrap_or_default();
625 let mut stmt = conn.prepare(&format!(
626 "SELECT s.symbol_id FROM symbols s
627 WHERE s.symbol_id IN (SELECT value FROM json_each(?1)) AND {}",
628 inherited_attribute_write("s")
629 ))?;
630 let found = stmt
631 .query_map(params![ids], |row| row.get(0))?
632 .collect::<Result<_, _>>()?;
633 Ok(found)
634}
635
636fn search_symbols_sql(variables_wanted: bool, include_tests: bool, limit: usize) -> String {
637 let mut sql = format!(
638 "SELECT symbol_id, file_id, path, language, name, kind, signature, doc_comment,
639 visibility, parent_symbol_id, start_line, start_column, end_line, end_column,
640 start_byte, end_byte, body_start_line, body_start_column, body_end_line,
641 body_end_column, body_start_byte, body_end_byte, body_hash, semantic_group,
642 is_test, test_container
643 FROM symbols s
644 WHERE (name = :query OR name LIKE :pattern ESCAPE '\\')
645 AND {KIND_FILTER}
646 AND (:path IS NULL OR replace(path, '\\', '/') = :path COLLATE NOCASE OR replace(path, '\\', '/') LIKE :path_like || '/%' ESCAPE '\\' OR replace(path, '\\', '/') LIKE '%/' || :path_like ESCAPE '\\')",
647 );
648 sql.push_str(&document_link_exclusion());
649
650 if !variables_wanted {
651 sql.push_str(&format!(" AND NOT {}", local_variable_predicate("s")));
652 }
653
654 if !include_tests {
655 sql.push_str(&format!(
656 " AND (+name = :query OR (is_test = 0 AND test_container = 0 AND NOT {}))",
657 test_path_predicate("s")
658 ));
659 }
660
661 sql.push_str(
662 " 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 ",
663 );
664 sql.push_str(&limit.to_string());
665 sql
666}
667
668pub fn search_symbols_scoped(
669 conn: &Connection,
670 query: &str,
671 kind_filter: Option<&str>,
672 path_filter: Option<&str>,
673 include_tests: bool,
674 limit: usize,
675) -> Result<Vec<Symbol>, QueryError> {
676 validate_result_limit(limit)?;
677 if limit == 0 {
678 return Ok(Vec::new());
679 }
680 let norm_kind = kind_filter.map(normalize_kind);
681 if let Some(owner) = query
682 .strip_suffix("::")
683 .or_else(|| query.strip_suffix('.'))
684 .and_then(|owner| owner.rsplit(['.', ':']).next())
685 .filter(|owner| !owner.is_empty())
686 {
687 return owner_members(
688 conn,
689 owner,
690 norm_kind.as_deref(),
691 path_filter,
692 include_tests,
693 limit,
694 );
695 }
696 if (query.contains("::") || query.contains('.'))
697 && let Some(sym) = get_symbol_by_name(conn, query, path_filter)?
698 {
699 let kind_matches = norm_kind
700 .as_deref()
701 .is_none_or(|kind| sym.kind == kind || crate::formatters::display_kind(&sym) == kind);
702 return Ok(if kind_matches { vec![sym] } else { Vec::new() });
703 }
704
705 let pattern = format!("%{}%", escape_like(query));
706 let prefix = format!("{}%", escape_like(query));
707 let normalized_path = path_filter.map(|p| {
708 p.replace('\\', "/")
709 .trim_start_matches("./")
710 .trim_matches('/')
711 .to_string()
712 });
713 let escaped_path = normalized_path.as_deref().map(escape_like);
714
715 let path_val = normalized_path.as_deref();
716 let path_like = escaped_path.as_deref();
717 let kind_val = norm_kind.as_deref();
718 let mut fetch = 4 * limit;
719 loop {
720 let sql = search_symbols_sql(
721 norm_kind.as_deref() == Some("variable"),
722 include_tests,
723 fetch,
724 );
725 let rows = conn
726 .prepare(&sql)?
727 .query_map(
728 rusqlite::named_params! {
729 ":query": query,
730 ":pattern": pattern,
731 ":prefix": prefix,
732 ":kind": kind_val,
733 ":path": path_val,
734 ":path_like": path_like,
735 },
736 map_symbol,
737 )?
738 .collect::<Result<Vec<Symbol>, _>>()?;
739 let fetched = rows.len();
740 let inherited = inherited_writes_among(conn, rows.iter().map(|s| s.symbol_id.as_str()))?;
741 let mut kept: Vec<Symbol> = rows
742 .into_iter()
743 .filter(|s| !inherited.contains(&s.symbol_id))
744 .collect();
745 let has_definition = kept.iter().any(|s| s.kind != "import");
746 let mut counted = 0;
747 let past_limit = kept.iter().position(|s| {
748 if !folds_into_import_line(query, s, has_definition) {
749 counted += 1;
750 }
751 counted > limit
752 });
753 if let Some(cut) = past_limit {
754 kept.truncate(cut);
755 return Ok(kept);
756 }
757 if fetched < fetch || fetch >= 4 * MAX_RESULT_LIMIT {
758 return Ok(kept);
759 }
760 fetch *= 2;
761 }
762}
763
764const MEMBER_LIST_OWNER_KINDS: &str = "'class', 'struct', 'interface', 'trait', 'enum', 'record', 'object', \
765 'protocol', 'union', 'module', 'namespace'";
766
767const KIND_FILTER: &str = "(:kind IS NULL OR s.kind = :kind OR :kind = CASE
770 WHEN s.language = 'html' AND s.kind = 'class' THEN 'element'
771 WHEN s.language = 'sql' AND s.kind = 'class' THEN 'table'
772 WHEN s.language = 'markdown' AND s.kind = 'module' THEN 'section'
773 WHEN s.language = 'markdown' AND s.kind = 'import' THEN 'link'
774 WHEN s.kind = 'property' AND s.language <> 'fsharp'
775 AND (s.signature GLOB 'self.*' OR s.signature GLOB 'this.*' OR s.signature GLOB 'cls.*')
776 THEN 'attribute'
777 END)";
778
779fn owner_members(
782 conn: &Connection,
783 owner: &str,
784 kind: Option<&str>,
785 path_filter: Option<&str>,
786 include_tests: bool,
787 limit: usize,
788) -> Result<Vec<Symbol>, QueryError> {
789 let path = path_filter.map(|p| {
790 p.replace('\\', "/")
791 .trim_start_matches("./")
792 .trim_matches('/')
793 .to_string()
794 });
795 let path_like = path.as_deref().map(escape_like);
796 let tests = if include_tests {
797 String::new()
798 } else {
799 format!(
800 " AND +s.is_test = 0 AND +s.test_container = 0 AND NOT {}",
801 test_path_predicate("s")
802 )
803 };
804 let docs = documentation_language_list();
805 let sql = format!(
806 "SELECT s.symbol_id, s.file_id, s.path, s.language, s.name, s.kind, s.signature, s.doc_comment,
807 s.visibility, s.parent_symbol_id, s.start_line, s.start_column, s.end_line, s.end_column,
808 s.start_byte, s.end_byte, s.body_start_line, s.body_start_column, s.body_end_line,
809 s.body_end_column, s.body_start_byte, s.body_end_byte, s.body_hash, s.semantic_group,
810 s.is_test, s.test_container
811 FROM symbols o JOIN symbols s ON s.parent_symbol_id = o.symbol_id
812 WHERE o.name = :owner
813 AND o.kind IN ({MEMBER_LIST_OWNER_KINDS})
814 AND (o.language NOT IN ({docs})
815 OR NOT EXISTS (SELECT 1 FROM symbols c WHERE c.name = :owner
816 AND c.kind IN ({MEMBER_LIST_OWNER_KINDS}) AND c.language NOT IN ({docs})))
817 AND s.kind NOT IN ('parameter', 'import')
818 AND {KIND_FILTER}
819 AND (:path IS NULL OR replace(s.path, '\\', '/') = :path COLLATE NOCASE
820 OR replace(s.path, '\\', '/') LIKE :path_like || '/%' ESCAPE '\\'
821 OR replace(s.path, '\\', '/') LIKE '%/' || :path_like ESCAPE '\\'){tests}
822 ORDER BY s.path, s.start_line"
823 );
824 let rows = conn
825 .prepare(&sql)?
826 .query_map(
827 rusqlite::named_params! {
828 ":owner": owner,
829 ":kind": kind,
830 ":path": path.as_deref(),
831 ":path_like": path_like.as_deref(),
832 },
833 map_symbol,
834 )?
835 .collect::<Result<Vec<Symbol>, _>>()?;
836 let inherited = inherited_writes_among(conn, rows.iter().map(|s| s.symbol_id.as_str()))?;
837 Ok(rows
838 .into_iter()
839 .filter(|s| !inherited.contains(&s.symbol_id))
840 .take(limit)
841 .collect())
842}
843
844pub fn folds_into_import_line(query: &str, row: &Symbol, has_definition: bool) -> bool {
848 has_definition
849 && row.kind == "import"
850 && (row.name == query
851 || row.name.ends_with(&format!(".{query}"))
852 || row.name.ends_with(&format!("::{query}")))
853}
854
855pub fn sanitize_fts5_query(query: &str) -> (String, String) {
864 let raw_words = query_words(query);
865 let split: Vec<(Vec<&str>, Option<&str>)> = raw_words
866 .iter()
867 .map(|raw| {
868 let parts = split_identifier(raw);
869 let whole = (parts.len() > 1).then_some(*raw);
870 (parts, whole)
871 })
872 .collect();
873 let any_content = split
874 .iter()
875 .any(|(parts, _)| parts.iter().any(|p| !is_stop_word(p)));
876
877 let mut and_groups: Vec<String> = Vec::new();
878 let mut or_terms: Vec<String> = Vec::new();
879 for (parts, whole) in split {
880 let parts: Vec<String> = parts
881 .into_iter()
882 .filter(|p| !any_content || !is_stop_word(p))
883 .map(fts5_term)
884 .collect();
885 let whole = whole.map(fts5_term);
886 let group = match (parts.is_empty(), whole.as_deref()) {
887 (true, None) => continue,
888 (true, Some(w)) => w.to_string(),
889 (false, None) => parts.join(" "),
890 (false, Some(w)) => format!("(({}) OR {w})", parts.join(" ")),
891 };
892 and_groups.push(group);
893 or_terms.extend(parts);
894 or_terms.extend(whole);
895 }
896
897 if and_groups.is_empty() {
898 return (String::new(), String::new());
899 }
900
901 let mut and_query = and_groups.join(" AND ");
902 if (2..=3).contains(&raw_words.len()) {
903 let all: String = raw_words.concat();
904 if all.len() <= 64 {
905 let all = fts5_term(&all);
906 and_query = format!("({and_query}) OR {all}");
907 or_terms.push(all);
908 }
909 }
910 (and_query, or_terms.join(" OR "))
911}
912
913const STOP_WORDS: &[&str] = &[
914 "a", "about", "after", "again", "all", "already", "also", "always", "an", "and", "another",
915 "any", "are", "as", "at", "be", "because", "been", "before", "being", "between", "both", "but",
916 "by", "can", "could", "did", "do", "does", "each", "either", "else", "ever", "every", "for",
917 "from", "had", "has", "have", "here", "how", "if", "in", "instead", "is", "it", "its",
918 "itself", "just", "many", "may", "might", "more", "most", "much", "must", "neither", "never",
919 "no", "nor", "not", "of", "on", "once", "one", "only", "or", "other", "our", "per", "rather",
920 "same", "should", "since", "so", "some", "still", "such", "than", "that", "the", "their",
921 "them", "then", "there", "these", "they", "this", "those", "through", "to", "too", "two",
922 "until", "very", "via", "was", "we", "were", "what", "when", "where", "whether", "which",
923 "while", "who", "whom", "why", "will", "with", "within", "without", "would", "yet", "you",
924 "your",
925];
926
927fn is_stop_word(word: &str) -> bool {
928 STOP_WORDS.contains(&word.to_ascii_lowercase().as_str())
929}
930
931fn query_words(query: &str) -> Vec<&str> {
933 query
934 .split(|c: char| !c.is_alphanumeric() && c != '_')
935 .filter(|s| !s.is_empty())
936 .collect()
937}
938
939fn trigram_name_terms(query: &str) -> Vec<String> {
944 let mut terms: Vec<String> = Vec::new();
945 for word in query_words(query) {
946 for term in std::iter::once(word).chain(split_identifier(word)) {
947 let lower = term.to_lowercase();
948 if lower.chars().count() >= 3 && !terms.contains(&lower) {
949 terms.push(lower);
950 }
951 }
952 }
953 let any_content = terms.iter().any(|t| !is_stop_word(t));
954 terms.retain(|t| !any_content || !is_stop_word(t));
955 terms
956}
957
958fn fts5_term(token: &str) -> String {
960 if token.chars().count() < 3 {
961 format!("\"{token}\"")
962 } else {
963 format!("\"{token}\"*")
964 }
965}
966
967fn name_prefix_query(name: &str) -> String {
970 name.split(|c: char| !c.is_alphanumeric() && c != '_')
971 .filter(|s| !s.is_empty())
972 .map(|s| format!("\"{s}\"*"))
973 .collect::<Vec<_>>()
974 .join(" ")
975}
976
977fn split_identifier(word: &str) -> Vec<&str> {
980 let mut out = Vec::new();
981 split_identifier_into(word, &mut out);
982 out
983}
984
985fn split_identifier_into<'a>(word: &'a str, out: &mut Vec<&'a str>) {
988 let mut chars = word.char_indices().peekable();
989 let Some((_, first)) = chars.next() else {
990 return;
991 };
992 let mut prev = char_class(first);
993 let mut start = 0;
994 while let Some((idx, c)) = chars.next() {
995 let cur = char_class(c);
996 let next = chars.peek().map_or(OTHER, |(_, n)| char_class(*n));
997 if identifier_boundary(prev, cur, next) {
998 push_piece(out, &word[start..idx]);
999 start = idx;
1000 }
1001 prev = cur;
1002 }
1003 push_piece(out, &word[start..]);
1004}
1005
1006const OTHER: u8 = 0;
1007const UNDERSCORE: u8 = 1;
1008const UPPER: u8 = 2;
1009const LOWER: u8 = 3;
1010const DIGIT: u8 = 4;
1011
1012fn char_class(c: char) -> u8 {
1013 if c == '_' {
1014 UNDERSCORE
1015 } else if c.is_uppercase() {
1016 UPPER
1017 } else if c.is_lowercase() {
1018 LOWER
1019 } else if c.is_ascii_digit() {
1020 DIGIT
1021 } else {
1022 OTHER
1023 }
1024}
1025
1026fn byte_class(b: u8) -> u8 {
1027 match b {
1028 b'_' => UNDERSCORE,
1029 b'A'..=b'Z' => UPPER,
1030 b'a'..=b'z' => LOWER,
1031 b'0'..=b'9' => DIGIT,
1032 _ => OTHER,
1033 }
1034}
1035
1036fn identifier_boundary(prev: u8, cur: u8, next: u8) -> bool {
1039 cur == UNDERSCORE
1040 || prev == UNDERSCORE
1041 || (cur == UPPER && (prev == LOWER || prev == DIGIT))
1042 || (cur == UPPER && prev == UPPER && next == LOWER)
1043 || ((cur == DIGIT) != (prev == DIGIT))
1044}
1045
1046fn push_piece<'a>(out: &mut Vec<&'a str>, piece: &'a str) {
1047 if !piece.is_empty() && piece != "_" {
1048 out.push(piece);
1049 }
1050}
1051
1052fn text_tokens_into<'a>(text: &'a str, out: &mut Vec<&'a str>) {
1056 if !text.is_ascii() {
1057 for word in text.split(|c: char| !c.is_alphanumeric() && c != '_') {
1058 split_identifier_into(word, out);
1059 }
1060 return;
1061 }
1062 let bytes = text.as_bytes();
1063 let mut start: Option<usize> = None;
1064 let mut prev = OTHER;
1065 for (i, &b) in bytes.iter().enumerate() {
1066 let cur = byte_class(b);
1067 if cur == OTHER {
1068 if let Some(s) = start.take() {
1069 push_piece(out, &text[s..i]);
1070 }
1071 continue;
1072 }
1073 match start {
1074 None => start = Some(i),
1075 Some(s) => {
1076 let next = bytes.get(i + 1).map_or(OTHER, |n| byte_class(*n));
1077 if identifier_boundary(prev, cur, next) {
1078 push_piece(out, &text[s..i]);
1079 start = Some(i);
1080 }
1081 }
1082 }
1083 prev = cur;
1084 }
1085 if let Some(s) = start {
1086 push_piece(out, &text[s..]);
1087 }
1088}
1089
1090#[derive(Clone)]
1094pub(crate) struct Candidate {
1095 pub result: SymbolSearchResult,
1096 pub bm25: Option<f64>,
1097 pub exact_name: bool,
1098 pub word_match: bool,
1099 pub name_match: bool,
1100 pub owner_match: bool,
1102 pub name_terms: Vec<String>,
1103 pub documentation: bool,
1104 pub nested: bool,
1106 pub owner: Option<String>,
1108 owner_context: Option<String>,
1110}
1111
1112const OWNER_KINDS: &str =
1114 "('class', 'struct', 'interface', 'trait', 'enum', 'record', 'object', 'protocol', 'union')";
1115
1116fn candidate_columns(conn: &Connection) -> String {
1117 format!(
1118 "s.rowid AS row_id, s.symbol_id, s.file_id, s.path, s.language, s.name, s.kind,
1119 s.signature, s.doc_comment, s.visibility, s.parent_symbol_id, s.start_line,
1120 s.start_column, s.end_line, s.end_column, s.start_byte, s.end_byte,
1121 s.body_start_line, s.body_start_column, s.body_end_line, s.body_end_column,
1122 s.body_start_byte, s.body_end_byte, s.body_hash, s.semantic_group, s.is_test,
1123 s.test_container,
1124 (s.language IN ({doc_langs}) OR NOT ({not_doc})) AS documentation,
1125 (s.kind IN ('function', 'method')
1126 AND EXISTS (SELECT 1 FROM symbols nest WHERE nest.symbol_id = s.parent_symbol_id
1127 AND nest.kind IN ('function', 'method', 'constructor')
1128 AND s.start_byte > nest.start_byte AND s.end_byte <= nest.end_byte)) AS nested,
1129 (SELECT o.name FROM symbols o WHERE o.symbol_id = s.parent_symbol_id
1130 AND o.kind IN {OWNER_KINDS}) AS owner_name",
1131 doc_langs = documentation_language_list(),
1132 not_doc = not_documentation(conn, "s")
1133 )
1134}
1135
1136fn document_link_exclusion() -> String {
1139 format!(
1140 " AND NOT (s.kind = 'import' AND s.language IN ({}))",
1141 documentation_language_list()
1142 )
1143}
1144
1145fn candidate_filters(searching_variables: bool, include_tests: bool) -> String {
1146 let mut sql = format!(
1147 " AND {KIND_FILTER}
1148 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 '\\')",
1149 );
1150 sql.push_str(&document_link_exclusion());
1151 if !searching_variables {
1152 sql.push_str(&format!(" AND NOT {}", local_variable_predicate("s")));
1153 }
1154 if !include_tests {
1155 sql.push_str(" AND +s.is_test = 0 AND +s.test_container = 0");
1158 sql.push_str(&format!(" AND NOT {}", test_path_predicate("s")));
1159 }
1160 sql
1161}
1162
1163fn map_search_candidate(row: &Row, terms: &[String]) -> rusqlite::Result<(i64, Candidate)> {
1164 let symbol = map_symbol(row)?;
1165 let lower_name = symbol.name.to_lowercase();
1166 let name_terms = terms
1167 .iter()
1168 .filter(|t| lower_name.contains(t.as_str()))
1169 .cloned()
1170 .collect();
1171 let candidate = Candidate {
1172 result: SymbolSearchResult {
1173 symbol,
1174 score: 0.0,
1175 snippet: None,
1176 explain: None,
1177 },
1178 bm25: None,
1179 exact_name: false,
1180 word_match: false,
1181 name_match: false,
1182 owner_match: false,
1183 name_terms,
1184 documentation: row.get::<_, Option<i64>>("documentation")? == Some(1),
1185 nested: row.get::<_, Option<i64>>("nested")? == Some(1),
1186 owner: row.get("owner_name")?,
1187 owner_context: None,
1188 };
1189 Ok((row.get("row_id")?, candidate))
1190}
1191
1192pub(crate) fn collect_search_candidates(
1199 conn: &Connection,
1200 query: &str,
1201 kind_filter: Option<&str>,
1202 path_filter: Option<&str>,
1203 include_tests: bool,
1204 limit: usize,
1205) -> Result<Vec<Candidate>, QueryError> {
1206 let (and_q, or_q) = sanitize_fts5_query(query);
1207 let terms = trigram_name_terms(query);
1208 let normalized_path = path_filter.map(|p| {
1209 p.replace('\\', "/")
1210 .trim_start_matches("./")
1211 .trim_matches('/')
1212 .to_string()
1213 });
1214 let escaped_path = normalized_path.as_deref().map(escape_like);
1215 let norm_kind = kind_filter.map(normalize_kind);
1216 let searching_variables = norm_kind.as_deref() == Some("variable");
1217 let path_val = normalized_path.as_deref();
1218 let path_like = escaped_path.as_deref();
1219 let kind_val = norm_kind.as_deref();
1220 let columns = candidate_columns(conn);
1221 let filters = candidate_filters(searching_variables, include_tests);
1222 let word_cap = (limit * 4).clamp(40, 160);
1223 let name_cap = (limit * 2).clamp(20, 40);
1224
1225 let new_candidate = |row: &Row| map_search_candidate(row, &terms);
1226
1227 let mut candidates: Vec<Candidate> = Vec::new();
1228 let mut by_rowid: HashMap<i64, usize> = HashMap::new();
1229 let mut admit = |rowid: i64, incoming: Candidate| match by_rowid.get(&rowid).copied() {
1230 Some(i) => {
1231 let existing = &mut candidates[i];
1232 existing.exact_name |= incoming.exact_name;
1233 existing.word_match |= incoming.word_match;
1234 existing.name_match |= incoming.name_match;
1235 existing.owner_match |= incoming.owner_match;
1236 if incoming.bm25.is_some() && existing.bm25.is_none() {
1237 existing.bm25 = incoming.bm25;
1238 existing.result = incoming.result;
1239 }
1240 }
1241 None => {
1242 by_rowid.insert(rowid, candidates.len());
1243 candidates.push(incoming);
1244 }
1245 };
1246
1247 let has_trigram = has_table(conn, "symbol_names_tri");
1248 let exact_query = query.trim();
1249 let exact_phrase = format!("\"{}\"", exact_query.replace('"', "\"\""));
1250 let exact_via_trigram = has_trigram && exact_query.chars().count() >= 3;
1251 let exact_sql = if exact_via_trigram {
1252 format!(
1253 "SELECT {columns} FROM symbol_names_tri
1254 CROSS JOIN symbols s ON s.rowid = symbol_names_tri.rowid
1255 WHERE symbol_names_tri MATCH :exact AND length(s.name) = length(:query) {filters}
1256 ORDER BY s.path ASC, s.start_line ASC LIMIT {MAX_RESULT_LIMIT}"
1257 )
1258 } else {
1259 format!(
1260 "SELECT {columns} FROM symbols s WHERE s.name = :query {filters}
1261 ORDER BY s.path ASC, s.start_line ASC LIMIT {MAX_RESULT_LIMIT}"
1262 )
1263 };
1264 let mut exact_params: Vec<(&str, &dyn ToSql)> = vec![
1265 (":query", &exact_query),
1266 (":kind", &kind_val),
1267 (":path", &path_val),
1268 (":path_like", &path_like),
1269 ];
1270 if exact_via_trigram {
1271 exact_params.push((":exact", &exact_phrase));
1272 }
1273 let exact_rows = conn
1274 .prepare(&exact_sql)?
1275 .query_map(exact_params.as_slice(), new_candidate)?
1276 .collect::<Result<Vec<_>, _>>()?;
1277 for (rowid, mut candidate) in exact_rows {
1278 candidate.exact_name = true;
1279 admit(rowid, candidate);
1280 }
1281
1282 let owner_sql = if has_trigram {
1283 format!(
1284 "SELECT {columns} FROM symbol_names_tri
1285 CROSS JOIN symbols o ON o.rowid = symbol_names_tri.rowid
1286 CROSS JOIN symbols s ON s.parent_symbol_id = o.symbol_id
1287 WHERE symbol_names_tri MATCH :phrase AND length(o.name) = length(:word)
1288 AND o.kind IN {OWNER_KINDS} {filters}
1289 ORDER BY s.path ASC, s.start_line ASC LIMIT {name_cap}"
1290 )
1291 } else {
1292 format!(
1293 "SELECT {columns} FROM symbols o
1294 CROSS JOIN symbols s ON s.parent_symbol_id = o.symbol_id
1295 WHERE o.name = :word COLLATE NOCASE AND :phrase IS NOT NULL
1296 AND o.kind IN {OWNER_KINDS} {filters}
1297 ORDER BY s.path ASC, s.start_line ASC LIMIT {name_cap}"
1298 )
1299 };
1300 let mut owner_stmt = conn.prepare(&owner_sql)?;
1301 for word in rerank_words(query).iter().filter(|w| w.len() >= 3) {
1302 let phrase = format!("\"{word}\"");
1303 let owner_rows = owner_stmt
1304 .query_map(
1305 rusqlite::named_params! {
1306 ":word": word,
1307 ":phrase": phrase,
1308 ":kind": kind_val,
1309 ":path": path_val,
1310 ":path_like": path_like,
1311 },
1312 new_candidate,
1313 )?
1314 .collect::<Result<Vec<_>, _>>()?;
1315 for (rowid, mut candidate) in owner_rows {
1316 candidate.owner_match = true;
1317 admit(rowid, candidate);
1318 }
1319 }
1320
1321 if searching_variables {
1322 let pattern = format!("%{}%", escape_like(exact_query));
1323 let local_sql = format!(
1324 "SELECT {columns} FROM symbols s
1325 WHERE {local} AND (s.name = :query OR s.name LIKE :pattern ESCAPE '\\') {filters}
1326 ORDER BY (s.name = :query) DESC, length(s.name) ASC, s.path ASC LIMIT {limit}",
1327 local = local_variable_predicate("s")
1328 );
1329 let local_rows = conn
1330 .prepare(&local_sql)?
1331 .query_map(
1332 rusqlite::named_params! {
1333 ":query": exact_query,
1334 ":pattern": pattern,
1335 ":kind": kind_val,
1336 ":path": path_val,
1337 ":path_like": path_like,
1338 },
1339 new_candidate,
1340 )?
1341 .collect::<Result<Vec<_>, _>>()?;
1342 for (rowid, mut candidate) in local_rows {
1343 candidate.exact_name = candidate.result.symbol.name == exact_query;
1344 candidate.name_match = true;
1345 admit(rowid, candidate);
1346 }
1347 }
1348
1349 let word_sql = format!(
1350 "SELECT {columns},
1351 bm25(symbols_fts, 10.0, 5.0, 1.0) AS rank_score,
1352 snippet(symbols_fts, 2, '[', ']', '...', 12) AS doc_snippet,
1353 snippet(symbols_fts, 1, '[', ']', '...', 12) AS sig_snippet,
1354 snippet(symbols_fts, 0, '[', ']', '...', 12) AS name_snippet
1355 FROM symbols_fts
1356 CROSS JOIN symbols s ON s.rowid = symbols_fts.rowid
1357 WHERE symbols_fts MATCH :match {filters}
1358 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}",
1359 doc_langs = documentation_language_list(),
1360 not_doc = not_documentation(conn, "s")
1361 );
1362 let word_rows = |match_clause: &str| -> Result<Vec<(i64, Candidate)>, QueryError> {
1363 let map_fn = |row: &Row| -> rusqlite::Result<(i64, Candidate)> {
1364 let (rowid, mut candidate) = new_candidate(row)?;
1365 let score: f64 = row.get("rank_score")?;
1366 let doc_snip: Option<String> = row.get("doc_snippet").ok();
1367 let sig_snip: Option<String> = row.get("sig_snippet").ok();
1368 let name_snip: Option<String> = row.get("name_snippet").ok();
1369 let highlighted = |s: &Option<String>| s.as_ref().is_some_and(|s| s.contains('['));
1370 candidate.result.snippet = if highlighted(&doc_snip) {
1371 doc_snip
1372 } else if highlighted(&sig_snip) {
1373 sig_snip
1374 } else if highlighted(&name_snip) {
1375 name_snip
1376 } else {
1377 doc_snip.or(sig_snip).or(name_snip)
1378 };
1379 candidate.result.score = score;
1380 candidate.bm25 = Some(score);
1381 candidate.word_match = true;
1382 Ok((rowid, candidate))
1383 };
1384 Ok(conn
1385 .prepare(&word_sql)?
1386 .query_map(
1387 rusqlite::named_params! {
1388 ":match": match_clause,
1389 ":query": query.trim(),
1390 ":kind": kind_val,
1391 ":path": path_val,
1392 ":path_like": path_like,
1393 },
1394 map_fn,
1395 )?
1396 .collect::<Result<Vec<_>, _>>()?)
1397 };
1398 if !and_q.is_empty() {
1399 let and_rows = word_rows(&and_q)?;
1400 let mut word_admitted: HashSet<i64> = and_rows.iter().map(|(rowid, _)| *rowid).collect();
1401 for (rowid, candidate) in and_rows {
1402 admit(rowid, candidate);
1403 }
1404 if and_q != or_q {
1405 for (rowid, candidate) in word_rows(&or_q)? {
1406 if word_admitted.len() >= word_cap && !word_admitted.contains(&rowid) {
1407 break;
1408 }
1409 word_admitted.insert(rowid);
1410 admit(rowid, candidate);
1411 }
1412 }
1413 }
1414
1415 if !terms.is_empty() && has_trigram {
1416 let match_clause = terms
1417 .iter()
1418 .map(|t| format!("\"{t}\""))
1419 .collect::<Vec<_>>()
1420 .join(" OR ");
1421 let name_sql = format!(
1422 "SELECT {columns} FROM symbol_names_tri
1423 CROSS JOIN symbols s ON s.rowid = symbol_names_tri.rowid
1424 WHERE symbol_names_tri MATCH :match {filters}
1425 ORDER BY bm25(symbol_names_tri) ASC, length(s.name) ASC, s.path ASC LIMIT {name_cap}"
1426 );
1427 let name_rows = conn
1428 .prepare(&name_sql)?
1429 .query_map(
1430 rusqlite::named_params! {
1431 ":match": match_clause,
1432 ":kind": kind_val,
1433 ":path": path_val,
1434 ":path_like": path_like,
1435 },
1436 new_candidate,
1437 )?
1438 .collect::<Result<Vec<_>, _>>()?;
1439 for (rowid, mut candidate) in name_rows {
1440 candidate.name_match = true;
1441 admit(rowid, candidate);
1442 }
1443 }
1444
1445 let inherited = inherited_writes_among(
1446 conn,
1447 candidates
1448 .iter()
1449 .map(|c| c.result.symbol.symbol_id.as_str()),
1450 )?;
1451 candidates.retain(|c| !inherited.contains(&c.result.symbol.symbol_id));
1452 Ok(candidates)
1453}
1454
1455fn expand_ranked_owner_members(
1458 conn: &Connection,
1459 candidates: &mut Vec<Candidate>,
1460 ranked: &[(SymbolSearchResult, SearchExplain)],
1461 query: &str,
1462 kind_filter: Option<&str>,
1463 path_filter: Option<&str>,
1464 include_tests: bool,
1465) -> Result<bool, QueryError> {
1466 let owners: Vec<_> = ranked
1469 .iter()
1470 .take(20)
1471 .filter(|(result, explain)| {
1472 explain.documentation == 0.0
1473 && OWNER_KINDS.contains(&format!("'{}'", result.symbol.kind))
1474 })
1475 .take(3)
1476 .map(|(result, _)| &result.symbol)
1477 .collect();
1478 if owners.is_empty() {
1479 return Ok(false);
1480 }
1481 let normalized_path = path_filter.map(|p| {
1482 p.replace('\\', "/")
1483 .trim_start_matches("./")
1484 .trim_matches('/')
1485 .to_string()
1486 });
1487 let escaped_path = normalized_path.as_deref().map(escape_like);
1488 let norm_kind = kind_filter.map(normalize_kind);
1489 let columns = candidate_columns(conn);
1490 let filters = candidate_filters(norm_kind.as_deref() == Some("variable"), include_tests);
1491 let mut stmt = conn.prepare(&format!(
1492 "SELECT {columns} FROM symbols s WHERE s.parent_symbol_id = :owner {filters}
1493 ORDER BY s.start_line, s.start_column, s.symbol_id LIMIT 40"
1494 ))?;
1495 let terms = trigram_name_terms(query);
1496 let stemmer = Stemmer::create(Algorithm::English);
1497 let words = stemmed_query_words(query, &stemmer);
1498 let mut members = Vec::new();
1499 for owner in owners {
1500 let context = format!(
1501 "{} {}",
1502 owner.name,
1503 head_bytes(owner.doc_comment.as_deref().unwrap_or(""), TEXT_HEAD_BYTES)
1504 );
1505 let rows = stmt.query_map(
1506 rusqlite::named_params! {
1507 ":owner": owner.symbol_id,
1508 ":kind": norm_kind.as_deref(),
1509 ":path": normalized_path.as_deref(),
1510 ":path_like": escaped_path.as_deref(),
1511 },
1512 |row| map_search_candidate(row, &terms),
1513 )?;
1514 for row in rows {
1515 let (_, mut member) = row?;
1516 let symbol = &member.result.symbol;
1517 let mut tokens = Vec::new();
1518 let named = name_hits(&symbol.name, &words, &stemmer)
1519 .iter()
1520 .any(|hit| *hit > 0);
1521 let text_match = [symbol.signature.as_deref(), symbol.doc_comment.as_deref()]
1522 .into_iter()
1523 .any(|text| {
1524 text_hits(
1525 text.map(|s| head_bytes(s, TEXT_HEAD_BYTES)),
1526 &words,
1527 &mut tokens,
1528 )
1529 .into_iter()
1530 .any(|hit| hit)
1531 });
1532 if named || text_match {
1533 member.owner_context = Some(context.clone());
1534 members.push(member);
1535 }
1536 }
1537 }
1538 let inherited = inherited_writes_among(
1539 conn,
1540 members.iter().map(|c| c.result.symbol.symbol_id.as_str()),
1541 )?;
1542 let mut changed = false;
1543 for member in members {
1544 let id = &member.result.symbol.symbol_id;
1545 if inherited.contains(id) {
1546 continue;
1547 }
1548 if let Some(existing) = candidates
1549 .iter_mut()
1550 .find(|c| c.result.symbol.symbol_id == *id)
1551 {
1552 existing.owner_context = member.owner_context;
1553 } else {
1554 candidates.push(member);
1555 }
1556 changed = true;
1557 }
1558 Ok(changed)
1559}
1560
1561pub fn fts_search_symbols_scoped(
1563 conn: &Connection,
1564 query: &str,
1565 kind_filter: Option<&str>,
1566 path_filter: Option<&str>,
1567 include_tests: bool,
1568 limit: usize,
1569) -> Result<Vec<SymbolSearchResult>, QueryError> {
1570 fts_search_symbols_explained(
1571 conn,
1572 query,
1573 kind_filter,
1574 path_filter,
1575 include_tests,
1576 limit,
1577 false,
1578 )
1579}
1580
1581pub fn fts_search_symbols_explained(
1584 conn: &Connection,
1585 query: &str,
1586 kind_filter: Option<&str>,
1587 path_filter: Option<&str>,
1588 include_tests: bool,
1589 limit: usize,
1590 explain: bool,
1591) -> Result<Vec<SymbolSearchResult>, QueryError> {
1592 validate_result_limit(limit)?;
1593 if limit == 0 {
1594 return Ok(Vec::new());
1595 }
1596 let (and_q, _) = sanitize_fts5_query(query);
1597 if and_q.is_empty() {
1598 return Ok(Vec::new());
1599 }
1600
1601 let normalized_path = path_filter.map(|p| {
1602 p.replace('\\', "/")
1603 .trim_start_matches("./")
1604 .trim_matches('/')
1605 .to_string()
1606 });
1607 let norm_kind = kind_filter.map(normalize_kind);
1608 let escaped_path = normalized_path.as_deref().map(escape_like);
1609 let searching_variables = norm_kind.as_deref() == Some("variable");
1610
1611 let name_search = |local_clause: &str| -> Result<Vec<SymbolSearchResult>, QueryError> {
1612 let pattern = format!("%{}%", escape_like(query));
1613 let mut sql = format!(
1614 "SELECT symbol_id, file_id, path, language, name, kind, signature, doc_comment,
1615 visibility, parent_symbol_id, start_line, start_column, end_line, end_column,
1616 start_byte, end_byte, body_start_line, body_start_column, body_end_line,
1617 body_end_column, body_start_byte, body_end_byte, body_hash, semantic_group,
1618 is_test, test_container
1619 FROM symbols s
1620 WHERE (name = :query OR name LIKE :pattern ESCAPE '\\')
1621 AND {KIND_FILTER}
1622 AND (:path IS NULL OR replace(path, '\\', '/') = :path COLLATE NOCASE OR replace(path, '\\', '/') LIKE :path_like || '/%' ESCAPE '\\' OR replace(path, '\\', '/') LIKE '%/' || :path_like ESCAPE '\\')",
1623 );
1624 sql.push_str(&document_link_exclusion());
1625 sql.push_str(local_clause);
1626 if !include_tests {
1627 sql.push_str(" AND is_test = 0 AND test_container = 0");
1628 sql.push_str(&format!(" AND NOT {}", test_path_predicate("s")));
1629 }
1630 sql.push_str(
1631 " ORDER BY (name = :query) DESC, (kind IN ('function', 'struct', 'class', 'trait', 'method', 'enum', 'interface', 'type')) DESC, length(name) ASC, path ASC LIMIT ",
1632 );
1633 sql.push_str(&limit.to_string());
1634
1635 let mut stmt = conn.prepare(&sql)?;
1636 let path_val = normalized_path.as_deref();
1637 let path_like = escaped_path.as_deref();
1638 let kind_val = norm_kind.as_deref();
1639 let rows = stmt
1640 .query_map(
1641 rusqlite::named_params! {
1642 ":query": query,
1643 ":pattern": pattern,
1644 ":kind": kind_val,
1645 ":path": path_val,
1646 ":path_like": path_like,
1647 },
1648 map_symbol,
1649 )?
1650 .collect::<Result<Vec<_>, _>>()?;
1651
1652 Ok(rows
1653 .into_iter()
1654 .map(|s| SymbolSearchResult {
1655 symbol: s,
1656 score: 0.0,
1657 snippet: None,
1658 explain: None,
1659 })
1660 .collect())
1661 };
1662
1663 if !has_table(conn, "symbols_fts") {
1664 let local_clause = if searching_variables {
1665 String::new()
1666 } else {
1667 format!(" AND NOT {}", local_variable_predicate("s"))
1668 };
1669 return name_search(&local_clause);
1670 }
1671
1672 let mut candidates =
1673 collect_search_candidates(conn, query, kind_filter, path_filter, include_tests, limit)?;
1674 let started = std::time::Instant::now();
1675 let idf = idf_weights(conn, &rerank_words(query));
1676 let mut ranked = rerank_with(candidates.clone(), query, include_tests, Some(&idf));
1677 let mut rerank_us = started.elapsed().as_micros();
1678 let mut candidate_count = candidates.len();
1679 if expand_ranked_owner_members(
1680 conn,
1681 &mut candidates,
1682 &ranked,
1683 query,
1684 kind_filter,
1685 path_filter,
1686 include_tests,
1687 )? {
1688 candidate_count = candidates.len();
1689 let started = std::time::Instant::now();
1690 ranked = rerank_with(candidates, query, include_tests, Some(&idf));
1691 rerank_us += started.elapsed().as_micros();
1692 }
1693 let unique_query_terms: HashSet<String> = rerank_words(query).into_iter().collect();
1694 let min_multi_term_results = ((limit * 3).div_ceil(4)).max(15).min(limit);
1695 let multi_term_results = ranked
1696 .iter()
1697 .filter(|(_, breakdown)| covered_query_term_count(&breakdown.terms) >= 2)
1698 .count();
1699 if unique_query_terms.len() > 1 && multi_term_results >= min_multi_term_results {
1700 let mut strong_results_seen = 0;
1701 ranked.retain(|(_, breakdown)| {
1702 if covered_query_term_count(&breakdown.terms) >= 2 {
1703 strong_results_seen += 1;
1704 true
1705 } else {
1706 strong_results_seen < min_multi_term_results || has_distinctive_name_term(breakdown)
1707 }
1708 });
1709 }
1710 Ok(ranked
1711 .into_iter()
1712 .take(limit)
1713 .map(|(mut result, mut breakdown)| {
1714 if explain {
1715 breakdown.candidates = candidate_count;
1716 breakdown.rerank_us = rerank_us;
1717 result.explain = Some(breakdown);
1718 }
1719 result
1720 })
1721 .collect())
1722}
1723
1724const W_NAME_WHOLE: f64 = 100.0;
1725const W_NAME_ALL_WORDS: f64 = 60.0;
1726const W_KIND_DEFINITION: f64 = 4.0;
1727const W_KIND_MEMBER: f64 = 0.0;
1728const W_KIND_IMPORT: f64 = -50.0;
1729const W_NESTED: f64 = -2.0;
1731const W_PATH_ROLE: f64 = -10.0;
1732const W_DOCUMENTATION_ROW: f64 = -200.0;
1733const W_TEST_INTENT: f64 = 5.0;
1734const W_TERMS: f64 = 52.0;
1735const MAX_TERM_CREDIT: f64 = 3.0;
1736const TEXT_CREDIT: f64 = 1.0;
1737const FIELD_DIVERSITY_BONUS: f64 = 6.0;
1738const DISTINCTIVE_NAME_IDF_FACTOR: f64 = 1.1;
1739const OWNER_CREDIT: f64 = 3.0;
1741const OWNER_CONTEXT_CREDIT: f64 = 0.5;
1742const TEXT_HEAD_BYTES: usize = 400;
1743const DOC_HEAD_BYTES: usize = 640;
1744
1745pub const DEFINITION_KINDS: &[&str] = &[
1747 "function",
1748 "method",
1749 "class",
1750 "struct",
1751 "trait",
1752 "interface",
1753 "enum",
1754 "type",
1755];
1756const MEMBER_KINDS: &[&str] = &["enum_member", "field", "property", "constant", "variable"];
1757const DEMOTED_PATH_SEGMENTS: &[&str] = &["scripts", "examples", "benchmarks", "fixtures", "vendor"];
1758const TEST_INTENT_WORDS: &[&str] = &["test", "tests", "spec", "specs"];
1759
1760struct QueryWord {
1761 word: String,
1762 stem: String,
1763}
1764
1765fn stemmed_query_words(query: &str, stemmer: &Stemmer) -> Vec<QueryWord> {
1766 rerank_words(query)
1767 .into_iter()
1768 .map(|word| QueryWord {
1769 stem: stemmer.stem(&word).into_owned(),
1770 word,
1771 })
1772 .collect()
1773}
1774
1775struct Hits {
1778 name: Vec<u8>,
1779 owner: Vec<bool>,
1782 signature: Vec<bool>,
1783 doc: Vec<bool>,
1784 owner_context: Vec<bool>,
1785}
1786
1787const DF_CAP: usize = 20_000;
1790
1791fn idf_weights(conn: &Connection, words: &[String]) -> Vec<f64> {
1796 let n = conn
1797 .query_row("SELECT max(rowid) FROM symbols", [], |r| {
1798 r.get::<_, Option<i64>>(0)
1799 })
1800 .ok()
1801 .flatten()
1802 .unwrap_or(0) as f64;
1803 let mut count = conn
1804 .prepare(&format!(
1805 "SELECT count(*) FROM (SELECT rowid FROM symbols_fts WHERE symbols_fts MATCH ?1 LIMIT {DF_CAP})"
1806 ))
1807 .ok();
1808 words
1809 .iter()
1810 .map(|word| {
1811 let df = count
1812 .as_mut()
1813 .and_then(|stmt| document_frequency(stmt, &format!("\"{word}\"")))
1814 .unwrap_or(0);
1815 (1.0 + n / (df as f64 + 1.0)).ln()
1816 })
1817 .collect()
1818}
1819
1820fn document_frequency(stmt: &mut rusqlite::Statement<'_>, term: &str) -> Option<i64> {
1821 stmt.query_row([term], |r| r.get(0)).ok()
1822}
1823
1824fn term_credits(hits: &Hits, words: &[QueryWord]) -> Vec<(String, String, f64)> {
1828 words
1829 .iter()
1830 .enumerate()
1831 .map(|(i, w)| {
1832 let (field, credit) = match hits.name[i] {
1833 3 => ("name", 3.0),
1834 2 => ("name", 2.0),
1835 _ if hits.owner[i] => ("owner", OWNER_CREDIT),
1836 _ if hits.signature[i] => ("signature", TEXT_CREDIT),
1837 _ if hits.doc[i] => ("doc", TEXT_CREDIT),
1838 1 => ("name", 1.0),
1839 _ if hits.owner_context[i] => ("owner_context", OWNER_CONTEXT_CREDIT),
1840 _ => ("none", 0.0),
1841 };
1842 (w.word.clone(), field.to_string(), credit)
1843 })
1844 .collect()
1845}
1846
1847fn credited_field_bonus(terms: &[(String, String, f64)]) -> f64 {
1851 let query_terms: HashSet<&str> = terms.iter().map(|(word, _, _)| word.as_str()).collect();
1852 let covered_terms: HashSet<&str> = terms
1853 .iter()
1854 .filter(|(_, _, credit)| *credit >= TEXT_CREDIT)
1855 .map(|(word, _, _)| word.as_str())
1856 .collect();
1857 let adequate_query_coverage =
1858 !query_terms.is_empty() && covered_terms.len() * 2 >= query_terms.len();
1859 let mut document_terms = HashSet::new();
1860 let has_exact_name_term = terms
1861 .iter()
1862 .any(|(_, field, credit)| field == "name" && *credit == 3.0);
1863 let fields = terms.iter().filter(|(_, _, credit)| *credit > 0.0).fold(
1864 0u8,
1865 |fields, (word, field, _)| {
1866 if field == "doc" {
1867 document_terms.insert(word.as_str());
1869 }
1870 fields
1871 | match field.as_str() {
1872 "name" => 1,
1873 "owner" => 2,
1874 "signature" => 4,
1875 "doc" => 8,
1876 "owner_context" => 16,
1877 _ => 0,
1878 }
1879 },
1880 );
1881 if has_exact_name_term
1882 && fields & 4 != 0
1883 && fields & 8 != 0
1884 && document_terms.len() >= 2
1885 && adequate_query_coverage
1886 {
1887 FIELD_DIVERSITY_BONUS
1888 } else {
1889 0.0
1890 }
1891}
1892
1893fn covered_query_term_count(terms: &[(String, String, f64)]) -> usize {
1894 terms
1895 .iter()
1896 .filter(|(_, _, credit)| *credit > 0.0)
1897 .map(|(word, _, _)| word.as_str())
1898 .collect::<HashSet<_>>()
1899 .len()
1900}
1901
1902fn has_distinctive_name_term(explain: &SearchExplain) -> bool {
1903 let weights = &explain.word_weights;
1904 let Some(mean_weight) = (!weights.is_empty())
1905 .then(|| weights.iter().map(|(_, weight)| weight).sum::<f64>() / weights.len() as f64)
1906 else {
1907 return false;
1908 };
1909 explain
1910 .terms
1911 .iter()
1912 .zip(weights)
1913 .any(|((term, field, credit), (weighted_term, weight))| {
1914 term == weighted_term
1915 && field == "name"
1916 && *credit >= 2.0
1917 && *weight >= mean_weight * DISTINCTIVE_NAME_IDF_FACTOR
1918 })
1919}
1920
1921fn term_score(terms: &[(String, String, f64)], weights: &[f64]) -> f64 {
1922 let total: f64 = weights.iter().sum();
1923 if total == 0.0 {
1924 return 0.0;
1925 }
1926 let credited: f64 = terms
1927 .iter()
1928 .zip(weights)
1929 .map(|((_, _, credit), weight)| credit * weight)
1930 .sum();
1931 W_TERMS * credited / (MAX_TERM_CREDIT * total)
1932}
1933
1934fn rerank_words(query: &str) -> Vec<String> {
1937 let words: Vec<String> = query_words(query)
1938 .into_iter()
1939 .flat_map(split_identifier)
1940 .map(str::to_lowercase)
1941 .collect();
1942 let any_content = words.iter().any(|w| !is_stop_word(w));
1943 words
1944 .into_iter()
1945 .filter(|w| !any_content || !is_stop_word(w))
1946 .collect()
1947}
1948
1949fn collapse(text: &str) -> String {
1950 text.chars()
1951 .filter(|c| c.is_alphanumeric())
1952 .flat_map(char::to_lowercase)
1953 .collect()
1954}
1955
1956fn head_bytes(text: &str, bytes: usize) -> &str {
1957 let mut end = bytes.min(text.len());
1958 while !text.is_char_boundary(end) {
1959 end -= 1;
1960 }
1961 &text[..end]
1962}
1963
1964fn token_run_equals(tokens: &[String], word: &str) -> bool {
1965 (0..tokens.len()).any(|start| {
1966 let mut joined = String::new();
1967 for token in &tokens[start..] {
1968 joined.push_str(token);
1969 if joined.len() >= word.len() {
1970 return joined == word;
1971 }
1972 }
1973 false
1974 })
1975}
1976
1977fn name_hits(name: &str, words: &[QueryWord], stemmer: &Stemmer) -> Vec<u8> {
1981 let tokens: Vec<String> = split_identifier(name)
1982 .into_iter()
1983 .map(str::to_lowercase)
1984 .collect();
1985 let stems: Vec<String> = tokens
1986 .iter()
1987 .map(|t| stemmer.stem(t).into_owned())
1988 .collect();
1989 let collapsed = collapse(name);
1990 words
1991 .iter()
1992 .map(|w| {
1993 if token_run_equals(&tokens, &w.word) {
1994 3
1995 } else if stems.contains(&w.stem) {
1996 2
1997 } else if w.word.chars().count() >= 3 && collapsed.contains(&w.word) {
1998 1
1999 } else {
2000 0
2001 }
2002 })
2003 .collect()
2004}
2005
2006fn lowercase_prefix_match(token: &str, prefix: &str, exact: bool) -> bool {
2009 if token.is_ascii() && prefix.is_ascii() {
2010 let Some(head) = token.as_bytes().get(..prefix.len()) else {
2011 return false;
2012 };
2013 return head.eq_ignore_ascii_case(prefix.as_bytes())
2014 && (!exact || token.len() == prefix.len());
2015 }
2016 let mut lower = token.chars().flat_map(char::to_lowercase);
2017 for expected in prefix.chars() {
2018 if lower.next() != Some(expected) {
2019 return false;
2020 }
2021 }
2022 !exact || lower.next().is_none()
2023}
2024
2025fn text_hits<'a>(
2029 text: Option<&'a str>,
2030 words: &[QueryWord],
2031 tokens: &mut Vec<&'a str>,
2032) -> Vec<bool> {
2033 tokens.clear();
2034 text_tokens_into(text.unwrap_or(""), tokens);
2035 words
2036 .iter()
2037 .map(|w| {
2038 let stem_prefix = w.stem.chars().count() >= 3;
2039 let exact_word = w.word.chars().count() < 3;
2040 tokens.iter().any(|t| {
2041 lowercase_prefix_match(t, &w.word, exact_word)
2042 || (stem_prefix && lowercase_prefix_match(t, &w.stem, false))
2043 })
2044 })
2045 .collect()
2046}
2047
2048fn kind_prior(kind: &str) -> f64 {
2049 let kind = normalize_kind(kind);
2050 match kind.as_str() {
2051 "import" => W_KIND_IMPORT,
2052 k if DEFINITION_KINDS.contains(&k) => W_KIND_DEFINITION,
2053 k if MEMBER_KINDS.contains(&k) => W_KIND_MEMBER,
2054 _ => 0.0,
2055 }
2056}
2057
2058fn path_role(path: &str, words: &[QueryWord], stemmer: &Stemmer) -> f64 {
2059 let Some(segment) = path.split(['/', '\\']).find(|seg| {
2060 DEMOTED_PATH_SEGMENTS
2061 .iter()
2062 .any(|d| d.eq_ignore_ascii_case(seg))
2063 }) else {
2064 return 0.0;
2065 };
2066 let segment = segment.to_lowercase();
2067 let segment_stem = stemmer.stem(&segment);
2068 let named = words.iter().any(|w| {
2069 w.word == segment || w.word == segment_stem || w.stem == segment || w.stem == segment_stem
2070 });
2071 if named { 0.0 } else { W_PATH_ROLE }
2072}
2073
2074fn bracket_longest_term(name: &str, terms: &[String]) -> String {
2075 let lower = name.to_lowercase();
2076 if lower.len() != name.len() {
2077 return name.to_string();
2078 }
2079 let mut best: Option<(usize, usize)> = None;
2080 for term in terms {
2081 if let Some(start) = lower.find(term.as_str()) {
2082 let end = start + term.len();
2083 let longer = best.is_none_or(|(s, e)| end - start > e - s);
2084 if longer && name.is_char_boundary(start) && name.is_char_boundary(end) {
2085 best = Some((start, end));
2086 }
2087 }
2088 }
2089 match best {
2090 Some((start, end)) => {
2091 format!("{}[{}]{}", &name[..start], &name[start..end], &name[end..])
2092 }
2093 None => name.to_string(),
2094 }
2095}
2096
2097fn branch_snippet(candidate: &Candidate) -> Option<String> {
2098 let name = &candidate.result.symbol.name;
2099 if candidate.word_match {
2100 candidate.result.snippet.clone()
2101 } else if candidate.exact_name {
2102 Some(name.clone())
2103 } else {
2104 Some(bracket_longest_term(name, &candidate.name_terms))
2105 }
2106}
2107
2108fn rerank_with(
2113 candidates: Vec<Candidate>,
2114 query: &str,
2115 include_tests: bool,
2116 idf: Option<&[f64]>,
2117) -> Vec<(SymbolSearchResult, SearchExplain)> {
2118 let stemmer = Stemmer::create(Algorithm::English);
2119 let words = stemmed_query_words(query, &stemmer);
2120 let collapsed_query = collapse(query);
2121 let test_intent = include_tests
2122 && words
2123 .iter()
2124 .any(|w| TEST_INTENT_WORDS.contains(&w.word.as_str()));
2125
2126 let mut tokens: Vec<&str> = Vec::new();
2127 let hits: Vec<Hits> = candidates
2128 .iter()
2129 .map(|candidate| {
2130 let symbol = &candidate.result.symbol;
2131 let name = name_hits(&symbol.name, &words, &stemmer);
2132 let named = name.iter().any(|strength| *strength > 0);
2133 Hits {
2134 owner: match candidate.owner.as_deref().map(collapse) {
2135 Some(owner) if named => {
2136 let owner_stem = stemmer.stem(&owner);
2137 words
2138 .iter()
2139 .map(|w| owner == w.word || owner_stem == w.stem)
2140 .collect()
2141 }
2142 _ => vec![false; words.len()],
2143 },
2144 name,
2145 signature: text_hits(
2146 symbol
2147 .signature
2148 .as_deref()
2149 .map(|signature| head_bytes(signature, TEXT_HEAD_BYTES)),
2150 &words,
2151 &mut tokens,
2152 ),
2153 doc: text_hits(
2154 symbol
2155 .doc_comment
2156 .as_deref()
2157 .map(|doc| head_bytes(doc, DOC_HEAD_BYTES)),
2158 &words,
2159 &mut tokens,
2160 ),
2161 owner_context: text_hits(candidate.owner_context.as_deref(), &words, &mut tokens),
2162 }
2163 })
2164 .collect();
2165 let term_weights: Vec<f64> = match idf {
2166 Some(weights) if weights.len() == words.len() => weights.to_vec(),
2167 _ => vec![1.0; words.len()],
2168 };
2169 let word_weights: Vec<(String, f64)> = words
2170 .iter()
2171 .zip(&term_weights)
2172 .map(|(w, weight)| (w.word.clone(), *weight))
2173 .collect();
2174
2175 let mut scored: Vec<(SymbolSearchResult, SearchExplain)> = candidates
2176 .into_iter()
2177 .zip(hits)
2178 .map(|(candidate, hits)| {
2179 let symbol = &candidate.result.symbol;
2180 let name_strength: u32 = hits.name.iter().map(|s| u32::from(*s)).sum();
2181 let tier = if !collapsed_query.is_empty() && collapse(&symbol.name) == collapsed_query {
2182 "whole"
2183 } else if !hits.name.is_empty() && hits.name.iter().all(|s| *s >= 2) {
2184 "all"
2185 } else if hits.name.iter().any(|s| *s > 0) {
2186 "partial"
2187 } else {
2188 "none"
2189 };
2190 let terms = term_credits(&hits, &words);
2191 let field_diversity_bonus = if matches!(symbol.kind.as_str(), "function" | "method") {
2192 credited_field_bonus(&terms)
2193 } else {
2194 0.0
2195 };
2196 let explain = SearchExplain {
2197 bm25: candidate.bm25,
2198 branches: [
2199 (candidate.exact_name, "exact"),
2200 (candidate.word_match, "word"),
2201 (candidate.name_match, "name"),
2202 (candidate.owner_match, "owner"),
2203 (candidate.owner_context.is_some(), "owner_context"),
2204 ]
2205 .into_iter()
2206 .filter(|(hit, _)| *hit)
2207 .map(|(_, branch)| branch.to_string())
2208 .collect(),
2209 name_tier: tier.to_string(),
2210 name_strength,
2211 term_score: term_score(&terms, &term_weights),
2212 field_diversity_bonus,
2213 name_bonus: match tier {
2214 "whole"
2215 if DEFINITION_KINDS.contains(&normalize_kind(&symbol.kind).as_str()) =>
2216 {
2217 W_NAME_WHOLE
2218 }
2219 "whole" | "all" => W_NAME_ALL_WORDS,
2220 _ => 0.0,
2221 },
2222 kind_prior: kind_prior(&symbol.kind),
2223 path_role: path_role(&symbol.path, &words, &stemmer),
2224 documentation: if candidate.documentation {
2225 W_DOCUMENTATION_ROW
2226 } else {
2227 0.0
2228 },
2229 test_intent: if test_intent && (symbol.is_test || symbol.test_container) {
2230 W_TEST_INTENT
2231 } else {
2232 0.0
2233 },
2234 nested: if candidate.nested { W_NESTED } else { 0.0 },
2235 terms,
2236 word_weights: word_weights.clone(),
2237 candidates: 0,
2238 rerank_us: 0,
2239 };
2240 let score = explain.term_score
2241 + explain.field_diversity_bonus
2242 + explain.name_bonus
2243 + explain.kind_prior
2244 + explain.path_role
2245 + explain.documentation
2246 + explain.test_intent
2247 + explain.nested;
2248 let snippet = branch_snippet(&candidate);
2249 let mut result = candidate.result;
2250 result.score = score;
2251 result.snippet = snippet;
2252 (result, explain)
2253 })
2254 .collect();
2255
2256 scored.sort_by(|(a, ea), (b, eb)| {
2257 b.score
2258 .total_cmp(&a.score)
2259 .then_with(|| eb.name_strength.cmp(&ea.name_strength))
2260 .then_with(|| {
2261 a.symbol
2262 .name
2263 .starts_with('_')
2264 .cmp(&b.symbol.name.starts_with('_'))
2265 })
2266 .then_with(|| ea.bm25.is_none().cmp(&eb.bm25.is_none()))
2267 .then_with(|| ea.bm25.unwrap_or(0.0).total_cmp(&eb.bm25.unwrap_or(0.0)))
2268 .then_with(|| a.symbol.name.len().cmp(&b.symbol.name.len()))
2269 .then_with(|| a.symbol.path.cmp(&b.symbol.path))
2270 .then_with(|| a.symbol.name.cmp(&b.symbol.name))
2271 });
2272 scored
2273}
2274
2275const LOW_SIGNAL_KINDS_SQL: &str =
2276 "'import','variable','parameter','field','property','module','namespace'";
2277
2278pub fn find_related_tests(
2280 conn: &Connection,
2281 target_symbol: &Symbol,
2282 limit: usize,
2283) -> Result<Vec<Symbol>, QueryError> {
2284 if limit == 0 || DOCUMENTATION_LANGUAGES.contains(&target_symbol.language.as_str()) {
2285 return Ok(Vec::new());
2286 }
2287
2288 const COLUMNS: &str = "s.symbol_id, s.file_id, s.path, s.language, s.name, s.kind, s.signature, s.doc_comment,
2289 s.visibility, s.parent_symbol_id, s.start_line, s.start_column, s.end_line, s.end_column,
2290 s.start_byte, s.end_byte, s.body_start_line, s.body_start_column, s.body_end_line,
2291 s.body_end_column, s.body_start_byte, s.body_end_byte, s.body_hash, s.semantic_group,
2292 s.is_test, s.test_container";
2293 const TEST_ORDER: &str = "s.is_test DESC, s.path, s.start_line, s.symbol_id";
2295 let not_setup = |alias: &str| {
2296 if has_column(conn, "symbols", "test_lifecycle") {
2297 format!("COALESCE({alias}.test_lifecycle, 0) = 0")
2298 } else {
2299 "1 = 1".to_string()
2300 }
2301 };
2302 let is_test = format!(
2303 "((s.is_test = 1 OR s.test_container = 1
2304 OR ({} AND s.kind IN ('function', 'method') AND ({}
2305 OR EXISTS (SELECT 1 FROM symbols tc
2306 WHERE tc.symbol_id = s.parent_symbol_id
2307 AND (tc.test_container = 1
2308 OR (tc.kind IN ('function', 'method') AND {}
2309 AND (tc.is_test = 1 OR {})))))))
2310 AND {})",
2311 test_path_predicate("s"),
2312 names_a_test_sql("s"),
2313 not_setup("tc"),
2314 names_a_test_sql("tc"),
2315 not_setup("s")
2316 );
2317 let not_documentation = not_documentation(conn, "s");
2318
2319 let mut tests = Vec::new();
2320 let mut seen_ids = std::collections::HashSet::from([target_symbol.symbol_id.clone()]);
2321
2322 let callers_sql = format!(
2323 "SELECT {COLUMNS}
2324 FROM symbols s
2325 JOIN relationships r ON r.from_symbol_id = s.symbol_id
2326 WHERE r.to_symbol_id = ?1 AND {is_test} AND {not_documentation}
2327 ORDER BY {TEST_ORDER}
2328 LIMIT ?2"
2329 );
2330
2331 if let Ok(mut stmt) = conn.prepare(&callers_sql)
2332 && let Ok(rows) = stmt.query_map(params![target_symbol.symbol_id, limit as i64], map_symbol)
2333 {
2334 for row in rows.flatten() {
2335 let row = enclosing_test(conn, row)?;
2336 if seen_ids.insert(row.symbol_id.clone()) {
2337 tests.push(row);
2338 if tests.len() >= limit {
2339 return Ok(tests);
2340 }
2341 }
2342 }
2343 }
2344
2345 let remaining = limit - tests.len();
2347 if remaining > 0 && has_pending_namespace_column(conn) {
2348 let pending_sql = format!(
2349 "SELECT DISTINCT {COLUMNS}
2350 FROM pending_relationships p
2351 JOIN symbols s ON p.from_symbol_id = s.symbol_id
2352 JOIN symbols s_from ON s_from.symbol_id = s.symbol_id
2353 JOIN symbols s_target ON s_target.symbol_id = ?1
2354 LEFT JOIN symbols s_target_parent ON s_target.parent_symbol_id = s_target_parent.symbol_id
2355 WHERE p.target_terminal_name = s_target.name
2356 AND {is_test}
2357 AND {not_documentation}
2358 AND {pred}
2359 ORDER BY {TEST_ORDER}
2360 LIMIT ?2",
2361 pred = pending_target_predicate(conn, "s_target", "s_target_parent")
2362 );
2363
2364 if let Ok(mut stmt) = conn.prepare(&pending_sql)
2365 && let Ok(rows) = stmt.query_map(
2366 params![target_symbol.symbol_id, remaining as i64],
2367 map_symbol,
2368 )
2369 {
2370 for row in rows.flatten() {
2371 let row = enclosing_test(conn, row)?;
2372 if seen_ids.insert(row.symbol_id.clone()) {
2373 tests.push(row);
2374 if tests.len() >= limit {
2375 return Ok(tests);
2376 }
2377 }
2378 }
2379 }
2380 }
2381
2382 let mut test_by_id = conn.prepare(&format!(
2384 "SELECT {COLUMNS} FROM symbols s WHERE s.symbol_id = ?1 AND {is_test} AND {not_documentation}"
2385 ))?;
2386 for site in find_references_for_symbol(
2387 conn,
2388 &target_symbol.name,
2389 "callers",
2390 MAX_RESULT_LIMIT,
2391 &target_symbol.symbol_id,
2392 )
2393 .unwrap_or_default()
2394 {
2395 if site.kind != "member_access (candidate)"
2396 && !seen_ids.contains(&site.from_symbol_id)
2397 && let Some(test) = test_by_id
2398 .query_row(params![site.from_symbol_id], map_symbol)
2399 .optional()?
2400 {
2401 let test = enclosing_test(conn, test)?;
2402 if seen_ids.insert(test.symbol_id.clone()) {
2403 tests.push(test);
2404 if tests.len() >= limit {
2405 return Ok(tests);
2406 }
2407 }
2408 }
2409 }
2410
2411 if (target_symbol.kind == "constructor" || target_symbol.name == "__init__")
2413 && tests.len() < limit
2414 && let Some(class) = target_symbol
2415 .parent_symbol_id
2416 .as_deref()
2417 .map(|id| get_symbol_by_id(conn, id))
2418 .transpose()?
2419 .flatten()
2420 .filter(|class| class.kind == "class" || class.kind == "struct")
2421 {
2422 for test in find_related_tests(conn, &class, limit - tests.len())? {
2423 if seen_ids.insert(test.symbol_id.clone()) {
2424 tests.push(test);
2425 }
2426 }
2427 return Ok(tests);
2428 }
2429
2430 let name = &target_symbol.name;
2431 let dunder = name.len() > 4 && name.starts_with("__") && name.ends_with("__");
2432 let definitions: i64 = conn.query_row(
2433 "SELECT COUNT(*) FROM symbols WHERE name = ?1 AND kind NOT IN ('import', 'export')",
2434 params![name],
2435 |row| row.get(0),
2436 )?;
2437 if dunder || definitions > 1 {
2438 return Ok(tests);
2439 }
2440
2441 let remaining = limit - tests.len();
2442 let name_sql = format!(
2445 "SELECT {COLUMNS}
2446 FROM symbols s
2447 WHERE (s.name LIKE ?1 ESCAPE '\\' OR s.signature LIKE ?1 ESCAPE '\\')
2448 AND {is_test}
2449 AND {not_documentation}
2450 ORDER BY (s.name LIKE ?1 ESCAPE '\\') DESC, {TEST_ORDER}
2451 LIMIT ?2"
2452 );
2453
2454 if let Ok(mut stmt) = conn.prepare(&name_sql)
2455 && let Ok(rows) = stmt.query_map(
2456 params![format!("%{}%", escape_like(name)), (remaining * 2) as i64],
2457 map_symbol,
2458 )
2459 {
2460 for row in rows.flatten() {
2461 let row = enclosing_test(conn, row)?;
2462 if seen_ids.insert(row.symbol_id.clone()) {
2463 tests.push(row);
2464 if tests.len() >= limit {
2465 return Ok(tests);
2466 }
2467 }
2468 }
2469 }
2470
2471 let remaining = limit - tests.len();
2472 let fts_exists: bool = conn
2473 .query_row(
2474 "SELECT 1 FROM sqlite_master WHERE type='table' AND name='symbols_fts'",
2475 [],
2476 |_| Ok(true),
2477 )
2478 .unwrap_or(false);
2479
2480 if remaining > 0 && fts_exists {
2481 let fts_sql = format!(
2482 "SELECT {COLUMNS}
2483 FROM symbols_fts
2484 CROSS JOIN symbols s ON s.rowid = symbols_fts.rowid
2485 WHERE symbols_fts MATCH ?1 AND {is_test} AND {not_documentation}
2486 ORDER BY {TEST_ORDER}
2487 LIMIT ?2"
2488 );
2489
2490 let and_q = name_prefix_query(&target_symbol.name);
2491 if !and_q.is_empty()
2492 && let Ok(mut stmt) = conn.prepare(&fts_sql)
2493 && let Ok(rows) = stmt.query_map(params![and_q, (remaining * 2) as i64], map_symbol)
2494 {
2495 for row in rows.flatten() {
2496 let row = enclosing_test(conn, row)?;
2497 if seen_ids.insert(row.symbol_id.clone()) {
2498 tests.push(row);
2499 if tests.len() >= limit {
2500 break;
2501 }
2502 }
2503 }
2504 }
2505 }
2506
2507 Ok(tests)
2508}
2509
2510pub fn qualify_members<'a>(
2514 conn: &Connection,
2515 symbols: impl IntoIterator<Item = &'a mut Symbol>,
2516) -> Result<(), QueryError> {
2517 let mut stmt = conn.prepare(&format!(
2518 "SELECT name FROM symbols
2519 WHERE symbol_id = ?1
2520 AND kind IN ('class', 'struct', 'interface', 'trait', 'enum', 'record', 'object', 'protocol', 'union')
2521 AND language NOT IN ({})",
2522 documentation_language_list()
2523 ))?;
2524 for symbol in symbols {
2525 let Some(parent) = symbol.parent_symbol_id.as_deref() else {
2526 continue;
2527 };
2528 if let Some(owner) = stmt
2529 .query_row(params![parent], |row| row.get::<_, String>(0))
2530 .optional()?
2531 && !symbol.name.starts_with(&format!("{owner}."))
2532 {
2533 symbol.name = format!("{owner}.{}", symbol.name);
2534 }
2535 }
2536 Ok(())
2537}
2538
2539pub fn get_symbol_by_name(
2541 conn: &Connection,
2542 name: &str,
2543 path_filter: Option<&str>,
2544) -> Result<Option<Symbol>, QueryError> {
2545 get_symbol_by_name_internal(conn, name, path_filter, false)
2546}
2547
2548pub fn get_symbol_by_name_exact(
2550 conn: &Connection,
2551 name: &str,
2552 exact_path: &str,
2553) -> Result<Option<Symbol>, QueryError> {
2554 get_symbol_by_name_internal(conn, name, Some(exact_path), true)
2555}
2556
2557pub fn get_symbol_by_id(conn: &Connection, symbol_id: &str) -> Result<Option<Symbol>, QueryError> {
2559 conn.query_row(
2560 "SELECT symbol_id, file_id, path, language, name, kind, signature, doc_comment,
2561 visibility, parent_symbol_id, start_line, start_column, end_line, end_column,
2562 start_byte, end_byte, body_start_line, body_start_column, body_end_line,
2563 body_end_column, body_start_byte, body_end_byte, body_hash, semantic_group,
2564 is_test, test_container
2565 FROM symbols WHERE symbol_id = ?1",
2566 [symbol_id],
2567 map_symbol,
2568 )
2569 .optional()
2570 .map_err(QueryError::from)
2571}
2572
2573fn split_qualified_name(name: &str) -> (Vec<&str>, &str) {
2575 let separator = if name.contains("::") {
2576 "::"
2577 } else if name.contains('.') {
2578 "."
2579 } else {
2580 return (Vec::new(), name);
2581 };
2582 let mut segments: Vec<&str> = name.split(separator).collect();
2583 let terminal = segments.pop().unwrap_or(name);
2584 (segments, terminal)
2585}
2586
2587fn ancestor_names(conn: &Connection, symbol_id: &str) -> Result<Vec<String>, QueryError> {
2589 let mut stmt = conn.prepare(
2590 "SELECT p.symbol_id, p.name FROM symbols s
2591 JOIN symbols p ON s.parent_symbol_id = p.symbol_id
2592 WHERE s.symbol_id = ?1",
2593 )?;
2594 let mut names = Vec::new();
2595 let mut current = symbol_id.to_string();
2596 for _ in 0..32 {
2597 let mut rows = stmt.query(params![current])?;
2598 let Some(row) = rows.next()? else { break };
2599 let parent_id: String = row.get(0)?;
2600 let parent_name: String = row.get(1)?;
2601 names.push(parent_name);
2602 current = parent_id;
2603 }
2604 Ok(names)
2605}
2606
2607const ANCESTOR_WALK_ROW_CAP: i64 = 2000;
2610
2611fn chain_contains(chain: &[String], wanted: &[&str]) -> bool {
2612 let mut remaining = chain.iter();
2613 wanted
2614 .iter()
2615 .all(|segment| remaining.any(|name| name == segment))
2616}
2617
2618fn name_candidates(
2619 conn: &Connection,
2620 name: &str,
2621 path_filter: Option<&str>,
2622 exact_path: bool,
2623) -> Result<Vec<Symbol>, QueryError> {
2624 let (ancestor_segments, terminal_name) = split_qualified_name(name);
2625 let parent_name = ancestor_segments.last().copied();
2626
2627 let sql = "SELECT s.symbol_id, s.file_id, s.path, s.language, s.name, s.kind, s.signature, s.doc_comment,
2628 s.visibility, s.parent_symbol_id, s.start_line, s.start_column, s.end_line, s.end_column,
2629 s.start_byte, s.end_byte, s.body_start_line, s.body_start_column, s.body_end_line,
2630 s.body_end_column, s.body_start_byte, s.body_end_byte, s.body_hash, s.semantic_group,
2631 s.is_test, s.test_container
2632 FROM symbols s
2633 LEFT JOIN symbols p ON s.parent_symbol_id = p.symbol_id
2634 WHERE (s.name = :name OR (s.name = :term AND (:parent IS NULL OR p.name = :parent)))
2635 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 '\\')))
2636 ORDER BY (s.kind != 'import') DESC,
2637 (s.kind IN ('function', 'struct', 'class', 'trait', 'method', 'enum', 'interface', 'type')) DESC,
2638 (s.name = :name) DESC,
2639 (:path IS NOT NULL AND (s.path = :path COLLATE NOCASE OR s.path = :path_bs COLLATE NOCASE)) DESC,
2640 s.is_test ASC
2641 LIMIT :limit";
2642
2643 let row_cap: i64 = if ancestor_segments.len() > 1 {
2644 ANCESTOR_WALK_ROW_CAP
2645 } else {
2646 25
2647 };
2648 let mut stmt = conn.prepare(sql)?;
2649 let normalized_path = path_filter.map(|p| p.replace('\\', "/").trim_matches('/').to_string());
2650 let backslash_path = normalized_path.as_deref().map(|p| p.replace('/', "\\"));
2651 let path_like = normalized_path.as_deref().map(escape_like);
2652 let path_like_bs = backslash_path.as_deref().map(escape_like);
2653
2654 let mut rows = stmt.query(rusqlite::named_params! {
2655 ":name": name,
2656 ":term": terminal_name,
2657 ":parent": parent_name,
2658 ":path": normalized_path.as_deref(),
2659 ":path_bs": backslash_path.as_deref(),
2660 ":path_like": path_like.as_deref(),
2661 ":path_like_bs": path_like_bs.as_deref(),
2662 ":exact": if exact_path { 1 } else { 0 },
2663 ":limit": row_cap,
2664 })?;
2665
2666 let mut matches: Vec<Symbol> = Vec::new();
2667 while let Some(row) = rows.next()? {
2668 matches.push(map_symbol(row)?);
2669 }
2670 drop(rows);
2671
2672 if ancestor_segments.len() > 1 {
2673 let required: Vec<&str> = ancestor_segments.iter().rev().copied().collect();
2674 let mut kept = Vec::with_capacity(matches.len());
2675 for symbol in matches {
2676 if symbol.name == name
2677 || chain_contains(&ancestor_names(conn, &symbol.symbol_id)?, &required)
2678 {
2679 kept.push(symbol);
2680 }
2681 }
2682 matches = kept;
2683 }
2684
2685 if matches.is_empty() {
2686 return Ok(Vec::new());
2687 }
2688
2689 if matches.len() == 1 {
2690 return Ok(vec![matches.remove(0)]);
2691 }
2692
2693 let candidates: Vec<Symbol> = if matches.iter().any(|s| s.kind != "import") {
2695 matches.into_iter().filter(|s| s.kind != "import").collect()
2696 } else {
2697 matches
2698 };
2699 let writes = inherited_writes_among(conn, candidates.iter().map(|s| s.symbol_id.as_str()))?;
2700 let candidates: Vec<Symbol> = if candidates.iter().any(|s| !writes.contains(&s.symbol_id)) {
2701 candidates
2702 .into_iter()
2703 .filter(|s| !writes.contains(&s.symbol_id))
2704 .collect()
2705 } else {
2706 candidates
2707 };
2708
2709 if candidates.len() == 1 {
2710 return Ok(vec![candidates.into_iter().next().unwrap()]);
2711 }
2712
2713 let exact_name_matches: Vec<_> = candidates
2715 .iter()
2716 .filter(|s| s.name == name)
2717 .cloned()
2718 .collect();
2719 if exact_name_matches.len() == 1 {
2720 return Ok(vec![exact_name_matches.into_iter().next().unwrap()]);
2721 }
2722
2723 let definition_candidates = if exact_name_matches.is_empty() {
2724 &candidates
2725 } else {
2726 &exact_name_matches
2727 };
2728 let def_matches: Vec<_> = definition_candidates
2729 .iter()
2730 .filter(|s| {
2731 matches!(
2732 s.kind.as_str(),
2733 "function"
2734 | "struct"
2735 | "class"
2736 | "trait"
2737 | "method"
2738 | "enum"
2739 | "interface"
2740 | "type"
2741 )
2742 })
2743 .cloned()
2744 .collect();
2745 if def_matches.len() == 1 {
2746 return Ok(vec![def_matches.into_iter().next().unwrap()]);
2747 }
2748
2749 let active_pool = if !def_matches.is_empty() {
2750 def_matches
2751 } else if !exact_name_matches.is_empty() {
2752 exact_name_matches
2753 } else {
2754 candidates
2755 };
2756
2757 if let Some(ref p) = normalized_path {
2759 let exact_path_matches: Vec<_> = active_pool
2760 .iter()
2761 .filter(|s| s.path == *p)
2762 .cloned()
2763 .collect();
2764 if exact_path_matches.len() == 1 {
2765 return Ok(vec![exact_path_matches.into_iter().next().unwrap()]);
2766 }
2767 }
2768
2769 if active_pool.len() == 1 {
2770 return Ok(vec![active_pool.into_iter().next().unwrap()]);
2771 }
2772
2773 let mut outside_tests = active_pool.iter().filter(|s| !is_test_path(&s.path));
2774 if let (Some(only), None) = (outside_tests.next(), outside_tests.next()) {
2775 return Ok(vec![only.clone()]);
2776 }
2777
2778 let mut outer = outer_definitions(conn, &active_pool)?;
2779 if outer.len() == 1 {
2780 return Ok(vec![outer.remove(0)]);
2781 }
2782
2783 Ok(active_pool)
2784}
2785
2786fn get_symbol_by_name_internal(
2787 conn: &Connection,
2788 name: &str,
2789 path_filter: Option<&str>,
2790 exact_path: bool,
2791) -> Result<Option<Symbol>, QueryError> {
2792 let mut candidates = name_candidates(conn, name, path_filter, exact_path)?;
2793 match candidates.len() {
2794 0 | 1 => Ok(candidates.pop()),
2795 _ => Err(ambiguous_symbol(name, &candidates)),
2796 }
2797}
2798
2799fn ambiguous_symbol(name: &str, candidates: &[Symbol]) -> QueryError {
2800 let mut candidate_list = String::new();
2801 for s in candidates {
2802 candidate_list.push_str(&format!(
2803 "- {} `{}` in {}:{} (id={})\n",
2804 crate::formatters::display_kind(s),
2805 s.name,
2806 s.path,
2807 s.start_line,
2808 s.symbol_id
2809 ));
2810 }
2811 QueryError::AmbiguousSymbol(name.to_string(), candidates.len(), candidate_list)
2812}
2813
2814pub fn get_symbol_overloads(
2817 conn: &Connection,
2818 name: &str,
2819 path_filter: Option<&str>,
2820) -> Result<Vec<Symbol>, QueryError> {
2821 let candidates = name_candidates(conn, name, path_filter, false)?;
2822 let first = candidates.first();
2823 let overloads = candidates.iter().all(|s| {
2824 first.is_some_and(|first| {
2825 s.name == first.name
2826 && s.path == first.path
2827 && s.parent_symbol_id == first.parent_symbol_id
2828 })
2829 });
2830 if overloads {
2831 Ok(candidates)
2832 } else {
2833 Err(ambiguous_symbol(name, &candidates))
2834 }
2835}
2836
2837fn outer_definitions(conn: &Connection, pool: &[Symbol]) -> Result<Vec<Symbol>, QueryError> {
2840 let definitions: Vec<&Symbol> = pool
2841 .iter()
2842 .filter(|s| {
2843 s.kind != "export"
2844 || !pool.iter().any(|other| {
2845 other.kind != "export"
2846 && other.path == s.path
2847 && other.start_line == s.start_line
2848 })
2849 })
2850 .collect();
2851 let mut parent_kind = conn.prepare("SELECT kind FROM symbols WHERE symbol_id = ?1")?;
2852 let mut outer = Vec::new();
2853 for symbol in &definitions {
2854 let kind = match &symbol.parent_symbol_id {
2855 Some(parent) => parent_kind
2856 .query_row([parent], |row| row.get::<_, String>(0))
2857 .optional()?,
2858 None => None,
2859 };
2860 if !matches!(kind.as_deref(), Some("function" | "method" | "constructor")) {
2861 outer.push((*symbol).clone());
2862 }
2863 }
2864 Ok(if outer.is_empty() {
2865 definitions.into_iter().cloned().collect()
2866 } else {
2867 outer
2868 })
2869}
2870
2871pub fn workspace_name(conn: &Connection) -> String {
2873 conn.query_row(
2874 "SELECT value FROM artifact_metadata WHERE key = 'root_path'",
2875 [],
2876 |row| row.get::<_, String>(0),
2877 )
2878 .ok()
2879 .map(|root| root.replace('\\', "/"))
2880 .and_then(|root| {
2881 root.trim_end_matches('/')
2882 .rsplit('/')
2883 .next()
2884 .filter(|name| !name.is_empty())
2885 .map(str::to_string)
2886 })
2887 .unwrap_or_else(|| "this workspace".to_string())
2888}
2889
2890fn edit_distance(left: &str, right: &str) -> usize {
2891 let left: Vec<char> = left.chars().collect();
2892 let right: Vec<char> = right.chars().collect();
2893 let mut previous: Vec<usize> = (0..=right.len()).collect();
2894 let mut current = vec![0usize; right.len() + 1];
2895 for (i, a) in left.iter().enumerate() {
2896 current[0] = i + 1;
2897 for (j, b) in right.iter().enumerate() {
2898 let substitution = previous[j] + usize::from(a != b);
2899 current[j + 1] = substitution.min(previous[j + 1] + 1).min(current[j] + 1);
2900 }
2901 std::mem::swap(&mut previous, &mut current);
2902 }
2903 previous[right.len()]
2904}
2905
2906fn near_names_by_trigram(
2908 conn: &Connection,
2909 name: &str,
2910 path_filter: Option<&str>,
2911) -> Result<Vec<Symbol>, QueryError> {
2912 if !has_table(conn, "symbol_names_tri") {
2913 return Ok(Vec::new());
2914 }
2915 let lower = name.to_lowercase();
2916 let chars: Vec<char> = lower.chars().collect();
2917 let chunks: Vec<String> = chars
2918 .windows(3)
2919 .map(|window| window.iter().collect::<String>())
2920 .filter(|chunk| chunk.chars().all(|c| c.is_alphanumeric() || c == '_'))
2921 .collect();
2922 if chunks.is_empty() {
2923 return Ok(Vec::new());
2924 }
2925 let match_clause = chunks
2926 .iter()
2927 .map(|chunk| format!("\"{chunk}\""))
2928 .collect::<Vec<_>>()
2929 .join(" OR ");
2930
2931 let normalized_path = path_filter.map(|p| {
2932 p.replace('\\', "/")
2933 .trim_start_matches("./")
2934 .trim_matches('/')
2935 .to_string()
2936 });
2937 let escaped_path = normalized_path.as_deref().map(escape_like);
2938 let kind_val: Option<&str> = None;
2939 let columns = candidate_columns(conn);
2940 let filters = candidate_filters(false, false);
2941 let sql = format!(
2942 "SELECT {columns} FROM symbol_names_tri
2943 CROSS JOIN symbols s ON s.rowid = symbol_names_tri.rowid
2944 WHERE symbol_names_tri MATCH :match {filters}
2945 ORDER BY bm25(symbol_names_tri) ASC, length(s.name) ASC, s.path ASC LIMIT 20"
2946 );
2947 let rows = conn
2948 .prepare(&sql)?
2949 .query_map(
2950 rusqlite::named_params! {
2951 ":match": match_clause,
2952 ":kind": kind_val,
2953 ":path": normalized_path.as_deref(),
2954 ":path_like": escaped_path.as_deref(),
2955 },
2956 map_symbol,
2957 )?
2958 .collect::<Result<Vec<_>, _>>()?;
2959
2960 let budget = (chars.len() / 4).max(2);
2961 let mut scored: Vec<(usize, Symbol)> = rows
2962 .into_iter()
2963 .map(|symbol| (edit_distance(&lower, &symbol.name.to_lowercase()), symbol))
2964 .filter(|(distance, _)| *distance <= budget)
2965 .collect();
2966 scored.sort_by_key(|(distance, symbol)| (*distance, symbol.name.chars().count()));
2967 Ok(scored
2968 .into_iter()
2969 .map(|(_, symbol)| symbol)
2970 .take(3)
2971 .collect())
2972}
2973
2974pub fn suggest_symbol_names(
2976 conn: &Connection,
2977 name: &str,
2978 path_filter: Option<&str>,
2979) -> Vec<Symbol> {
2980 let (ancestors, terminal) = split_qualified_name(name);
2981 let mut found =
2982 search_symbols_scoped(conn, terminal, None, path_filter, false, 3).unwrap_or_default();
2983 if found.is_empty() && terminal.chars().count() >= 4 {
2984 found = near_names_by_trigram(conn, terminal, path_filter).unwrap_or_default();
2985 }
2986 if let Some(parent) = ancestors.last().copied() {
2987 let mut ranked: Vec<(bool, Symbol)> = found
2988 .into_iter()
2989 .map(|symbol| {
2990 let shares_parent = ancestor_names(conn, &symbol.symbol_id)
2991 .unwrap_or_default()
2992 .first()
2993 .is_some_and(|found_parent| found_parent == parent);
2994 (!shares_parent, symbol)
2995 })
2996 .collect();
2997 ranked.sort_by_key(|(demoted, _)| *demoted);
2998 found = ranked.into_iter().map(|(_, symbol)| symbol).collect();
2999 }
3000 found.truncate(3);
3001 found
3002}
3003
3004pub fn suggest_file_paths(conn: &Connection, rel_path: &str) -> Vec<String> {
3006 let wanted = rel_path.replace('\\', "/");
3007 let wanted = wanted.trim_start_matches("./").trim_matches('/');
3008 let basename = wanted.rsplit('/').next().unwrap_or(wanted).to_lowercase();
3009 if basename.is_empty() {
3010 return Vec::new();
3011 }
3012 let stem = basename.split('.').next().unwrap_or(&basename).to_string();
3013 let segments: Vec<&str> = wanted.split('/').collect();
3014 let tail = if segments.len() >= 2 {
3015 segments[segments.len() - 2..].join("/").to_lowercase()
3016 } else {
3017 basename.clone()
3018 };
3019
3020 let path_expr = "replace(files.path, '\\', '/')";
3021 let file_name = format!(
3022 "lower(replace({path_expr}, rtrim({path_expr}, replace({path_expr}, '/', '')), ''))"
3023 );
3024 let rules = [
3025 (format!("{file_name} = :value"), basename.clone()),
3026 (
3027 format!("{file_name} LIKE :value ESCAPE '\\'"),
3028 format!("{}%", escape_like(&stem)),
3029 ),
3030 (
3031 format!("lower({path_expr}) LIKE :value ESCAPE '\\'"),
3032 format!("%{}", escape_like(&tail)),
3033 ),
3034 ];
3035
3036 for (predicate, value) in rules {
3037 let sql = format!(
3038 "SELECT {path_expr} FROM files WHERE {predicate}
3039 ORDER BY length(files.path) ASC, files.path ASC LIMIT 3"
3040 );
3041 let found: Vec<String> = conn
3042 .prepare(&sql)
3043 .and_then(|mut stmt| {
3044 stmt.query_map(rusqlite::named_params! { ":value": value }, |row| {
3045 row.get::<_, String>(0)
3046 })?
3047 .collect()
3048 })
3049 .unwrap_or_default();
3050 if !found.is_empty() {
3051 return found;
3052 }
3053 }
3054
3055 let max_edits = (basename.chars().count() / 4).clamp(1, 2);
3056 let mut near: Vec<(usize, String)> = Vec::new();
3057 let _ = conn
3058 .prepare(&format!("SELECT {path_expr} FROM files"))
3059 .and_then(|mut stmt| {
3060 for path in stmt.query_map([], |row| row.get::<_, String>(0))?.flatten() {
3061 let name = path.rsplit('/').next().unwrap_or(&path).to_lowercase();
3062 let edits = edit_distance(&name, &basename);
3063 if edits <= max_edits {
3064 near.push((edits, path));
3065 }
3066 }
3067 Ok(())
3068 });
3069 near.sort();
3070 near.into_iter().take(3).map(|(_, path)| path).collect()
3071}
3072
3073pub fn symbol_not_found_parts(
3075 conn: &Connection,
3076 name: &str,
3077 path_filter: Option<&str>,
3078) -> (String, String) {
3079 let list = |candidates: &[Symbol]| {
3080 candidates
3081 .iter()
3082 .map(|s| format!(" - {} `{}` ({}:{})", s.kind, s.name, s.path, s.start_line))
3083 .collect::<Vec<_>>()
3084 .join("\n")
3085 };
3086 let terminal = split_qualified_name(name).1;
3087 if let Some(path) = path_filter {
3088 let elsewhere: Vec<Symbol> = suggest_symbol_names(conn, name, None)
3089 .into_iter()
3090 .filter(|s| s.name == terminal)
3091 .collect();
3092 if !elsewhere.is_empty() {
3093 let hint = format!(
3094 "`{path}` does not define it; it is defined in:\n{}",
3095 list(&elsewhere)
3096 );
3097 return (workspace_name(conn), hint);
3098 }
3099 }
3100 let candidates = suggest_symbol_names(conn, name, path_filter);
3101 let hint = if candidates.is_empty() {
3102 "No similar name is indexed; check the workspace and spelling.".to_string()
3103 } else {
3104 format!("Did you mean one of:\n{}", list(&candidates))
3105 };
3106 (workspace_name(conn), hint)
3107}
3108
3109pub fn file_not_found_parts(conn: &Connection, rel_path: &str) -> (String, String) {
3111 let candidates = suggest_file_paths(conn, rel_path);
3112 let hint = if candidates.is_empty() {
3113 "No similar path is indexed; check the workspace and spelling.".to_string()
3114 } else {
3115 let list = candidates
3116 .iter()
3117 .map(|path| format!(" - {path}"))
3118 .collect::<Vec<_>>()
3119 .join("\n");
3120 format!("Did you mean one of:\n{list}")
3121 };
3122 (workspace_name(conn), hint)
3123}
3124
3125pub fn find_references(
3127 conn: &Connection,
3128 symbol_name: &str,
3129 direction: &str,
3130 limit: usize,
3131) -> Result<Vec<ReferenceSite>, QueryError> {
3132 find_references_ext(conn, symbol_name, direction, limit, false)
3133}
3134
3135pub fn find_references_ext(
3137 conn: &Connection,
3138 symbol_name: &str,
3139 direction: &str,
3140 limit: usize,
3141 include_external: bool,
3142) -> Result<Vec<ReferenceSite>, QueryError> {
3143 find_references_scoped(conn, symbol_name, direction, limit, include_external, None)
3144}
3145
3146pub fn find_references_scoped(
3148 conn: &Connection,
3149 symbol_name: &str,
3150 direction: &str,
3151 limit: usize,
3152 include_external: bool,
3153 path_filter: Option<&str>,
3154) -> Result<Vec<ReferenceSite>, QueryError> {
3155 validate_result_limit(limit)?;
3156 if direction != "callers" && direction != "callees" {
3157 return Err(QueryError::InvalidDirection(direction.to_string()));
3158 }
3159
3160 match get_symbol_by_name(conn, symbol_name, path_filter)? {
3161 Some(target) => find_references_internal(
3162 conn,
3163 &target.name,
3164 direction,
3165 limit,
3166 Some(&target.symbol_id),
3167 include_external,
3168 ),
3169 None => {
3170 let (workspace, hint) = symbol_not_found_parts(conn, symbol_name, path_filter);
3171 Err(QueryError::SymbolNotFound {
3172 name: symbol_name.to_string(),
3173 workspace,
3174 hint,
3175 })
3176 }
3177 }
3178}
3179
3180pub fn find_references_for_symbol(
3181 conn: &Connection,
3182 symbol_name: &str,
3183 direction: &str,
3184 limit: usize,
3185 symbol_id: &str,
3186) -> Result<Vec<ReferenceSite>, QueryError> {
3187 find_references_for_symbol_ext(conn, symbol_name, direction, limit, symbol_id, false)
3188}
3189
3190pub fn find_references_for_symbol_ext(
3191 conn: &Connection,
3192 symbol_name: &str,
3193 direction: &str,
3194 limit: usize,
3195 symbol_id: &str,
3196 include_external: bool,
3197) -> Result<Vec<ReferenceSite>, QueryError> {
3198 find_references_internal(
3199 conn,
3200 symbol_name,
3201 direction,
3202 limit,
3203 Some(symbol_id),
3204 include_external,
3205 )
3206}
3207
3208fn call_site_proximity(candidate_path: &str) -> String {
3211 path_proximity(candidate_path, "p.path")
3212}
3213
3214pub(crate) fn path_proximity(candidate_path: &str, site_path: &str) -> String {
3217 let normalized = format!("replace({candidate_path}, '\\', '/')");
3218 let call_site = format!("replace({site_path}, '\\', '/')");
3219 format!(
3220 "CASE WHEN {normalized} = {call_site} THEN 2
3221 WHEN rtrim({normalized}, replace({normalized}, '/', '')) = rtrim({call_site}, replace({call_site}, '/', '')) THEN 1
3222 ELSE 0 END"
3223 )
3224}
3225
3226fn is_relative_import_path(value: &str) -> String {
3228 format!("({value} LIKE '.%' AND instr({value}, '/') > 0)")
3229}
3230
3231fn relative_import_matches(value: &str, target: &str) -> String {
3235 let stem = format!(
3236 "CASE WHEN {value} GLOB '*.[jt]s' THEN substr({value}, 1, length({value}) - 3)
3237 WHEN {value} GLOB '*.[mc][jt]s' OR {value} GLOB '*.[jt]sx'
3238 THEN substr({value}, 1, length({value}) - 4)
3239 ELSE {value} END"
3240 );
3241 let caller = "replace(p.path, '\\', '/')";
3242 let parent_dir = "rtrim(substr(hop.dir, 1, length(hop.dir) - 1), replace(substr(hop.dir, 1, length(hop.dir) - 1), '/', ''))";
3243 let files = ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts", "d.ts"]
3244 .iter()
3245 .flat_map(|ext| {
3246 [
3247 format!("hop.dir || hop.rest || '.{ext}'"),
3248 format!("hop.dir || hop.rest || '/index.{ext}'"),
3249 ]
3250 })
3251 .collect::<Vec<_>>()
3252 .join(", ");
3253 format!(
3254 "EXISTS (
3255 WITH RECURSIVE hop(dir, rest) AS (
3256 SELECT rtrim({caller}, replace({caller}, '/', '')),
3257 CASE WHEN ({stem}) LIKE './%' THEN substr(({stem}), 3) ELSE ({stem}) END
3258 UNION ALL
3259 SELECT {parent_dir}, substr(hop.rest, 4) FROM hop WHERE hop.rest LIKE '../%'
3260 )
3261 SELECT 1 FROM hop
3262 WHERE hop.rest NOT LIKE '../%' AND hop.rest != ''
3263 AND replace({target}.path, '\\', '/') IN ({files})
3264 )"
3265 )
3266}
3267
3268const JS_FAMILY_LANGUAGES: &str = "'javascript', 'typescript', 'tsx', 'jsx', 'vue'";
3270fn identifier_language_matches(identifier: &str, target: &str) -> String {
3273 format!(
3274 "({target}.language = {identifier}.language
3275 OR ({identifier}.language IN ({JS_FAMILY_LANGUAGES})
3276 AND {target}.language IN ({JS_FAMILY_LANGUAGES}))
3277 OR ({identifier}.language IN ('razor', 'csharp')
3278 AND {target}.language IN ('razor', 'csharp'))
3279 OR ({identifier}.language = 'qml' AND {target}.language = 'cpp'))"
3280 )
3281}
3282
3283const NESTED_TYPE_KINDS: &str =
3284 "'class', 'struct', 'interface', 'enum', 'record', 'trait', 'protocol'";
3285
3286const IMPLICIT_RECEIVER_LANGUAGES: &[&str] = &[
3289 "java", "csharp", "kotlin", "swift", "cpp", "scala", "dart", "ruby", "vbnet",
3290];
3291
3292fn receiver_builder_calls(conn: &Connection) -> String {
3293 let resolved_calls = if has_column(conn, "relationships", "from_symbol_id")
3296 && has_column(conn, "relationships", "to_symbol_id")
3297 && has_column(conn, "relationships", "kind")
3298 && has_column(conn, "relationships", "path")
3299 && has_column(conn, "relationships", "start_line")
3300 {
3301 "UNION
3302 SELECT resolved_builder.name, assign_call.path, resolved_builder.symbol_id
3303 FROM symbols assigned
3304 JOIN relationships assign_call
3305 ON assign_call.from_symbol_id = p.from_symbol_id
3306 AND assign_call.start_line = assigned.start_line
3307 AND +assign_call.kind = 'calls'
3308 JOIN symbols resolved_builder ON resolved_builder.symbol_id = assign_call.to_symbol_id
3309 WHERE assigned.parent_symbol_id = p.from_symbol_id
3310 AND +assigned.name = p.target_receiver
3311 AND +assigned.kind = 'variable'
3312 AND assigned.start_line <= p.start_line"
3313 } else {
3314 ""
3315 };
3316 let fixture_calls = if has_column(conn, "symbols", "test_lifecycle") {
3317 "UNION
3318 SELECT fixture_call.target_terminal_name, fixture.path, NULL
3319 FROM symbols fixture
3320 JOIN pending_relationships fixture_call ON fixture_call.from_symbol_id = fixture.symbol_id
3321 WHERE fixture.name = p.target_receiver
3322 AND fixture.kind IN ('function', 'method')
3323 AND +fixture.test_lifecycle = 1
3324 AND EXISTS (
3325 SELECT 1 FROM symbols parameter
3326 WHERE parameter.parent_symbol_id = p.from_symbol_id
3327 AND +parameter.name = p.target_receiver
3328 AND (parameter.kind = 'parameter'
3329 OR json_valid(parameter.metadata_json)
3330 AND json_extract(parameter.metadata_json, '$.role') = 'parameter')
3331 )
3332 AND (fixture.path = p.path
3333 OR fixture.path LIKE '%conftest.py'
3334 AND substr(p.path, 1, length(fixture.path) - 11)
3335 = substr(fixture.path, 1, length(fixture.path) - 11))"
3336 } else {
3337 ""
3338 };
3339 format!(
3341 "SELECT assign_call.target_terminal_name, assign_call.path, NULL
3342 FROM symbols assigned
3343 JOIN pending_relationships assign_call
3344 ON assign_call.from_symbol_id = p.from_symbol_id
3345 AND assign_call.start_line = assigned.start_line
3346 AND +assign_call.kind = 'calls'
3347 WHERE assigned.parent_symbol_id = p.from_symbol_id
3348 AND +assigned.name = p.target_receiver
3349 AND +assigned.kind = 'variable'
3350 AND assigned.start_line <= p.start_line
3351 {resolved_calls}
3352 {fixture_calls}"
3353 )
3354}
3355
3356fn builder_return_type_sql(builder: &str) -> String {
3357 format!(
3358 "CASE WHEN json_valid({builder}.metadata_json)
3359 THEN COALESCE(NULLIF(json_extract({builder}.metadata_json, '$.returnType'), ''),
3360 json_extract({builder}.metadata_json, '$.inferredReturnType')) END"
3361 )
3362}
3363
3364fn receiver_definition_scope(target: &str, origin: &str) -> String {
3365 let source = "json_extract(binding.metadata_json, '$.source')";
3366 let relative = is_relative_import_path(source);
3367 let relative_match = relative_import_matches(source, target).replace("p.path", origin);
3368 let caller = format!("replace({origin}, '\\', '/')");
3369 let parent_dir = "rtrim(substr(hop.dir, 1, length(hop.dir) - 1), replace(substr(hop.dir, 1, length(hop.dir) - 1), '/', ''))";
3370 format!(
3371 "({target}.path = {origin} OR NOT EXISTS (
3372 SELECT 1 FROM symbols local
3373 WHERE local.name = {target}.name AND local.path = {origin}
3374 AND local.kind = {target}.kind
3375 )) AND NOT EXISTS (
3376 SELECT 1 FROM symbols binding
3377 WHERE binding.name = {target}.name AND binding.path = {origin}
3378 AND binding.kind = 'import' AND json_valid(binding.metadata_json)
3379 AND NOT ({target}.path = {origin} AND binding.start_line < {target}.start_line)
3380 AND COALESCE({source}, '') != ''
3381 AND NOT CASE WHEN {relative} THEN {relative_match}
3382 WHEN {source} LIKE '.%' THEN EXISTS (
3383 WITH RECURSIVE hop(dir, rest) AS (
3384 SELECT rtrim({caller}, replace({caller}, '/', '')), substr({source}, 2)
3385 UNION ALL
3386 SELECT {parent_dir}, substr(hop.rest, 2) FROM hop WHERE hop.rest LIKE '.%'
3387 )
3388 SELECT 1 FROM hop WHERE hop.rest NOT LIKE '.%'
3389 AND (replace({target}.path, '\\', '/') = hop.dir || replace(hop.rest, '.', '/') || '.py'
3390 OR replace({target}.path, '\\', '/') LIKE
3391 rtrim(hop.dir || replace(hop.rest, '.', '/'), '/') || '/%')
3392 )
3393 ELSE instr('/' || replace({target}.path, '\\', '/'), '/' || replace({source}, '.', '/') || '.') > 0
3394 OR instr('/' || replace({target}.path, '\\', '/'), '/' || replace({source}, '.', '/') || '/') > 0
3395 END
3396 )"
3397 )
3398}
3399
3400fn member_receiver_match(conn: &Connection) -> String {
3401 let callable_member = "p.kind = 'member_access'
3402 AND CASE WHEN json_valid(p.metadata_json)
3403 THEN json_extract(p.metadata_json, '$.role') END IS NOT 'signal_handler'
3404 AND EXISTS (
3405 SELECT 1 FROM symbols selected
3406 JOIN symbols owner ON owner.symbol_id = selected.parent_symbol_id
3407 WHERE selected.symbol_id = ?3
3408 AND selected.kind IN ('method', 'function', 'constructor')
3409 AND owner.kind IN ('class', 'struct', 'interface', 'trait', 'enum', 'protocol')
3410 )";
3411 let unproven_member = "p.kind = 'member_access' AND NOT p.has_metadata";
3415 let documentation_member = format!(
3416 "{unproven_member} AND p.language IN ({})",
3417 documentation_language_list()
3418 );
3419 if !has_pending_namespace_column(conn) || !has_column(conn, "symbols", "metadata_json") {
3420 return format!(
3421 "CASE WHEN {documentation_member} THEN -1 WHEN {callable_member} THEN 0
3422 WHEN {unproven_member} THEN -1 ELSE 1 END"
3423 );
3424 }
3425 let matched = pending_target_predicate(conn, "candidate", "candidate_parent");
3426 let builders = receiver_builder_calls(conn);
3427 let builder_return_type = builder_return_type_sql("builder");
3428 let class_scope = receiver_definition_scope("receiver_class", "builder_call.path");
3429 let builder_scope = receiver_definition_scope("builder", "builder_call.path");
3430 let returned_scope = receiver_definition_scope("returned_class", "builder.path");
3431 format!(
3432 "CASE WHEN {documentation_member} THEN -1
3433 WHEN NOT ({callable_member}) THEN CASE WHEN {unproven_member} THEN -1 ELSE 1 END
3434 WHEN {unproven_member} THEN 0
3435 WHEN (SELECT COUNT(*) FROM symbols binding
3436 WHERE binding.parent_symbol_id = p.from_symbol_id
3437 AND +binding.name = p.target_receiver
3438 AND +binding.kind IN ('variable', 'parameter')
3439 AND binding.start_line <= p.start_line) > 1 THEN 0
3440 WHEN EXISTS (
3441 WITH builder_call(name, path, resolved_symbol_id) AS ({builders})
3442 SELECT 1 FROM builder_call
3443 WHERE (SELECT COUNT(*) FROM builder_call) > 1
3444 AND builder_call.resolved_symbol_id IS NULL
3445 AND NOT EXISTS (
3446 SELECT 1 FROM symbols receiver_class
3447 WHERE receiver_class.name = builder_call.name AND receiver_class.kind = 'class'
3448 AND (receiver_class.symbol_id = builder_call.resolved_symbol_id
3449 OR builder_call.resolved_symbol_id IS NULL AND {class_scope})
3450 )
3451 AND NOT EXISTS (
3452 SELECT 1 FROM symbols builder
3453 JOIN symbols returned_class ON returned_class.name =
3454 {builder_return_type}
3455 WHERE builder.name = builder_call.name AND builder.kind IN ('function', 'method')
3456 AND (builder.symbol_id = builder_call.resolved_symbol_id
3457 OR CASE WHEN builder_call.resolved_symbol_id IS NULL
3458 AND EXISTS (
3459 SELECT 1 FROM symbols returned_class_probe
3460 WHERE returned_class_probe.name = {builder_return_type}
3461 AND returned_class_probe.kind = 'class'
3462 )
3463 THEN {builder_scope} ELSE 0 END)
3464 AND returned_class.kind = 'class'
3465 AND {returned_scope}
3466 )
3467 ) THEN 0
3468 ELSE (
3469 SELECT CASE WHEN COUNT(*) > 1 THEN 0
3470 WHEN MAX(candidate.symbol_id = ?3) = 1 THEN 1
3471 WHEN COUNT(*) = 1 THEN -1
3472 WHEN EXISTS (
3473 WITH builder_call(name, path, resolved_symbol_id) AS ({builders})
3474 SELECT 1 FROM builder_call
3475 JOIN symbols imported ON imported.name = builder_call.name
3476 AND imported.path = builder_call.path AND imported.kind = 'import'
3477 JOIN symbols selected ON selected.symbol_id = ?3
3478 JOIN pending_relationships base ON base.from_symbol_id = selected.parent_symbol_id
3479 AND base.kind = 'extends' AND base.target_terminal_name = builder_call.name
3480 JOIN symbols base_import ON base_import.name = base.target_terminal_name
3481 AND base_import.path = base.path AND base_import.kind = 'import'
3482 WHERE json_valid(imported.metadata_json) AND json_valid(base_import.metadata_json)
3483 AND json_extract(imported.metadata_json, '$.source')
3484 = json_extract(base_import.metadata_json, '$.source')
3485 ) THEN -1
3486 ELSE 0 END
3487 FROM symbols candidate
3488 JOIN symbols candidate_parent ON candidate_parent.symbol_id = candidate.parent_symbol_id
3489 WHERE candidate.name = p.name
3490 AND candidate.kind IN ('method', 'function', 'constructor')
3491 AND candidate_parent.kind IN ('class', 'struct', 'interface', 'trait', 'enum', 'protocol')
3492 AND {matched}
3493 ) END"
3494 )
3495}
3496
3497pub(crate) fn module_import_binding_sql(target: &str, binding_name: &str) -> String {
3500 let target_path = format!("('/' || replace({target}.path, '\\', '/'))");
3501 let dotted = "replace(module_path.value, '/', '.')";
3502 let segment = format!("replace({dotted}, rtrim({dotted}, replace({dotted}, '.', '')), '')");
3503 let relative = is_relative_import_path("import_source.value");
3504 let relative_match = relative_import_matches("import_source.value", target);
3505 format!(
3506 "EXISTS (
3507 SELECT 1 FROM symbols module_import
3508 CROSS JOIN json_each(json_array(
3509 json_extract(module_import.metadata_json, '$.source')
3510 )) import_source
3511 CROSS JOIN json_each(json_array(
3512 json_extract(module_import.metadata_json, '$.importedName'),
3513 import_source.value
3514 )) module_path
3515 WHERE module_import.kind = 'import'
3516 AND module_import.path = p.path
3517 AND module_import.name = {binding_name}
3518 AND json_valid(module_import.metadata_json)
3519 AND CASE WHEN {relative} THEN {relative_match}
3520 ELSE {segment} != ''
3521 AND ({target_path} LIKE '%/' || {segment} || '/%'
3522 OR {target_path} LIKE '%/' || {segment} || '.%')
3523 END
3524 )"
3525 )
3526}
3527
3528pub(crate) fn built_receiver_sql(conn: &Connection, target: &str) -> Option<String> {
3532 if !has_column(conn, "symbols", "metadata_json") {
3533 return None;
3534 }
3535 let builder_calls = receiver_builder_calls(conn);
3536 let builder_return_type = builder_return_type_sql("builder");
3537 let builder_scope = receiver_definition_scope("builder", "builder_call.path");
3538 let returned_class_scope = receiver_definition_scope("returned_class", "builder.path");
3539 let built_scope = receiver_definition_scope("built_class", "built_name.path");
3540 Some(format!(
3542 "CASE WHEN EXISTS (
3543 SELECT 1 FROM symbols taken
3544 WHERE taken.parent_symbol_id = p.from_symbol_id
3545 AND taken.name = p.target_receiver
3546 AND taken.kind IN ('parameter', 'variable')
3547 ) THEN EXISTS (
3548 WITH RECURSIVE builder_call(name, path, resolved_symbol_id) AS ({builder_calls}),
3549 built_name(name, path, resolved_symbol_id) AS (
3550 SELECT name, path, resolved_symbol_id FROM builder_call
3551 UNION
3552 SELECT returned_class.name, builder.path, returned_class.symbol_id
3553 FROM builder_call
3554 JOIN symbols builder ON builder.name = builder_call.name
3555 AND builder.kind IN ('function', 'method')
3556 AND (builder.symbol_id = builder_call.resolved_symbol_id
3557 OR CASE WHEN builder_call.resolved_symbol_id IS NULL
3558 AND EXISTS (
3559 SELECT 1 FROM symbols returned_class_probe
3560 WHERE returned_class_probe.name = {builder_return_type}
3561 AND returned_class_probe.kind = 'class'
3562 )
3563 THEN {builder_scope} ELSE 0 END)
3564 JOIN symbols returned_class ON returned_class.name = {builder_return_type}
3565 AND returned_class.kind = 'class' AND {returned_class_scope}
3566 ),
3567 built(symbol_id, depth) AS (
3568 SELECT built_class.symbol_id, 0
3569 FROM built_name
3570 CROSS JOIN symbols built_class ON built_class.name = built_name.name
3571 WHERE built_class.kind = 'class' AND {built_scope}
3572 AND (built_name.resolved_symbol_id IS NULL
3573 OR built_class.symbol_id = built_name.resolved_symbol_id)
3574 UNION
3575 SELECT r.to_symbol_id, built.depth + 1
3576 FROM relationships r JOIN built ON r.from_symbol_id = built.symbol_id
3577 WHERE +r.kind = 'extends' AND built.depth < 8
3578 UNION
3579 SELECT c.symbol_id, built.depth + 1
3580 FROM pending_relationships pe
3581 JOIN built ON pe.from_symbol_id = built.symbol_id
3582 JOIN symbols c ON c.name = pe.target_terminal_name
3583 WHERE +pe.kind = 'extends' AND built.depth < 8
3584 AND c.kind IN ('class', 'interface', 'struct', 'trait', 'protocol')
3585 )
3586 SELECT 1 FROM built WHERE built.symbol_id = {target}.parent_symbol_id
3587 AND NOT EXISTS (
3588 SELECT 1 FROM built closer
3589 JOIN symbols nearer ON nearer.parent_symbol_id = closer.symbol_id
3590 WHERE nearer.name = {target}.name
3591 AND nearer.kind IN ('method', 'function', 'constructor', 'property')
3592 AND closer.depth < built.depth
3593 )
3594 ) ELSE 0 END"
3595 ))
3596}
3597
3598pub(crate) fn receiver_type_sql(conn: &Connection, parent: &str) -> String {
3601 let receiver_type_scope = if has_column(conn, "symbols", "metadata_json") {
3603 receiver_definition_scope(parent, "p.path")
3604 } else {
3605 "1 = 1".to_string()
3606 };
3607 format!(
3608 "EXISTS (
3609 SELECT 1 FROM symbols receiver
3610 JOIN type_facts receiver_type ON receiver_type.symbol_id = receiver.symbol_id
3611 WHERE receiver.name = p.target_receiver
3612 AND receiver.path = p.path
3613 AND receiver_type.resolved_type = {parent}.name
3614 AND NOT EXISTS (
3615 SELECT 1 FROM symbols shadow
3616 WHERE shadow.name = receiver.name
3617 AND shadow.path = receiver.path
3618 AND shadow.parent_symbol_id = p.from_symbol_id
3619 AND shadow.kind IN ('variable', 'parameter')
3620 AND shadow.symbol_id != receiver.symbol_id
3621 )
3622 ) AND {receiver_type_scope}"
3623 )
3624}
3625
3626pub(crate) fn class_chain_sql(target: &str, own_class_rule: &str) -> String {
3633 let rival_rank = path_proximity("rival.path", "pe.path");
3634 let base_rank = path_proximity("c.path", "pe.path");
3635 let inherited_write = inherited_attribute_write("nearer");
3636 format!(
3637 "EXISTS (
3638 WITH RECURSIVE base(symbol_id, depth) AS (
3639 SELECT s_from.parent_symbol_id, 0
3640 UNION
3641 SELECT r.to_symbol_id, base.depth + 1
3642 FROM relationships r JOIN base ON r.from_symbol_id = base.symbol_id
3643 WHERE +r.kind = 'extends' AND base.depth < 8
3644 UNION
3645 SELECT c.symbol_id, base.depth + 1
3646 FROM pending_relationships pe
3647 JOIN base ON pe.from_symbol_id = base.symbol_id
3648 JOIN symbols c ON c.name = pe.target_terminal_name
3649 WHERE +pe.kind = 'extends' AND base.depth < 8
3650 AND c.kind IN ('class', 'interface', 'struct', 'trait', 'protocol')
3651 AND NOT EXISTS (
3652 SELECT 1 FROM symbols rival
3653 WHERE rival.name = c.name
3654 AND rival.kind IN ('class', 'interface', 'struct', 'trait', 'protocol')
3655 AND {rival_rank} > {base_rank})
3656 )
3657 SELECT 1 FROM base
3658 WHERE base.symbol_id = {target}.parent_symbol_id
3659 AND (base.depth > 0 OR {own_class_rule})
3660 AND NOT EXISTS (
3661 SELECT 1 FROM base closer
3662 JOIN symbols nearer ON nearer.parent_symbol_id = closer.symbol_id
3663 WHERE nearer.name = {target}.name
3664 AND nearer.kind IN ('method', 'function', 'constructor', 'property')
3665 AND closer.depth < base.depth
3666 AND (closer.depth > 0 OR p.target_receiver IS NOT 'super')
3667 AND NOT {inherited_write}
3668 )
3669 )"
3670 )
3671}
3672
3673pub(crate) fn implicit_receiver_language_list() -> String {
3675 IMPLICIT_RECEIVER_LANGUAGES
3676 .iter()
3677 .map(|language| format!("'{language}'"))
3678 .collect::<Vec<_>>()
3679 .join(", ")
3680}
3681
3682pub(crate) fn enclosing_scope_sql(target: &str) -> String {
3688 let implicit_receiver_languages = implicit_receiver_language_list();
3689 format!(
3690 "EXISTS (
3691 WITH RECURSIVE lexical(symbol_id, depth) AS (
3692 SELECT s_from.parent_symbol_id, 1
3693 UNION ALL
3694 SELECT outer_scope.parent_symbol_id, lexical.depth + 1
3695 FROM symbols outer_scope JOIN lexical ON outer_scope.symbol_id = lexical.symbol_id
3696 WHERE outer_scope.parent_symbol_id IS NOT NULL AND lexical.depth < 32
3697 )
3698 SELECT 1 FROM lexical JOIN symbols scope ON scope.symbol_id = lexical.symbol_id
3699 WHERE lexical.symbol_id = {target}.parent_symbol_id
3700 AND lexical.depth > 1
3701 AND (scope.kind NOT IN ({MEMBER_LIST_OWNER_KINDS})
3702 OR scope.kind IN ('module', 'namespace')
3703 OR {target}.kind IN ({NESTED_TYPE_KINDS})
3704 AND s_from.language IN ({implicit_receiver_languages}))
3705 AND NOT EXISTS (
3706 SELECT 1 FROM lexical closer
3707 JOIN symbols nearer ON nearer.parent_symbol_id = closer.symbol_id
3708 WHERE closer.depth < lexical.depth
3709 AND nearer.name = {target}.name
3710 AND nearer.kind IN ({target}.kind, 'import')
3711 )
3712 )"
3713 )
3714}
3715
3716pub(crate) fn pending_target_predicate(conn: &Connection, target: &str, parent: &str) -> String {
3719 let ns = "json_each(CASE WHEN json_valid(p.target_namespace_json) THEN p.target_namespace_json ELSE '[]' END)";
3720 let target_path = format!("('/' || replace({target}.path, '\\', '/'))");
3721 let like_value = "replace(replace(replace(value, '\\', '\\\\'), '%', '\\%'), '_', '\\_')";
3722 let closer_rank = call_site_proximity("closer.path");
3723 let target_rank = call_site_proximity(&format!("{target}.path"));
3724 let relative = is_relative_import_path("import_source.value");
3725 let relative_match = relative_import_matches("import_source.value", target);
3726 let receiver_import_elsewhere = if has_column(conn, "symbols", "metadata_json") {
3728 format!(
3729 "AND NOT EXISTS (
3730 SELECT 1 FROM symbols receiver_import
3731 CROSS JOIN json_each(json_array(
3732 json_extract(receiver_import.metadata_json, '$.source')
3733 )) import_source
3734 WHERE receiver_import.kind = 'import'
3735 AND receiver_import.path = p.path
3736 AND receiver_import.name = p.target_receiver
3737 AND json_valid(receiver_import.metadata_json)
3738 AND {relative}
3739 AND NOT {relative_match}
3740 )"
3741 )
3742 } else {
3743 String::new()
3744 };
3745 let module_import = module_import_binding_sql(target, "p.target_receiver");
3746 let import_alias_receiver = if has_column(conn, "symbols", "metadata_json") {
3747 format!("OR EXISTS (
3748 SELECT 1 FROM symbols alias_import
3749 WHERE alias_import.kind = 'import'
3750 AND alias_import.path = p.path
3751 AND json_valid(alias_import.metadata_json)
3752 AND (json_extract(alias_import.metadata_json, '$.alias') = p.target_receiver
3753 OR json_extract(alias_import.metadata_json, '$.local_name') = p.target_receiver)
3754 AND COALESCE(json_extract(alias_import.metadata_json, '$.source'), '') NOT LIKE 'Qt%'
3755 )
3756 OR {module_import}"
3757 )
3758 } else {
3759 String::new()
3760 };
3761 let receiver_type = receiver_type_sql(conn, parent);
3762 let fixture_receiver = match built_receiver_sql(conn, target) {
3763 Some(built) => format!("OR {built}"),
3764 None => String::new(),
3765 };
3766 let inherited = class_chain_sql(target, &format!("{target}.kind IN ('variable', 'field')"));
3767 let receiver_chain = class_chain_sql(target, "p.target_receiver IS NOT 'super'");
3768 let implicit_receiver_languages = implicit_receiver_language_list();
3769 let enclosing = enclosing_scope_sql(target);
3770 let rust_path = rust_path_rule(target, ns);
3771 format!(
3772 "(
3773 NOT (p.kind IS 'extends' AND p.from_symbol_id = {target}.symbol_id)
3774 AND (
3775 (
3776 {target}.parent_symbol_id IS NOT NULL
3777 AND {parent}.name IS NOT NULL
3778 AND (
3779 EXISTS (SELECT 1 FROM {ns} WHERE value = {parent}.name)
3780 OR (EXISTS (SELECT 1 FROM {ns} WHERE value = 'Self')
3781 AND s_from.parent_symbol_id = {target}.parent_symbol_id)
3782 OR (p.target_receiver IS NOT NULL AND p.target_receiver != '' AND {parent}.name = p.target_receiver
3783 {receiver_import_elsewhere})
3784 OR ((p.target_receiver IN ('self', 'this', 'cls', 'Self', 'super')
3785 OR EXISTS (SELECT 1 FROM {ns} WHERE value = 'Self'))
3786 AND {receiver_chain})
3787 OR {receiver_type}
3788 {fixture_receiver}
3789 )
3790 AND NOT EXISTS (
3791 SELECT 1 FROM {ns}
3792 WHERE value NOT IN ('std', 'core', 'alloc', 'crate', 'super', 'self', 'Self', {parent}.name)
3793 AND NOT EXISTS (
3794 WITH RECURSIVE ancestor(symbol_id, depth) AS (
3795 SELECT {target}.parent_symbol_id, 0
3796 UNION ALL
3797 SELECT s.parent_symbol_id, ancestor.depth + 1
3798 FROM symbols s JOIN ancestor ON s.symbol_id = ancestor.symbol_id
3799 WHERE s.parent_symbol_id IS NOT NULL AND ancestor.depth < 32
3800 )
3801 SELECT 1 FROM ancestor JOIN symbols a ON a.symbol_id = ancestor.symbol_id
3802 WHERE a.name = value
3803 )
3804 AND {target_path} NOT LIKE '%/' || {like_value} || '.%' ESCAPE '\\'
3805 AND {target_path} NOT LIKE '%/' || {like_value} || '/%' ESCAPE '\\'
3806 )
3807 )
3808 OR (
3809 (p.target_namespace_json IS NULL OR p.target_namespace_json = '[]')
3810 AND (
3811 p.target_receiver IS NULL
3812 OR p.target_receiver = ''
3813 {import_alias_receiver}
3814 )
3815 AND ({target}.parent_symbol_id IS NULL
3816 OR s_from.parent_symbol_id = {target}.parent_symbol_id
3817 OR {target}.parent_symbol_id = p.from_symbol_id
3818 OR CASE WHEN p.target_receiver IS NULL OR p.target_receiver = ''
3819 THEN {enclosing}
3820 ELSE 0 END
3821 OR CASE WHEN (p.target_receiver IS NULL OR p.target_receiver = '')
3822 AND s_from.language IN ({implicit_receiver_languages})
3823 THEN {parent}.path = s_from.path
3824 AND {parent}.name = (
3825 SELECT caller_owner.name FROM symbols caller_owner
3826 WHERE caller_owner.symbol_id = s_from.parent_symbol_id
3827 AND caller_owner.kind = {parent}.kind
3828 )
3829 OR {inherited}
3830 ELSE 0 END)
3831 AND (p.target_receiver IS NOT NULL AND p.target_receiver != '' OR {target}.path = p.path OR NOT EXISTS (
3832 SELECT 1 FROM symbols shadow
3833 WHERE shadow.name = {target}.name
3834 AND shadow.path = p.path
3835 AND shadow.symbol_id != p.from_symbol_id
3836 AND shadow.kind NOT IN ('import', 'export', 'module', 'namespace')
3837 AND (shadow.parent_symbol_id IS NULL OR shadow.parent_symbol_id = p.from_symbol_id)
3838 ))
3839 AND ({target}.parent_symbol_id IS NOT NULL OR NOT EXISTS (
3840 SELECT 1 FROM symbols closer
3841 WHERE closer.name = {target}.name
3842 AND closer.symbol_id != {target}.symbol_id
3843 AND closer.parent_symbol_id IS NULL
3844 AND closer.kind = {target}.kind
3845 AND NOT (p.kind IS 'extends' AND closer.symbol_id = p.from_symbol_id)
3846 AND {closer_rank} > {target_rank}
3847 ))
3848 )
3849 OR (
3850 {target}.parent_symbol_id IS NULL
3851 AND (
3852 EXISTS (
3853 SELECT 1 FROM {ns}
3854 WHERE value NOT IN ('std', 'core', 'alloc', 'crate', 'super')
3855 AND {target_path} LIKE '%/' || {like_value} || '.%' ESCAPE '\\'
3856 )
3857 OR ({rust_path})
3858 )
3859 )
3860 )
3861 )"
3862 )
3863}
3864
3865fn rust_path_rule(target: &str, ns: &str) -> String {
3872 let target_path = format!("('/' || replace({target}.path, '\\', '/'))");
3873 let caller_path = "('/' || replace(p.path, '\\', '/'))";
3874 let skipped = "'std', 'core', 'alloc', 'crate', 'self', 'super', 'Self'";
3875 let like_segment = "replace(replace(replace(seg.value, '\\', '\\\\'), '%', '\\%'), '_', '\\_')";
3876 let in_module_path = format!(
3877 "(module_path.value LIKE '%/' || {like_segment} || '.%' ESCAPE '\\'
3878 OR module_path.value LIKE '%/' || {like_segment} || '/%' ESCAPE '\\')"
3879 );
3880 format!(
3881 "s_from.language = 'rust'
3882 AND {target}.language = 'rust'
3883 AND EXISTS (
3884 SELECT 1
3885 FROM json_each(json_array(replace({target_path}, '/src/', char(1)))) marked
3886 CROSS JOIN json_each(json_array(
3887 CASE WHEN instr(marked.value, char(1)) > 0
3888 THEN length({target_path})
3889 - length(replace(marked.value, rtrim(marked.value, replace(marked.value, char(1), '')), ''))
3890 ELSE 0 END)) src
3891 CROSS JOIN json_each(json_array(
3892 CASE WHEN src.value > 0 THEN replace(substr({target_path}, src.value), '-', '_') ELSE '' END)) module_path
3893 WHERE (
3894 EXISTS (
3895 SELECT 1 FROM {ns} head
3896 WHERE head.key = 0
3897 AND (head.value IN ('crate', 'self', 'super')
3898 AND ({target}.path = p.path
3899 OR src.value > 0
3900 AND substr({caller_path}, 1, src.value) = substr({target_path}, 1, src.value))
3901 OR head.value NOT IN ({skipped})
3902 AND src.value >= length(head.value) + 6
3903 AND replace(substr({target_path}, src.value - length(head.value) - 5, length(head.value) + 6), '-', '_')
3904 = '/' || head.value || '/src/')
3905 )
3906 AND NOT EXISTS (
3907 SELECT 1 FROM {ns} seg
3908 WHERE seg.key > 0 AND seg.value NOT IN ({skipped}) AND NOT {in_module_path}
3909 )
3910 OR src.value > 0
3911 AND EXISTS (SELECT 1 FROM {ns} head WHERE head.key = 0 AND head.value NOT IN ({skipped}))
3912 AND NOT EXISTS (
3913 SELECT 1 FROM {ns} seg
3914 WHERE seg.value NOT IN ({skipped}) AND NOT {in_module_path}
3915 )
3916 )
3917 )"
3918 )
3919}
3920
3921pub(crate) const DOCUMENTATION_LANGUAGES: &[&str] = &[
3924 "markdown", "yaml", "toml", "json", "html", "css", "xml", "ini", "text",
3925];
3926
3927pub(crate) fn documentation_language_list() -> String {
3928 DOCUMENTATION_LANGUAGES
3929 .iter()
3930 .map(|l| format!("'{l}'"))
3931 .collect::<Vec<_>>()
3932 .join(", ")
3933}
3934
3935fn not_documentation(conn: &Connection, alias: &str) -> String {
3936 let has_content_type: bool = conn
3937 .query_row(
3938 "SELECT 1 FROM pragma_table_info('symbols') WHERE name = 'content_type'",
3939 [],
3940 |_| Ok(true),
3941 )
3942 .unwrap_or(false);
3943 if has_content_type {
3944 format!("({alias}.content_type IS NULL OR {alias}.content_type != 'documentation')")
3945 } else {
3946 "1 = 1".to_string()
3947 }
3948}
3949
3950pub(crate) fn has_column(conn: &Connection, table: &str, column: &str) -> bool {
3951 conn.query_row(
3952 "SELECT 1 FROM pragma_table_info(?1) WHERE name = ?2",
3953 [table, column],
3954 |_| Ok(true),
3955 )
3956 .unwrap_or(false)
3957}
3958
3959pub(crate) fn has_table(conn: &Connection, name: &str) -> bool {
3960 conn.query_row(
3961 "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1",
3962 [name],
3963 |_| Ok(true),
3964 )
3965 .unwrap_or(false)
3966}
3967
3968fn has_pending_namespace_column(conn: &Connection) -> bool {
3969 let has_ns: bool = conn
3970 .query_row(
3971 "SELECT 1 FROM pragma_table_info('pending_relationships') WHERE name = 'target_namespace_json'",
3972 [],
3973 |_| Ok(true),
3974 )
3975 .unwrap_or(false);
3976 let has_display: bool = conn
3977 .query_row(
3978 "SELECT 1 FROM pragma_table_info('pending_relationships') WHERE name = 'target_display_name'",
3979 [],
3980 |_| Ok(true),
3981 )
3982 .unwrap_or(false);
3983 let has_receiver: bool = conn
3984 .query_row(
3985 "SELECT 1 FROM pragma_table_info('pending_relationships') WHERE name = 'target_receiver'",
3986 [],
3987 |_| Ok(true),
3988 )
3989 .unwrap_or(false);
3990 has_ns && has_display && has_receiver
3991}
3992
3993fn find_references_internal(
3994 conn: &Connection,
3995 symbol_name: &str,
3996 direction: &str,
3997 limit: usize,
3998 symbol_id: Option<&str>,
3999 include_external: bool,
4000) -> Result<Vec<ReferenceSite>, QueryError> {
4001 let mut sites = find_direct_references(
4002 conn,
4003 symbol_name,
4004 direction,
4005 limit,
4006 symbol_id,
4007 include_external,
4008 )?;
4009 let class = match symbol_id.filter(|_| direction == "callers") {
4010 Some(id) => constructed_class(conn, id)?,
4011 None => None,
4012 };
4013 if let Some((class_id, class_name)) = class.filter(|_| sites.len() < limit) {
4014 let mut builds = find_direct_references(
4015 conn,
4016 &class_name,
4017 "callers",
4018 limit - sites.len(),
4019 Some(&class_id),
4020 include_external,
4021 )?;
4022 builds.retain(|site| site.kind == "calls");
4023 sites.extend(builds);
4024 sites.sort_by(|a, b| {
4025 a.path
4026 .cmp(&b.path)
4027 .then(a.start_line.cmp(&b.start_line))
4028 .then(a.start_column.cmp(&b.start_column))
4029 });
4030 }
4031 if direction == "callers" {
4032 populate_lambda_enclosing_names(conn, &mut sites)?;
4033 }
4034 Ok(sites)
4035}
4036
4037pub(crate) fn populate_lambda_enclosing_names(
4038 conn: &Connection,
4039 sites: &mut [ReferenceSite],
4040) -> Result<(), QueryError> {
4041 if !sites
4042 .iter()
4043 .any(|site| has_generated_lambda_name(&site.from_symbol_name))
4044 {
4045 return Ok(());
4046 }
4047 let mut stmt = conn.prepare(
4048 "SELECT caller.language, caller.kind, caller.signature, parent.name
4049 FROM symbols caller
4050 LEFT JOIN symbols parent ON parent.symbol_id = caller.parent_symbol_id
4051 WHERE caller.symbol_id = ?1",
4052 )?;
4053 for site in sites
4054 .iter_mut()
4055 .filter(|site| has_generated_lambda_name(&site.from_symbol_name))
4056 {
4057 let details = stmt
4058 .query_row([&site.from_symbol_id], |row| {
4059 Ok((
4060 row.get::<_, String>(0)?,
4061 row.get::<_, String>(1)?,
4062 row.get::<_, Option<String>>(2)?,
4063 row.get::<_, Option<String>>(3)?,
4064 ))
4065 })
4066 .optional()?;
4067 if let Some((language, kind, signature, enclosing_symbol_name)) = details
4068 && is_generated_lambda_name(
4069 &site.from_symbol_name,
4070 &language,
4071 &kind,
4072 signature.as_deref(),
4073 )
4074 {
4075 site.enclosing_symbol_name = enclosing_symbol_name;
4076 }
4077 }
4078 Ok(())
4079}
4080
4081pub(crate) fn constructed_class(
4083 conn: &Connection,
4084 symbol_id: &str,
4085) -> Result<Option<(String, String)>, QueryError> {
4086 Ok(conn
4087 .query_row(
4088 "SELECT class.symbol_id, class.name
4089 FROM symbols ctor JOIN symbols class ON class.symbol_id = ctor.parent_symbol_id
4090 WHERE ctor.symbol_id = ?1 AND ctor.kind = 'constructor'
4091 AND class.kind IN ('class', 'struct', 'record')",
4092 params![symbol_id],
4093 |row| Ok((row.get(0)?, row.get(1)?)),
4094 )
4095 .optional()?)
4096}
4097
4098pub(crate) struct DirectIdentifier {
4100 pub identifier_id: String,
4101 pub site: ReferenceSite,
4102}
4103
4104pub(crate) fn direct_identifier_rows(
4107 conn: &Connection,
4108 symbol_name: &str,
4109 symbol_id: Option<&str>,
4110 remaining: usize,
4111) -> Result<Vec<DirectIdentifier>, QueryError> {
4112 let receiver_match = member_receiver_match(conn);
4113 let type_usage_shadow = format!(
4116 "i.kind = 'type_usage'
4117 AND ?2 IS NOT NULL
4118 AND selected_target.kind IN ({NESTED_TYPE_KINDS})
4119 AND EXISTS (
4120 WITH RECURSIVE lexical(symbol_id, depth) AS (
4121 SELECT s_from.symbol_id, 0
4122 UNION ALL
4123 SELECT scope.parent_symbol_id, lexical.depth + 1
4124 FROM symbols scope JOIN lexical ON scope.symbol_id = lexical.symbol_id
4125 WHERE scope.parent_symbol_id IS NOT NULL AND lexical.depth < 32
4126 ), visible_types(symbol_id, depth) AS (
4127 SELECT shadow.symbol_id, lexical.depth
4128 FROM lexical
4129 JOIN symbols shadow ON shadow.parent_symbol_id = lexical.symbol_id
4130 WHERE shadow.name = selected_target.name
4131 AND shadow.kind IN ({NESTED_TYPE_KINDS})
4132 AND shadow.language = i.language
4133 UNION ALL
4134 SELECT shadow.symbol_id, 100
4135 FROM symbols shadow
4136 WHERE shadow.name = selected_target.name
4137 AND shadow.kind IN ({NESTED_TYPE_KINDS})
4138 AND shadow.language = i.language
4139 AND shadow.parent_symbol_id IS NULL
4140 AND replace(shadow.path, '\\', '/') = replace(i.path, '\\', '/')
4141 )
4142 SELECT 1 FROM visible_types nearest
4143 WHERE nearest.symbol_id != selected_target.symbol_id
4144 AND nearest.depth = (SELECT MIN(depth) FROM visible_types)
4145 )"
4146 );
4147 let receiver_match = receiver_match.replace("?3", "?1");
4148 let language_match = identifier_language_matches("i", "selected_target");
4149 let mut receiver_stmt = conn.prepare(&format!(
4150 "SELECT {receiver_match}
4151 FROM (SELECT name, kind, path, start_line, start_column,
4152 containing_symbol_id AS from_symbol_id,
4153 CASE WHEN json_valid(metadata_json) THEN metadata_json ELSE '{{}}' END AS metadata_json,
4154 COALESCE(json_valid(metadata_json), 0) AS has_metadata,
4155 language,
4156 CASE WHEN json_valid(metadata_json) THEN json_extract(metadata_json, '$.receiver') END AS target_receiver,
4157 '[]' AS target_namespace_json
4158 FROM identifiers WHERE identifier_id = ?2) p
4159 LEFT JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id"
4160 ))?;
4161 let mut ident_stmt = conn.prepare(&format!(
4162 "WITH raw AS MATERIALIZED (
4163 SELECT identifier_id, name, kind, path, start_line, start_column,
4164 containing_symbol_id, language,
4165 CASE WHEN json_valid(metadata_json) THEN metadata_json ELSE '{{}}' END AS metadata_json
4166 FROM identifiers WHERE name = ?1 AND kind IN ('type_usage', 'member_access')
4167 )
4168 SELECT i.identifier_id,
4169 COALESCE(s_from.name, ''),
4170 COALESCE(i.containing_symbol_id, ''),
4171 i.name,
4172 CASE WHEN json_valid(i.metadata_json)
4173 AND json_extract(i.metadata_json, '$.role') = 'signal_handler'
4174 THEN CASE WHEN json_extract(i.metadata_json, '$.receiver') = (
4175 SELECT CASE WHEN t.kind IN ('class', 'struct', 'enum', 'interface', 'trait', 'module', 'namespace')
4176 THEN t.name ELSE tp.name END
4177 FROM symbols t
4178 LEFT JOIN symbols tp ON tp.symbol_id = t.parent_symbol_id
4179 WHERE t.symbol_id = ?2)
4180 THEN 'handler' ELSE 'handler (candidate)' END
4181 ELSE i.kind END,
4182 i.path,
4183 i.start_line,
4184 i.start_column
4185 FROM raw i
4186 LEFT JOIN symbols s_from ON i.containing_symbol_id = s_from.symbol_id
4187 LEFT JOIN symbols selected_target ON selected_target.symbol_id = ?2
4188 WHERE i.name = ?1 AND i.kind IN ('type_usage', 'member_access')
4189 AND COALESCE(s_from.kind, '') != 'import'
4190 AND (?2 IS NULL OR {language_match})
4191 AND NOT ({type_usage_shadow})
4192 AND NOT EXISTS (
4193 SELECT 1 FROM relationships covered
4194 JOIN symbols covered_to ON covered.to_symbol_id = covered_to.symbol_id
4195 WHERE covered_to.name = i.name
4196 AND covered.path = i.path
4197 AND covered.start_line = i.start_line
4198 AND (?2 IS NULL OR covered.to_symbol_id = ?2)
4199 )
4200 AND NOT EXISTS (
4201 SELECT 1 FROM pending_relationships covered
4202 WHERE covered.target_terminal_name = i.name
4203 AND covered.path = i.path
4204 AND covered.start_line = i.start_line
4205 )
4206 AND (?2 IS NULL OR NOT EXISTS (
4207 SELECT 1 FROM symbols owner
4208 JOIN symbols member ON member.parent_symbol_id = owner.symbol_id
4209 WHERE owner.name = CASE WHEN json_valid(i.metadata_json) THEN json_extract(i.metadata_json, '$.receiver') END
4210 AND member.name = i.name
4211 AND owner.name IS NOT (SELECT parent.name FROM symbols target
4212 JOIN symbols parent ON parent.symbol_id = target.parent_symbol_id
4213 WHERE target.symbol_id = ?2)
4214 ))
4215 AND NOT (i.kind = 'member_access' AND EXISTS (
4216 SELECT 1 FROM symbols target
4217 LEFT JOIN symbols scope ON scope.symbol_id = target.parent_symbol_id
4218 WHERE target.symbol_id = ?2
4219 AND (scope.kind IN ('function', 'method', 'constructor')
4220 OR (scope.symbol_id IS NULL
4221 AND target.path != i.path
4222 AND target.language IN ('javascript', 'typescript', 'tsx')
4223 AND COALESCE(target.visibility, 'private') = 'private'))
4224 ))
4225 AND NOT COALESCE((i.kind = 'member_access'
4226 AND json_valid(i.metadata_json)
4227 AND json_extract(i.metadata_json, '$.role') IS NOT 'signal_handler'
4228 AND json_extract(i.metadata_json, '$.receiver') GLOB '[A-Z]*'
4229 AND NOT EXISTS (SELECT 1 FROM symbols known
4230 WHERE known.name = json_extract(i.metadata_json, '$.receiver')
4231 AND known.kind NOT IN ('variable', 'parameter', 'method'))), 0)
4232 ORDER BY i.path, i.start_line, i.start_column, i.identifier_id"
4233 ))?;
4234 let mut supported = Vec::new();
4235 let mut candidates = Vec::new();
4236 let mut supported_keys = HashSet::new();
4237 let mut candidate_keys = HashSet::new();
4238 let site_key = |site: &ReferenceSite| {
4239 (
4240 site.from_symbol_id.clone(),
4241 site.to_symbol_name.clone(),
4242 site.kind.clone(),
4243 site.path.clone(),
4244 site.start_line,
4245 )
4246 };
4247 let mut ident_rows = ident_stmt.query(params![symbol_name, symbol_id])?;
4248 while let Some(row) = ident_rows.next()? {
4249 let identifier_id: String = row.get(0)?;
4250 let mut site = ReferenceSite {
4251 from_symbol_name: row.get(1)?,
4252 from_symbol_id: row.get(2)?,
4253 to_symbol_name: row.get(3)?,
4254 kind: row.get(4)?,
4255 path: row.get::<_, String>(5)?.replace('\\', "/"),
4256 start_line: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4257 start_column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
4258 occurrences: None,
4259 target: None,
4260 enclosing_symbol_name: None,
4261 };
4262 let receiver_match: i64 =
4263 receiver_stmt.query_row(params![symbol_id, identifier_id], |row| row.get(0))?;
4264 if receiver_match < 0 {
4265 continue;
4266 }
4267 if receiver_match == 0 && site.kind == "member_access" {
4268 site.kind = "member_access (candidate)".to_string();
4269 }
4270 if site.kind == "member_access (candidate)" {
4271 if candidates.len() < remaining && candidate_keys.insert(site_key(&site)) {
4272 candidates.push(DirectIdentifier {
4273 identifier_id,
4274 site,
4275 });
4276 }
4277 } else if supported_keys.insert(site_key(&site)) {
4278 supported.push(DirectIdentifier {
4279 identifier_id,
4280 site,
4281 });
4282 if supported.len() >= remaining {
4283 break;
4284 }
4285 }
4286 }
4287
4288 let site_order = |a: &DirectIdentifier, b: &DirectIdentifier| {
4289 a.site
4290 .path
4291 .cmp(&b.site.path)
4292 .then(a.site.start_line.cmp(&b.site.start_line))
4293 .then(a.site.start_column.cmp(&b.site.start_column))
4294 };
4295 supported.sort_by(site_order);
4296 supported.truncate(remaining);
4297 candidates.sort_by(site_order);
4298 candidates.truncate(remaining - supported.len());
4299 let mut rows = supported;
4300 rows.extend(candidates);
4301 Ok(rows)
4302}
4303
4304pub(crate) fn grouped_member_access_rows(
4307 conn: &Connection,
4308 symbol_id: &str,
4309 remaining: usize,
4310) -> Result<Vec<ReferenceSite>, QueryError> {
4311 let language_match = identifier_language_matches("i", "target");
4312 let mut receiver_stmt = conn.prepare(&format!(
4313 "SELECT COALESCE(s.name, ''),
4314 COALESCE(i.containing_symbol_id, ''),
4315 i.name,
4316 i.kind,
4317 i.path,
4318 MIN(i.start_line),
4319 i.start_column,
4320 COUNT(*)
4321 FROM identifiers i
4322 LEFT JOIN symbols s ON i.containing_symbol_id = s.symbol_id
4323 JOIN symbols target ON target.symbol_id = ?2
4324 LEFT JOIN symbols target_parent ON target_parent.symbol_id = target.parent_symbol_id
4325 WHERE i.kind = 'member_access'
4326 AND i.name != target.name
4327 AND COALESCE(s.kind, '') != 'import'
4328 AND target.kind IN ('class', 'struct', 'enum', 'interface', 'trait', 'module', 'namespace')
4329 AND json_valid(i.metadata_json)
4330 AND json_extract(i.metadata_json, '$.receiver') = target.name
4331 AND (json_extract(i.metadata_json, '$.receiver_qualifier') IS NULL
4332 OR json_extract(i.metadata_json, '$.receiver_qualifier') = target_parent.name)
4333 AND {language_match}
4334 AND NOT EXISTS (
4335 SELECT 1 FROM symbols nearer
4336 WHERE nearer.name = target.name
4337 AND nearer.path = i.path
4338 AND nearer.symbol_id != target.symbol_id
4339 AND nearer.kind IN ({NESTED_TYPE_KINDS}, 'module', 'namespace')
4340 )
4341 GROUP BY i.path
4342 ORDER BY i.path, i.start_line
4343 LIMIT ?1"
4344 ))?;
4345 let rows = receiver_stmt.query_map(params![remaining as i64, symbol_id], |row| {
4346 Ok(ReferenceSite {
4347 from_symbol_name: row.get(0)?,
4348 from_symbol_id: row.get(1)?,
4349 to_symbol_name: row.get(2)?,
4350 kind: row.get(3)?,
4351 path: row.get::<_, String>(4)?.replace('\\', "/"),
4352 start_line: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
4353 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4354 occurrences: Some(row.get::<_, i64>(7)? as usize),
4355 target: None,
4356 enclosing_symbol_name: None,
4357 })
4358 })?;
4359 Ok(rows.collect::<Result<_, _>>()?)
4360}
4361
4362fn find_direct_references(
4363 conn: &Connection,
4364 symbol_name: &str,
4365 direction: &str,
4366 limit: usize,
4367 symbol_id: Option<&str>,
4368 include_external: bool,
4369) -> Result<Vec<ReferenceSite>, QueryError> {
4370 let mut results = Vec::new();
4371
4372 if direction == "callers" {
4373 let mut stmt = conn.prepare(
4375 "SELECT s_from.name AS from_name,
4376 r.from_symbol_id,
4377 s_to.name AS to_name,
4378 r.kind,
4379 r.path,
4380 r.start_line,
4381 r.start_column
4382 FROM relationships r
4383 JOIN symbols s_from ON r.from_symbol_id = s_from.symbol_id
4384 JOIN symbols s_to ON r.to_symbol_id = s_to.symbol_id
4385 WHERE s_to.name = ?1 AND (?3 IS NULL OR r.to_symbol_id = ?3)
4386 LIMIT ?2",
4387 )?;
4388
4389 let rows = stmt.query_map(params![symbol_name, limit as i64, symbol_id], |row| {
4390 Ok(ReferenceSite {
4391 from_symbol_name: row.get(0)?,
4392 from_symbol_id: row.get(1)?,
4393 to_symbol_name: row.get(2)?,
4394 kind: row.get(3)?,
4395 path: row.get::<_, String>(4)?.replace('\\', "/"),
4396 start_line: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
4397 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4398 occurrences: None,
4399 target: None,
4400 enclosing_symbol_name: None,
4401 })
4402 })?;
4403
4404 for r in rows {
4405 results.push(r?);
4406 }
4407
4408 if results.len() < limit {
4410 let remaining = limit - results.len();
4411 if has_pending_namespace_column(conn) {
4412 if let Some(sid) = symbol_id {
4413 let mut pending_stmt = conn.prepare(
4414 &format!("SELECT s_from.name AS from_name,
4415 p.from_symbol_id,
4416 p.target_terminal_name AS to_name,
4417 p.kind,
4418 p.path,
4419 p.start_line,
4420 p.start_column
4421 FROM pending_relationships p
4422 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4423 JOIN symbols s_target ON s_target.symbol_id = ?3
4424 LEFT JOIN symbols s_target_parent ON s_target.parent_symbol_id = s_target_parent.symbol_id
4425 WHERE p.target_terminal_name = ?1
4426 AND {pred}
4427 LIMIT ?2", pred = pending_target_predicate(conn, "s_target", "s_target_parent")),
4428 )?;
4429
4430 let p_rows = pending_stmt.query_map(
4431 params![symbol_name, remaining as i64, sid],
4432 |row| {
4433 Ok(ReferenceSite {
4434 from_symbol_name: row.get(0)?,
4435 from_symbol_id: row.get(1)?,
4436 to_symbol_name: row.get(2)?,
4437 kind: row.get(3)?,
4438 path: row.get::<_, String>(4)?.replace('\\', "/"),
4439 start_line: Some(row.get::<_, i64>(5)? as usize),
4440 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4441 occurrences: None,
4442 target: None,
4443 enclosing_symbol_name: None,
4444 })
4445 },
4446 )?;
4447 for r in p_rows {
4448 results.push(r?);
4449 }
4450 } else {
4451 let mut pending_stmt = conn.prepare(
4452 "SELECT s_from.name AS from_name,
4453 p.from_symbol_id,
4454 p.target_terminal_name AS to_name,
4455 p.kind,
4456 p.path,
4457 p.start_line,
4458 p.start_column
4459 FROM pending_relationships p
4460 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4461 WHERE p.target_terminal_name = ?1
4462 AND (
4463 (p.target_namespace_json IS NULL OR p.target_namespace_json = '[]')
4464 OR EXISTS (
4465 SELECT 1 FROM symbols s_any
4466 JOIN symbols s_any_parent ON s_any.parent_symbol_id = s_any_parent.symbol_id
4467 WHERE s_any.name = p.target_terminal_name
4468 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)
4469 )
4470 )
4471 LIMIT ?2",
4472 )?;
4473
4474 let p_rows =
4475 pending_stmt.query_map(params![symbol_name, remaining as i64], |row| {
4476 Ok(ReferenceSite {
4477 from_symbol_name: row.get(0)?,
4478 from_symbol_id: row.get(1)?,
4479 to_symbol_name: row.get(2)?,
4480 kind: row.get(3)?,
4481 path: row.get::<_, String>(4)?.replace('\\', "/"),
4482 start_line: Some(row.get::<_, i64>(5)? as usize),
4483 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4484 occurrences: None,
4485 target: None,
4486 enclosing_symbol_name: None,
4487 })
4488 })?;
4489 for r in p_rows {
4490 results.push(r?);
4491 }
4492 }
4493 } else {
4494 let is_nested = if let Some(sid) = symbol_id {
4495 conn.query_row(
4496 "SELECT 1 FROM symbols WHERE symbol_id = ?1 AND parent_symbol_id IS NOT NULL",
4497 params![sid],
4498 |_| Ok(true),
4499 )
4500 .unwrap_or(false)
4501 } else {
4502 false
4503 };
4504
4505 if !is_nested {
4506 let mut pending_stmt = conn.prepare(
4507 "SELECT s_from.name AS from_name,
4508 p.from_symbol_id,
4509 p.target_terminal_name AS to_name,
4510 p.kind,
4511 p.path,
4512 p.start_line,
4513 p.start_column
4514 FROM pending_relationships p
4515 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4516 WHERE p.target_terminal_name = ?1
4517 LIMIT ?2",
4518 )?;
4519
4520 let p_rows =
4521 pending_stmt.query_map(params![symbol_name, remaining as i64], |row| {
4522 Ok(ReferenceSite {
4523 from_symbol_name: row.get(0)?,
4524 from_symbol_id: row.get(1)?,
4525 to_symbol_name: row.get(2)?,
4526 kind: row.get(3)?,
4527 path: row.get::<_, String>(4)?.replace('\\', "/"),
4528 start_line: Some(row.get::<_, i64>(5)? as usize),
4529 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4530 occurrences: None,
4531 target: None,
4532 enclosing_symbol_name: None,
4533 })
4534 })?;
4535
4536 for r in p_rows {
4537 results.push(r?);
4538 }
4539 }
4540 }
4541 }
4542
4543 if results.len() < limit && has_table(conn, "identifiers") {
4544 let remaining = limit - results.len();
4545 let identifiers = direct_identifier_rows(conn, symbol_name, symbol_id, remaining)?;
4546 results.extend(identifiers.into_iter().map(|row| row.site));
4547 if let Some(sid) = symbol_id.filter(|_| results.len() < limit) {
4548 let remaining = limit - results.len();
4549 results.extend(grouped_member_access_rows(conn, sid, remaining)?);
4550 }
4551 }
4552 } else {
4553 let mut stmt = conn.prepare(
4555 "SELECT s_from.name AS from_name,
4556 r.from_symbol_id,
4557 s_to.name AS to_name,
4558 r.kind,
4559 r.path,
4560 r.start_line,
4561 r.start_column
4562 FROM relationships r
4563 JOIN symbols s_from ON r.from_symbol_id = s_from.symbol_id
4564 JOIN symbols s_to ON r.to_symbol_id = s_to.symbol_id
4565 WHERE s_from.name = ?1 AND (?3 IS NULL OR r.from_symbol_id = ?3)
4566 LIMIT ?2",
4567 )?;
4568
4569 let rows = stmt.query_map(params![symbol_name, limit as i64, symbol_id], |row| {
4570 Ok(ReferenceSite {
4571 from_symbol_name: row.get(0)?,
4572 from_symbol_id: row.get(1)?,
4573 to_symbol_name: row.get(2)?,
4574 kind: row.get(3)?,
4575 path: row.get::<_, String>(4)?.replace('\\', "/"),
4576 start_line: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
4577 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4578 occurrences: None,
4579 target: None,
4580 enclosing_symbol_name: None,
4581 })
4582 })?;
4583
4584 for r in rows {
4585 results.push(r?);
4586 }
4587
4588 if results.len() < limit {
4590 let remaining = limit - results.len();
4591 let p_rows: Vec<ReferenceSite> = if has_pending_namespace_column(conn) {
4592 let sql = if include_external {
4593 String::from("SELECT DISTINCT s_from.name AS from_name,
4594 p.from_symbol_id,
4595 COALESCE(NULLIF(p.target_display_name, ''), p.target_terminal_name) AS to_name,
4596 p.kind,
4597 p.path,
4598 p.start_line,
4599 p.start_column
4600 FROM pending_relationships p
4601 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4602 WHERE s_from.name = ?1 AND (?3 IS NULL OR p.from_symbol_id = ?3)
4603 LIMIT ?2")
4604 } else {
4605 format!("SELECT DISTINCT s_from.name AS from_name,
4606 p.from_symbol_id,
4607 p.target_terminal_name AS to_name,
4608 p.kind,
4609 p.path,
4610 p.start_line,
4611 p.start_column
4612 FROM pending_relationships p
4613 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4614 WHERE s_from.name = ?1 AND (?3 IS NULL OR p.from_symbol_id = ?3)
4615 AND EXISTS (
4616 SELECT 1 FROM symbols s_to
4617 LEFT JOIN symbols s_to_parent ON s_to.parent_symbol_id = s_to_parent.symbol_id
4618 WHERE s_to.name = p.target_terminal_name
4619 AND {kind}
4620 AND {pred}
4621 )
4622 LIMIT ?2", kind = callee_target_kind("s_to_parent"), pred = pending_target_predicate(conn, "s_to", "s_to_parent"))
4623 };
4624 let mut pending_stmt = conn.prepare(&sql)?;
4625 let rows = pending_stmt.query_map(
4626 params![symbol_name, remaining as i64, symbol_id],
4627 |row| {
4628 Ok(ReferenceSite {
4629 from_symbol_name: row.get(0)?,
4630 from_symbol_id: row.get(1)?,
4631 to_symbol_name: row.get(2)?,
4632 kind: row.get(3)?,
4633 path: row.get::<_, String>(4)?.replace('\\', "/"),
4634 start_line: Some(row.get::<_, i64>(5)? as usize),
4635 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4636 occurrences: None,
4637 target: None,
4638 enclosing_symbol_name: None,
4639 })
4640 },
4641 )?;
4642 let mut out = Vec::new();
4643 for r in rows {
4644 out.push(r?);
4645 }
4646 out
4647 } else {
4648 let sql = if include_external {
4649 "SELECT DISTINCT s_from.name AS from_name,
4650 p.from_symbol_id,
4651 p.target_terminal_name AS to_name,
4652 p.kind,
4653 p.path,
4654 p.start_line,
4655 p.start_column
4656 FROM pending_relationships p
4657 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4658 WHERE s_from.name = ?1 AND (?3 IS NULL OR p.from_symbol_id = ?3)
4659 LIMIT ?2"
4660 } else {
4661 "SELECT DISTINCT s_from.name AS from_name,
4662 p.from_symbol_id,
4663 p.target_terminal_name AS to_name,
4664 p.kind,
4665 p.path,
4666 p.start_line,
4667 p.start_column
4668 FROM pending_relationships p
4669 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4670 WHERE s_from.name = ?1 AND (?3 IS NULL OR p.from_symbol_id = ?3)
4671 AND EXISTS (SELECT 1 FROM symbols s_to WHERE s_to.name = p.target_terminal_name)
4672 LIMIT ?2"
4673 };
4674 let mut pending_stmt = conn.prepare(sql)?;
4675
4676 let rows = pending_stmt.query_map(
4677 params![symbol_name, remaining as i64, symbol_id],
4678 |row| {
4679 Ok(ReferenceSite {
4680 from_symbol_name: row.get(0)?,
4681 from_symbol_id: row.get(1)?,
4682 to_symbol_name: row.get(2)?,
4683 kind: row.get(3)?,
4684 path: row.get::<_, String>(4)?.replace('\\', "/"),
4685 start_line: Some(row.get::<_, i64>(5)? as usize),
4686 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4687 occurrences: None,
4688 target: None,
4689 enclosing_symbol_name: None,
4690 })
4691 },
4692 )?;
4693 let mut out = Vec::new();
4694 for r in rows {
4695 out.push(r?);
4696 }
4697 out
4698 };
4699
4700 for r in p_rows {
4701 results.push(r);
4702 }
4703 }
4704 results.sort_by(|a, b| {
4705 a.path
4706 .cmp(&b.path)
4707 .then(a.start_line.cmp(&b.start_line))
4708 .then(a.start_column.cmp(&b.start_column))
4709 });
4710 let mut sites = merge_same_site(results);
4711 describe_callee_targets(conn, &mut sites)?;
4712 return Ok(sites);
4713 }
4714
4715 Ok(merge_same_site(results))
4716}
4717
4718fn callee_target_kind(parent: &str) -> String {
4721 format!(
4722 "(s_to.kind NOT IN ('import', 'variable', 'parameter', 'field', 'property', 'module', 'namespace')
4723 OR p.kind = 'calls' AND s_to.kind IN ('variable', 'field')
4724 AND {parent}.kind IN ('class', 'struct', 'interface', 'trait', 'record', 'object', 'protocol'))"
4725 )
4726}
4727
4728fn describe_callee_targets(
4731 conn: &Connection,
4732 sites: &mut [ReferenceSite],
4733) -> Result<(), QueryError> {
4734 let owner = "CASE WHEN par.kind IN ('class', 'struct', 'interface', 'trait', 'enum', 'record', 'object', 'protocol', 'union')
4735 THEN par.name || '.' ELSE '' END";
4736 let pending = if has_pending_namespace_column(conn) {
4737 format!(
4738 "UNION
4739 SELECT {owner} || s_to.name, replace(s_to.path, '\\', '/'), s_to.start_line
4740 FROM pending_relationships p
4741 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4742 JOIN symbols s_to ON s_to.name = p.target_terminal_name
4743 LEFT JOIN symbols par ON s_to.parent_symbol_id = par.symbol_id
4744 LEFT JOIN symbols s_to_parent ON s_to.parent_symbol_id = s_to_parent.symbol_id
4745 WHERE p.from_symbol_id = ?1 AND p.start_line = ?2
4746 AND (p.target_terminal_name = ?3 OR p.target_display_name = ?3)
4747 AND {kind}
4748 AND {pred}",
4749 kind = callee_target_kind("s_to_parent"),
4750 pred = pending_target_predicate(conn, "s_to", "s_to_parent")
4751 )
4752 } else {
4753 String::new()
4754 };
4755 let mut stmt = conn.prepare(&format!(
4756 "SELECT {owner} || s_to.name, replace(s_to.path, '\\', '/'), s_to.start_line
4757 FROM relationships r
4758 JOIN symbols s_to ON r.to_symbol_id = s_to.symbol_id
4759 LEFT JOIN symbols par ON s_to.parent_symbol_id = par.symbol_id
4760 WHERE r.from_symbol_id = ?1 AND r.start_line = ?2 AND s_to.name = ?3
4761 {pending}
4762 ORDER BY 2, 3
4763 LIMIT 4"
4764 ))?;
4765 for site in sites.iter_mut() {
4766 let Some(line) = site.start_line else {
4767 continue;
4768 };
4769 let targets = stmt
4770 .query_map(
4771 params![site.from_symbol_id, line as i64, site.to_symbol_name],
4772 |row| {
4773 Ok(format!(
4774 "`{}` ({}:{})",
4775 row.get::<_, String>(0)?,
4776 row.get::<_, String>(1)?,
4777 row.get::<_, i64>(2)?
4778 ))
4779 },
4780 )?
4781 .collect::<Result<Vec<_>, _>>()?;
4782 site.target = match targets.len() {
4783 0 => None,
4784 1 => targets.into_iter().next(),
4785 2 | 3 => Some(format!("one of {}", targets.join(", "))),
4786 _ => Some(format!(
4787 "more than 3 definitions named `{}`",
4788 site.to_symbol_name
4789 )),
4790 };
4791 }
4792 Ok(())
4793}
4794
4795pub fn import_sites(
4801 conn: &Connection,
4802 name: &str,
4803 path_filter: Option<&str>,
4804) -> Result<Vec<(String, usize)>, QueryError> {
4805 import_sites_from(conn, name, path_filter, false)
4806}
4807
4808pub fn import_sites_of_absent(
4811 conn: &Connection,
4812 name: &str,
4813 path: &str,
4814 member: bool,
4815) -> Result<Vec<(String, usize)>, QueryError> {
4816 let mut sites = Vec::new();
4817 if !member {
4818 sites.extend(import_sites_from(conn, name, Some(path), true)?);
4819 sites.extend(rust_use_lines(conn, name, path)?);
4820 }
4821 sites.extend(java_import_lines(conn, name, path, member)?);
4822 sites.sort();
4823 sites.dedup();
4824 Ok(sites)
4825}
4826
4827fn java_import_lines(
4835 conn: &Connection,
4836 name: &str,
4837 path: &str,
4838 member: bool,
4839) -> Result<Vec<(String, usize)>, QueryError> {
4840 let normalized = format!("/{}", path.replace('\\', "/"));
4841 let Some(class_file) = normalized.strip_suffix(".java") else {
4842 return Ok(Vec::new());
4843 };
4844 let class = class_file.rsplit('/').next().unwrap_or_default();
4845 let names_file = |dotted: &str| class_file.ends_with(&format!("/{}", dotted.replace('.', "/")));
4846 let mut stmt = conn.prepare(
4847 "SELECT replace(path, '\\', '/'), start_line, COALESCE(signature, '') FROM symbols
4848 WHERE kind = 'import' AND language = 'java'
4849 AND (name = ?1 OR ?2 AND signature LIKE '%.' || ?3 || '.%')",
4850 )?;
4851 let rows = stmt.query_map(params![name, !member, class], |row| {
4852 Ok((
4853 row.get::<_, String>(0)?,
4854 row.get::<_, i64>(1)? as usize,
4855 row.get::<_, String>(2)?,
4856 ))
4857 })?;
4858 let mut lines = Vec::new();
4859 for row in rows {
4860 let (import_path, line, signature) = row?;
4861 let imported = signature
4862 .trim()
4863 .trim_start_matches("import ")
4864 .trim_start()
4865 .trim_start_matches("static ")
4866 .trim()
4867 .trim_end_matches(';')
4868 .trim();
4869 let (owner, last) = imported.rsplit_once('.').unwrap_or(("", imported));
4870 let named = if member {
4871 last == name && names_file(owner)
4872 } else {
4873 names_file(imported) || names_file(owner)
4874 };
4875 if named {
4876 lines.push((import_path, line));
4877 }
4878 }
4879 Ok(lines)
4880}
4881
4882fn rust_use_lines(
4883 conn: &Connection,
4884 name: &str,
4885 path: &str,
4886) -> Result<Vec<(String, usize)>, QueryError> {
4887 let mut stmt = conn.prepare(
4888 "SELECT DISTINCT replace(pr.path, '\\', '/'), pr.start_line
4889 FROM pending_relationships pr
4890 JOIN files f ON f.file_id = pr.file_id AND f.language = 'rust'
4891 WHERE pr.kind = 'imports' AND pr.target_terminal_name = ?1
4892 AND pr.start_line IS NOT NULL AND json_valid(pr.target_namespace_json)
4893 AND json_extract(pr.target_namespace_json, '$[#-1]') NOT IN ('crate', 'self', 'super')
4894 AND (?2 GLOB '*/' || json_extract(pr.target_namespace_json, '$[#-1]') || '.rs'
4895 OR ?2 GLOB '*/' || json_extract(pr.target_namespace_json, '$[#-1]') || '/mod.rs')",
4896 )?;
4897 let target_path = format!("/{}", path.replace('\\', "/"));
4898 let rows = stmt.query_map(params![name, target_path], |row| {
4899 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize))
4900 })?;
4901 Ok(rows.collect::<Result<_, _>>()?)
4902}
4903
4904fn import_sites_from(
4905 conn: &Connection,
4906 name: &str,
4907 path_filter: Option<&str>,
4908 absent: bool,
4909) -> Result<Vec<(String, usize)>, QueryError> {
4910 if !has_column(conn, "symbols", "metadata_json") {
4911 return Ok(Vec::new());
4912 }
4913 let targets = if absent {
4914 "(SELECT ?1 AS name, ?2 AS path, NULL AS parent_symbol_id, 'function' AS kind)"
4915 } else {
4916 "symbols"
4917 };
4918 let extensions: String = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"]
4920 .iter()
4921 .map(|ext| {
4922 format!(
4923 " WHEN module_path.value LIKE '%.{ext}'
4924 THEN substr(module_path.value, 1, length(module_path.value) - {})",
4925 ext.len() + 1
4926 )
4927 })
4928 .collect();
4929 let source = format!(
4930 "(CASE WHEN i.language IN ('javascript', 'typescript', 'tsx', 'jsx', 'vue') THEN
4931 CASE{extensions} ELSE module_path.value END
4932 ELSE module_path.value END)"
4933 );
4934 let dotted = format!("replace({source}, '/', '.')");
4935 let segment = format!("replace({dotted}, rtrim({dotted}, replace({dotted}, '.', '')), '')");
4936 let target_path = "('/' || replace(target.path, '\\', '/'))";
4937 let dir = |path: &str| {
4939 format!("rtrim(replace({path}, '\\', '/'), replace(replace({path}, '\\', '/'), '/', ''))")
4940 };
4941 let (import_dir, target_dir) = (dir("i.path"), dir("target.path"));
4942 let relative = is_relative_import_path("module_path.value");
4943 let relative_match =
4944 relative_import_matches("module_path.value", "target").replace("p.path", "i.path");
4945 let mut stmt = conn.prepare(&format!(
4946 "SELECT DISTINCT replace(i.path, '\\', '/'), i.start_line
4947 FROM {targets} target
4948 JOIN symbols i ON i.name = target.name AND i.kind = 'import'
4949 CROSS JOIN json_each(json_array(json_extract(i.metadata_json, '$.source'))) module_path
4950 WHERE target.name = ?1
4951 AND target.parent_symbol_id IS NULL
4952 AND target.kind NOT IN ({LOW_SIGNAL_KINDS_SQL})
4953 AND (?2 IS NULL OR replace(target.path, '\\', '/') = ?2
4954 OR replace(target.path, '\\', '/') LIKE ?3 ESCAPE '\\')
4955 AND json_valid(i.metadata_json)
4956 AND i.language NOT IN ({docs})
4957 AND (
4958 {segment} != ''
4959 AND ({target_path} GLOB '*/' || {segment} || '/*'
4960 OR {target_path} GLOB '*/' || {segment} || '.*')
4961 OR module_path.value != '' AND replace(module_path.value, '.', '') = ''
4962 AND substr({import_dir}, 1, length({target_dir})) = {target_dir}
4963 )
4964 AND NOT (i.language IN ({JS_FAMILY_LANGUAGES}) AND {relative} AND NOT {relative_match})
4965 ORDER BY 1, 2",
4966 docs = documentation_language_list()
4967 ))?;
4968 let norm = path_filter.map(|p| p.replace('\\', "/").trim_matches('/').to_string());
4969 let prefix = norm.as_ref().map(|p| format!("{}/%", escape_like(p)));
4970 let rows = stmt.query_map(params![name, norm, prefix], |row| {
4971 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize))
4972 })?;
4973 Ok(rows.collect::<Result<_, _>>()?)
4974}
4975
4976fn merge_same_site(sites: Vec<ReferenceSite>) -> Vec<ReferenceSite> {
4979 let mut merged: Vec<ReferenceSite> = Vec::with_capacity(sites.len());
4980 let mut index = HashMap::new();
4981 for site in sites {
4982 let key = (
4983 site.from_symbol_id.clone(),
4984 site.to_symbol_name.clone(),
4985 site.kind.clone(),
4986 site.path.clone(),
4987 site.start_line,
4988 );
4989 match index.get(&key) {
4990 Some(&at) => {
4991 let kept: &mut ReferenceSite = &mut merged[at];
4992 kept.start_column = kept.start_column.min(site.start_column);
4993 }
4994 None => {
4995 index.insert(key, merged.len());
4996 merged.push(site);
4997 }
4998 }
4999 }
5000 merged
5001}
5002
5003pub fn find_callee_signatures(
5006 conn: &Connection,
5007 symbol_name: &str,
5008 symbol_id: &str,
5009 limit: usize,
5010 include_external: bool,
5011) -> Result<Vec<String>, QueryError> {
5012 let mut stmt = conn.prepare(
5013 "SELECT DISTINCT s_to.name, s_to.signature, s_to.path, s_to.start_line, s_to.kind
5014 FROM relationships r
5015 JOIN symbols s_from ON r.from_symbol_id = s_from.symbol_id
5016 JOIN symbols s_to ON r.to_symbol_id = s_to.symbol_id
5017 WHERE s_from.name = ?1 AND r.from_symbol_id = ?2
5018 LIMIT ?3",
5019 )?;
5020
5021 let rows = stmt.query_map(params![symbol_name, symbol_id, (limit * 2) as i64], |row| {
5022 Ok((
5023 row.get::<_, String>(0)?,
5024 row.get::<_, Option<String>>(1)?,
5025 row.get::<_, String>(2)?.replace('\\', "/"),
5026 row.get::<_, Option<i64>>(3)?.unwrap_or(1) as usize,
5027 row.get::<_, String>(4)?,
5028 ))
5029 })?;
5030
5031 let mut signatures = Vec::new();
5032 let mut variants = Vec::new();
5033
5034 for r in rows.flatten() {
5035 let (name, sig_opt, path, line, kind) = r;
5036 let sig = sig_opt.unwrap_or(name);
5037 let entry = format!("{sig} ({path}:{line})");
5038 if kind == "variant" {
5039 if !variants.contains(&entry) {
5040 variants.push(entry);
5041 }
5042 } else if !signatures.contains(&entry) {
5043 signatures.push(entry);
5044 }
5045 }
5046
5047 if signatures.len() < limit {
5048 let remaining = (limit - signatures.len()) * 2;
5049 let p_rows: Vec<(String, Option<String>, String, usize, String)> =
5050 if has_pending_namespace_column(conn) {
5051 let mut p_stmt = conn.prepare(
5052 &format!("SELECT DISTINCT s_to.name, s_to.signature, s_to.path, s_to.start_line, s_to.kind
5053 FROM pending_relationships p
5054 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
5055 JOIN symbols s_to ON s_to.name = p.target_terminal_name
5056 LEFT JOIN symbols s_parent ON s_to.parent_symbol_id = s_parent.symbol_id
5057 WHERE s_from.name = ?1 AND p.from_symbol_id = ?2
5058 AND {kind}
5059 AND {pred}
5060 LIMIT ?3", kind = callee_target_kind("s_parent"), pred = pending_target_predicate(conn, "s_to", "s_parent")),
5061 )?;
5062
5063 let rows =
5064 p_stmt.query_map(params![symbol_name, symbol_id, remaining as i64], |row| {
5065 Ok((
5066 row.get::<_, String>(0)?,
5067 row.get::<_, Option<String>>(1)?,
5068 row.get::<_, String>(2)?.replace('\\', "/"),
5069 row.get::<_, Option<i64>>(3)?.unwrap_or(1) as usize,
5070 row.get::<_, String>(4)?,
5071 ))
5072 })?;
5073 rows.flatten().collect()
5074 } else {
5075 let mut p_stmt = conn.prepare(
5076 "SELECT DISTINCT s_to.name, s_to.signature, s_to.path, s_to.start_line, s_to.kind
5077 FROM pending_relationships p
5078 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
5079 JOIN symbols s_to ON s_to.name = p.target_terminal_name
5080 WHERE s_from.name = ?1 AND p.from_symbol_id = ?2
5081 AND s_to.kind NOT IN ('import', 'variable', 'parameter', 'field', 'property', 'module', 'namespace')
5082 LIMIT ?3",
5083 )?;
5084
5085 let rows =
5086 p_stmt.query_map(params![symbol_name, symbol_id, remaining as i64], |row| {
5087 Ok((
5088 row.get::<_, String>(0)?,
5089 row.get::<_, Option<String>>(1)?,
5090 row.get::<_, String>(2)?.replace('\\', "/"),
5091 row.get::<_, Option<i64>>(3)?.unwrap_or(1) as usize,
5092 row.get::<_, String>(4)?,
5093 ))
5094 })?;
5095 rows.flatten().collect()
5096 };
5097
5098 for r in p_rows {
5099 let (name, sig_opt, path, line, kind) = r;
5100 let sig = sig_opt.unwrap_or(name);
5101 let entry = format!("{sig} ({path}:{line})");
5102 if kind == "variant" {
5103 if !variants.contains(&entry) {
5104 variants.push(entry);
5105 }
5106 } else if !signatures.contains(&entry) {
5107 signatures.push(entry);
5108 }
5109 }
5110 }
5111
5112 if include_external && signatures.len() < limit {
5113 let remaining = (limit - signatures.len()) * 2;
5114 let ext_rows: Vec<(String, String, usize)> = if has_pending_namespace_column(conn) {
5115 let mut ext_stmt = conn.prepare(
5116 &format!("SELECT DISTINCT COALESCE(NULLIF(p.target_display_name, ''), p.target_terminal_name), p.path, p.start_line
5117 FROM pending_relationships p
5118 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
5119 WHERE s_from.name = ?1 AND p.from_symbol_id = ?2
5120 AND NOT EXISTS (
5121 SELECT 1 FROM symbols s_to
5122 LEFT JOIN symbols s_parent ON s_to.parent_symbol_id = s_parent.symbol_id
5123 WHERE s_to.name = p.target_terminal_name
5124 AND {kind}
5125 AND {pred}
5126 )
5127 LIMIT ?3", kind = callee_target_kind("s_parent"), pred = pending_target_predicate(conn, "s_to", "s_parent")),
5128 )?;
5129
5130 let rows =
5131 ext_stmt.query_map(params![symbol_name, symbol_id, remaining as i64], |row| {
5132 Ok((
5133 row.get::<_, String>(0)?,
5134 row.get::<_, String>(1)?.replace('\\', "/"),
5135 row.get::<_, Option<i64>>(2)?.unwrap_or(1) as usize,
5136 ))
5137 })?;
5138 rows.flatten().collect()
5139 } else {
5140 let mut ext_stmt = conn.prepare(
5141 "SELECT DISTINCT p.target_terminal_name, p.path, p.start_line
5142 FROM pending_relationships p
5143 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
5144 WHERE s_from.name = ?1 AND p.from_symbol_id = ?2
5145 AND NOT EXISTS (
5146 SELECT 1 FROM symbols s_to
5147 WHERE s_to.name = p.target_terminal_name
5148 AND s_to.kind NOT IN ('import', 'variable', 'parameter', 'field', 'property', 'module', 'namespace')
5149 )
5150 LIMIT ?3",
5151 )?;
5152
5153 let rows =
5154 ext_stmt.query_map(params![symbol_name, symbol_id, remaining as i64], |row| {
5155 Ok((
5156 row.get::<_, String>(0)?,
5157 row.get::<_, String>(1)?.replace('\\', "/"),
5158 row.get::<_, Option<i64>>(2)?.unwrap_or(1) as usize,
5159 ))
5160 })?;
5161 rows.flatten().collect()
5162 };
5163
5164 for r in ext_rows {
5165 let (name, path, line) = r;
5166 let entry = format!("{name} ({path}:{line})");
5167 if !signatures.contains(&entry) {
5168 signatures.push(entry);
5169 }
5170 }
5171 }
5172
5173 for v in variants {
5174 if signatures.len() >= limit {
5175 break;
5176 }
5177 if !signatures.contains(&v) {
5178 signatures.push(v);
5179 }
5180 }
5181
5182 signatures.truncate(limit);
5183 Ok(signatures)
5184}
5185
5186const SQL_FAMILIES: &[&str] = &["sql."];
5187
5188pub(crate) const ROUTE_FAMILIES: &[&str] = &[
5189 ".route.",
5190 ".attribute_route.",
5191 ".scope_route.",
5192 ".file_route.",
5193 ".route_handler.",
5194 ".route_reference.",
5195 ".route_group.",
5196 ".route_prefix.",
5197 ".resource_route.",
5198 ".router_mount.",
5199 ".include_router.",
5200 ".server_route.",
5201 ".route_definition.",
5202];
5203
5204const CONFIG_FAMILIES: &[&str] = &["toml.", "yaml.", "json.", "env."];
5205
5206const MODEL_FAMILIES: &[&str] = &[
5207 "sql.table_definition.v1",
5208 "sql.column_definition.v1",
5209 "sql.constraint.v1",
5210 "sql.foreign_key.v1",
5211 "sql.index_definition.v1",
5212 "json.schema.v1",
5213];
5214
5215const SIGNAL_FAMILIES: &[&str] = &[".signal_declaration."];
5216
5217const IMPORT_FAMILIES: &[&str] = &[".import_statement.", ".import."];
5218
5219const BINDING_FAMILIES: &[&str] = &[".binding."];
5220
5221const COMPONENT_FAMILIES: &[&str] = &[".object_instantiation.", ".object_type."];
5222
5223const MODULE_FAMILIES: &[&str] = &[".module."];
5224
5225const PRAGMA_FAMILIES: &[&str] = &[".pragma.", "javascript.qml_directive.v1"];
5226
5227const PROPERTY_FAMILIES: &[&str] = &[".property_declaration.", ".qt_property."];
5228
5229pub const CATEGORY_ALIASES: &[(&str, &[&str])] = &[
5234 ("sql", SQL_FAMILIES),
5235 ("query", SQL_FAMILIES),
5236 ("queries", SQL_FAMILIES),
5237 ("route", ROUTE_FAMILIES),
5238 ("routes", ROUTE_FAMILIES),
5239 ("config", CONFIG_FAMILIES),
5240 ("model", MODEL_FAMILIES),
5241 ("models", MODEL_FAMILIES),
5242 ("signal", SIGNAL_FAMILIES),
5243 ("signals", SIGNAL_FAMILIES),
5244 ("import", IMPORT_FAMILIES),
5245 ("imports", IMPORT_FAMILIES),
5246 ("binding", BINDING_FAMILIES),
5247 ("bindings", BINDING_FAMILIES),
5248 ("component", COMPONENT_FAMILIES),
5249 ("components", COMPONENT_FAMILIES),
5250 ("module", MODULE_FAMILIES),
5251 ("modules", MODULE_FAMILIES),
5252 ("pragma", PRAGMA_FAMILIES),
5253 ("property", PROPERTY_FAMILIES),
5254 ("properties", PROPERTY_FAMILIES),
5255];
5256
5257fn category_families(category: &str) -> Option<&'static [&'static str]> {
5258 let wanted = category.trim().to_ascii_lowercase();
5259 CATEGORY_ALIASES
5260 .iter()
5261 .find(|(alias, _)| *alias == wanted)
5262 .map(|(_, families)| *families)
5263}
5264
5265pub(crate) fn matches_family(pattern_id: &str, rule: &str) -> bool {
5266 if rule.starts_with('.') {
5267 pattern_id.contains(rule)
5268 } else if rule.ends_with('.') {
5269 pattern_id.starts_with(rule)
5270 } else {
5271 pattern_id == rule
5272 }
5273}
5274
5275fn family_clause(column: &str, families: &[&str]) -> String {
5276 let alternatives: Vec<String> = families
5277 .iter()
5278 .map(|rule| {
5279 let escaped = escape_like(rule);
5280 if rule.starts_with('.') {
5281 format!("{column} LIKE '%{escaped}%' ESCAPE '\\'")
5282 } else if rule.ends_with('.') {
5283 format!("{column} LIKE '{escaped}%' ESCAPE '\\'")
5284 } else {
5285 format!("{column} = '{rule}'")
5286 }
5287 })
5288 .collect();
5289 format!("({})", alternatives.join(" OR "))
5290}
5291
5292pub fn is_category_alias(category: &str) -> bool {
5294 category_families(category).is_some()
5295}
5296
5297pub fn alias_fact_counts(categories: &[(String, usize)]) -> Vec<(&'static str, usize, usize)> {
5301 let mut reported: Vec<&[&str]> = Vec::new();
5302 let mut counts = Vec::new();
5303 for (alias, families) in CATEGORY_ALIASES {
5304 if reported.contains(families) {
5305 continue;
5306 }
5307 reported.push(families);
5308 let mut patterns = 0;
5309 let mut facts = 0;
5310 for (pattern_id, count) in categories {
5311 if families.iter().any(|rule| matches_family(pattern_id, rule)) {
5312 patterns += 1;
5313 facts += count;
5314 }
5315 }
5316 if facts > 0 {
5317 counts.push((*alias, patterns, facts));
5318 }
5319 }
5320 counts
5321}
5322
5323pub fn find_structural_facts_scoped(
5325 conn: &Connection,
5326 category: &str,
5327 path_filter: Option<&str>,
5328 limit: usize,
5329) -> Result<Vec<StructuralFact>, QueryError> {
5330 validate_result_limit(limit)?;
5331 let norm_path = path_filter
5332 .map(|p| {
5333 p.replace('\\', "/")
5334 .trim_start_matches("./")
5335 .trim_matches('/')
5336 .to_string()
5337 })
5338 .filter(|p| !p.is_empty());
5339 let dir_prefix = norm_path
5340 .as_deref()
5341 .map(|p| format!("{}/%", escape_like(p)));
5342 let cat_pattern = format!("%{}%", escape_like(category));
5343
5344 let cat_clause = match category_families(category) {
5345 Some(families) => family_clause("sf.pattern_id", families),
5346 None => {
5347 "(sf.pattern_id LIKE :cat ESCAPE '\\' OR sf.capture_name LIKE :cat ESCAPE '\\' OR sf.node_kind LIKE :cat ESCAPE '\\')"
5348 .to_string()
5349 }
5350 };
5351
5352 let sql = format!(
5353 "SELECT sf.structural_fact_id, sf.path, sf.language, sf.pattern_id,
5354 sf.capture_name, sf.node_kind, s.name AS containing_symbol_name,
5355 sf.start_line, sf.end_line, sf.confidence,
5356 COALESCE(
5357 CASE WHEN json_extract(sf.metadata_json, '$.key_path') LIKE '$.%'
5358 THEN substr(json_extract(sf.metadata_json, '$.key_path'), 3)
5359 ELSE json_extract(sf.metadata_json, '$.key_path') END,
5360 json_extract(sf.metadata_json, '$.key'),
5361 CASE WHEN json_extract(sf.metadata_json, '$.normalized_route_template') IS NOT NULL
5362 THEN COALESCE(json_extract(sf.metadata_json, '$.verb') || ' ', '')
5363 || json_extract(sf.metadata_json, '$.normalized_route_template') END,
5364 CASE WHEN sf.pattern_id = 'cpp.qt_property.v1'
5365 THEN json_extract(sf.metadata_json, '$.name') END
5366 ) AS display_key,
5367 sf.metadata_json
5368 FROM structural_facts sf
5369 LEFT JOIN symbols s ON sf.containing_symbol_id = s.symbol_id
5370 WHERE (:cat IS NOT NULL AND {cat_clause})
5371 AND (:path IS NULL OR replace(sf.path, '\\', '/') = :path COLLATE NOCASE OR replace(sf.path, '\\', '/') LIKE :dir_prefix ESCAPE '\\')
5372 ORDER BY sf.path ASC, sf.start_line ASC,
5373 COALESCE(
5374 json_extract(sf.metadata_json, '$.effective_route_template'),
5375 json_extract(sf.metadata_json, '$.route_template'),
5376 json_extract(sf.metadata_json, '$.normalized_route_template'),
5377 ''
5378 ) ASC,
5379 COALESCE(json_extract(sf.metadata_json, '$.verb'), '') ASC,
5380 sf.structural_fact_id ASC
5381 LIMIT :limit"
5382 );
5383
5384 let mut stmt = conn.prepare(&sql)?;
5385 let rows = stmt.query_map(
5386 rusqlite::named_params! {
5387 ":cat": cat_pattern,
5388 ":path": norm_path.as_deref(),
5389 ":dir_prefix": dir_prefix.as_deref(),
5390 ":limit": limit as i64,
5391 },
5392 |row| {
5393 Ok(StructuralFact {
5394 structural_fact_id: row.get(0)?,
5395 path: row.get::<_, String>(1)?.replace('\\', "/"),
5396 language: row.get(2)?,
5397 pattern_id: row.get(3)?,
5398 capture_name: row.get(4)?,
5399 node_kind: row.get(5)?,
5400 key: row.get(10)?,
5401 metadata: row
5402 .get::<_, Option<String>>(11)?
5403 .map(|json| serde_json::from_str(&json))
5404 .transpose()
5405 .map_err(|err| {
5406 rusqlite::Error::FromSqlConversionFailure(
5407 11,
5408 rusqlite::types::Type::Text,
5409 Box::new(err),
5410 )
5411 })?,
5412 containing_symbol_name: row.get(6)?,
5413 start_line: row.get::<_, i64>(7)? as usize,
5414 end_line: row.get::<_, i64>(8)? as usize,
5415 confidence: row.get(9)?,
5416 })
5417 },
5418 )?;
5419
5420 let mut results = Vec::new();
5421 for r in rows {
5422 let mut fact = r?;
5423 let display = fact.metadata.as_ref().and_then(|metadata| {
5424 route_display(metadata).or_else(|| mount_display(&fact.capture_name, metadata))
5425 });
5426 if let Some(display) = display {
5427 fact.key = Some(display);
5428 }
5429 results.push(fact);
5430 }
5431 Ok(results)
5432}
5433
5434fn route_display(metadata: &serde_json::Value) -> Option<String> {
5438 let text = |key: &str| metadata.get(key).and_then(|value| value.as_str());
5439 let normalized = text("normalized_route_template")?;
5440 let template = match (text("effective_route_template"), text("route_template")) {
5441 (Some(effective), _) => effective,
5442 (None, Some(raw)) if route_shape(raw) == route_shape(normalized) => raw,
5443 _ => normalized,
5444 };
5445 Some(match text("verb") {
5446 Some(verb) => format!("{verb} {template}"),
5447 None => template.to_string(),
5448 })
5449}
5450
5451fn mount_display(capture: &str, metadata: &serde_json::Value) -> Option<String> {
5454 let text = |key: &str| metadata.get(key).and_then(|value| value.as_str());
5455 if text("query_family") != Some("framework") {
5456 return None;
5457 }
5458 let target = text("mount_target")?;
5459 Some(
5460 match text("normalized_mount_path").or_else(|| text("mount_path")) {
5461 Some(path) => format!("{capture} `{target}` at {path}"),
5462 None => format!("{capture} `{target}`"),
5463 },
5464 )
5465}
5466
5467fn route_shape(template: &str) -> String {
5469 let mut shape = String::new();
5470 let mut chars = template.chars().peekable();
5471 while let Some(c) = chars.next() {
5472 match c {
5473 '<' | '{' => {
5474 let close = if c == '<' { '>' } else { '}' };
5475 for inner in chars.by_ref() {
5476 if inner == close {
5477 break;
5478 }
5479 }
5480 shape.push('*');
5481 }
5482 ':' => {
5483 while chars.peek().is_some_and(|next| *next != '/') {
5484 chars.next();
5485 }
5486 shape.push('*');
5487 }
5488 _ => shape.push(c),
5489 }
5490 }
5491 shape
5492}
5493
5494pub fn find_structural_facts(
5496 conn: &Connection,
5497 category: &str,
5498 limit: usize,
5499) -> Result<Vec<StructuralFact>, QueryError> {
5500 find_structural_facts_scoped(conn, category, None, limit)
5501}
5502
5503pub fn find_literals_scoped(
5505 conn: &Connection,
5506 category: &str,
5507 path_filter: Option<&str>,
5508 limit: usize,
5509) -> Result<Vec<LiteralFact>, QueryError> {
5510 validate_result_limit(limit)?;
5511 let norm_path = path_filter
5512 .map(|p| {
5513 p.replace('\\', "/")
5514 .trim_start_matches("./")
5515 .trim_matches('/')
5516 .to_string()
5517 })
5518 .filter(|p| !p.is_empty());
5519 let dir_prefix = norm_path
5520 .as_deref()
5521 .map(|p| format!("{}/%", escape_like(p)));
5522 let cat_pattern = format!("%{}%", escape_like(category));
5523
5524 let cat_lower = category.trim().to_ascii_lowercase();
5525 let cat_clause = match cat_lower.as_str() {
5526 "config" => {
5527 "(l.kind LIKE '%config%' OR l.kind LIKE '%toml%' OR l.kind LIKE '%json%' OR l.kind LIKE '%yaml%')"
5528 }
5529 "route" | "routes" => "l.kind LIKE '%route%'",
5530 "query" | "queries" | "sql" => "(l.kind LIKE '%sql%' OR l.kind LIKE '%query%')",
5531 "model" | "models" => "l.kind LIKE '%model%'",
5532 _ => "(l.kind LIKE :cat ESCAPE '\\' OR l.literal_text LIKE :cat ESCAPE '\\')",
5533 };
5534
5535 let sql = format!(
5536 "SELECT l.literal_id, l.path, l.literal_text, l.kind, l.carrier,
5537 l.start_line, s.name AS containing_symbol_name
5538 FROM literals l
5539 LEFT JOIN symbols s ON l.containing_symbol_id = s.symbol_id
5540 WHERE (:cat IS NOT NULL AND {cat_clause})
5541 AND (:path IS NULL OR replace(l.path, '\\', '/') = :path COLLATE NOCASE OR replace(l.path, '\\', '/') LIKE :dir_prefix ESCAPE '\\')
5542 ORDER BY l.path ASC, l.start_line ASC
5543 LIMIT :limit"
5544 );
5545
5546 let mut stmt = conn.prepare(&sql)?;
5547 let rows = stmt.query_map(
5548 rusqlite::named_params! {
5549 ":cat": cat_pattern,
5550 ":path": norm_path.as_deref(),
5551 ":dir_prefix": dir_prefix.as_deref(),
5552 ":limit": limit as i64,
5553 },
5554 |row| {
5555 Ok(LiteralFact {
5556 literal_id: row.get(0)?,
5557 path: row.get::<_, String>(1)?.replace('\\', "/"),
5558 literal_text: row.get(2)?,
5559 kind: row.get(3)?,
5560 carrier: row.get(4)?,
5561 start_line: row.get::<_, i64>(5)? as usize,
5562 containing_symbol_name: row.get(6)?,
5563 })
5564 },
5565 )?;
5566
5567 let mut results = Vec::new();
5568 for r in rows {
5569 results.push(r?);
5570 }
5571 Ok(results)
5572}
5573
5574pub fn find_literals(
5576 conn: &Connection,
5577 category: &str,
5578 limit: usize,
5579) -> Result<Vec<LiteralFact>, QueryError> {
5580 find_literals_scoped(conn, category, None, limit)
5581}
5582
5583pub fn list_structural_fact_categories_scoped(
5585 conn: &Connection,
5586 path_filter: Option<&str>,
5587) -> Result<Vec<(String, usize)>, QueryError> {
5588 let norm_path = path_filter
5589 .map(|p| {
5590 p.replace('\\', "/")
5591 .trim_start_matches("./")
5592 .trim_matches('/')
5593 .to_string()
5594 })
5595 .filter(|p| !p.is_empty());
5596 let dir_prefix = norm_path
5597 .as_deref()
5598 .map(|p| format!("{}/%", escape_like(p)));
5599
5600 let mut categories = Vec::new();
5601
5602 let sql = "SELECT pattern_id, COUNT(*) AS cnt FROM structural_facts
5603 WHERE (:path IS NULL OR replace(path, '\\', '/') = :path COLLATE NOCASE OR replace(path, '\\', '/') LIKE :dir_prefix ESCAPE '\\')
5604 GROUP BY pattern_id ORDER BY cnt DESC";
5605 let mut stmt = conn.prepare(sql)?;
5606 let rows = stmt.query_map(
5607 rusqlite::named_params! {
5608 ":path": norm_path.as_deref(),
5609 ":dir_prefix": dir_prefix.as_deref(),
5610 },
5611 |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize)),
5612 )?;
5613 for r in rows {
5614 categories.push(r?);
5615 }
5616
5617 let lit_sql = "SELECT kind, COUNT(*) AS cnt FROM literals
5618 WHERE (:path IS NULL OR replace(path, '\\', '/') = :path COLLATE NOCASE OR replace(path, '\\', '/') LIKE :dir_prefix ESCAPE '\\')
5619 GROUP BY kind ORDER BY cnt DESC";
5620 let mut lit_stmt = conn.prepare(lit_sql)?;
5621 let lit_rows = lit_stmt.query_map(
5622 rusqlite::named_params! {
5623 ":path": norm_path.as_deref(),
5624 ":dir_prefix": dir_prefix.as_deref(),
5625 },
5626 |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize)),
5627 )?;
5628 for r in lit_rows {
5629 categories.push(r?);
5630 }
5631
5632 Ok(categories)
5633}
5634
5635pub fn list_structural_fact_categories(
5637 conn: &Connection,
5638) -> Result<Vec<(String, usize)>, QueryError> {
5639 list_structural_fact_categories_scoped(conn, None)
5640}
5641
5642pub fn find_type_facts(conn: &Connection, symbol_id: &str) -> Result<Vec<TypeFact>, QueryError> {
5644 let has_table: bool = conn
5645 .query_row(
5646 "SELECT 1 FROM sqlite_master WHERE type='table' AND name='type_facts'",
5647 [],
5648 |_| Ok(true),
5649 )
5650 .unwrap_or(false);
5651 if !has_table {
5652 return Ok(Vec::new());
5653 }
5654
5655 let mut stmt = conn.prepare(
5656 "SELECT type_fact_id, symbol_id, language, resolved_type, generic_params_json
5657 FROM type_facts
5658 WHERE symbol_id = ?1",
5659 )?;
5660
5661 let rows = stmt.query_map(params![symbol_id], |row| {
5662 Ok(TypeFact {
5663 type_fact_id: row.get(0)?,
5664 symbol_id: row.get(1)?,
5665 language: row.get(2)?,
5666 resolved_type: row.get(3)?,
5667 generic_params: row.get(4)?,
5668 })
5669 })?;
5670
5671 let mut results = Vec::new();
5672 for r in rows {
5673 results.push(r?);
5674 }
5675 Ok(results)
5676}
5677
5678pub fn find_related_types(
5682 conn: &Connection,
5683 target: &Symbol,
5684 limit: usize,
5685) -> Result<Vec<String>, QueryError> {
5686 if !has_table(conn, "type_facts") {
5687 return Ok(Vec::new());
5688 }
5689 let mut stmt = conn.prepare(
5690 "SELECT d.name, d.kind, d.signature, d.path, d.start_line
5691 FROM symbols d
5692 WHERE d.kind IN ('struct', 'enum', 'trait', 'class', 'interface', 'type', 'union')
5693 AND d.name IN (
5694 SELECT t.resolved_type FROM type_facts t
5695 JOIN symbols s ON s.symbol_id = t.symbol_id
5696 WHERE s.symbol_id = ?1 OR s.parent_symbol_id = ?1
5697 )
5698 ORDER BY d.path = ?2 DESC, length(d.path), d.path, d.start_line",
5699 )?;
5700 let rows = stmt.query_map(params![target.symbol_id, target.path], |row| {
5701 Ok((
5702 row.get::<_, String>(0)?,
5703 row.get::<_, String>(1)?,
5704 row.get::<_, Option<String>>(2)?,
5705 row.get::<_, String>(3)?,
5706 row.get::<_, i64>(4)?,
5707 ))
5708 })?;
5709
5710 let mut seen = std::collections::HashSet::new();
5711 let mut types = Vec::new();
5712 for row in rows {
5713 let (name, kind, signature, path, line) = row?;
5714 if types.len() >= limit {
5715 break;
5716 }
5717 if !seen.insert(name.clone()) {
5718 continue;
5719 }
5720 let signature = signature.unwrap_or_else(|| format!("{kind} {name}"));
5721 types.push(format!(
5722 "{} ({path}:{line})",
5723 type_declaration_line(&signature)
5724 ));
5725 }
5726 Ok(types)
5727}
5728
5729fn type_declaration_line(signature: &str) -> String {
5731 let mut declaration = signature;
5732 while declaration.starts_with("#[")
5733 && let Some((_, rest)) = declaration.split_once("] ")
5734 {
5735 declaration = rest;
5736 }
5737 let line = declaration.split_whitespace().collect::<Vec<_>>().join(" ");
5738 if line.chars().count() <= 120 {
5739 return line;
5740 }
5741 line.chars().take(119).collect::<String>() + "…"
5742}
5743
5744fn names_a_test_sql(alias: &str) -> String {
5755 ["test%", "%test", "%tests", "%spec", "%specs"]
5756 .map(|pattern| format!("{alias}.name LIKE '{pattern}'"))
5757 .join(" OR ")
5758}
5759
5760fn enclosing_test(conn: &Connection, row: Symbol) -> Result<Symbol, QueryError> {
5763 if row.is_test || row.test_container || names_a_test(&row.name) {
5764 return Ok(row);
5765 }
5766 let parent = row
5767 .parent_symbol_id
5768 .as_deref()
5769 .map(|id| get_symbol_by_id(conn, id))
5770 .transpose()?
5771 .flatten();
5772 Ok(match parent {
5773 Some(parent) if matches!(parent.kind.as_str(), "function" | "method") => parent,
5774 _ => row,
5775 })
5776}
5777
5778pub(crate) fn names_a_test(name: &str) -> bool {
5779 let lowered = name.to_ascii_lowercase();
5780 lowered.starts_with("test")
5781 || ["test", "tests", "spec", "specs"]
5782 .iter()
5783 .any(|suffix| lowered.ends_with(suffix))
5784}
5785
5786pub fn is_test_path(path: &str) -> bool {
5787 let p = path.replace('\\', "/");
5788 let cut = p.rfind('/').map_or(0, |i| i + 1);
5789 let directories = format!("/{}/", p[..cut].to_lowercase());
5790 let file_name = &p[cut..];
5791 let lower_name = file_name.to_lowercase();
5792 directories.contains("/test/")
5793 || directories.contains("/tests/")
5794 || directories.contains("/autotests/")
5795 || directories.contains("/__tests__/")
5796 || lower_name.starts_with("tst_")
5797 || (lower_name.starts_with("test_")
5798 && (lower_name.ends_with(".py") || lower_name.ends_with(".rb")))
5799 || lower_name.contains("_test.")
5800 || lower_name.contains(".test.")
5801 || lower_name.contains(".spec.")
5802 || lower_name == "test.rs"
5803 || lower_name == "tests.rs"
5804 || file_name.ends_with("Tests.cs")
5805}
5806
5807pub(crate) fn test_path_predicate(alias: &str) -> String {
5816 let guard = format!(
5817 "(lower({alias}.path) LIKE '%test%' OR lower({alias}.path) LIKE '%spec%' OR lower({alias}.path) LIKE '%tst\\_%' ESCAPE '\\')"
5818 );
5819 let p = format!("replace({alias}.path, '\\', '/')");
5820 let directories = format!("'/' || lower(rtrim({p}, replace({p}, '/', ''))) || '/'");
5821 let file_name = format!("replace({p}, rtrim({p}, replace({p}, '/', '')), '')");
5822 let lower_name = format!("lower({file_name})");
5823 let like = |subject: &String, pattern: &str| format!("{subject} LIKE '{pattern}' ESCAPE '\\'");
5824 let clauses = [
5825 like(&directories, "%/test/%"),
5826 like(&directories, "%/tests/%"),
5827 like(&directories, "%/autotests/%"),
5828 like(&directories, "%/\\_\\_tests\\_\\_/%"),
5829 like(&lower_name, "tst\\_%"),
5830 like(&lower_name, "test\\_%.py"),
5831 like(&lower_name, "test\\_%.rb"),
5832 like(&lower_name, "%\\_test.%"),
5833 like(&lower_name, "%.test.%"),
5834 like(&lower_name, "%.spec.%"),
5835 format!("{lower_name} = 'test.rs'"),
5836 format!("{lower_name} = 'tests.rs'"),
5837 format!("{file_name} GLOB '*Tests.cs'"),
5838 ]
5839 .join(" OR ");
5840 format!("({guard} AND ({clauses}))")
5841}
5842
5843pub fn compute_blast_radius_scoped(
5846 conn: &Connection,
5847 seed_symbols: &[&str],
5848 symbol_path_filter: Option<&str>,
5849 seed_paths: &[&str],
5850 max_depth: usize,
5851 limit: usize,
5852) -> Result<BlastRadiusResult, QueryError> {
5853 compute_blast_radius_scoped_with_ids(
5854 conn,
5855 seed_symbols,
5856 &[],
5857 symbol_path_filter,
5858 seed_paths,
5859 max_depth,
5860 limit,
5861 )
5862}
5863
5864const MAX_BLAST_DEPTH: usize = 5;
5865
5866pub fn compute_blast_radius_scoped_with_ids(
5868 conn: &Connection,
5869 seed_symbols: &[&str],
5870 seed_ids: &[&str],
5871 symbol_path_filter: Option<&str>,
5872 seed_paths: &[&str],
5873 max_depth: usize,
5874 limit: usize,
5875) -> Result<BlastRadiusResult, QueryError> {
5876 validate_result_limit(limit)?;
5877 let max_depth = max_depth.min(MAX_BLAST_DEPTH);
5878 let mut resolved_seed_symbols = seed_symbols
5879 .iter()
5880 .map(|name| {
5881 get_symbol_by_name(conn, name, symbol_path_filter)?.ok_or_else(|| {
5882 let (workspace, hint) = symbol_not_found_parts(conn, name, symbol_path_filter);
5883 QueryError::SymbolNotFound {
5884 name: (*name).to_string(),
5885 workspace,
5886 hint,
5887 }
5888 })
5889 })
5890 .collect::<Result<Vec<_>, _>>()?;
5891 for id in seed_ids {
5892 let symbol = get_symbol_by_id(conn, id)?.ok_or_else(|| {
5893 let workspace = workspace_name(conn);
5894 QueryError::SymbolNotFound {
5895 name: format!("symbol id {id}"),
5896 workspace,
5897 hint: "Run lookup_symbol again and select a current id.".to_string(),
5898 }
5899 })?;
5900 resolved_seed_symbols.push(symbol);
5901 }
5902 let mut seeds = Vec::new();
5903 let seed_type = if (!seed_symbols.is_empty() || !seed_ids.is_empty()) && !seed_paths.is_empty()
5904 {
5905 for s in seed_symbols {
5906 seeds.push(s.to_string());
5907 }
5908 for id in seed_ids {
5909 seeds.push(id.to_string());
5910 }
5911 for p in seed_paths {
5912 seeds.push(p.to_string());
5913 }
5914 "mixed".to_string()
5915 } else if !seed_symbols.is_empty() || !seed_ids.is_empty() {
5916 for s in seed_symbols {
5917 seeds.push(s.to_string());
5918 }
5919 for id in seed_ids {
5920 seeds.push(id.to_string());
5921 }
5922 "symbol".to_string()
5923 } else if !seed_paths.is_empty() {
5924 for p in seed_paths {
5925 seeds.push(p.to_string());
5926 }
5927 "file".to_string()
5928 } else {
5929 return Ok(BlastRadiusResult {
5930 seed_type: "none".to_string(),
5931 seeds: Vec::new(),
5932 likely_tests: Vec::new(),
5933 impacted_symbols: Vec::new(),
5934 likely_tests_truncated: false,
5935 impacted_symbols_truncated: false,
5936 traversal_ceiling_reached: false,
5937 limit_at_maximum: false,
5938 likely_tests_found: 0,
5939 impacted_symbols_found: 0,
5940 stopped_at_depth: None,
5941 });
5942 };
5943
5944 let mut where_clauses = Vec::new();
5945 let mut params_vec: Vec<rusqlite::types::Value> = Vec::new();
5946
5947 if !resolved_seed_symbols.is_empty() {
5948 let placeholders: Vec<String> = (1..=resolved_seed_symbols.len())
5949 .map(|i| format!("?{}", i))
5950 .collect();
5951 where_clauses.push(format!("symbol_id IN ({})", placeholders.join(", ")));
5952 for symbol in &resolved_seed_symbols {
5953 params_vec.push(rusqlite::types::Value::Text(symbol.symbol_id.clone()));
5954 }
5955 }
5956
5957 if !seed_paths.is_empty() {
5958 let mut path_conds = Vec::new();
5959 for p in seed_paths.iter() {
5960 let raw = p
5961 .replace('\\', "/")
5962 .trim_start_matches("./")
5963 .trim_matches('/')
5964 .to_string();
5965 let exact_idx = params_vec.len() + 1;
5966 params_vec.push(rusqlite::types::Value::Text(raw.clone()));
5967 let dir_pattern = format!("{}/%", escape_like(&raw));
5968 let like_idx = params_vec.len() + 1;
5969 params_vec.push(rusqlite::types::Value::Text(dir_pattern));
5970 path_conds.push(format!(
5971 "replace(path, '\\', '/') = ?{exact_idx} COLLATE NOCASE OR replace(path, '\\', '/') LIKE ?{like_idx} ESCAPE '\\'"
5972 ));
5973 }
5974 where_clauses.push(format!("({})", path_conds.join(" OR ")));
5975 }
5976
5977 let seed_condition = where_clauses.join(" OR ");
5978 let max_depth_idx = params_vec.len() + 1;
5979 let walk_depth = if max_depth < MAX_BLAST_DEPTH {
5981 max_depth + 1
5982 } else {
5983 max_depth
5984 };
5985 params_vec.push(rusqlite::types::Value::Integer(walk_depth as i64));
5986
5987 let mut traversal_ceiling_reached = false;
5988 let mut callers_beyond_depth = false;
5989
5990 let has_relationships: bool = conn
5991 .query_row(
5992 "SELECT 1 FROM sqlite_master WHERE type='table' AND name='relationships'",
5993 [],
5994 |_| Ok(true),
5995 )
5996 .unwrap_or(false);
5997
5998 let has_pending: bool = conn
5999 .query_row(
6000 "SELECT 1 FROM sqlite_master WHERE type='table' AND name='pending_relationships'",
6001 [],
6002 |_| Ok(true),
6003 )
6004 .unwrap_or(false);
6005
6006 let mut likely_tests = Vec::new();
6007 let mut impacted_symbols = Vec::new();
6008 let mut seen_test_keys = HashSet::new();
6009
6010 let mut recursive_branches = Vec::new();
6011
6012 if has_relationships {
6013 recursive_branches.push(format!(
6014 "SELECT r.from_symbol_id, iw.depth + 1, iw.via
6015 FROM relationships r
6016 JOIN impact_walk iw ON r.to_symbol_id = iw.symbol_id
6017 JOIN symbols s_from ON r.from_symbol_id = s_from.symbol_id
6018 WHERE iw.depth < ?{max_depth_idx}
6019 AND s_from.kind NOT IN ({LOW_SIGNAL_KINDS_SQL})"
6020 ));
6021 }
6022
6023 if has_pending {
6024 let (parent_join, ns_condition) = if conn
6025 .query_row(
6026 "SELECT 1 FROM pragma_table_info('pending_relationships') WHERE name='target_namespace_json'",
6027 [],
6028 |_| Ok(true),
6029 )
6030 .unwrap_or(false)
6031 {
6032 (
6033 "LEFT JOIN symbols s_target_parent ON s_target.parent_symbol_id = s_target_parent.symbol_id
6034 LEFT JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id",
6035 format!("AND {pred}", pred = pending_target_predicate(conn, "s_target", "s_target_parent")),
6036 )
6037 } else {
6038 ("", String::new())
6039 };
6040
6041 recursive_branches.push(format!(
6042 "SELECT p.from_symbol_id, iw.depth + 1, iw.via
6043 FROM pending_relationships p
6044 JOIN symbols s_target ON p.target_terminal_name = s_target.name
6045 JOIN impact_walk iw ON s_target.symbol_id = iw.symbol_id
6046 {parent_join}
6047 WHERE iw.depth < ?{max_depth_idx}
6048 AND s_target.kind NOT IN ({LOW_SIGNAL_KINDS_SQL})
6049 {ns_condition}"
6050 ));
6051 }
6052
6053 if has_relationships {
6056 recursive_branches.push(format!(
6057 "SELECT r.from_symbol_id, iw.depth + 1, 1
6058 FROM impact_walk iw
6059 CROSS JOIN symbols ctor ON ctor.symbol_id = iw.symbol_id
6060 JOIN relationships r ON r.to_symbol_id = ctor.parent_symbol_id
6061 WHERE iw.depth < ?{max_depth_idx}
6062 AND ctor.kind = 'constructor'
6063 AND +r.kind = 'calls'"
6064 ));
6065 }
6066 if has_pending && has_pending_namespace_column(conn) {
6067 recursive_branches.push(format!(
6068 "SELECT p.from_symbol_id, iw.depth + 1, 1
6069 FROM impact_walk iw
6070 CROSS JOIN symbols ctor ON ctor.symbol_id = iw.symbol_id
6071 CROSS JOIN symbols s_target ON s_target.symbol_id = ctor.parent_symbol_id
6072 JOIN pending_relationships p ON p.target_terminal_name = s_target.name
6073 LEFT JOIN symbols s_target_parent ON s_target.parent_symbol_id = s_target_parent.symbol_id
6074 LEFT JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
6075 WHERE iw.depth < ?{max_depth_idx}
6076 AND ctor.kind = 'constructor'
6077 AND s_target.kind IN ('class', 'struct', 'record')
6078 AND +p.kind = 'calls'
6079 AND {pred}",
6080 pred = pending_target_predicate(conn, "s_target", "s_target_parent")
6081 ));
6082 }
6083
6084 let lifecycle = if has_column(conn, "symbols", "test_lifecycle") {
6085 "COALESCE(s.test_lifecycle, 0) != 0"
6086 } else {
6087 "0"
6088 };
6089 let mut fixtures = Vec::new();
6090 let mut setups = Vec::new();
6091 let mut constructor_only_test_keys = HashSet::new();
6092 let mut non_constructor_test_keys = HashSet::new();
6093 if !recursive_branches.is_empty() {
6094 let recursive_sql = recursive_branches.join("\n UNION \n");
6095 let not_documentation = not_documentation(conn, "s");
6096 let sql = format!(
6097 "WITH RECURSIVE impact_walk(symbol_id, depth, via) AS (
6098 SELECT symbol_id, 0, 0
6099 FROM symbols
6100 WHERE ({seed_condition})
6101 AND kind NOT IN ({LOW_SIGNAL_KINDS_SQL})
6102
6103 UNION
6104
6105 {recursive_sql}
6106 )
6107 SELECT s.name, s.kind, s.path, s.start_line, s.is_test, s.test_container,
6108 MIN(iw.depth) as min_depth, MIN(iw.via) as min_via,
6109 {lifecycle} AS is_fixture, s.parent_symbol_id,
6110 EXISTS (SELECT 1 FROM symbols owner
6111 WHERE owner.symbol_id = s.parent_symbol_id
6112 AND COALESCE(owner.test_container, 0) != 0) AS in_test_class,
6113 s.language, s.signature
6114 FROM impact_walk iw
6115 CROSS JOIN symbols s ON iw.symbol_id = s.symbol_id
6116 WHERE s.kind NOT IN ({LOW_SIGNAL_KINDS_SQL})
6117 AND {not_documentation}
6118 GROUP BY s.symbol_id, s.name, s.kind, s.path, s.start_line, s.is_test, s.test_container
6119 HAVING MIN(iw.depth) > 0
6120 ORDER BY MIN(iw.depth) > {max_depth} ASC, MIN(iw.via) ASC, min_depth ASC, s.path ASC, s.name ASC
6121 LIMIT 201"
6122 );
6123
6124 let mut stmt = conn.prepare(&sql)?;
6125 let param_refs: Vec<&dyn rusqlite::ToSql> = params_vec
6126 .iter()
6127 .map(|v| v as &dyn rusqlite::ToSql)
6128 .collect();
6129
6130 let rows = stmt.query_map(param_refs.as_slice(), |row| {
6131 Ok((
6132 row.get::<_, String>(0)?,
6133 row.get::<_, String>(1)?,
6134 row.get::<_, String>(2)?,
6135 row.get::<_, i64>(3)? as usize,
6136 row.get::<_, bool>(4)?,
6137 row.get::<_, bool>(5)?,
6138 row.get::<_, i64>(6)? as usize,
6139 row.get::<_, i64>(7)? as usize,
6140 row.get::<_, bool>(8)?,
6141 row.get::<_, Option<String>>(9)?,
6142 row.get::<_, bool>(10)?,
6143 row.get::<_, String>(11)?,
6144 row.get::<_, Option<String>>(12)?,
6145 ))
6146 })?;
6147
6148 for (index, r) in rows.enumerate() {
6149 let (
6150 name,
6151 kind,
6152 raw_path,
6153 line,
6154 is_test,
6155 test_container,
6156 depth,
6157 min_via,
6158 is_fixture,
6159 parent,
6160 in_test_class,
6161 language,
6162 signature,
6163 ) = r?;
6164 if depth > max_depth {
6165 callers_beyond_depth = true;
6166 break;
6167 }
6168 if index == 200 {
6169 traversal_ceiling_reached = true;
6170 break;
6171 }
6172 let path = raw_path.replace('\\', "/");
6173 let is_test_target = is_test
6174 || test_container
6175 || is_fixture
6176 || (is_test_path(&path) && (in_test_class || names_a_test(&name)));
6177 let constructor_only = min_via > 0;
6178 if is_test_target {
6179 let key = format!("{}:{}", path, line);
6180 let test_key = (path.clone(), line);
6181 if constructor_only {
6182 constructor_only_test_keys.insert(test_key);
6183 } else {
6184 non_constructor_test_keys.insert(test_key);
6185 }
6186 if seen_test_keys.insert(key) {
6187 let lowered = name.to_ascii_lowercase();
6188 let pytest_fixture = path.ends_with(".py")
6189 && !["setup", "teardown", "asyncsetup", "asyncteardown"]
6190 .iter()
6191 .any(|prefix| lowered.starts_with(prefix));
6192 let caller = if depth == 1 {
6193 "direct caller".to_string()
6194 } else {
6195 format!("indirect caller [depth {depth}]")
6196 };
6197 let reason = if is_fixture && pytest_fixture {
6198 fixtures.push((name.clone(), path.clone(), line, constructor_only));
6199 format!("fixture ({caller})")
6200 } else if is_fixture {
6201 if let Some(class_id) = parent {
6202 setups.push((
6203 name.clone(),
6204 class_id,
6205 path.clone(),
6206 line,
6207 constructor_only,
6208 ));
6209 }
6210 format!("setup ({caller})")
6211 } else {
6212 caller
6213 };
6214 likely_tests.push(TestTarget {
6215 name,
6216 path,
6217 line,
6218 reason,
6219 });
6220 }
6221 } else {
6222 let enclosing_symbol_name =
6223 if is_generated_lambda_name(&name, &language, &kind, signature.as_deref()) {
6224 match parent.as_deref() {
6225 Some(parent_id) => {
6226 get_symbol_by_id(conn, parent_id)?.map(|parent| parent.name)
6227 }
6228 None => None,
6229 }
6230 } else {
6231 None
6232 };
6233 impacted_symbols.push(ImpactedSymbol {
6234 name,
6235 kind,
6236 path,
6237 line,
6238 depth,
6239 enclosing_symbol_name,
6240 });
6241 }
6242 }
6243 }
6244
6245 let mut replaced = HashSet::new();
6247 for (fixture, fixture_path, line, constructor_only) in &fixtures {
6248 let shown = fixture_name(conn, fixture, fixture_path);
6249 let users = fixture_users(conn, fixture, fixture_path)?;
6250 if !users.is_empty() {
6251 replaced.insert((fixture_path.clone(), *line));
6252 }
6253 for test in users {
6254 let test_key = (test.path.clone(), test.line);
6255 if *constructor_only {
6256 constructor_only_test_keys.insert(test_key);
6257 } else {
6258 non_constructor_test_keys.insert(test_key);
6259 }
6260 if seen_test_keys.insert(format!("{}:{}", test.path, test.line)) {
6261 likely_tests.push(TestTarget {
6262 reason: format!("uses fixture `{shown}`"),
6263 ..test
6264 });
6265 }
6266 }
6267 }
6268
6269 for (setup, class_id, setup_path, line, constructor_only) in &setups {
6270 let tests = tests_in_class(conn, class_id)?;
6271 if !tests.is_empty() {
6272 replaced.insert((setup_path.clone(), *line));
6273 }
6274 for test in tests {
6275 let test_key = (test.path.clone(), test.line);
6276 if *constructor_only {
6277 constructor_only_test_keys.insert(test_key);
6278 } else {
6279 non_constructor_test_keys.insert(test_key);
6280 }
6281 if seen_test_keys.insert(format!("{}:{}", test.path, test.line)) {
6282 likely_tests.push(TestTarget {
6283 reason: format!("setup `{setup}` runs before it"),
6284 ..test
6285 });
6286 }
6287 }
6288 }
6289 likely_tests.retain(|test| !replaced.contains(&(test.path.clone(), test.line)));
6290 constructor_only_test_keys.retain(|key| !non_constructor_test_keys.contains(key));
6291
6292 qualify_test_methods(conn, &mut likely_tests)?;
6293 if seed_type == "file" {
6296 let mut constructor_tests_by_file =
6297 std::collections::BTreeMap::<String, Vec<TestTarget>>::new();
6298 let mut other_tests = Vec::with_capacity(likely_tests.len());
6299 for test in likely_tests.drain(..) {
6300 if constructor_only_test_keys.contains(&(test.path.clone(), test.line)) {
6301 constructor_tests_by_file
6302 .entry(test.path.clone())
6303 .or_default()
6304 .push(test);
6305 } else {
6306 other_tests.push(test);
6307 }
6308 }
6309 for (path, mut tests) in constructor_tests_by_file {
6310 if tests.len() > 1 {
6311 let count = tests.len();
6312 other_tests.push(TestTarget {
6313 name: path.clone(),
6314 path,
6315 line: 1,
6316 reason: format!("constructor-only callers ({count} targets)"),
6317 });
6318 } else if let Some(test) = tests.pop() {
6319 other_tests.push(test);
6320 }
6321 }
6322 likely_tests = other_tests;
6323 }
6324 let likely_tests_found = likely_tests.len();
6325 let impacted_symbols_found = impacted_symbols.len();
6326 let likely_tests_truncated = likely_tests.len() > limit;
6327 let impacted_symbols_truncated = impacted_symbols.len() > limit;
6328 if likely_tests.len() > limit {
6329 likely_tests.truncate(limit);
6330 }
6331 if impacted_symbols.len() > limit {
6332 impacted_symbols.truncate(limit);
6333 }
6334
6335 Ok(BlastRadiusResult {
6336 seed_type,
6337 seeds,
6338 likely_tests,
6339 impacted_symbols,
6340 likely_tests_truncated,
6341 impacted_symbols_truncated,
6342 traversal_ceiling_reached,
6343 limit_at_maximum: limit >= MAX_RESULT_LIMIT,
6344 likely_tests_found,
6345 impacted_symbols_found,
6346 stopped_at_depth: callers_beyond_depth.then_some(max_depth),
6347 })
6348}
6349
6350fn qualify_test_methods(conn: &Connection, tests: &mut [TestTarget]) -> Result<(), QueryError> {
6353 let mut stmt = conn.prepare(
6354 "SELECT class.name FROM symbols t
6355 JOIN symbols class ON class.symbol_id = t.parent_symbol_id
6356 WHERE replace(t.path, '\\', '/') = ?1 AND t.start_line = ?2 AND t.name = ?3
6357 AND class.kind = 'class'
6358 LIMIT 1",
6359 )?;
6360 for test in tests.iter_mut().filter(|test| test.name != test.path) {
6361 if let Some(class) = stmt
6362 .query_row(params![test.path, test.line as i64, test.name], |row| {
6363 row.get::<_, String>(0)
6364 })
6365 .optional()?
6366 {
6367 test.name = format!("{class}::{}", test.name);
6368 }
6369 }
6370 Ok(())
6371}
6372
6373fn fixture_name(conn: &Connection, function: &str, path: &str) -> String {
6376 let signature: Option<String> = conn
6377 .query_row(
6378 "SELECT signature FROM symbols
6379 WHERE name = ?1 AND replace(path, '\\', '/') = ?2 AND signature LIKE '%fixture(%'
6380 LIMIT 1",
6381 params![function, path],
6382 |row| row.get(0),
6383 )
6384 .optional()
6385 .ok()
6386 .flatten();
6387 signature
6388 .as_deref()
6389 .and_then(declared_fixture_name)
6390 .unwrap_or_else(|| function.to_string())
6391}
6392
6393pub(crate) fn declared_fixture_name(signature: &str) -> Option<String> {
6395 let args = &signature[signature.find("fixture(")? + "fixture(".len()..];
6396 let args = &args[..args.find(')').unwrap_or(args.len())];
6397 let value = args[args.find("name=")? + "name=".len()..].trim_start();
6398 let quote = value.chars().next().filter(|c| *c == '"' || *c == '\'')?;
6399 let end = value[1..].find(quote)?;
6400 Some(value[1..1 + end].to_string())
6401}
6402
6403fn fixture_users(
6407 conn: &Connection,
6408 name: &str,
6409 fixture_path: &str,
6410) -> Result<Vec<TestTarget>, QueryError> {
6411 let lifecycle = if has_column(conn, "symbols", "test_lifecycle") {
6412 "COALESCE(t.test_lifecycle, 0) != 0"
6413 } else {
6414 "0"
6415 };
6416 let mut stmt = conn.prepare(&format!(
6417 "SELECT DISTINCT t.name, t.path, t.start_line, {lifecycle}
6418 FROM symbols parameter
6419 JOIN symbols t ON t.symbol_id = parameter.parent_symbol_id
6420 WHERE parameter.name = ?1
6421 AND (parameter.kind = 'parameter'
6422 OR (parameter.kind = 'variable' AND parameter.start_byte < t.body_start_byte))
6423 AND t.is_test = 1
6424 AND replace(t.path, '\\', '/') LIKE ?2 ESCAPE '\\'
6425 ORDER BY t.path, t.start_line
6426 LIMIT 2000"
6427 ))?;
6428 let mut users = Vec::new();
6431 let mut listed = HashSet::new();
6432 let mut visited = HashSet::new();
6433 let mut pending = vec![(name.to_string(), fixture_path.to_string())];
6434 while let Some((function, fixture_path)) = pending.pop() {
6435 if !visited.insert((function.clone(), fixture_path.clone())) {
6436 continue;
6437 }
6438 let name = fixture_name(conn, &function, &fixture_path);
6439 let scope = match fixture_path.rsplit_once('/') {
6440 Some((dir, "conftest.py")) => format!("{}/%", escape_like(dir)),
6441 None if fixture_path == "conftest.py" => "%".to_string(),
6442 _ => escape_like(&fixture_path),
6443 };
6444 let rows = stmt.query_map(params![name, scope], |row| {
6445 Ok((
6446 TestTarget {
6447 name: row.get(0)?,
6448 path: row.get::<_, String>(1)?.replace('\\', "/"),
6449 line: row.get::<_, i64>(2)? as usize,
6450 reason: String::new(),
6451 },
6452 row.get::<_, bool>(3)?,
6453 ))
6454 })?;
6455 for row in rows {
6456 let (user, is_fixture) = row?;
6457 if is_fixture {
6458 pending.push((user.name, user.path));
6459 } else if listed.insert((user.path.clone(), user.line)) {
6460 users.push(user);
6461 }
6462 }
6463 }
6464 Ok(users)
6465}
6466
6467fn tests_in_class(conn: &Connection, class_id: &str) -> Result<Vec<TestTarget>, QueryError> {
6469 let not_lifecycle = if has_column(conn, "symbols", "test_lifecycle") {
6470 "COALESCE(test_lifecycle, 0) = 0"
6471 } else {
6472 "1"
6473 };
6474 let mut stmt = conn.prepare(&format!(
6475 "SELECT name, path, start_line FROM symbols
6476 WHERE parent_symbol_id = ?1 AND is_test = 1 AND {not_lifecycle}
6477 ORDER BY start_line"
6478 ))?;
6479 let rows = stmt.query_map([class_id], |row| {
6480 Ok(TestTarget {
6481 name: row.get(0)?,
6482 path: row.get::<_, String>(1)?.replace('\\', "/"),
6483 line: row.get::<_, i64>(2)? as usize,
6484 reason: String::new(),
6485 })
6486 })?;
6487 Ok(rows.collect::<Result<_, _>>()?)
6488}
6489
6490pub fn compute_blast_radius(
6492 conn: &Connection,
6493 seed_symbols: &[&str],
6494 seed_paths: &[&str],
6495 max_depth: usize,
6496 limit: usize,
6497) -> Result<BlastRadiusResult, QueryError> {
6498 compute_blast_radius_scoped(conn, seed_symbols, None, seed_paths, max_depth, limit)
6499}
6500
6501#[cfg(test)]
6502mod tests {
6503 #[test]
6504 fn a_type_declaration_drops_attributes_and_fits_on_one_short_line() {
6505 assert_eq!(
6506 super::type_declaration_line(
6507 "#[derive(Debug)] #[serde(tag = \"t\")] pub struct Workspace"
6508 ),
6509 "pub struct Workspace"
6510 );
6511 let record = format!(
6512 "public sealed record Node(\n{})",
6513 " string Id,\n".repeat(20)
6514 );
6515 let line = super::type_declaration_line(&record);
6516 assert!(
6517 line.starts_with("public sealed record Node( string Id, string Id,"),
6518 "{line}"
6519 );
6520 assert_eq!(line.chars().count(), 120);
6521 assert!(line.ends_with('…'));
6522 }
6523
6524 #[test]
6525 fn a_mount_names_what_it_mounts_and_where() {
6526 let mount = |capture: &str, json: &str| {
6527 super::mount_display(capture, &serde_json::from_str(json).unwrap())
6528 };
6529 assert_eq!(
6530 mount(
6531 "blueprint_registration",
6532 r#"{"framework":"flask","mount_target":"auth.bp","query_family":"framework"}"#
6533 )
6534 .as_deref(),
6535 Some("blueprint_registration `auth.bp`")
6536 );
6537 assert_eq!(
6538 mount(
6539 "mount",
6540 r#"{"mount_path":"/jobs","mount_target":"Sidekiq::Web","normalized_mount_path":"/admin/jobs","query_family":"framework"}"#
6541 )
6542 .as_deref(),
6543 Some("mount `Sidekiq::Web` at /admin/jobs")
6544 );
6545 assert_eq!(
6546 mount(
6547 "property",
6548 r#"{"mount_target":"x","query_family":"metadata"}"#
6549 ),
6550 None
6551 );
6552 }
6553
6554 #[test]
6555 fn a_route_shows_its_source_template_and_keeps_a_joined_prefix() {
6556 let route = |json: &str| super::route_display(&serde_json::from_str(json).unwrap());
6557 assert_eq!(
6558 route(
6559 r#"{"verb":"GET","route_template":"/<int:id>/update","normalized_route_template":"/:id/update"}"#
6560 ),
6561 Some("GET /<int:id>/update".into())
6562 );
6563 assert_eq!(
6564 route(
6565 r#"{"verb":"POST","route_template":"/register","effective_route_template":"/auth/register","normalized_route_template":"/auth/register"}"#
6566 ),
6567 Some("POST /auth/register".into())
6568 );
6569 assert_eq!(
6570 route(
6571 r#"{"verb":"GET","route_template":"/users/{id}","normalized_route_template":"/api/v1/users/:id"}"#
6572 ),
6573 Some("GET /api/v1/users/:id".into())
6574 );
6575 assert_eq!(route(r#"{"key":"x"}"#), None);
6576 }
6577
6578 #[test]
6579 fn a_fixture_declared_with_a_name_is_used_by_that_name() {
6580 assert_eq!(
6581 super::declared_fixture_name("@pytest.fixture(name=\"async_app\") def _async_app()"),
6582 Some("async_app".into())
6583 );
6584 assert_eq!(
6585 super::declared_fixture_name(
6586 "@pytest.fixture(scope='session', name='db') def make_db()"
6587 ),
6588 Some("db".into())
6589 );
6590 assert_eq!(
6591 super::declared_fixture_name("@pytest.fixture def app()"),
6592 None
6593 );
6594 assert_eq!(
6595 super::declared_fixture_name("@pytest.fixture(params=[1]) def f(name='x')"),
6596 None
6597 );
6598 }
6599
6600 #[test]
6601 fn result_limit_rejects_values_above_the_shared_ceiling() {
6602 assert!(validate_result_limit(MAX_RESULT_LIMIT).is_ok());
6603 assert!(matches!(
6604 validate_result_limit(usize::MAX),
6605 Err(QueryError::InvalidResultLimit(usize::MAX))
6606 ));
6607 }
6608
6609 #[test]
6610 fn find_references_rejects_an_unbounded_limit_before_sql_execution() {
6611 let conn = Connection::open_in_memory().unwrap();
6612
6613 assert!(matches!(
6614 find_references_scoped(&conn, "target", "callers", usize::MAX, false, None),
6615 Err(QueryError::InvalidResultLimit(usize::MAX))
6616 ));
6617 }
6618
6619 use super::*;
6620 use crate::db::{ensure_fts_index, open_read_write};
6621
6622 #[test]
6623 fn count_parse_diagnostics_counts_rows_for_one_file() {
6624 let dir = crate::safe_tempdir();
6625 let conn = open_read_write(&dir.path().join("parse_diagnostics.db")).unwrap();
6626
6627 assert_eq!(count_parse_diagnostics(&conn, "src/lib.rs"), 0);
6628
6629 conn.execute_batch(
6630 "CREATE TABLE parse_diagnostics (
6631 diagnostic_id TEXT, file_id TEXT, path TEXT, language TEXT, kind TEXT
6632 );
6633 INSERT INTO parse_diagnostics VALUES ('d1', 'f1', 'src/lib.rs', 'rust', 'error');
6634 INSERT INTO parse_diagnostics VALUES ('d2', 'f1', 'src/lib.rs', 'rust', 'error');
6635 INSERT INTO parse_diagnostics VALUES ('d3', 'f2', 'src/other.rs', 'rust', 'error');",
6636 )
6637 .unwrap();
6638
6639 assert_eq!(count_parse_diagnostics(&conn, "src/lib.rs"), 2);
6640 assert_eq!(count_parse_diagnostics(&conn, "src\\lib.rs"), 2);
6641 assert_eq!(count_parse_diagnostics(&conn, "src/clean.rs"), 0);
6642 }
6643
6644 #[test]
6645 fn test_sanitize_fts5_query() {
6646 let (and_q, or_q) = sanitize_fts5_query("parse tokens");
6647 assert_eq!(and_q, "(\"parse\"* AND \"tokens\"*) OR \"parsetokens\"*");
6648 assert_eq!(or_q, "\"parse\"* OR \"tokens\"* OR \"parsetokens\"*");
6649
6650 let (and_q, or_q) = sanitize_fts5_query(" Option<T> ");
6651 assert_eq!(and_q, "(\"Option\"* AND \"T\") OR \"OptionT\"*");
6652 assert_eq!(or_q, "\"Option\"* OR \"T\" OR \"OptionT\"*");
6653
6654 let (and_q, or_q) = sanitize_fts5_query(" ");
6655 assert!(and_q.is_empty());
6656 assert!(or_q.is_empty());
6657 }
6658
6659 #[test]
6660 fn sanitize_splits_case_boundaries_and_drops_stop_words() {
6661 let (and_q, or_q) = sanitize_fts5_query("ValidateSyntax");
6662 assert_eq!(
6663 and_q,
6664 "((\"Validate\"* \"Syntax\"*) OR \"ValidateSyntax\"*)"
6665 );
6666 assert_eq!(or_q, "\"Validate\"* OR \"Syntax\"* OR \"ValidateSyntax\"*");
6667
6668 let (and_q, _) = sanitize_fts5_query("find tests related to a symbol");
6669 assert_eq!(
6670 and_q,
6671 "\"find\"* AND \"tests\"* AND \"related\"* AND \"symbol\"*"
6672 );
6673
6674 let (and_q, or_q) = sanitize_fts5_query("parseHTTPResponse2");
6675 assert_eq!(
6676 and_q,
6677 "((\"parse\"* \"HTTP\"* \"Response\"* \"2\") OR \"parseHTTPResponse2\"*)"
6678 );
6679 assert!(or_q.ends_with("OR \"parseHTTPResponse2\"*"));
6680
6681 let (and_q, _) = sanitize_fts5_query("validate_syntax");
6682 assert_eq!(
6683 and_q,
6684 "((\"validate\"* \"syntax\"*) OR \"validate_syntax\"*)"
6685 );
6686
6687 let (and_q, _) = sanitize_fts5_query("isReady");
6688 assert_eq!(and_q, "((\"Ready\"*) OR \"isReady\"*)");
6689
6690 let (and_q, _) = sanitize_fts5_query("before");
6691 assert_eq!(and_q, "\"before\"*");
6692
6693 let (and_q, _) = sanitize_fts5_query("fooBar quux");
6694 assert_eq!(
6695 and_q,
6696 "(((\"foo\"* \"Bar\"*) OR \"fooBar\"*) AND \"quux\"*) OR \"fooBarquux\"*"
6697 );
6698
6699 let (and_q, _) = sanitize_fts5_query("the for a");
6700 assert_eq!(and_q, "(\"the\"* AND \"for\"* AND \"a\") OR \"thefora\"*");
6701 }
6702
6703 fn search_fixture(rows: &str) -> Connection {
6704 let conn = Connection::open_in_memory().unwrap();
6705 conn.execute_batch(&format!(
6706 "CREATE TABLE symbols (
6707 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT,
6708 kind TEXT, signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
6709 start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
6710 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
6711 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
6712 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
6713 semantic_group TEXT, is_test INTEGER, test_container INTEGER, content_type TEXT
6714 );
6715 INSERT INTO symbols VALUES {rows};"
6716 ))
6717 .unwrap();
6718 ensure_fts_index(&conn).unwrap();
6719 conn
6720 }
6721
6722 fn code_row(id: &str, path: &str, language: &str, name: &str, doc: &str) -> String {
6723 format!(
6724 "('{id}', 'f_{id}', '{path}', '{language}', '{name}', 'function', 'fn {name}()', '{doc}', 'pub', NULL,
6725 10, 0, 20, 1, 100, 250, 12, 4, 19, 1, 120, 240, 'h_{id}', NULL, 0, 0, 'code')"
6726 )
6727 }
6728
6729 fn doc_row(id: &str, name: &str, doc: &str) -> String {
6730 format!(
6731 "('{id}', 'f_{id}', 'docs/{id}.md', 'markdown', '{name}', 'module', '{name}', '{doc}', NULL, NULL,
6732 3, 0, 3, 1, 10, 40, NULL, NULL, NULL, NULL, NULL, NULL, 'h_{id}', NULL, 0, 0, 'documentation')"
6733 )
6734 }
6735
6736 #[test]
6737 fn ranked_owner_context_recovers_members_and_enriches_existing_rows() {
6738 let mut rows = vec![
6739 code_row(
6740 "planner",
6741 "src/planner.rs",
6742 "rust",
6743 "EditPlanner",
6744 "Plans edit operations for a function body",
6745 ),
6746 code_row(
6747 "target",
6748 "src/planner.rs",
6749 "rust",
6750 "ReplaceBodyImplementation",
6751 "",
6752 ),
6753 code_row("existing", "src/planner.rs", "rust", "Body", ""),
6754 code_row(
6755 "unrelated",
6756 "src/planner.rs",
6757 "rust",
6758 "SaveConfiguration",
6759 "",
6760 ),
6761 ];
6762 rows.extend((0..200).map(|i| {
6764 code_row(
6765 &format!("noise{i}"),
6766 "src/noise.rs",
6767 "rust",
6768 &format!("body{i:03}"),
6769 "edit planner body",
6770 )
6771 }));
6772 let conn = search_fixture(&rows.join(","));
6773 conn.execute_batch(
6774 "UPDATE symbols SET kind = 'class' WHERE symbol_id = 'planner';
6775 UPDATE symbols SET kind = 'method', parent_symbol_id = 'planner'
6776 WHERE symbol_id IN ('target', 'existing', 'unrelated');",
6777 )
6778 .unwrap();
6779 let ordinary =
6780 collect_search_candidates(&conn, "edit planner body", None, None, false, 200).unwrap();
6781 assert!(
6782 !ordinary
6783 .iter()
6784 .any(|c| c.result.symbol.symbol_id == "target")
6785 );
6786 assert!(
6787 ordinary
6788 .iter()
6789 .any(|c| c.result.symbol.symbol_id == "existing")
6790 );
6791 let found =
6792 fts_search_symbols_explained(&conn, "edit planner body", None, None, false, 200, true)
6793 .unwrap();
6794 for id in ["target", "existing"] {
6795 let members: Vec<_> = found.iter().filter(|r| r.symbol.symbol_id == id).collect();
6796 assert_eq!(members.len(), 1);
6797 let explain = members[0].explain.as_ref().unwrap();
6798 assert!(explain.branches.iter().any(|b| b == "owner_context"));
6799 for word in ["edit", "planner"] {
6800 assert!(
6801 explain
6802 .terms
6803 .contains(&(word.into(), "owner_context".into(), 0.5))
6804 );
6805 }
6806 assert!(explain.terms.contains(&("body".into(), "name".into(), 3.0)));
6807 }
6808 assert!(!found.iter().any(|r| r.symbol.symbol_id == "unrelated"));
6809 for exact in ["EditPlanner", "Body"] {
6810 assert_eq!(search_names(&conn, exact)[0], exact);
6811 }
6812 let scoped = fts_search_symbols_scoped(
6813 &conn,
6814 "edit planner body",
6815 None,
6816 Some("src/noise.rs"),
6817 false,
6818 200,
6819 )
6820 .unwrap();
6821 assert!(scoped.iter().all(|r| r.symbol.path == "src/noise.rs"));
6822 let methods =
6823 fts_search_symbols_scoped(&conn, "edit planner body", Some("method"), None, false, 200)
6824 .unwrap();
6825 assert!(methods.iter().all(|r| r.symbol.kind == "method"));
6826 assert!(methods.iter().all(|r| r.explain.is_none()));
6828 }
6829
6830 #[test]
6831 fn ranked_owner_expansion_is_bounded_and_preserves_member_filters() {
6832 let mut rows = Vec::new();
6833 for owner in 0..4 {
6834 rows.push(code_row(
6835 &format!("o{owner}"),
6836 "src/lib.rs",
6837 "rust",
6838 &format!("Planner{owner}"),
6839 "body planner",
6840 ));
6841 for member in 0..41 {
6842 rows.push(code_row(
6843 &format!("o{owner}m{member:02}"),
6844 "src/lib.rs",
6845 "rust",
6846 &format!("Body{member:02}"),
6847 "",
6848 ));
6849 }
6850 }
6851 rows.push(doc_row("docowner", "PlannerDocs", "body planner"));
6852 rows.push(code_row(
6853 "docchild",
6854 "src/lib.rs",
6855 "rust",
6856 "BodyFromDocs",
6857 "",
6858 ));
6859 rows.push(code_row(
6860 "testchild",
6861 "tests/lib.rs",
6862 "rust",
6863 "BodyTest",
6864 "",
6865 ));
6866 rows.push(code_row(
6867 "linkchild",
6868 "docs/link.md",
6869 "markdown",
6870 "BodyLink",
6871 "",
6872 ));
6873 rows.push(code_row(
6874 "nestedchild",
6875 "src/lib.rs",
6876 "rust",
6877 "BodyNested",
6878 "",
6879 ));
6880 let conn = search_fixture(&rows.join(","));
6881 for owner in 0..4 {
6882 conn.execute(
6883 "UPDATE symbols SET kind = 'class' WHERE symbol_id = ?1",
6884 [format!("o{owner}")],
6885 )
6886 .unwrap();
6887 conn.execute(
6888 "UPDATE symbols SET kind = 'method', parent_symbol_id = ?1 WHERE symbol_id LIKE ?2",
6889 params![format!("o{owner}"), format!("o{owner}m%")],
6890 )
6891 .unwrap();
6892 }
6893 conn.execute_batch(
6894 "UPDATE symbols SET kind = 'class' WHERE symbol_id = 'docowner';
6895 UPDATE symbols SET parent_symbol_id = 'docowner' WHERE symbol_id = 'docchild';
6896 UPDATE symbols SET parent_symbol_id = 'o0' WHERE symbol_id IN ('testchild', 'linkchild');
6897 UPDATE symbols SET kind = 'import' WHERE symbol_id = 'linkchild';
6898 UPDATE symbols SET parent_symbol_id = 'o0m00' WHERE symbol_id = 'nestedchild';",
6899 ).unwrap();
6900 let mut candidates =
6901 collect_search_candidates(&conn, "planner body", None, None, false, 200).unwrap();
6902 candidates.retain(|c| c.result.symbol.parent_symbol_id.is_none());
6904 let ranked = rerank_with(candidates.clone(), "planner body", false, None);
6905 assert!(
6906 expand_ranked_owner_members(
6907 &conn,
6908 &mut candidates,
6909 &ranked,
6910 "planner body",
6911 None,
6912 None,
6913 false
6914 )
6915 .unwrap()
6916 );
6917 let expanded: Vec<_> = candidates
6918 .iter()
6919 .filter(|c| c.owner_context.is_some())
6920 .collect();
6921 assert_eq!(expanded.len(), 120);
6922 for owner in 0..3 {
6923 assert_eq!(
6924 expanded
6925 .iter()
6926 .filter(|c| c.result.symbol.parent_symbol_id.as_deref()
6927 == Some(&format!("o{owner}")))
6928 .count(),
6929 40
6930 );
6931 assert!(
6932 !expanded
6933 .iter()
6934 .any(|c| c.result.symbol.symbol_id == format!("o{owner}m40"))
6935 );
6936 }
6937 for excluded in ["docchild", "testchild", "linkchild", "nestedchild"] {
6938 assert!(
6939 !expanded
6940 .iter()
6941 .any(|c| c.result.symbol.symbol_id == excluded)
6942 );
6943 }
6944 let mut late_ranked = rerank_with(
6946 (0..20).map(|i| function(&format!("body{i}"))).collect(),
6947 "body",
6948 false,
6949 None,
6950 );
6951 late_ranked.push(ranked[0].clone());
6952 assert!(
6953 !expand_ranked_owner_members(
6954 &conn,
6955 &mut Vec::new(),
6956 &late_ranked,
6957 "planner body",
6958 None,
6959 None,
6960 false
6961 )
6962 .unwrap()
6963 );
6964 }
6965
6966 #[test]
6967 fn lookup_and_search_skip_documentation_links_recorded_as_imports() {
6968 let conn = search_fixture(
6969 &[
6970 "('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(),
6971 "('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(),
6972 ]
6973 .join(","),
6974 );
6975
6976 let lookup: Vec<String> = search_symbols(&conn, "Flask", Some("import"), true, 10)
6977 .unwrap()
6978 .into_iter()
6979 .map(|s| s.path)
6980 .collect();
6981 assert_eq!(lookup, vec!["src/app.py"]);
6982 let search: Vec<String> =
6983 fts_search_symbols_scoped(&conn, "Flask", Some("import"), None, true, 10)
6984 .unwrap()
6985 .into_iter()
6986 .map(|r| r.symbol.path)
6987 .collect();
6988 assert_eq!(search, vec!["src/app.py"]);
6989 }
6990
6991 #[test]
6992 fn lookup_ranks_a_prefix_match_above_a_substring_match() {
6993 let conn = search_fixture(
6994 &[
6995 code_row("c1", "tests/a.rs", "rust", "test_test_client", ""),
6996 code_row("c2", "tests/b.rs", "rust", "test_client_open_environ", ""),
6997 ]
6998 .join(","),
6999 );
7000
7001 let names: Vec<String> = search_symbols(&conn, "test_cli", None, true, 10)
7002 .unwrap()
7003 .into_iter()
7004 .map(|s| s.name)
7005 .collect();
7006
7007 assert_eq!(names, vec!["test_client_open_environ", "test_test_client"]);
7008 }
7009
7010 #[test]
7011 fn lookup_ranks_a_declared_property_above_a_self_assignment() {
7012 let property = |id: &str, path: &str, signature: &str| {
7013 format!(
7014 "('{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')"
7015 )
7016 };
7017 let conn = search_fixture(
7018 &[
7019 property("p1", "src/flask/app.py", "self.debug = get_debug_flag()"),
7020 property(
7021 "p2",
7022 "src/flask/sansio/app.py",
7023 "@property def debug(self) -> bool",
7024 ),
7025 ]
7026 .join(","),
7027 );
7028
7029 let paths: Vec<String> = search_symbols(&conn, "debug", None, false, 10)
7030 .unwrap()
7031 .into_iter()
7032 .map(|s| s.path)
7033 .collect();
7034
7035 assert_eq!(paths, vec!["src/flask/sansio/app.py", "src/flask/app.py"]);
7036 }
7037
7038 fn search_names(conn: &Connection, query: &str) -> Vec<String> {
7039 fts_search_symbols_scoped(conn, query, None, None, false, 10)
7040 .unwrap()
7041 .into_iter()
7042 .map(|r| r.symbol.name)
7043 .collect()
7044 }
7045
7046 const TEST_PATH_CASES: &[(&str, bool)] = &[
7047 ("tests/foo.py", true),
7048 ("tests/x.py", true),
7049 ("tests/tools/test_web.py", true),
7050 ("src/tests/x.rs", true),
7051 ("__tests__/a.ts", true),
7052 ("a/__tests__/b.ts", true),
7053 ("test/x.java", true),
7054 ("src/test/Helper.java", true),
7055 ("src/test_utils.py", true),
7056 ("lib/test_helper.rb", true),
7057 ("test_config.py", true),
7058 ("pkg/test_data/x.json", false),
7059 ("src/test_detection.rs", false),
7060 ("autotests/tst_pagerow.qml", true),
7061 ("autotests/helper.qml", true),
7062 ("src/autotests/columnview.cpp", true),
7063 ("tst_foo.qml", true),
7064 ("src/tst_columnview.qml", true),
7065 ("autotests\\tst_bar.qml", true),
7066 ("autotests_helper/x.rs", false),
7067 ("src/autotest.rs", false),
7068 ("src/tstamp.rs", false),
7069 ("src/tst.rs", false),
7070 ("crates/julie-index/src/analysis/test_quality.rs", false),
7071 ("x/foo_test.go", true),
7072 ("x/foo.test.ts", true),
7073 ("x/foo.spec.js", true),
7074 ("src/lib_test.rs", true),
7075 ("src/test.rs", true),
7076 ("tests.rs", true),
7077 ("src/tests.rs", true),
7078 ("Foo.Tests.cs", true),
7079 ("x/FooTests.cs", true),
7080 ("src/FooTests.cs", true),
7081 ("src/Foo.Tests.cs", true),
7082 ("tests/Foo.cs", true),
7083 ("x/parser.spec.ts", true),
7084 ("test_x.py", true),
7085 ("test.rs", true),
7086 ("tests\\x.py", true),
7087 ("src/protocol.spec.v1/parser.rs", false),
7088 ("pkg/test_support/runtime.py", false),
7089 ("src/Contests.cs", false),
7090 ("spec/x.rb", false),
7091 ("crates/x/src/impact/likely_tests.rs", false),
7092 ("x/foo_tests.rs", false),
7093 ("src/latest.rs", false),
7094 ("x/latest.go", false),
7095 ("x/manifest.rs", false),
7096 ("src/attest.rs", false),
7097 ("contest/x.py", false),
7098 ("src/testing.rs", false),
7099 ("src/main.rs", false),
7100 ("pkg/service.go", false),
7101 ];
7102
7103 #[test]
7104 fn test_path_rule_and_its_sql_mirror_agree_on_every_path() {
7105 let conn = Connection::open_in_memory().unwrap();
7106 let sql = format!(
7107 "SELECT {} FROM (SELECT :path AS path) s",
7108 test_path_predicate("s")
7109 );
7110 let mut stmt = conn.prepare(&sql).unwrap();
7111 for (path, expected) in TEST_PATH_CASES {
7112 assert_eq!(is_test_path(path), *expected, "rust rule: {path}");
7113 let from_sql: bool = stmt
7114 .query_row(rusqlite::named_params! { ":path": path }, |row| row.get(0))
7115 .unwrap();
7116 assert_eq!(from_sql, *expected, "sql mirror: {path}");
7117 }
7118 }
7119
7120 #[test]
7121 fn test_name_rule_and_its_sql_mirror_agree_on_every_name() {
7122 let conn = Connection::open_in_memory().unwrap();
7123 let sql = format!(
7124 "SELECT {} FROM (SELECT :name AS name) s",
7125 names_a_test_sql("s")
7126 );
7127 let mut stmt = conn.prepare(&sql).unwrap();
7128 for name in [
7129 "test_run",
7130 "TestRoutes",
7131 "RoutesTest",
7132 "RoutesTests",
7133 "runSpec",
7134 "Specs",
7135 "create_app",
7136 "attest_value",
7137 "inspection",
7138 ] {
7139 let from_sql: bool = stmt
7140 .query_row(rusqlite::named_params! { ":name": name }, |row| row.get(0))
7141 .unwrap();
7142 assert_eq!(from_sql, names_a_test(name), "sql mirror: {name}");
7143 }
7144 }
7145
7146 #[test]
7147 fn unflagged_test_file_rows_are_hidden_unless_tests_are_included() {
7148 let conn = search_fixture(
7149 &[
7150 code_row(
7151 "a",
7152 "src/parser.rs",
7153 "rust",
7154 "parse_sidecar",
7155 "Parse a sidecar.",
7156 ),
7157 code_row(
7158 "b",
7159 "src/tests/helpers.py",
7160 "python",
7161 "parse_sidecar_fixture",
7162 "Parse a sidecar.",
7163 ),
7164 ]
7165 .join(", "),
7166 );
7167
7168 let default_search: Vec<String> =
7169 fts_search_symbols_scoped(&conn, "parse sidecar", None, None, false, 10)
7170 .unwrap()
7171 .into_iter()
7172 .map(|r| r.symbol.name)
7173 .collect();
7174 assert_eq!(default_search, vec!["parse_sidecar".to_string()]);
7175
7176 let with_tests: Vec<String> =
7177 fts_search_symbols_scoped(&conn, "parse sidecar", None, None, true, 10)
7178 .unwrap()
7179 .into_iter()
7180 .map(|r| r.symbol.name)
7181 .collect();
7182 assert!(with_tests.contains(&"parse_sidecar_fixture".to_string()));
7183
7184 let default_lookup: Vec<String> =
7185 search_symbols_scoped(&conn, "parse_sidecar", None, None, false, 10)
7186 .unwrap()
7187 .into_iter()
7188 .map(|s| s.name)
7189 .collect();
7190 assert_eq!(default_lookup, vec!["parse_sidecar".to_string()]);
7191
7192 let lookup_with_tests: Vec<String> =
7193 search_symbols_scoped(&conn, "parse_sidecar", None, None, true, 10)
7194 .unwrap()
7195 .into_iter()
7196 .map(|s| s.name)
7197 .collect();
7198 assert!(lookup_with_tests.contains(&"parse_sidecar_fixture".to_string()));
7199 }
7200
7201 #[test]
7202 fn count_file_symbols_prefers_the_exact_case_path_like_the_loader() {
7203 let conn = search_fixture(
7204 &[
7205 code_row("a", "src/Foo.rs", "rust", "one", ""),
7206 code_row("b", "src/foo.rs", "rust", "two", ""),
7207 code_row("c", "src/foo.rs", "rust", "three", ""),
7208 ]
7209 .join(", "),
7210 );
7211 for path in ["src/Foo.rs", "src/foo.rs", "src/FOO.rs", "src\\foo.rs"] {
7212 assert_eq!(
7213 count_file_symbols(&conn, path),
7214 load_file_symbols(&conn, path).unwrap().len(),
7215 "{path}"
7216 );
7217 }
7218 assert_eq!(count_file_symbols(&conn, "src/Foo.rs"), 1);
7219 assert_eq!(count_file_symbols(&conn, "src/FOO.rs"), 3);
7220 }
7221
7222 #[test]
7223 fn lookup_statement_is_not_planned_as_a_multi_index_or() {
7224 let conn = search_fixture(&code_row("a", "src/lib.rs", "rust", "needle", ""));
7225 conn.execute_batch(
7226 "CREATE INDEX idx_symbols_name_kind ON symbols(name, kind);
7227 CREATE INDEX idx_symbols_test_container ON symbols(test_container);
7228 CREATE INDEX idx_symbols_is_test ON symbols(is_test);",
7229 )
7230 .unwrap();
7231 let sql = format!(
7232 "EXPLAIN QUERY PLAN {}",
7233 search_symbols_sql(false, false, 20)
7234 );
7235 let plan: Vec<String> = conn
7236 .prepare(&sql)
7237 .unwrap()
7238 .query_map(
7239 rusqlite::named_params! {
7240 ":query": "needle",
7241 ":pattern": "%needle%",
7242 ":kind": None::<&str>,
7243 ":path": None::<&str>,
7244 ":path_like": None::<&str>,
7245 },
7246 |row| row.get::<_, String>(3),
7247 )
7248 .unwrap()
7249 .collect::<Result<_, _>>()
7250 .unwrap();
7251 assert!(
7252 !plan.iter().any(|step| step.contains("MULTI-INDEX OR")),
7253 "{plan:?}"
7254 );
7255 }
7256
7257 #[test]
7258 fn qualified_lookup_in_a_test_file_returns_the_named_row() {
7259 let conn = search_fixture(
7260 &[
7261 "('c', 'f_c', 'src/tests/helpers.py', 'python', 'Helpers', 'class', 'class Helpers', '', 'pub', NULL,
7262 1, 0, 9, 1, 0, 90, 1, 0, 9, 1, 5, 88, 'h_c', NULL, 0, 0, 'code')".to_string(),
7263 "('d', 'f_d', 'src/tests/helpers.py', 'python', 'load_fixture', 'method', 'def load_fixture()', '', 'pub', 'c',
7264 10, 0, 20, 1, 100, 250, 12, 4, 19, 1, 120, 240, 'h_d', NULL, 0, 0, 'code')".to_string(),
7265 ]
7266 .join(", "),
7267 );
7268
7269 assert_eq!(
7270 search_symbols_scoped(&conn, "Helpers.load_fixture", None, None, false, 10)
7271 .unwrap()
7272 .len(),
7273 1
7274 );
7275 assert_eq!(
7276 search_symbols_scoped(&conn, "Helpers.load_fixture", None, None, true, 10)
7277 .unwrap()
7278 .len(),
7279 1
7280 );
7281 assert!(
7282 search_symbols_scoped(&conn, "load_fix", None, None, false, 10)
7283 .unwrap()
7284 .is_empty()
7285 );
7286 }
7287
7288 #[test]
7289 fn concept_query_prefers_partial_code_match_over_full_doc_match() {
7290 let conn = search_fixture(
7291 &[
7292 doc_row(
7293 "d1",
7294 "Safety guarantees",
7295 "Pre-flight syntax validation runs before the edit touches disk",
7296 ),
7297 doc_row(
7298 "d2",
7299 "Audit",
7300 "The syntax validation before an edit is the invariant",
7301 ),
7302 code_row(
7303 "c1",
7304 "src/syntax.rs",
7305 "rust",
7306 "validate_syntax",
7307 "Validate the syntax of a file",
7308 ),
7309 code_row(
7310 "c2",
7311 "src/edit.rs",
7312 "rust",
7313 "replace_symbol_body",
7314 "Atomic edit with validation",
7315 ),
7316 ]
7317 .join(","),
7318 );
7319
7320 let names = search_names(&conn, "syntax validation before edit");
7321
7322 assert_eq!(names[0], "validate_syntax");
7323 assert!(names.contains(&"replace_symbol_body".to_string()));
7324 assert!(names.contains(&"Safety guarantees".to_string()));
7325 }
7326
7327 #[test]
7328 fn camel_case_query_finds_snake_case_symbol_and_vice_versa() {
7329 let conn = search_fixture(
7330 &[
7331 code_row("c1", "src/syntax.rs", "rust", "validate_syntax", ""),
7332 code_row("c2", "src/syntax.ts", "typescript", "validateSyntax", ""),
7333 ]
7334 .join(","),
7335 );
7336
7337 let mut camel = search_names(&conn, "ValidateSyntax");
7338 camel.sort();
7339 assert_eq!(camel, vec!["validateSyntax", "validate_syntax"]);
7340 let mut words = search_names(&conn, "validate syntax");
7341 words.sort();
7342 assert_eq!(words, vec!["validateSyntax", "validate_syntax"]);
7343 }
7344
7345 #[test]
7346 fn stop_word_prefixed_camel_case_symbol_is_still_found() {
7347 let conn = search_fixture(
7348 &[
7349 code_row("c1", "src/state.ts", "typescript", "isReady", ""),
7350 code_row("c2", "src/hooks.rs", "rust", "before", ""),
7351 code_row(
7352 "c3",
7353 "src/x.rs",
7354 "rust",
7355 "fooBar",
7356 "has fooBar but not the other word",
7357 ),
7358 ]
7359 .join(","),
7360 );
7361
7362 assert_eq!(search_names(&conn, "isReady"), vec!["isReady"]);
7363 assert_eq!(search_names(&conn, "before"), vec!["before"]);
7364 }
7365
7366 #[test]
7367 fn related_tests_use_the_name_as_typed_without_splitting() {
7368 let conn = search_fixture(
7369 &[
7370 code_row("c1", "src/state.ts", "typescript", "isReady", ""),
7371 "('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(),
7372 "('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(),
7373 ]
7374 .join(","),
7375 );
7376 let target = get_symbol_by_name(&conn, "isReady", None).unwrap().unwrap();
7377
7378 let names: Vec<String> = find_related_tests(&conn, &target, 5)
7379 .unwrap()
7380 .into_iter()
7381 .map(|t| t.name)
7382 .collect();
7383
7384 assert_eq!(names, vec!["isReady_reports_true"]);
7385 }
7386
7387 #[test]
7388 fn related_tests_match_underscores_literally_and_skip_dunder_names() {
7389 let conn = search_fixture(
7390 &[
7391 code_row("c1", "src/app.py", "python", "wsgi_app", ""),
7392 code_row("c2", "src/app.py", "python", "__init__", ""),
7393 "('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(),
7394 "('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(),
7395 "('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(),
7396 ]
7397 .join(","),
7398 );
7399 let names = |name: &str| -> Vec<String> {
7400 let target = get_symbol_by_name(&conn, name, None).unwrap().unwrap();
7401 find_related_tests(&conn, &target, 5)
7402 .unwrap()
7403 .into_iter()
7404 .map(|t| t.name)
7405 .collect()
7406 };
7407
7408 assert_eq!(names("wsgi_app"), vec!["test_wsgi_app"]);
7409 assert!(names("__init__").is_empty());
7410 }
7411
7412 #[test]
7413 fn related_tests_admit_unflagged_test_names_in_test_paths_without_helpers_or_low_signal_rows() {
7414 let conn = search_fixture(
7415 "('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'),
7416 ('direct', 'f_direct', 'tests/direct.rs', 'rust', 'test_direct', 'function', 'fn test_direct()', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_direct', NULL, 0, 0, 'code'),
7417 ('pending', 'f_pending', 'autotests/tst_pending.qml', 'qml', 'test_pending', 'function', 'function test_pending() {}', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_pending', NULL, 0, 0, 'code'),
7418 ('name', 'f_name', 'tests/name.rs', 'rust', 'test_calculate_named', 'function', 'fn test_calculate_named()', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_name', NULL, 0, 0, 'code'),
7419 ('fts', 'f_fts', 'tests/fts.rs', 'rust', 'test_fts', 'function', 'fn test_fts()', 'calculate behavior', NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_fts', NULL, 0, 0, 'code'),
7420 ('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'),
7421 ('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'),
7422 ('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'),
7423 ('helper', 'f_helper', 'tests/apps/factory.rs', 'rust', 'build_calculate_app', 'function', 'fn build_calculate_app()', NULL, NULL, NULL, 1, 0, 5, 1, 0, 50, NULL, NULL, NULL, NULL, NULL, NULL, 'h_helper', NULL, 0, 0, 'code')",
7424 );
7425 conn.execute_batch(
7426 "CREATE TABLE relationships (from_symbol_id TEXT, to_symbol_id TEXT, kind TEXT, path TEXT, start_line INTEGER, start_column INTEGER);
7427 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);
7428 CREATE TABLE type_facts (type_fact_id TEXT, symbol_id TEXT, language TEXT, resolved_type TEXT, generic_params_json TEXT);
7429 INSERT INTO relationships VALUES ('direct', 'target', 'calls', 'tests/direct.rs', 1, 0);
7430 INSERT INTO pending_relationships VALUES ('pending', 'calculate', 'calls', 'autotests/tst_pending.qml', 1, 0, NULL, '[]', 'calculate');",
7431 )
7432 .unwrap();
7433 let target = get_symbol_by_name(&conn, "calculate", None)
7434 .unwrap()
7435 .unwrap();
7436
7437 let names: Vec<String> = find_related_tests(&conn, &target, 5)
7438 .unwrap()
7439 .into_iter()
7440 .map(|test| test.name)
7441 .collect();
7442
7443 assert_eq!(
7444 names,
7445 vec![
7446 "test_direct",
7447 "test_pending",
7448 "test_calculate_named",
7449 "test_fts"
7450 ]
7451 );
7452 }
7453
7454 #[test]
7455 fn exact_name_ranks_before_longer_names_with_the_same_tokens() {
7456 let conn = search_fixture(
7457 &[
7458 code_row(
7459 "c1",
7460 "src/queries.rs",
7461 "rust",
7462 "fts_search_symbols_scoped",
7463 "search symbols scoped with fts",
7464 ),
7465 code_row("c2", "src/queries.rs", "rust", "search_symbols_scoped", ""),
7466 ]
7467 .join(","),
7468 );
7469
7470 assert_eq!(
7471 search_names(&conn, "search_symbols_scoped")[0],
7472 "search_symbols_scoped"
7473 );
7474 }
7475
7476 fn sidecar_fixture() -> Connection {
7477 search_fixture(
7478 &[
7479 code_row("c1", "src/sidecar.rs", "rust", "parseSha256Sidecar", ""),
7480 code_row(
7481 "c2",
7482 "src/sidecar.rs",
7483 "rust",
7484 "parse_sidecar_file",
7485 "parse the sha256 sidecar file",
7486 ),
7487 ]
7488 .join(","),
7489 )
7490 }
7491
7492 fn candidate<'a>(candidates: &'a [Candidate], name: &str) -> &'a Candidate {
7493 candidates
7494 .iter()
7495 .find(|c| c.result.symbol.name == name)
7496 .unwrap_or_else(|| panic!("{name} is not a candidate"))
7497 }
7498
7499 #[test]
7500 fn name_substring_admits_a_symbol_the_word_branch_cannot_reach() {
7501 let conn = sidecar_fixture();
7502
7503 let candidates = collect_search_candidates(&conn, "sha256", None, None, false, 10).unwrap();
7504
7505 let target = candidate(&candidates, "parseSha256Sidecar");
7506 assert!(target.name_match);
7507 assert!(!target.word_match);
7508 assert!(!target.exact_name);
7509 assert!(target.name_terms.contains(&"sha256".to_string()));
7510 }
7511
7512 #[test]
7513 fn a_row_matching_every_word_does_not_hide_a_row_matching_some() {
7514 let conn = search_fixture(
7515 &[
7516 code_row(
7517 "c1",
7518 "examples/demo.rs",
7519 "rust",
7520 "demo",
7521 "restore offline state",
7522 ),
7523 code_row(
7524 "c2",
7525 "src/replay.rs",
7526 "rust",
7527 "replay",
7528 "restore offline records",
7529 ),
7530 ]
7531 .join(","),
7532 );
7533
7534 let candidates =
7535 collect_search_candidates(&conn, "restore offline state", None, None, false, 10)
7536 .unwrap();
7537
7538 assert!(candidate(&candidates, "demo").word_match);
7539 assert!(candidate(&candidates, "replay").word_match);
7540 assert_eq!(search_names(&conn, "restore offline state")[0], "replay");
7541 }
7542
7543 #[test]
7544 fn name_branch_admits_the_target_when_word_matches_exceed_the_cap() {
7545 let mut rows: Vec<String> = (1..=170)
7546 .map(|i| {
7547 code_row(
7548 &format!("h{i:03}"),
7549 "src/sidecar.rs",
7550 "rust",
7551 &format!("sidecar_helper_{i:03}"),
7552 "parse sidecar file",
7553 )
7554 })
7555 .collect();
7556 rows.push(code_row(
7557 "c1",
7558 "src/sidecar.rs",
7559 "rust",
7560 "parseSha256Sidecar",
7561 "",
7562 ));
7563 let conn = search_fixture(&rows.join(","));
7564
7565 let candidates = collect_search_candidates(
7566 &conn,
7567 "parse the sha256 sidecar file",
7568 None,
7569 None,
7570 false,
7571 40,
7572 )
7573 .unwrap();
7574
7575 assert!(candidate(&candidates, "parseSha256Sidecar").name_match);
7576 assert_eq!(candidates.iter().filter(|c| c.word_match).count(), 160);
7577 }
7578
7579 #[test]
7580 fn the_or_pass_fills_the_word_cap_but_never_exceeds_it() {
7581 let mut rows: Vec<String> = (1..=20)
7582 .map(|i| {
7583 code_row(
7584 &format!("a{i:02}"),
7585 "src/a.rs",
7586 "rust",
7587 &format!("both_{i:02}"),
7588 "restore offline",
7589 )
7590 })
7591 .collect();
7592 rows.extend((1..=50).map(|i| {
7593 code_row(
7594 &format!("p{i:02}"),
7595 "src/p.rs",
7596 "rust",
7597 &format!("partial_{i:02}"),
7598 "restore records",
7599 )
7600 }));
7601 let conn = search_fixture(&rows.join(","));
7602
7603 let candidates =
7604 collect_search_candidates(&conn, "restore offline", None, None, false, 10).unwrap();
7605
7606 let word_rows: Vec<&Candidate> = candidates.iter().filter(|c| c.word_match).collect();
7607 assert_eq!(word_rows.len(), 40);
7608 assert_eq!(
7609 word_rows
7610 .iter()
7611 .filter(|c| c.result.symbol.name.starts_with("both_"))
7612 .count(),
7613 20
7614 );
7615 }
7616
7617 #[test]
7618 fn exact_name_is_admitted_regardless_of_case() {
7619 let conn = search_fixture(&code_row("c1", "src/q.rs", "rust", "xyzzy_q", ""));
7620
7621 let candidates =
7622 collect_search_candidates(&conn, "XYZZY_Q", None, None, false, 10).unwrap();
7623 assert!(candidate(&candidates, "xyzzy_q").exact_name);
7624
7625 conn.execute_batch("DROP TABLE symbol_names_tri").unwrap();
7626 let candidates =
7627 collect_search_candidates(&conn, "xyzzy_q", None, None, false, 10).unwrap();
7628 assert!(candidate(&candidates, "xyzzy_q").exact_name);
7629 }
7630
7631 #[test]
7632 fn exact_name_with_a_quote_is_admitted_through_the_trigram_index() {
7633 let conn = search_fixture(&code_row(
7634 "c1",
7635 "src/say.js",
7636 "javascript",
7637 "say \"hi\"",
7638 "",
7639 ));
7640
7641 let candidates =
7642 collect_search_candidates(&conn, "say \"hi\"", None, None, false, 10).unwrap();
7643
7644 let target = candidate(&candidates, "say \"hi\"");
7645 assert!(target.exact_name && target.name_match);
7646 }
7647
7648 #[test]
7649 fn a_row_matched_by_every_branch_is_one_candidate_with_all_flags() {
7650 let conn = search_fixture(
7651 &[
7652 code_row("c1", "src/a.rs", "rust", "sidecar", ""),
7653 code_row("c2", "src/b.rs", "rust", "sidecar_helper", ""),
7654 ]
7655 .join(","),
7656 );
7657
7658 let candidates =
7659 collect_search_candidates(&conn, "sidecar", None, None, false, 10).unwrap();
7660
7661 assert_eq!(candidates.len(), 2);
7662 let target = candidate(&candidates, "sidecar");
7663 assert!(target.exact_name && target.word_match && target.name_match);
7664 assert!(target.bm25.is_some());
7665 let helper = candidate(&candidates, "sidecar_helper");
7666 assert!(!helper.exact_name && helper.word_match && helper.name_match);
7667 }
7668
7669 #[test]
7670 fn an_index_without_the_trigram_table_returns_word_rows_only() {
7671 let conn = sidecar_fixture();
7672 conn.execute_batch("DROP TABLE symbol_names_tri").unwrap();
7673
7674 let candidates = collect_search_candidates(&conn, "sha256", None, None, false, 10).unwrap();
7675
7676 let names: Vec<&str> = candidates
7677 .iter()
7678 .map(|c| c.result.symbol.name.as_str())
7679 .collect();
7680 assert_eq!(names, vec!["parse_sidecar_file"]);
7681 assert!(candidates.iter().all(|c| c.word_match && !c.name_match));
7682 assert_eq!(search_names(&conn, "sha256"), vec!["parse_sidecar_file"]);
7683 }
7684
7685 #[test]
7686 fn words_under_three_characters_skip_the_name_branch() {
7687 let conn = search_fixture(
7688 &[
7689 code_row("c1", "src/a.rs", "rust", "ab", ""),
7690 code_row("c2", "src/b.rs", "rust", "cab", ""),
7691 ]
7692 .join(","),
7693 );
7694
7695 let candidates = collect_search_candidates(&conn, "ab", None, None, false, 10).unwrap();
7696
7697 assert!(candidates.iter().all(|c| !c.name_match));
7698 assert!(candidate(&candidates, "ab").exact_name);
7699 }
7700
7701 #[test]
7702 fn trigram_terms_include_the_identifier_parts_of_each_word() {
7703 assert_eq!(
7704 trigram_name_terms("collapse_name"),
7705 vec!["collapse_name", "collapse", "name"]
7706 );
7707 assert_eq!(
7708 trigram_name_terms("parse the sha256 sidecar"),
7709 vec!["parse", "sha256", "sha", "256", "sidecar"]
7710 );
7711 assert_eq!(trigram_name_terms("isReady"), vec!["isready", "ready"]);
7712 assert_eq!(trigram_name_terms("the before"), vec!["the", "before"]);
7713 assert!(trigram_name_terms("ab").is_empty());
7714 }
7715
7716 #[test]
7717 fn snake_case_query_admits_a_pascal_case_name_through_the_name_branch() {
7718 let conn = search_fixture(
7719 &[
7720 code_row("c1", "src/collapse.rs", "rust", "CollapseName", ""),
7721 code_row("c2", "src/other.rs", "rust", "name_collapsed", ""),
7722 ]
7723 .join(","),
7724 );
7725
7726 let candidates =
7727 collect_search_candidates(&conn, "collapse_name", None, None, false, 10).unwrap();
7728
7729 let target = candidate(&candidates, "CollapseName");
7730 assert!(target.name_match);
7731 assert_eq!(target.name_terms, vec!["collapse", "name"]);
7732 assert_eq!(search_names(&conn, "collapse_name")[0], "CollapseName");
7733 }
7734
7735 fn plain_candidate(name: &str, kind: &str, path: &str) -> Candidate {
7736 Candidate {
7737 result: SymbolSearchResult {
7738 symbol: Symbol {
7739 symbol_id: format!("{path}:{name}"),
7740 file_id: "f".into(),
7741 path: path.into(),
7742 language: "rust".into(),
7743 name: name.into(),
7744 kind: kind.into(),
7745 signature: None,
7746 doc_comment: None,
7747 visibility: None,
7748 parent_symbol_id: None,
7749 start_line: 1,
7750 start_column: 0,
7751 end_line: 1,
7752 end_column: 0,
7753 start_byte: 0,
7754 end_byte: 0,
7755 body_start_line: None,
7756 body_start_column: None,
7757 body_end_line: None,
7758 body_end_column: None,
7759 body_start_byte: None,
7760 body_end_byte: None,
7761 body_hash: None,
7762 semantic_group: None,
7763 is_test: false,
7764 test_container: false,
7765 },
7766 score: 0.0,
7767 snippet: None,
7768 explain: None,
7769 },
7770 bm25: None,
7771 exact_name: false,
7772 word_match: false,
7773 name_match: false,
7774 owner_match: false,
7775 name_terms: Vec::new(),
7776 documentation: false,
7777 nested: false,
7778 owner: None,
7779 owner_context: None,
7780 }
7781 }
7782
7783 fn function(name: &str) -> Candidate {
7784 plain_candidate(name, "function", "src/lib.rs")
7785 }
7786
7787 fn ranked(candidates: Vec<Candidate>, query: &str) -> Vec<(SymbolSearchResult, SearchExplain)> {
7788 rerank_with(candidates, query, false, None)
7789 }
7790
7791 fn ranked_names(candidates: Vec<Candidate>, query: &str) -> Vec<String> {
7792 ranked(candidates, query)
7793 .into_iter()
7794 .map(|(r, _)| r.symbol.name)
7795 .collect()
7796 }
7797
7798 fn documented(name: &str, signature: Option<&str>, doc: &str) -> Candidate {
7799 let mut candidate = function(name);
7800 candidate.result.symbol.signature = signature.map(str::to_string);
7801 candidate.result.symbol.doc_comment = Some(doc.into());
7802 candidate
7803 }
7804
7805 fn strip_ansi_case() -> Vec<Candidate> {
7806 vec![
7807 documented("strip_ansi", None, "Remove ANSI escape sequences"),
7808 documented(
7809 "_strip_code_fences",
7810 Some("def _strip_code_fences(text: str)"),
7811 "The first fenced code block's body, or the stripped text",
7812 ),
7813 ]
7814 }
7815
7816 #[test]
7817 fn rerank_words_split_identifiers_and_drop_stop_words_only_beside_content_words() {
7818 assert_eq!(
7819 rerank_words("parse the sha256 sidecar file"),
7820 vec!["parse", "sha", "256", "sidecar", "file"]
7821 );
7822 assert_eq!(
7823 rerank_words("parse_sha256_sidecar"),
7824 vec!["parse", "sha", "256", "sidecar"]
7825 );
7826 assert_eq!(
7827 rerank_words("ParseHTTPResponse"),
7828 vec!["parse", "http", "response"]
7829 );
7830 assert_eq!(rerank_words("is_ok"), vec!["ok"]);
7831 assert_eq!(rerank_words("the before"), vec!["the", "before"]);
7832 }
7833
7834 #[test]
7835 fn stop_words_cover_english_function_words_but_not_identifier_directions() {
7836 for word in ["was", "whether", "another"] {
7837 assert!(is_stop_word(word), "{word} must be a stop word");
7838 }
7839 for word in ["down", "into", "run"] {
7840 assert!(!is_stop_word(word), "{word} must stay a content word");
7841 }
7842 assert_eq!(rerank_words("what was the file"), vec!["file"]);
7843 }
7844
7845 #[test]
7846 fn a_public_name_sorts_before_its_private_twin_at_an_equal_score() {
7847 let mut private = function("_create_skill");
7848 private.bm25 = Some(-9.0);
7849 let mut public = function("create_skill");
7850 public.bm25 = Some(-1.0);
7851
7852 let rows = ranked(vec![private, public], "create skill");
7853
7854 assert_eq!(rows[0].0.score, rows[1].0.score);
7855 assert_eq!(rows[0].1.name_strength, rows[1].1.name_strength);
7856 assert_eq!(
7857 rows.iter()
7858 .map(|(r, _)| r.symbol.name.as_str())
7859 .collect::<Vec<_>>(),
7860 vec!["create_skill", "_create_skill"]
7861 );
7862 }
7863
7864 #[test]
7865 fn a_whole_name_constant_yields_to_a_function_that_holds_the_word_with_context() {
7866 let rows = ranked(
7867 vec![
7868 plain_candidate("Glob", "constant", "src/glob.rs"),
7869 function("matches_glob_pattern"),
7870 ],
7871 "glob",
7872 );
7873
7874 assert_eq!(rows[0].0.symbol.name, "matches_glob_pattern");
7875 assert_eq!(rows[1].1.name_tier, "whole");
7876 assert_eq!(rows[1].1.name_bonus, W_NAME_ALL_WORDS);
7877 }
7878
7879 #[test]
7880 fn name_tiers_are_whole_then_all_words_then_partial_then_none() {
7881 let rows = ranked(
7882 vec![
7883 function("validate_everything"),
7884 function("validate_syntax_now"),
7885 function("validate_syntax"),
7886 function("unrelated"),
7887 ],
7888 "validate syntax",
7889 );
7890 let tiers: Vec<(&str, &str, f64)> = rows
7891 .iter()
7892 .map(|(r, e)| (r.symbol.name.as_str(), e.name_tier.as_str(), e.name_bonus))
7893 .collect();
7894
7895 assert_eq!(
7896 tiers,
7897 vec![
7898 ("validate_syntax", "whole", W_NAME_WHOLE),
7899 ("validate_syntax_now", "all", W_NAME_ALL_WORDS),
7900 ("validate_everything", "partial", 0.0),
7901 ("unrelated", "none", 0.0),
7902 ]
7903 );
7904 assert_eq!(rows[0].0.score, W_NAME_WHOLE + W_TERMS + W_KIND_DEFINITION);
7905 assert_eq!(
7906 rows[1].0.score,
7907 W_NAME_ALL_WORDS + W_TERMS + W_KIND_DEFINITION
7908 );
7909 assert_eq!(rows[2].0.score, W_TERMS / 2.0 + W_KIND_DEFINITION);
7910 assert_eq!(rows[3].0.score, W_KIND_DEFINITION);
7911 }
7912
7913 #[test]
7914 fn distinct_scoring_prefers_three_terms_covered_once_over_two_terms_repeated() {
7915 assert_eq!(
7916 ranked_names(strip_ansi_case(), "strip ansi escape codes"),
7917 vec!["strip_ansi", "_strip_code_fences"]
7918 );
7919 }
7920
7921 #[test]
7922 fn distinct_scoring_denies_the_all_words_bonus_to_a_substring_only_name() {
7923 let candidates = vec![
7924 function("execute_julie_extract"),
7925 documented("slice_bytes", None, "cut a byte range"),
7926 ];
7927
7928 let rows = ranked(candidates, "cut");
7929 let bonuses: Vec<(&str, &str, f64)> = rows
7930 .iter()
7931 .map(|(r, e)| (r.symbol.name.as_str(), e.name_tier.as_str(), e.name_bonus))
7932 .collect();
7933
7934 assert_eq!(
7935 bonuses,
7936 vec![
7937 ("execute_julie_extract", "partial", 0.0),
7938 ("slice_bytes", "none", 0.0),
7939 ]
7940 );
7941 assert_eq!(rows[0].0.score, rows[1].0.score);
7942 }
7943
7944 #[test]
7945 fn distinct_scoring_keeps_the_whole_name_and_all_words_tiers_in_order() {
7946 let candidates = vec![
7947 function("validate_everything"),
7948 function("validate_syntax_now"),
7949 function("validate_syntax"),
7950 function("unrelated"),
7951 ];
7952
7953 assert_eq!(
7954 ranked_names(candidates, "validate syntax"),
7955 vec![
7956 "validate_syntax",
7957 "validate_syntax_now",
7958 "validate_everything",
7959 "unrelated"
7960 ]
7961 );
7962 }
7963
7964 #[test]
7965 fn idf_weights_rank_a_term_in_one_row_above_a_term_in_most_rows() {
7966 let mut rows: Vec<String> = (0..10)
7967 .map(|i| {
7968 code_row(
7969 &format!("s{i}"),
7970 &format!("src/f{i}.rs"),
7971 "rust",
7972 &format!("search_{i}"),
7973 "searches the index",
7974 )
7975 })
7976 .collect();
7977 rows.push(code_row(
7978 "rare",
7979 "src/rare.rs",
7980 "rust",
7981 "sanitize_input",
7982 "sanitize the input",
7983 ));
7984 let conn = search_fixture(&rows.join(", "));
7985 let terms = vec!["sanitize".to_string(), "search".to_string()];
7986
7987 let weights = idf_weights(&conn, &terms);
7988 assert!(weights[0] > weights[1]);
7989 }
7990
7991 #[test]
7992 fn idf_weights_count_a_term_the_way_the_index_tokenizer_stems_it() {
7993 let conn = search_fixture(&code_row(
7994 "n",
7995 "src/news.rs",
7996 "rust",
7997 "fetch_news",
7998 "fetch the news feed",
7999 ));
8000 let terms = vec!["news".to_string(), "unseen".to_string()];
8001
8002 let weights = idf_weights(&conn, &terms);
8003
8004 assert!(weights[0] < weights[1]);
8005 }
8006
8007 #[test]
8008 fn a_signature_hit_past_the_head_byte_cap_does_not_credit_its_term() {
8009 let crediting_field = |padding: usize| {
8010 let mut row = function("handler");
8011 row.result.symbol.signature = Some(format!(
8012 "fn handler({}sidecar: u8)",
8013 "a: u8, ".repeat(padding)
8014 ));
8015 ranked(vec![row], "sidecar")[0].1.terms[0].1.clone()
8016 };
8017
8018 assert_eq!(crediting_field(4), "signature");
8019 assert_eq!(crediting_field(80), "none");
8020 }
8021
8022 #[test]
8023 fn explain_terms_name_the_crediting_field_of_every_query_term() {
8024 let rows = ranked(strip_ansi_case(), "strip ansi escape codes");
8025 let terms: Vec<(&str, &str, f64)> = rows[0]
8026 .1
8027 .terms
8028 .iter()
8029 .map(|(term, field, credit)| (term.as_str(), field.as_str(), *credit))
8030 .collect();
8031
8032 assert_eq!(rows[0].0.symbol.name, "strip_ansi");
8033 assert_eq!(
8034 terms,
8035 vec![
8036 ("strip", "name", 3.0),
8037 ("ansi", "name", 3.0),
8038 ("escape", "doc", TEXT_CREDIT),
8039 ("codes", "none", 0.0),
8040 ]
8041 );
8042 }
8043
8044 #[test]
8045 fn credited_fields_require_whole_name_multi_field_evidence_and_query_coverage() {
8046 let terms = vec![
8047 ("wsgi".into(), "name".into(), 3.0),
8048 ("response".into(), "signature".into(), TEXT_CREDIT),
8049 ("request".into(), "doc".into(), TEXT_CREDIT),
8050 ("error".into(), "doc".into(), TEXT_CREDIT),
8051 ];
8052 assert_eq!(credited_field_bonus(&terms), FIELD_DIVERSITY_BONUS);
8053 assert_eq!(credited_field_bonus(&terms[..2]), 0.0);
8054 assert_eq!(
8055 credited_field_bonus(&[
8056 ("wsgi".into(), "name".into(), 2.0),
8057 ("response".into(), "signature".into(), TEXT_CREDIT),
8058 ("request".into(), "doc".into(), TEXT_CREDIT),
8059 ("error".into(), "doc".into(), TEXT_CREDIT),
8060 ]),
8061 0.0
8062 );
8063 let sparse_terms = [
8064 ("terminal".into(), "name".into(), 3.0),
8065 ("docker".into(), "signature".into(), TEXT_CREDIT),
8066 ("local".into(), "doc".into(), TEXT_CREDIT),
8067 ("sandbox".into(), "doc".into(), TEXT_CREDIT),
8068 ("seven".into(), "none".into(), 0.0),
8069 ("ssh".into(), "none".into(), 0.0),
8070 ("singularity".into(), "none".into(), 0.0),
8071 ("modal".into(), "none".into(), 0.0),
8072 ("daytona".into(), "none".into(), 0.0),
8073 ("vercel".into(), "none".into(), 0.0),
8074 ];
8075 assert_eq!(credited_field_bonus(&sparse_terms), 0.0);
8076 assert_eq!(
8077 credited_field_bonus(&[
8078 ("response".into(), "name".into(), 3.0),
8079 ("request".into(), "name".into(), 3.0),
8080 ]),
8081 0.0
8082 );
8083 }
8084
8085 #[test]
8086 fn credited_field_bonus_requires_two_distinct_document_terms() {
8087 let terms = [
8088 ("strip".into(), "name".into(), 3.0),
8089 ("path".into(), "signature".into(), TEXT_CREDIT),
8090 ("windows".into(), "doc".into(), TEXT_CREDIT),
8091 ];
8092 assert_eq!(credited_field_bonus(&terms), 0.0);
8093 }
8094
8095 #[test]
8096 fn name_coverage_accepts_token_runs_substrings_and_stems() {
8097 let strengths = |name: &str, query: &str| {
8098 let stemmer = Stemmer::create(Algorithm::English);
8099 let words = stemmed_query_words(query, &stemmer);
8100 name_hits(name, &words, &stemmer)
8101 };
8102
8103 assert_eq!(strengths("parseSha256Sidecar", "sha 256"), vec![3, 3]);
8104 assert_eq!(strengths("parseSha256Sidecar", "sha256"), vec![3, 3]);
8105 assert_eq!(strengths("parseSha256Sidecar", "esha"), vec![1]);
8106 assert_eq!(strengths("validate_syntax", "validation"), vec![2]);
8107 assert_eq!(strengths("is_ok", "ok"), vec![3]);
8108 assert_eq!(strengths("isReady", "is"), vec![3]);
8109 assert_eq!(strengths("größe_berechnen", "größe"), vec![3]);
8110 assert_eq!(
8111 strengths("parseSha256Sidecar", "sidecar checksum"),
8112 vec![3, 0]
8113 );
8114 assert_eq!(
8115 strengths("parseSha256Sidecar", "checksum digest"),
8116 vec![0, 0]
8117 );
8118 }
8119
8120 #[test]
8121 fn a_rarer_term_moves_the_score_more_than_a_common_one() {
8122 let weights = [4.0, 1.0];
8123 let rows = rerank_with(
8124 vec![function("rare_helper"), function("common_helper")],
8125 "rare common",
8126 false,
8127 Some(&weights),
8128 );
8129
8130 assert_eq!(
8131 rows[0].1.word_weights,
8132 vec![("rare".to_string(), 4.0), ("common".to_string(), 1.0)]
8133 );
8134 assert_eq!(rows[0].0.symbol.name, "rare_helper");
8135 assert_eq!(rows[0].0.score, W_TERMS * 4.0 / 5.0 + W_KIND_DEFINITION);
8136 assert_eq!(rows[1].0.score, W_TERMS * 1.0 / 5.0 + W_KIND_DEFINITION);
8137 }
8138
8139 #[test]
8140 fn any_name_hit_outranks_a_zero_coverage_definition_for_long_queries() {
8141 let rows = ranked(
8142 vec![
8143 function("render_mode"),
8144 plain_candidate("retry_count", "constant", "src/scan.rs"),
8145 ],
8146 "how many times a failed download is tried again retry limit",
8147 );
8148
8149 assert_eq!(rows[0].0.symbol.name, "retry_count");
8150 assert_eq!(rows[0].1.name_tier, "partial");
8151 assert!(rows[0].0.score > rows[1].0.score);
8152 assert_eq!(rows[1].0.score, W_KIND_DEFINITION);
8153 }
8154
8155 #[test]
8156 fn a_doc_hit_past_the_head_byte_cap_does_not_credit_its_term() {
8157 let mut row = function("load");
8158 row.result.symbol.signature = Some("fn load(config: &Config) -> Loaded".into());
8159 row.result.symbol.doc_comment = Some(format!("{}settings", "é".repeat(321)));
8160 let (result, explain) = ranked(vec![row], "config settings").remove(0);
8161
8162 assert_eq!(
8163 explain.terms,
8164 vec![
8165 ("config".into(), "signature".into(), TEXT_CREDIT),
8166 ("settings".into(), "none".into(), 0.0),
8167 ]
8168 );
8169 assert_eq!(result.score, explain.term_score + W_KIND_DEFINITION);
8170 }
8171
8172 #[test]
8173 fn text_coverage_matches_whole_tokens_by_word_or_stem_prefix() {
8174 let crediting_field =
8175 |row: Candidate, query: &str| ranked(vec![row], query).remove(0).1.terms[0].1.clone();
8176 let doc_field = |doc: &str, query: &str| {
8177 let mut row = function("row");
8178 row.result.symbol.doc_comment = Some(doc.into());
8179 crediting_field(row, query)
8180 };
8181 let sig_field = |signature: &str, query: &str| {
8182 let mut row = function("row");
8183 row.result.symbol.signature = Some(signature.into());
8184 crediting_field(row, query)
8185 };
8186
8187 assert_eq!(doc_field("The system runs.", "stemming"), "none");
8188 assert_eq!(doc_field("The stemmer runs.", "stemming"), "doc");
8189 assert_eq!(doc_field("Compares stems.", "stemming"), "doc");
8190 assert_eq!(doc_field("An important port.", "porter"), "none");
8191 assert_eq!(
8192 sig_field("fn sha256sum(data: &[u8]) -> String", "sha256"),
8193 "signature"
8194 );
8195 assert_eq!(sig_field("fn is_ok()", "ok"), "signature");
8196 assert_eq!(sig_field("fn okay()", "ok"), "none");
8197 assert_eq!(
8198 sig_field("fn parseSha256Sidecar(text)", "sidecar"),
8199 "signature"
8200 );
8201 }
8202
8203 #[test]
8204 fn text_tokens_split_like_query_words_then_identifiers() {
8205 fn two_pass(text: &str) -> Vec<&str> {
8206 query_words(text)
8207 .into_iter()
8208 .flat_map(split_identifier)
8209 .collect()
8210 }
8211 fn one_pass(text: &str) -> Vec<&str> {
8212 let mut out = Vec::new();
8213 text_tokens_into(text, &mut out);
8214 out
8215 }
8216 let ascii = "fn parseHTTPResponse2(raw: &str, _id: u8) -> Vec<&str> // sha256_sum";
8217 let unicode = "Berechnet die Größe: größe_berechnen(pfad) -> ÜberGroß2x";
8218
8219 assert_eq!(one_pass(ascii), two_pass(ascii));
8220 assert_eq!(
8221 one_pass(ascii),
8222 vec![
8223 "fn", "parse", "HTTP", "Response", "2", "raw", "str", "id", "u", "8", "Vec", "str",
8224 "sha", "256", "sum",
8225 ]
8226 );
8227 assert_eq!(one_pass(unicode), two_pass(unicode));
8228 assert!(one_pass("").is_empty());
8229 assert!(one_pass("_ __ ...").is_empty());
8230 }
8231
8232 #[test]
8233 fn a_doc_credits_a_term_by_its_stem() {
8234 let mut row = function("check");
8235 row.result.symbol.doc_comment = Some("Validates the input.".into());
8236 let explain = ranked(vec![row], "validation").remove(0).1;
8237
8238 assert_eq!(
8239 explain.terms,
8240 vec![("validation".into(), "doc".into(), TEXT_CREDIT)]
8241 );
8242 }
8243
8244 #[test]
8245 fn kind_prior_orders_definitions_over_members_over_imports() {
8246 let rows = ranked(
8247 vec![
8248 plain_candidate("Scan", "import", "src/a.rs"),
8249 plain_candidate("Scan", "enum_member", "src/b.rs"),
8250 plain_candidate("Scan", "function", "src/c.rs"),
8251 ],
8252 "scan",
8253 );
8254 let order: Vec<(&str, f64)> = rows
8255 .iter()
8256 .map(|(r, e)| (r.symbol.path.as_str(), e.kind_prior))
8257 .collect();
8258
8259 assert_eq!(
8260 order,
8261 vec![
8262 ("src/c.rs", W_KIND_DEFINITION),
8263 ("src/b.rs", W_KIND_MEMBER),
8264 ("src/a.rs", W_KIND_IMPORT),
8265 ]
8266 );
8267 }
8268
8269 #[test]
8270 fn a_partial_name_match_on_a_member_beats_the_kind_prior_of_a_function() {
8271 let names = ranked_names(
8272 vec![
8273 function("RenderMode"),
8274 plain_candidate("MaxRetryCount", "constant", "pkg/scan.go"),
8275 ],
8276 "retry download limit timeout",
8277 );
8278
8279 assert_eq!(names[0], "MaxRetryCount");
8280 }
8281
8282 #[test]
8283 fn path_role_demotes_role_directories_unless_the_query_names_them() {
8284 let rows = |query: &str| {
8285 ranked(
8286 vec![
8287 plain_candidate("verifyChecksum", "function", "scripts/launcher.ts"),
8288 plain_candidate("verify_checksum", "function", "src/archive.rs"),
8289 ],
8290 query,
8291 )
8292 };
8293
8294 let plain = rows("verify checksum");
8295 assert_eq!(plain[0].0.symbol.path, "src/archive.rs");
8296 assert_eq!(plain[1].1.path_role, W_PATH_ROLE);
8297
8298 let named = rows("launcher script verify checksum");
8299 assert!(named.iter().all(|(_, e)| e.path_role == 0.0));
8300
8301 let only_launcher = rows("launcher verify checksum");
8302 assert_eq!(only_launcher[0].0.symbol.path, "src/archive.rs");
8303 assert_eq!(only_launcher[1].1.path_role, W_PATH_ROLE);
8304
8305 let windows = ranked(
8306 vec![plain_candidate(
8307 "verifyChecksum",
8308 "function",
8309 "scripts\\launcher.ts",
8310 )],
8311 "verify checksum",
8312 );
8313 assert_eq!(windows[0].1.path_role, W_PATH_ROLE);
8314 }
8315
8316 #[test]
8317 fn documentation_rows_sort_after_every_code_row() {
8318 let mut heading = plain_candidate("Verify checksum", "heading", "README.md");
8319 heading.documentation = true;
8320 heading.result.symbol.language = "markdown".into();
8321 heading.result.symbol.signature = Some("Verify checksum".into());
8322 heading.result.symbol.doc_comment = Some("Verify the checksum of the archive.".into());
8323 let rows = ranked(
8324 vec![
8325 heading,
8326 plain_candidate("unrelated", "variable", "src/a.rs"),
8327 ],
8328 "verify checksum",
8329 );
8330
8331 assert_eq!(rows[0].0.symbol.name, "unrelated");
8332 assert_eq!(rows[1].1.documentation, W_DOCUMENTATION_ROW);
8333 assert_eq!(rows[1].1.name_tier, "whole");
8334 assert!(rows[1].0.score < 0.0);
8335 }
8336
8337 #[test]
8338 fn a_helper_nested_in_a_function_ranks_below_an_equal_module_function() {
8339 let mut helper = plain_candidate("run_test", "function", "tests/test_basic.py");
8340 helper.nested = true;
8341 let module_function = plain_candidate("run_dotenv", "function", "tests/test_basic.py");
8342
8343 let rows = rerank_with(vec![helper, module_function], "run", true, None);
8344
8345 assert_eq!(rows[0].0.symbol.name, "run_dotenv");
8346 assert_eq!(rows[1].1.nested, W_NESTED);
8347 }
8348
8349 #[test]
8350 fn test_intent_boosts_test_rows_only_when_tests_are_included_and_named() {
8351 let rows = |query: &str, include_tests: bool| {
8352 let mut test_row = plain_candidate("payment_flow", "function", "tests/payment.rs");
8353 test_row.result.symbol.is_test = true;
8354 let plain_row = plain_candidate("payment_flow", "function", "src/payment.rs");
8355 rerank_with(vec![plain_row, test_row], query, include_tests, None)
8356 };
8357
8358 let boosted = rows("payment flow tests", true);
8359 assert_eq!(boosted[0].0.symbol.path, "tests/payment.rs");
8360 assert_eq!(boosted[0].1.test_intent, W_TEST_INTENT);
8361 assert_eq!(boosted[1].1.test_intent, 0.0);
8362
8363 assert!(
8364 rows("payment flow tests", false)
8365 .iter()
8366 .all(|(_, e)| e.test_intent == 0.0)
8367 );
8368 assert!(
8369 rows("payment flow", true)
8370 .iter()
8371 .all(|(_, e)| e.test_intent == 0.0)
8372 );
8373 }
8374
8375 #[test]
8376 fn ties_break_by_bm25_then_name_length_then_path() {
8377 let mut word_row = plain_candidate("payment", "function", "src/z.rs");
8378 word_row.word_match = true;
8379 word_row.bm25 = Some(-4.0);
8380 let mut weaker_word_row = plain_candidate("payment", "function", "src/a.rs");
8381 weaker_word_row.word_match = true;
8382 weaker_word_row.bm25 = Some(-2.0);
8383 let mut name_only = plain_candidate("payment", "function", "src/b.rs");
8384 name_only.name_match = true;
8385 let rows = ranked(
8386 vec![
8387 plain_candidate("payment", "function", "src/y.rs"),
8388 name_only,
8389 weaker_word_row,
8390 word_row,
8391 ],
8392 "payment",
8393 );
8394 let paths: Vec<&str> = rows.iter().map(|(r, _)| r.symbol.path.as_str()).collect();
8395
8396 assert_eq!(paths, vec!["src/z.rs", "src/a.rs", "src/b.rs", "src/y.rs"]);
8397
8398 let by_length = ranked_names(
8399 vec![
8400 function("payment_gateway_client"),
8401 function("payment_gateway"),
8402 ],
8403 "gateway",
8404 );
8405 assert_eq!(by_length, vec!["payment_gateway", "payment_gateway_client"]);
8406 }
8407
8408 #[test]
8409 fn a_whole_token_name_outranks_a_substring_name_with_better_bm25() {
8410 let mut token = function("csr");
8411 token.bm25 = Some(-1.0);
8412 let mut substring = function("action_csrf_token");
8413 substring.bm25 = Some(-5.0);
8414
8415 let rows = ranked(vec![substring, token], "csr adjacency");
8416
8417 assert!(rows[0].0.score > rows[1].0.score);
8418 assert_eq!(rows[0].0.symbol.name, "csr");
8419 }
8420
8421 #[test]
8422 fn an_acronym_token_outranks_a_name_that_only_contains_it() {
8423 let mut token = function("http_client");
8424 token.bm25 = Some(-1.0);
8425 let mut substring = function("shttpd_config");
8426 substring.bm25 = Some(-5.0);
8427
8428 let rows = ranked(vec![substring, token], "http");
8429
8430 assert!(rows[0].0.score > rows[1].0.score);
8431 assert_eq!(rows[0].0.symbol.name, "http_client");
8432 }
8433
8434 #[test]
8435 fn explain_reports_the_sum_of_the_name_strengths() {
8436 let rows = ranked(vec![function("action_csrf_token")], "csr token");
8437
8438 assert_eq!(rows[0].1.name_strength, 4);
8439 }
8440
8441 #[test]
8442 fn snippets_follow_the_admitting_branch() {
8443 let mut word_row = function("parse_sidecar_file");
8444 word_row.word_match = true;
8445 word_row.result.snippet = Some("parse the [sha256] sidecar file".into());
8446 let mut name_row = function("parseSha256Sidecar");
8447 name_row.name_match = true;
8448 name_row.name_terms = vec!["sha".into(), "sha256".into(), "256".into()];
8449 let mut exact_row = function("sha256");
8450 exact_row.exact_name = true;
8451 let rows = ranked(vec![word_row, name_row, exact_row], "sha256");
8452 let snippets: Vec<(&str, &str)> = rows
8453 .iter()
8454 .map(|(r, _)| (r.symbol.name.as_str(), r.snippet.as_deref().unwrap()))
8455 .collect();
8456
8457 assert_eq!(
8458 snippets,
8459 vec![
8460 ("sha256", "sha256"),
8461 ("parseSha256Sidecar", "parse[Sha256]Sidecar"),
8462 ("parse_sidecar_file", "parse the [sha256] sidecar file"),
8463 ]
8464 );
8465 assert_eq!(rows[1].1.branches, vec!["name"]);
8466 assert_eq!(rows[0].1.branches, vec!["exact"]);
8467 }
8468
8469 #[test]
8470 fn explain_is_attached_only_when_requested() {
8471 let conn = sidecar_fixture();
8472 let query = "sha256";
8473
8474 let silent = fts_search_symbols_scoped(&conn, query, None, None, false, 10).unwrap();
8475 assert!(silent.iter().all(|r| r.explain.is_none()));
8476 assert!(silent[0].score > 0.0);
8477 assert_eq!(
8478 serde_json::to_value(&silent[0]).unwrap().get("explain"),
8479 None
8480 );
8481
8482 let explained =
8483 fts_search_symbols_explained(&conn, query, None, None, false, 10, true).unwrap();
8484 let by_name = |name: &str| {
8485 explained
8486 .iter()
8487 .find(|r| r.symbol.name == name)
8488 .and_then(|r| r.explain.as_ref())
8489 .unwrap()
8490 };
8491 let name_only = by_name("parseSha256Sidecar");
8492 assert_eq!(name_only.candidates, 2);
8493 assert_eq!(name_only.branches, vec!["name"]);
8494 assert_eq!(name_only.bm25, None);
8495 let word_row = by_name("parse_sidecar_file");
8496 assert!(word_row.bm25.unwrap() < 0.0);
8497 assert_eq!(word_row.candidates, 2);
8498 assert!(
8499 serde_json::to_value(&explained[0])
8500 .unwrap()
8501 .get("explain")
8502 .is_some()
8503 );
8504 }
8505
8506 #[test]
8507 fn search_symbols_treats_like_wildcards_as_literals() {
8508 let dir = crate::safe_tempdir();
8509 let db_path = dir.path().join("search_symbols_treats_like_wildcards.db");
8510 let conn = open_read_write(&db_path).unwrap();
8511 conn.execute_batch(
8512 "CREATE TABLE symbols (
8513 symbol_id TEXT, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
8514 signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
8515 start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
8516 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
8517 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
8518 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
8519 semantic_group TEXT, is_test INTEGER, test_container INTEGER
8520 );
8521 INSERT INTO symbols VALUES (
8522 's', 'f', 'src/lib.rs', 'rust', 'ordinary', 'function', NULL, NULL, NULL, NULL,
8523 1, 0, 1, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0
8524 );
8525 INSERT INTO symbols VALUES (
8526 'p', 'f', 'src/lib.rs', 'rust', 'literal%name', 'function', NULL, NULL, NULL, NULL,
8527 1, 0, 1, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0
8528 );
8529 INSERT INTO symbols VALUES (
8530 'u', 'f', 'src/lib.rs', 'rust', 'literal_name', 'function', NULL, NULL, NULL, NULL,
8531 1, 0, 1, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0
8532 );
8533 CREATE TABLE files (
8534 file_id TEXT, path TEXT, language TEXT, content_hash TEXT,
8535 content_bytes INTEGER, line_count INTEGER, indexed_at TEXT
8536 );
8537 INSERT INTO files VALUES ('f1', 'src/literal_path/lib.rs', 'rust', 'hash', 0, 0, 'now');
8538 INSERT INTO files VALUES ('f2', 'src/literalXpath/lib.rs', 'rust', 'hash', 0, 0, 'now'
8539 );",
8540 )
8541 .unwrap();
8542
8543 assert_eq!(
8544 search_symbols(&conn, "%", None, false, 10).unwrap()[0].name,
8545 "literal%name"
8546 );
8547 assert_eq!(
8548 search_symbols(&conn, "_", None, false, 10).unwrap()[0].name,
8549 "literal_name"
8550 );
8551 assert_eq!(
8552 load_scoped_files(&conn, Some("src/literal_path"))
8553 .unwrap()
8554 .len(),
8555 1
8556 );
8557 }
8558
8559 #[test]
8560 fn find_references_for_symbol_limits_callees_by_symbol_id() {
8561 let dir = crate::safe_tempdir();
8562 let db_path = dir.path().join("find_references_for_symbol.db");
8563 let conn = open_read_write(&db_path).unwrap();
8564 conn.execute_batch(
8565 "CREATE TABLE symbols (
8566 symbol_id TEXT, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
8567 signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
8568 start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
8569 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
8570 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
8571 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
8572 semantic_group TEXT, is_test INTEGER, test_container INTEGER
8573 );
8574 CREATE TABLE relationships (
8575 from_symbol_id TEXT, to_symbol_id TEXT, kind TEXT, path TEXT,
8576 start_line INTEGER, start_column INTEGER
8577 );
8578 CREATE TABLE pending_relationships (
8579 from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT, path TEXT,
8580 start_line INTEGER, start_column INTEGER
8581 );
8582 INSERT INTO symbols VALUES
8583 ('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),
8584 ('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),
8585 ('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),
8586 ('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);
8587 INSERT INTO relationships VALUES
8588 ('other', 'other-callee', 'calls', 'b.rs', 1, 0),
8589 ('wanted', 'wanted-callee', 'calls', 'a.rs', 1, 0);",
8590 )
8591 .unwrap();
8592
8593 let references = find_references_for_symbol(&conn, "new", "callees", 1, "wanted").unwrap();
8594 assert_eq!(references.len(), 1);
8595 assert_eq!(references[0].to_symbol_name, "wanted_dep");
8596 }
8597
8598 #[test]
8599 fn test_fts_search_symbols_and_porter_stemming() {
8600 let dir = crate::safe_tempdir();
8601 let db_path = dir.path().join("fts_search_symbols.db");
8602 let conn = open_read_write(&db_path).unwrap();
8603
8604 conn.execute_batch(
8605 "CREATE TABLE symbols (
8606 symbol_id TEXT PRIMARY KEY,
8607 file_id TEXT,
8608 path TEXT,
8609 language TEXT,
8610 name TEXT,
8611 kind TEXT,
8612 signature TEXT,
8613 doc_comment TEXT,
8614 visibility TEXT,
8615 parent_symbol_id TEXT,
8616 start_line INTEGER,
8617 start_column INTEGER,
8618 end_line INTEGER,
8619 end_column INTEGER,
8620 start_byte INTEGER,
8621 end_byte INTEGER,
8622 body_start_line INTEGER,
8623 body_start_column INTEGER,
8624 body_end_line INTEGER,
8625 body_end_column INTEGER,
8626 body_start_byte INTEGER,
8627 body_end_byte INTEGER,
8628 body_hash TEXT,
8629 semantic_group TEXT,
8630 is_test INTEGER,
8631 test_container INTEGER
8632 );
8633 INSERT INTO symbols VALUES (
8634 's1', 'f1', 'src/payment.rs', 'rust', 'PaymentGateway', 'trait',
8635 'pub trait PaymentGateway', 'Core payment provider interface for transactions',
8636 'pub', NULL, 10, 0, 20, 1, 100, 250, 12, 4, 19, 1, 120, 240, 'hash1', 'type', 0, 0
8637 );
8638 INSERT INTO symbols VALUES (
8639 's2', 'f1', 'src/payment.rs', 'rust', 'StripeClient', 'struct',
8640 'pub struct StripeClient', 'Handles HTTP requests to stripe payment API',
8641 'pub', NULL, 25, 0, 35, 1, 300, 450, 27, 4, 34, 1, 320, 440, 'hash2', 'type', 0, 0
8642 );
8643 INSERT INTO symbols VALUES (
8644 's3', 'f2', 'src/parser.rs', 'rust', 'parse_tokens', 'function',
8645 'pub fn parse_tokens(stream: &TokenStream) -> Result<Vec<Token>>', 'Parses syntax tokens from stream',
8646 'pub', NULL, 5, 0, 15, 1, 50, 200, 7, 4, 14, 1, 70, 190, 'hash3', 'function', 0, 0
8647 );
8648 INSERT INTO symbols VALUES (
8649 's4', 'f3', 'tests/payment_test.rs', 'rust', 'test_payment_flow', 'function',
8650 'fn test_payment_flow()', 'Tests payment charge workflow',
8651 NULL, NULL, 5, 0, 15, 1, 50, 200, 7, 4, 14, 1, 70, 190, 'hash4', 'function', 1, 0
8652 );",
8653 )
8654 .unwrap();
8655
8656 ensure_fts_index(&conn).unwrap();
8657
8658 let results =
8660 fts_search_symbols_scoped(&conn, "parsing tokens", None, None, false, 10).unwrap();
8661 assert_eq!(results.len(), 1);
8662 assert_eq!(results[0].symbol.name, "parse_tokens");
8663 assert!(results[0].snippet.is_some());
8664
8665 let results =
8667 fts_search_symbols_scoped(&conn, "transactions", None, None, false, 10).unwrap();
8668 assert_eq!(results.len(), 1);
8669 assert_eq!(results[0].symbol.name, "PaymentGateway");
8670
8671 let results = fts_search_symbols_scoped(&conn, "payment", None, None, false, 10).unwrap();
8673 assert_eq!(results.len(), 2);
8674 assert!(results.iter().all(|r| !r.symbol.is_test));
8675
8676 let results = fts_search_symbols_scoped(&conn, "payment", None, None, true, 10).unwrap();
8678 assert_eq!(results.len(), 3);
8679
8680 let results =
8682 fts_search_symbols_scoped(&conn, "stripe kafka redis", None, None, false, 10).unwrap();
8683 assert_eq!(results.len(), 1);
8684 assert_eq!(results[0].symbol.name, "StripeClient");
8685 }
8686
8687 #[test]
8688 fn find_related_tests_returns_each_test_once_under_the_limit() {
8689 let dir = crate::safe_tempdir();
8690 let conn = open_read_write(&dir.path().join("related_tests_limit.db")).unwrap();
8691 conn.execute_batch(
8692 "CREATE TABLE symbols (
8693 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT,
8694 name TEXT, kind TEXT, signature TEXT, doc_comment TEXT,
8695 visibility TEXT, parent_symbol_id TEXT, start_line INTEGER,
8696 start_column INTEGER, end_line INTEGER, end_column INTEGER,
8697 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
8698 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
8699 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
8700 semantic_group TEXT, is_test INTEGER, test_container INTEGER
8701 );
8702 CREATE TABLE relationships (
8703 from_symbol_id TEXT, to_symbol_id TEXT, kind TEXT, path TEXT,
8704 start_line INTEGER, start_column INTEGER
8705 );
8706 CREATE TABLE pending_relationships (
8707 from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT, path TEXT,
8708 start_line INTEGER, start_column INTEGER,
8709 target_receiver TEXT, target_namespace_json TEXT, target_display_name TEXT
8710 );
8711 CREATE TABLE type_facts (
8712 type_fact_id TEXT, symbol_id TEXT, language TEXT, resolved_type TEXT, generic_params_json TEXT
8713 );
8714 INSERT INTO symbols VALUES
8715 ('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),
8716 ('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),
8717 ('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);
8718 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
8719 ('t_a', 'compute', 'calls', 'tests/a.rs', 3, 4, NULL, NULL, 'compute'),
8720 ('t_a', 'compute', 'calls', 'tests/a.rs', 5, 4, NULL, NULL, 'compute'),
8721 ('t_a', 'compute', 'calls', 'tests/a.rs', 7, 4, NULL, NULL, 'compute'),
8722 ('t_a', 'compute', 'calls', 'tests/a.rs', 9, 4, NULL, NULL, 'compute'),
8723 ('t_a', 'compute', 'calls', 'tests/a.rs', 11, 4, NULL, NULL, 'compute'),
8724 ('t_b', 'compute', 'calls', 'tests/b.rs', 3, 4, NULL, NULL, 'compute');",
8725 )
8726 .unwrap();
8727 let target = get_symbol_by_name(&conn, "compute", None).unwrap().unwrap();
8728
8729 let tests = find_related_tests(&conn, &target, 5).unwrap();
8730
8731 let mut names: Vec<&str> = tests.iter().map(|t| t.name.as_str()).collect();
8732 names.sort();
8733 assert_eq!(names, vec!["first_case", "second_case"]);
8734 }
8735
8736 #[test]
8737 fn documentation_rows_rank_after_code_in_search() {
8738 let dir = crate::safe_tempdir();
8739 let conn = open_read_write(&dir.path().join("doc_rank.db")).unwrap();
8740 conn.execute_batch(
8741 "CREATE TABLE symbols (
8742 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT,
8743 kind TEXT, signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
8744 start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
8745 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
8746 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
8747 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
8748 semantic_group TEXT, is_test INTEGER, test_container INTEGER, content_type TEXT
8749 );
8750 INSERT INTO symbols VALUES
8751 ('s_doc', 'f1', 'docs/plans/018.adoc', 'asciidoc', 'Reconcile offline edits',
8752 'heading', 'Reconcile offline edits', NULL, NULL, NULL,
8753 3, 0, 3, 1, 10, 40, 3, 0, 3, 1, 10, 40, 'hash_doc', NULL, 0, 0, 'documentation'),
8754 ('s_code', 'f2', 'src/sync.rs', 'rust', 'reconcile_offline_edits', 'function',
8755 'fn reconcile_offline_edits()', 'Reconcile offline edits at startup', 'pub', NULL,
8756 10, 0, 20, 1, 100, 250, 12, 4, 19, 1, 120, 240, 'hash_code', NULL, 0, 0, 'code');",
8757 )
8758 .unwrap();
8759 ensure_fts_index(&conn).unwrap();
8760
8761 let results =
8762 fts_search_symbols_scoped(&conn, "reconcile offline edits", None, None, false, 10)
8763 .unwrap();
8764
8765 assert_eq!(results.len(), 2);
8766 assert_eq!(results[0].symbol.name, "reconcile_offline_edits");
8767 assert_eq!(results[1].symbol.name, "Reconcile offline edits");
8768 }
8769
8770 #[test]
8771 fn test_queries_nocase_and_path_normalization() {
8772 let conn = Connection::open_in_memory().unwrap();
8773 conn.execute_batch(
8774 "CREATE TABLE files (
8775 file_id TEXT PRIMARY KEY,
8776 path TEXT NOT NULL,
8777 language TEXT,
8778 content_hash TEXT,
8779 content_bytes INTEGER,
8780 line_count INTEGER,
8781 indexed_at INTEGER
8782 );
8783 CREATE TABLE symbols (
8784 symbol_id TEXT PRIMARY KEY,
8785 file_id TEXT,
8786 path TEXT NOT NULL,
8787 language TEXT,
8788 name TEXT,
8789 kind TEXT,
8790 signature TEXT,
8791 doc_comment TEXT,
8792 visibility TEXT,
8793 parent_symbol_id TEXT,
8794 start_line INTEGER,
8795 start_column INTEGER,
8796 end_line INTEGER,
8797 end_column INTEGER,
8798 start_byte INTEGER,
8799 end_byte INTEGER,
8800 body_start_line INTEGER,
8801 body_start_column INTEGER,
8802 body_end_line INTEGER,
8803 body_end_column INTEGER,
8804 body_start_byte INTEGER,
8805 body_end_byte INTEGER,
8806 body_hash TEXT,
8807 semantic_group TEXT,
8808 is_test INTEGER,
8809 test_container INTEGER
8810 );
8811 -- Insert with backslashes and mixed casing to verify defensive normalization and COLLATE NOCASE
8812 INSERT INTO files VALUES ('f1', 'src\\Payment.rs', 'rust', 'hash1', 100, 10, '2026-09-14T00:00:00Z');
8813 INSERT INTO symbols VALUES (
8814 's1', 'f1', 'src\\Payment.rs', 'rust', 'ProcessPayment', 'function',
8815 'pub fn ProcessPayment()', NULL, 'pub', NULL, 1, 0, 5, 0, 0, 50,
8816 2, 4, 4, 1, 10, 45, 'bhash', 'function', 0, 0
8817 );",
8818 )
8819 .unwrap();
8820
8821 let file = get_file(&conn, "SRC/PAYMENT.RS")
8823 .unwrap()
8824 .expect("File should be found");
8825 assert_eq!(
8826 file.path, "src/Payment.rs",
8827 "Path should be normalized to forward slashes"
8828 );
8829
8830 let file2 = get_file(&conn, "src/payment.rs")
8831 .unwrap()
8832 .expect("File should be found");
8833 assert_eq!(file2.path, "src/Payment.rs");
8834
8835 let syms = load_file_symbols(&conn, "SRC/PAYMENT.RS").unwrap();
8837 assert_eq!(syms.len(), 1);
8838 assert_eq!(
8839 syms[0].path, "src/Payment.rs",
8840 "Symbol path should be normalized to forward slashes"
8841 );
8842
8843 let sym = get_symbol_by_name(&conn, "ProcessPayment", Some("SRC/PAYMENT.RS"))
8845 .unwrap()
8846 .expect("Symbol should be found with case-insensitive path filter");
8847 assert_eq!(sym.path, "src/Payment.rs");
8848 }
8849
8850 #[test]
8851 fn test_exact_case_prioritized_over_nocase() {
8852 let conn = Connection::open_in_memory().unwrap();
8853 conn.execute_batch(
8854 "CREATE TABLE files (
8855 file_id TEXT PRIMARY KEY,
8856 path TEXT NOT NULL,
8857 language TEXT,
8858 content_hash TEXT,
8859 content_bytes INTEGER,
8860 line_count INTEGER,
8861 indexed_at TEXT
8862 );
8863 CREATE TABLE symbols (
8864 symbol_id TEXT PRIMARY KEY,
8865 file_id TEXT,
8866 path TEXT NOT NULL,
8867 language TEXT,
8868 name TEXT NOT NULL,
8869 kind TEXT NOT NULL,
8870 signature TEXT,
8871 doc_comment TEXT,
8872 visibility TEXT,
8873 parent_symbol_id TEXT,
8874 start_line INTEGER,
8875 start_column INTEGER,
8876 end_line INTEGER,
8877 end_column INTEGER,
8878 start_byte INTEGER,
8879 end_byte INTEGER,
8880 body_start_line INTEGER,
8881 body_start_column INTEGER,
8882 body_end_line INTEGER,
8883 body_end_column INTEGER,
8884 body_start_byte INTEGER,
8885 body_end_byte INTEGER,
8886 body_hash TEXT,
8887 semantic_group TEXT,
8888 is_test INTEGER,
8889 test_container INTEGER
8890 );
8891 INSERT INTO files VALUES ('f1', 'src/Payment.rs', 'rust', 'h1', 100, 10, '2026-09-14T00:00:00Z');
8892 INSERT INTO files VALUES ('f2', 'src/payment.rs', 'rust', 'h2', 100, 10, '2026-09-14T00:00:00Z');
8893 INSERT INTO symbols VALUES (
8894 's1', 'f1', 'src/Payment.rs', 'rust', 'pay', 'function',
8895 'pub fn pay()', NULL, 'pub', NULL, 1, 0, 5, 0, 0, 50,
8896 2, 4, 4, 1, 10, 45, 'b1', 'function', 0, 0
8897 );
8898 INSERT INTO symbols VALUES (
8899 's2', 'f2', 'src/payment.rs', 'rust', 'pay', 'function',
8900 'pub fn pay()', NULL, 'pub', NULL, 1, 0, 5, 0, 0, 50,
8901 2, 4, 4, 1, 10, 45, 'b2', 'function', 0, 0
8902 );",
8903 )
8904 .unwrap();
8905
8906 let f_lower = get_file(&conn, "src/payment.rs").unwrap().unwrap();
8908 assert_eq!(f_lower.path, "src/payment.rs");
8909 assert_eq!(f_lower.file_id, "f2");
8910
8911 let f_upper = get_file(&conn, "src/Payment.rs").unwrap().unwrap();
8912 assert_eq!(f_upper.path, "src/Payment.rs");
8913 assert_eq!(f_upper.file_id, "f1");
8914
8915 let syms_lower = load_file_symbols(&conn, "src/payment.rs").unwrap();
8916 assert_eq!(syms_lower.len(), 1);
8917 assert_eq!(syms_lower[0].file_id, "f2");
8918
8919 let syms_upper = load_file_symbols(&conn, "src/Payment.rs").unwrap();
8920 assert_eq!(syms_upper.len(), 1);
8921 assert_eq!(syms_upper[0].file_id, "f1");
8922 }
8923
8924 #[test]
8925 fn test_conservative_pending_resolution_ignores_unmatched_namespace() {
8926 let dir = crate::safe_tempdir();
8927 let db_path = dir.path().join("conservative_resolution.db");
8928 let conn = open_read_write(&db_path).unwrap();
8929
8930 conn.execute_batch(
8931 "CREATE TABLE symbols (
8932 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT,
8933 name TEXT, kind TEXT, signature TEXT, doc_comment TEXT,
8934 visibility TEXT, parent_symbol_id TEXT, start_line INTEGER,
8935 start_column INTEGER, end_line INTEGER, end_column INTEGER,
8936 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
8937 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
8938 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
8939 semantic_group TEXT, is_test INTEGER, test_container INTEGER
8940 );
8941 CREATE TABLE relationships (
8942 from_symbol_id TEXT, to_symbol_id TEXT, kind TEXT, path TEXT,
8943 start_line INTEGER, start_column INTEGER
8944 );
8945 CREATE TABLE pending_relationships (
8946 from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT, path TEXT,
8947 start_line INTEGER, start_column INTEGER,
8948 target_receiver TEXT, target_namespace_json TEXT, target_display_name TEXT
8949 );
8950 CREATE TABLE type_facts (
8951 type_fact_id TEXT, symbol_id TEXT, language TEXT, resolved_type TEXT, generic_params_json TEXT
8952 );
8953 -- Workspace struct Workspace and method Workspace::new
8954 INSERT INTO symbols VALUES
8955 ('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),
8956 ('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),
8957 ('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);
8958
8959 -- my_func calls Vec::new() (external namespace 'Vec')
8960 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
8961 ('s_caller', 'new', 'calls', 'src/caller.rs', 3, 8, NULL, '[\"Vec\"]', 'Vec::new');",
8962 )
8963 .unwrap();
8964
8965 let sigs = find_callee_signatures(&conn, "my_func", "s_caller", 10, false).unwrap();
8967 assert!(sigs.is_empty(), "Expected 0 signatures, got: {:?}", sigs);
8968
8969 let refs = find_references_for_symbol(&conn, "my_func", "callees", 10, "s_caller").unwrap();
8970 assert!(refs.is_empty(), "Expected 0 references, got: {:?}", refs);
8971
8972 let callers = find_references_for_symbol(&conn, "new", "callers", 10, "s_ws_new").unwrap();
8974 assert!(
8975 callers.is_empty(),
8976 "Expected 0 callers for Workspace::new, got: {:?}",
8977 callers
8978 );
8979
8980 let blast = compute_blast_radius(&conn, &["new"], &["src/workspace.rs"], 2, 20).unwrap();
8982 assert!(
8983 !blast.impacted_symbols.iter().any(|s| s.name == "my_func"),
8984 "my_func should not be impacted before calling Workspace::new: {:?}",
8985 blast.impacted_symbols
8986 );
8987
8988 conn.execute(
8990 "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')",
8991 [],
8992 )
8993 .unwrap();
8994
8995 let sigs2 = find_callee_signatures(&conn, "my_func", "s_caller", 10, false).unwrap();
8996 assert_eq!(
8997 sigs2.len(),
8998 1,
8999 "Expected 1 signature for Workspace::new, got: {:?}",
9000 sigs2
9001 );
9002 assert!(sigs2[0].contains("pub fn new() -> Workspace"));
9003
9004 let blast2 = compute_blast_radius(&conn, &["new"], &["src/workspace.rs"], 2, 20).unwrap();
9006 assert!(
9007 blast2.impacted_symbols.iter().any(|s| s.name == "my_func"),
9008 "my_func should be impacted after calling Workspace::new: {:?}",
9009 blast2.impacted_symbols
9010 );
9011
9012 conn.execute(
9014 "INSERT INTO symbols VALUES
9015 ('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);",
9016 [],
9017 )
9018 .unwrap();
9019 conn.execute(
9020 "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')",
9021 [],
9022 )
9023 .unwrap();
9024
9025 let sigs_other = find_callee_signatures(&conn, "other_func", "s_other", 10, false).unwrap();
9027 assert!(
9028 sigs_other.is_empty(),
9029 "Bare call to new() from outside Workspace should not resolve to Workspace::new: {:?}",
9030 sigs_other
9031 );
9032
9033 conn.execute(
9035 "INSERT INTO symbols VALUES
9036 ('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);",
9037 [],
9038 )
9039 .unwrap();
9040 conn.execute(
9041 "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')",
9042 [],
9043 )
9044 .unwrap();
9045
9046 let sigs_sibling =
9047 find_callee_signatures(&conn, "helper", "s_ws_helper", 10, false).unwrap();
9048 assert_eq!(
9049 sigs_sibling.len(),
9050 1,
9051 "Sibling method calling bare new() should resolve to Workspace::new: {:?}",
9052 sigs_sibling
9053 );
9054
9055 let ext_sigs = find_callee_signatures(&conn, "my_func", "s_caller", 10, true).unwrap();
9057 assert!(
9058 ext_sigs.iter().any(|s| s.contains("Vec")),
9059 "include_external: true should include external Vec::new: {:?}",
9060 ext_sigs
9061 );
9062 }
9063
9064 #[test]
9065 fn test_find_structural_facts_and_literals_scoped() {
9066 let dir = crate::safe_tempdir();
9067 let db_path = dir.path().join("facts_test.db");
9068 let conn = open_read_write(&db_path).unwrap();
9069 conn.execute_batch(
9070 "CREATE TABLE symbols (
9071 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
9072 signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
9073 start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
9074 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
9075 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
9076 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
9077 semantic_group TEXT, is_test INTEGER, test_container INTEGER
9078 );
9079 CREATE TABLE structural_facts (
9080 structural_fact_id TEXT PRIMARY KEY, file_id TEXT, path TEXT NOT NULL, language TEXT,
9081 pattern_id TEXT, capture_name TEXT, node_kind TEXT, containing_symbol_id TEXT,
9082 start_line INTEGER, end_line INTEGER, confidence REAL, metadata_json TEXT
9083 );
9084 CREATE TABLE literals (
9085 literal_id TEXT PRIMARY KEY, file_id TEXT, path TEXT NOT NULL, language TEXT,
9086 kind TEXT, literal_text TEXT, carrier TEXT, containing_symbol_id TEXT,
9087 start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
9088 start_byte INTEGER, end_byte INTEGER
9089 );
9090 INSERT INTO structural_facts VALUES
9091 ('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\"}'),
9092 ('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\"}'),
9093 ('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\"}'),
9094 ('sf_sql', 'f3', 'src/db/queries.rs', 'rust', 'sql.select_query.v1', 'select_users', 'function', NULL, 30, 40, 1.0, NULL),
9095 ('sf_model', 'f4', 'src/models/user.rs', 'rust', 'sql.table_definition.v1', 'User', 'struct', NULL, 50, 60, 1.0, NULL),
9096 ('sf_css', 'f7', 'web/site.css', 'css', 'css.media_query.v1', 'media', 'media_statement', NULL, 1, 1, 1.0, NULL),
9097 ('sf_custom', 'f5', 'src/custom.rs', 'rust', 'my_custom_pattern', 'custom_name', 'item', NULL, 70, 80, 1.0, NULL);
9098 INSERT INTO literals VALUES
9099 ('lit_toml', 'f1', 'Cargo.toml', 'toml', 'toml_key', '\"version\"', 'key', NULL, 3, 0, 3, 9, 20, 29),
9100 ('lit_route', 'f2', 'src/routes/api.rs', 'rust', 'http_route', '\"/api/v1/users\"', 'string', NULL, 12, 0, 12, 15, 100, 115),
9101 ('lit_sql', 'f3', 'src/db/queries.rs', 'rust', 'sql_query', '\"SELECT * FROM users\"', 'string', NULL, 32, 0, 32, 21, 200, 221),
9102 ('lit_model', 'f4', 'src/models/user.rs', 'rust', 'model_table', '\"users_table\"', 'string', NULL, 52, 0, 52, 13, 300, 313);",
9103 )
9104 .unwrap();
9105
9106 let facts_config = find_structural_facts_scoped(&conn, "config", None, 10).unwrap();
9108 assert_eq!(facts_config.len(), 2);
9109 assert_eq!(facts_config[0].pattern_id, "yaml.key_value.v1");
9110 assert_eq!(facts_config[0].key.as_deref(), Some("on.name"));
9111 assert_eq!(facts_config[1].pattern_id, "toml.key_value.v1");
9112 assert_eq!(
9113 facts_config[1].key.as_deref(),
9114 Some("mcp_servers.code-kb.command")
9115 );
9116 let lits_config = find_literals_scoped(&conn, "config", None, 10).unwrap();
9117 assert_eq!(lits_config.len(), 1);
9118 assert_eq!(lits_config[0].kind, "toml_key");
9119
9120 let facts_route = find_structural_facts_scoped(&conn, "route", None, 10).unwrap();
9122 assert_eq!(facts_route.len(), 1);
9123 assert_eq!(facts_route[0].pattern_id, "axum.route.v1");
9124 assert_eq!(
9125 facts_route[0].key.as_deref(),
9126 Some("GET /api/v1/users/{id}")
9127 );
9128 let facts_routes = find_structural_facts_scoped(&conn, "routes", None, 10).unwrap();
9129 assert_eq!(facts_routes.len(), 1);
9130 let lits_route = find_literals_scoped(&conn, "route", None, 10).unwrap();
9131 assert_eq!(lits_route.len(), 1);
9132 assert_eq!(lits_route[0].kind, "http_route");
9133
9134 for q in &["query", "queries", "sql"] {
9136 let facts = find_structural_facts_scoped(&conn, q, None, 10).unwrap();
9137 assert_eq!(facts.len(), 2, "Failed for {}", q);
9138 assert!(facts.iter().all(|f| f.pattern_id.starts_with("sql.")));
9139 let lits = find_literals_scoped(&conn, q, None, 10).unwrap();
9140 assert_eq!(lits.len(), 1, "Failed for {}", q);
9141 assert_eq!(lits[0].kind, "sql_query");
9142 }
9143
9144 for m in &["model", "models"] {
9146 let facts = find_structural_facts_scoped(&conn, m, None, 10).unwrap();
9147 assert_eq!(facts.len(), 1, "Failed for {}", m);
9148 assert_eq!(facts[0].pattern_id, "sql.table_definition.v1");
9149 let lits = find_literals_scoped(&conn, m, None, 10).unwrap();
9150 assert_eq!(lits.len(), 1, "Failed for {}", m);
9151 assert_eq!(lits[0].kind, "model_table");
9152 }
9153
9154 let facts_custom = find_structural_facts_scoped(&conn, "custom_pattern", None, 10).unwrap();
9156 assert_eq!(facts_custom.len(), 1);
9157 assert_eq!(facts_custom[0].pattern_id, "my_custom_pattern");
9158 assert_eq!(facts_custom[0].key, None);
9159
9160 let facts_exact =
9162 find_structural_facts_scoped(&conn, "config", Some("Cargo.toml"), 10).unwrap();
9163 assert_eq!(facts_exact.len(), 1);
9164 let facts_miss =
9165 find_structural_facts_scoped(&conn, "config", Some("src/routes/api.rs"), 10).unwrap();
9166 assert_eq!(facts_miss.len(), 0);
9167
9168 let facts_dir =
9170 find_structural_facts_scoped(&conn, "route", Some("src/routes"), 10).unwrap();
9171 assert_eq!(facts_dir.len(), 1);
9172 let facts_dir_miss =
9173 find_structural_facts_scoped(&conn, "route", Some("src/db"), 10).unwrap();
9174 assert_eq!(facts_dir_miss.len(), 0);
9175
9176 let f_del = find_structural_facts(&conn, "config", 10).unwrap();
9178 assert_eq!(f_del.len(), 2);
9179 let l_del = find_literals(&conn, "config", 10).unwrap();
9180 assert_eq!(l_del.len(), 1);
9181 }
9182
9183 fn local_variable_fixture() -> Connection {
9184 let conn = Connection::open_in_memory().unwrap();
9185 conn.execute_batch(
9186 "CREATE TABLE symbols (
9187 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT,
9188 kind TEXT, signature TEXT, doc_comment TEXT, visibility TEXT,
9189 parent_symbol_id TEXT, start_line INTEGER, start_column INTEGER,
9190 end_line INTEGER, end_column INTEGER, start_byte INTEGER, end_byte INTEGER,
9191 body_start_line INTEGER, body_start_column INTEGER, body_end_line INTEGER,
9192 body_end_column INTEGER, body_start_byte INTEGER, body_end_byte INTEGER,
9193 body_hash TEXT, semantic_group TEXT, is_test INTEGER, test_container INTEGER
9194 );
9195 INSERT INTO symbols (symbol_id, file_id, path, language, name, kind, signature,
9196 parent_symbol_id, start_line, start_column, end_line, end_column,
9197 start_byte, end_byte, is_test, test_container)
9198 VALUES
9199 ('func', 'f1', 'src/db.rs', 'rust', 'open_conn', 'function',
9200 'fn open_conn() -> sqlite Connection', NULL, 1, 0, 9, 1, 0, 100, 0, 0),
9201 ('local', 'f1', 'src/db.rs', 'rust', 'conn', 'variable',
9202 'let conn: sqlite Connection', 'func', 2, 4, 2, 30, 10, 40, 0, 0),
9203 ('pool', 'f1', 'src/db.rs', 'rust', 'Pool', 'struct',
9204 'struct Pool sqlite', NULL, 12, 0, 16, 1, 120, 200, 0, 0),
9205 ('field', 'f1', 'src/db.rs', 'rust', 'conn', 'variable',
9206 'conn: sqlite Connection', 'pool', 13, 4, 13, 28, 130, 160, 0, 0),
9207 ('global', 'f1', 'src/db.rs', 'rust', 'conn', 'variable',
9208 'static conn: sqlite Connection', NULL, 20, 0, 20, 30, 210, 240, 0, 0),
9209 ('closure', 'f1', 'src/db.rs', 'rust', 'with_conn', 'variable',
9210 'let with_conn = |c: sqlite Connection|', 'func', 4, 4, 6, 5, 50, 90, 0, 0),
9211 ('nested', 'f1', 'src/db.rs', 'rust', 'conn', 'variable',
9212 'let conn = c sqlite', 'closure', 5, 8, 5, 24, 60, 80, 0, 0);",
9213 )
9214 .unwrap();
9215 conn
9216 }
9217
9218 fn matched_symbol_ids(conn: &Connection, query: &str) -> Vec<String> {
9219 let mut stmt = conn
9220 .prepare(
9221 "SELECT s.symbol_id FROM symbols_fts f
9222 JOIN symbols s ON s.rowid = f.rowid
9223 WHERE f.symbols_fts MATCH ?1 ORDER BY s.symbol_id",
9224 )
9225 .unwrap();
9226 let mut ids = stmt
9227 .query_map(params![query], |row| row.get::<_, String>(0))
9228 .unwrap()
9229 .collect::<Result<Vec<_>, _>>()
9230 .unwrap();
9231 ids.sort();
9232 ids
9233 }
9234
9235 #[test]
9236 fn fts_index_excludes_locals_and_rebuilds_a_stale_index() {
9237 let conn = local_variable_fixture();
9238 conn.execute_batch(
9239 "CREATE VIRTUAL TABLE symbols_fts USING fts5(
9240 name, signature, doc_comment,
9241 content='symbols', content_rowid='rowid', tokenize='porter unicode61'
9242 );
9243 INSERT INTO symbols_fts(rowid, name, signature, doc_comment)
9244 SELECT rowid, name, signature, doc_comment FROM symbols;",
9245 )
9246 .unwrap();
9247
9248 ensure_fts_index(&conn).unwrap();
9249
9250 assert_eq!(
9251 matched_symbol_ids(&conn, "sqlite"),
9252 vec!["field", "func", "global", "pool"]
9253 );
9254 }
9255
9256 #[test]
9257 fn lookup_excludes_locals_and_parameters() {
9258 let conn = local_variable_fixture();
9259
9260 let ids: Vec<String> = search_symbols_scoped(&conn, "conn", None, None, false, 10)
9261 .unwrap()
9262 .into_iter()
9263 .map(|s| s.symbol_id)
9264 .collect();
9265
9266 assert!(!ids.contains(&"local".to_string()));
9267 assert!(!ids.contains(&"nested".to_string()));
9268 assert!(ids.contains(&"field".to_string()));
9269 assert!(ids.contains(&"global".to_string()));
9270 }
9271
9272 #[test]
9273 fn search_excludes_locals_and_parameters() {
9274 let conn = local_variable_fixture();
9275 ensure_fts_index(&conn).unwrap();
9276
9277 let ids: Vec<String> = fts_search_symbols_scoped(&conn, "sqlite", None, None, false, 10)
9278 .unwrap()
9279 .into_iter()
9280 .map(|r| r.symbol.symbol_id)
9281 .collect();
9282
9283 assert!(!ids.contains(&"local".to_string()));
9284 assert!(ids.contains(&"func".to_string()));
9285 }
9286
9287 #[test]
9288 fn variable_kind_search_keeps_full_text_matching() {
9289 let conn = local_variable_fixture();
9290 ensure_fts_index(&conn).unwrap();
9291
9292 let ids: Vec<String> = fts_search_symbols_scoped(
9293 &conn,
9294 "sqlite connection",
9295 Some("variable"),
9296 None,
9297 false,
9298 10,
9299 )
9300 .unwrap()
9301 .into_iter()
9302 .map(|r| r.symbol.symbol_id)
9303 .collect();
9304
9305 assert!(ids.contains(&"global".to_string()));
9306 assert!(ids.contains(&"field".to_string()));
9307 }
9308
9309 #[test]
9310 fn qualified_lookup_returns_the_named_local_variable() {
9311 let conn = local_variable_fixture();
9312
9313 let ids: Vec<String> =
9314 search_symbols_scoped(&conn, "open_conn::conn", None, None, false, 10)
9315 .unwrap()
9316 .into_iter()
9317 .map(|s| s.symbol_id)
9318 .collect();
9319
9320 assert_eq!(ids, vec!["local".to_string()]);
9321 }
9322
9323 #[test]
9324 fn exact_local_variable_outranks_a_partial_global_match_within_the_limit() {
9325 let conn = Connection::open_in_memory().unwrap();
9326 conn.execute_batch(
9327 "CREATE TABLE symbols (
9328 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT,
9329 kind TEXT, signature TEXT, doc_comment TEXT, visibility TEXT,
9330 parent_symbol_id TEXT, start_line INTEGER, start_column INTEGER,
9331 end_line INTEGER, end_column INTEGER, start_byte INTEGER, end_byte INTEGER,
9332 body_start_line INTEGER, body_start_column INTEGER, body_end_line INTEGER,
9333 body_end_column INTEGER, body_start_byte INTEGER, body_end_byte INTEGER,
9334 body_hash TEXT, semantic_group TEXT, is_test INTEGER, test_container INTEGER
9335 );
9336 INSERT INTO symbols (symbol_id, file_id, path, language, name, kind, signature,
9337 parent_symbol_id, start_line, start_column, end_line, end_column,
9338 start_byte, end_byte, is_test, test_container)
9339 VALUES
9340 ('func', 'f1', 'src/sum.rs', 'rust', 'digest', 'function',
9341 'fn digest()', NULL, 1, 0, 9, 1, 0, 100, 0, 0),
9342 ('local', 'f1', 'src/sum.rs', 'rust', 'checksum', 'variable',
9343 'let checksum', 'func', 2, 4, 2, 30, 10, 40, 0, 0),
9344 ('global', 'f1', 'src/sum.rs', 'rust', 'getChecksum', 'variable',
9345 'const getChecksum', NULL, 20, 0, 20, 30, 210, 240, 0, 0);",
9346 )
9347 .unwrap();
9348 ensure_fts_index(&conn).unwrap();
9349
9350 let rows =
9351 fts_search_symbols_explained(&conn, "checksum", Some("variable"), None, false, 1, true)
9352 .unwrap();
9353
9354 assert_eq!(rows.len(), 1);
9355 assert_eq!(rows[0].symbol.symbol_id, "local");
9356 let explain = rows[0].explain.as_ref().unwrap();
9357 assert_eq!(explain.name_tier, "whole");
9358 assert_eq!(explain.branches, vec!["exact", "name"]);
9359 assert_eq!(explain.candidates, 2);
9360 }
9361
9362 #[test]
9363 fn variable_kind_filter_returns_locals_and_parameters() {
9364 let conn = local_variable_fixture();
9365 ensure_fts_index(&conn).unwrap();
9366
9367 let lookup_ids: Vec<String> =
9368 search_symbols_scoped(&conn, "conn", Some("variable"), None, false, 10)
9369 .unwrap()
9370 .into_iter()
9371 .map(|s| s.symbol_id)
9372 .collect();
9373 assert!(lookup_ids.contains(&"local".to_string()));
9374 assert!(lookup_ids.contains(&"nested".to_string()));
9375
9376 let search_ids: Vec<String> =
9377 fts_search_symbols_scoped(&conn, "conn", Some("variable"), None, false, 10)
9378 .unwrap()
9379 .into_iter()
9380 .map(|r| r.symbol.symbol_id)
9381 .collect();
9382 assert!(search_ids.contains(&"local".to_string()));
9383 }
9384}