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