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
3202pub(crate) fn 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
3485pub(crate) fn module_import_binding_sql(target: &str, binding_name: &str) -> String {
3488 let target_path = format!("('/' || replace({target}.path, '\\', '/'))");
3489 let dotted = "replace(module_path.value, '/', '.')";
3490 let segment = format!("replace({dotted}, rtrim({dotted}, replace({dotted}, '.', '')), '')");
3491 let relative = is_relative_import_path("import_source.value");
3492 let relative_match = relative_import_matches("import_source.value", target);
3493 format!(
3494 "EXISTS (
3495 SELECT 1 FROM symbols module_import
3496 CROSS JOIN json_each(json_array(
3497 json_extract(module_import.metadata_json, '$.source')
3498 )) import_source
3499 CROSS JOIN json_each(json_array(
3500 json_extract(module_import.metadata_json, '$.importedName'),
3501 import_source.value
3502 )) module_path
3503 WHERE module_import.kind = 'import'
3504 AND module_import.path = p.path
3505 AND module_import.name = {binding_name}
3506 AND json_valid(module_import.metadata_json)
3507 AND CASE WHEN {relative} THEN {relative_match}
3508 ELSE {segment} != ''
3509 AND ({target_path} LIKE '%/' || {segment} || '/%'
3510 OR {target_path} LIKE '%/' || {segment} || '.%')
3511 END
3512 )"
3513 )
3514}
3515
3516pub(crate) fn built_receiver_sql(conn: &Connection, target: &str) -> Option<String> {
3520 if !has_column(conn, "symbols", "metadata_json") {
3521 return None;
3522 }
3523 let builder_calls = receiver_builder_calls(conn);
3524 let builder_return_type = builder_return_type_sql("builder");
3525 let builder_scope = receiver_definition_scope("builder", "builder_call.path");
3526 let returned_class_scope = receiver_definition_scope("returned_class", "builder.path");
3527 let built_scope = receiver_definition_scope("built_class", "built_name.path");
3528 Some(format!(
3530 "CASE WHEN EXISTS (
3531 SELECT 1 FROM symbols taken
3532 WHERE taken.parent_symbol_id = p.from_symbol_id
3533 AND taken.name = p.target_receiver
3534 AND taken.kind IN ('parameter', 'variable')
3535 ) THEN EXISTS (
3536 WITH RECURSIVE builder_call(name, path, resolved_symbol_id) AS ({builder_calls}),
3537 built_name(name, path, resolved_symbol_id) AS (
3538 SELECT name, path, resolved_symbol_id FROM builder_call
3539 UNION
3540 SELECT returned_class.name, builder.path, returned_class.symbol_id
3541 FROM builder_call
3542 JOIN symbols builder ON builder.name = builder_call.name
3543 AND builder.kind IN ('function', 'method')
3544 AND (builder.symbol_id = builder_call.resolved_symbol_id
3545 OR CASE WHEN builder_call.resolved_symbol_id IS NULL
3546 AND EXISTS (
3547 SELECT 1 FROM symbols returned_class_probe
3548 WHERE returned_class_probe.name = {builder_return_type}
3549 AND returned_class_probe.kind = 'class'
3550 )
3551 THEN {builder_scope} ELSE 0 END)
3552 JOIN symbols returned_class ON returned_class.name = {builder_return_type}
3553 AND returned_class.kind = 'class' AND {returned_class_scope}
3554 ),
3555 built(symbol_id, depth) AS (
3556 SELECT built_class.symbol_id, 0
3557 FROM built_name
3558 CROSS JOIN symbols built_class ON built_class.name = built_name.name
3559 WHERE built_class.kind = 'class' AND {built_scope}
3560 AND (built_name.resolved_symbol_id IS NULL
3561 OR built_class.symbol_id = built_name.resolved_symbol_id)
3562 UNION
3563 SELECT r.to_symbol_id, built.depth + 1
3564 FROM relationships r JOIN built ON r.from_symbol_id = built.symbol_id
3565 WHERE +r.kind = 'extends' AND built.depth < 8
3566 UNION
3567 SELECT c.symbol_id, built.depth + 1
3568 FROM pending_relationships pe
3569 JOIN built ON pe.from_symbol_id = built.symbol_id
3570 JOIN symbols c ON c.name = pe.target_terminal_name
3571 WHERE +pe.kind = 'extends' AND built.depth < 8
3572 AND c.kind IN ('class', 'interface', 'struct', 'trait', 'protocol')
3573 )
3574 SELECT 1 FROM built WHERE built.symbol_id = {target}.parent_symbol_id
3575 AND NOT EXISTS (
3576 SELECT 1 FROM built closer
3577 JOIN symbols nearer ON nearer.parent_symbol_id = closer.symbol_id
3578 WHERE nearer.name = {target}.name
3579 AND nearer.kind IN ('method', 'function', 'constructor', 'property')
3580 AND closer.depth < built.depth
3581 )
3582 ) ELSE 0 END"
3583 ))
3584}
3585
3586pub(crate) fn receiver_type_sql(conn: &Connection, parent: &str) -> String {
3589 let receiver_type_scope = if has_column(conn, "symbols", "metadata_json") {
3591 receiver_definition_scope(parent, "p.path")
3592 } else {
3593 "1 = 1".to_string()
3594 };
3595 format!(
3596 "EXISTS (
3597 SELECT 1 FROM symbols receiver
3598 JOIN type_facts receiver_type ON receiver_type.symbol_id = receiver.symbol_id
3599 WHERE receiver.name = p.target_receiver
3600 AND receiver.path = p.path
3601 AND receiver_type.resolved_type = {parent}.name
3602 AND NOT EXISTS (
3603 SELECT 1 FROM symbols shadow
3604 WHERE shadow.name = receiver.name
3605 AND shadow.path = receiver.path
3606 AND shadow.parent_symbol_id = p.from_symbol_id
3607 AND shadow.kind IN ('variable', 'parameter')
3608 AND shadow.symbol_id != receiver.symbol_id
3609 )
3610 ) AND {receiver_type_scope}"
3611 )
3612}
3613
3614pub(crate) fn class_chain_sql(target: &str, own_class_rule: &str) -> String {
3621 let rival_rank = path_proximity("rival.path", "pe.path");
3622 let base_rank = path_proximity("c.path", "pe.path");
3623 let inherited_write = inherited_attribute_write("nearer");
3624 format!(
3625 "EXISTS (
3626 WITH RECURSIVE base(symbol_id, depth) AS (
3627 SELECT s_from.parent_symbol_id, 0
3628 UNION
3629 SELECT r.to_symbol_id, base.depth + 1
3630 FROM relationships r JOIN base ON r.from_symbol_id = base.symbol_id
3631 WHERE +r.kind = 'extends' AND base.depth < 8
3632 UNION
3633 SELECT c.symbol_id, base.depth + 1
3634 FROM pending_relationships pe
3635 JOIN base ON pe.from_symbol_id = base.symbol_id
3636 JOIN symbols c ON c.name = pe.target_terminal_name
3637 WHERE +pe.kind = 'extends' AND base.depth < 8
3638 AND c.kind IN ('class', 'interface', 'struct', 'trait', 'protocol')
3639 AND NOT EXISTS (
3640 SELECT 1 FROM symbols rival
3641 WHERE rival.name = c.name
3642 AND rival.kind IN ('class', 'interface', 'struct', 'trait', 'protocol')
3643 AND {rival_rank} > {base_rank})
3644 )
3645 SELECT 1 FROM base
3646 WHERE base.symbol_id = {target}.parent_symbol_id
3647 AND (base.depth > 0 OR {own_class_rule})
3648 AND NOT EXISTS (
3649 SELECT 1 FROM base closer
3650 JOIN symbols nearer ON nearer.parent_symbol_id = closer.symbol_id
3651 WHERE nearer.name = {target}.name
3652 AND nearer.kind IN ('method', 'function', 'constructor', 'property')
3653 AND closer.depth < base.depth
3654 AND (closer.depth > 0 OR p.target_receiver IS NOT 'super')
3655 AND NOT {inherited_write}
3656 )
3657 )"
3658 )
3659}
3660
3661pub(crate) fn implicit_receiver_language_list() -> String {
3663 IMPLICIT_RECEIVER_LANGUAGES
3664 .iter()
3665 .map(|language| format!("'{language}'"))
3666 .collect::<Vec<_>>()
3667 .join(", ")
3668}
3669
3670pub(crate) fn enclosing_scope_sql(target: &str) -> String {
3676 let implicit_receiver_languages = implicit_receiver_language_list();
3677 format!(
3678 "EXISTS (
3679 WITH RECURSIVE lexical(symbol_id, depth) AS (
3680 SELECT s_from.parent_symbol_id, 1
3681 UNION ALL
3682 SELECT outer_scope.parent_symbol_id, lexical.depth + 1
3683 FROM symbols outer_scope JOIN lexical ON outer_scope.symbol_id = lexical.symbol_id
3684 WHERE outer_scope.parent_symbol_id IS NOT NULL AND lexical.depth < 32
3685 )
3686 SELECT 1 FROM lexical JOIN symbols scope ON scope.symbol_id = lexical.symbol_id
3687 WHERE lexical.symbol_id = {target}.parent_symbol_id
3688 AND lexical.depth > 1
3689 AND (scope.kind NOT IN ({MEMBER_LIST_OWNER_KINDS})
3690 OR scope.kind IN ('module', 'namespace')
3691 OR {target}.kind IN ({NESTED_TYPE_KINDS})
3692 AND s_from.language IN ({implicit_receiver_languages}))
3693 AND NOT EXISTS (
3694 SELECT 1 FROM lexical closer
3695 JOIN symbols nearer ON nearer.parent_symbol_id = closer.symbol_id
3696 WHERE closer.depth < lexical.depth
3697 AND nearer.name = {target}.name
3698 AND nearer.kind IN ({target}.kind, 'import')
3699 )
3700 )"
3701 )
3702}
3703
3704pub(crate) fn pending_target_predicate(conn: &Connection, target: &str, parent: &str) -> String {
3707 let ns = "json_each(CASE WHEN json_valid(p.target_namespace_json) THEN p.target_namespace_json ELSE '[]' END)";
3708 let target_path = format!("('/' || replace({target}.path, '\\', '/'))");
3709 let like_value = "replace(replace(replace(value, '\\', '\\\\'), '%', '\\%'), '_', '\\_')";
3710 let closer_rank = call_site_proximity("closer.path");
3711 let target_rank = call_site_proximity(&format!("{target}.path"));
3712 let relative = is_relative_import_path("import_source.value");
3713 let relative_match = relative_import_matches("import_source.value", target);
3714 let receiver_import_elsewhere = if has_column(conn, "symbols", "metadata_json") {
3716 format!(
3717 "AND NOT EXISTS (
3718 SELECT 1 FROM symbols receiver_import
3719 CROSS JOIN json_each(json_array(
3720 json_extract(receiver_import.metadata_json, '$.source')
3721 )) import_source
3722 WHERE receiver_import.kind = 'import'
3723 AND receiver_import.path = p.path
3724 AND receiver_import.name = p.target_receiver
3725 AND json_valid(receiver_import.metadata_json)
3726 AND {relative}
3727 AND NOT {relative_match}
3728 )"
3729 )
3730 } else {
3731 String::new()
3732 };
3733 let module_import = module_import_binding_sql(target, "p.target_receiver");
3734 let import_alias_receiver = if has_column(conn, "symbols", "metadata_json") {
3735 format!("OR EXISTS (
3736 SELECT 1 FROM symbols alias_import
3737 WHERE alias_import.kind = 'import'
3738 AND alias_import.path = p.path
3739 AND json_valid(alias_import.metadata_json)
3740 AND (json_extract(alias_import.metadata_json, '$.alias') = p.target_receiver
3741 OR json_extract(alias_import.metadata_json, '$.local_name') = p.target_receiver)
3742 AND COALESCE(json_extract(alias_import.metadata_json, '$.source'), '') NOT LIKE 'Qt%'
3743 )
3744 OR {module_import}"
3745 )
3746 } else {
3747 String::new()
3748 };
3749 let receiver_type = receiver_type_sql(conn, parent);
3750 let fixture_receiver = match built_receiver_sql(conn, target) {
3751 Some(built) => format!("OR {built}"),
3752 None => String::new(),
3753 };
3754 let inherited = class_chain_sql(target, &format!("{target}.kind IN ('variable', 'field')"));
3755 let receiver_chain = class_chain_sql(target, "p.target_receiver IS NOT 'super'");
3756 let implicit_receiver_languages = implicit_receiver_language_list();
3757 let enclosing = enclosing_scope_sql(target);
3758 let rust_path = rust_path_rule(target, ns);
3759 format!(
3760 "(
3761 NOT (p.kind IS 'extends' AND p.from_symbol_id = {target}.symbol_id)
3762 AND (
3763 (
3764 {target}.parent_symbol_id IS NOT NULL
3765 AND {parent}.name IS NOT NULL
3766 AND (
3767 EXISTS (SELECT 1 FROM {ns} WHERE value = {parent}.name)
3768 OR (EXISTS (SELECT 1 FROM {ns} WHERE value = 'Self')
3769 AND s_from.parent_symbol_id = {target}.parent_symbol_id)
3770 OR (p.target_receiver IS NOT NULL AND p.target_receiver != '' AND {parent}.name = p.target_receiver
3771 {receiver_import_elsewhere})
3772 OR ((p.target_receiver IN ('self', 'this', 'cls', 'Self', 'super')
3773 OR EXISTS (SELECT 1 FROM {ns} WHERE value = 'Self'))
3774 AND {receiver_chain})
3775 OR {receiver_type}
3776 {fixture_receiver}
3777 )
3778 AND NOT EXISTS (
3779 SELECT 1 FROM {ns}
3780 WHERE value NOT IN ('std', 'core', 'alloc', 'crate', 'super', 'self', 'Self', {parent}.name)
3781 AND NOT EXISTS (
3782 WITH RECURSIVE ancestor(symbol_id, depth) AS (
3783 SELECT {target}.parent_symbol_id, 0
3784 UNION ALL
3785 SELECT s.parent_symbol_id, ancestor.depth + 1
3786 FROM symbols s JOIN ancestor ON s.symbol_id = ancestor.symbol_id
3787 WHERE s.parent_symbol_id IS NOT NULL AND ancestor.depth < 32
3788 )
3789 SELECT 1 FROM ancestor JOIN symbols a ON a.symbol_id = ancestor.symbol_id
3790 WHERE a.name = value
3791 )
3792 AND {target_path} NOT LIKE '%/' || {like_value} || '.%' ESCAPE '\\'
3793 AND {target_path} NOT LIKE '%/' || {like_value} || '/%' ESCAPE '\\'
3794 )
3795 )
3796 OR (
3797 (p.target_namespace_json IS NULL OR p.target_namespace_json = '[]')
3798 AND (
3799 p.target_receiver IS NULL
3800 OR p.target_receiver = ''
3801 {import_alias_receiver}
3802 )
3803 AND ({target}.parent_symbol_id IS NULL
3804 OR s_from.parent_symbol_id = {target}.parent_symbol_id
3805 OR {target}.parent_symbol_id = p.from_symbol_id
3806 OR CASE WHEN p.target_receiver IS NULL OR p.target_receiver = ''
3807 THEN {enclosing}
3808 ELSE 0 END
3809 OR CASE WHEN (p.target_receiver IS NULL OR p.target_receiver = '')
3810 AND s_from.language IN ({implicit_receiver_languages})
3811 THEN {parent}.path = s_from.path
3812 AND {parent}.name = (
3813 SELECT caller_owner.name FROM symbols caller_owner
3814 WHERE caller_owner.symbol_id = s_from.parent_symbol_id
3815 AND caller_owner.kind = {parent}.kind
3816 )
3817 OR {inherited}
3818 ELSE 0 END)
3819 AND (p.target_receiver IS NOT NULL AND p.target_receiver != '' OR {target}.path = p.path OR NOT EXISTS (
3820 SELECT 1 FROM symbols shadow
3821 WHERE shadow.name = {target}.name
3822 AND shadow.path = p.path
3823 AND shadow.symbol_id != p.from_symbol_id
3824 AND shadow.kind NOT IN ('import', 'export', 'module', 'namespace')
3825 AND (shadow.parent_symbol_id IS NULL OR shadow.parent_symbol_id = p.from_symbol_id)
3826 ))
3827 AND ({target}.parent_symbol_id IS NOT NULL OR NOT EXISTS (
3828 SELECT 1 FROM symbols closer
3829 WHERE closer.name = {target}.name
3830 AND closer.symbol_id != {target}.symbol_id
3831 AND closer.parent_symbol_id IS NULL
3832 AND closer.kind = {target}.kind
3833 AND NOT (p.kind IS 'extends' AND closer.symbol_id = p.from_symbol_id)
3834 AND {closer_rank} > {target_rank}
3835 ))
3836 )
3837 OR (
3838 {target}.parent_symbol_id IS NULL
3839 AND (
3840 EXISTS (
3841 SELECT 1 FROM {ns}
3842 WHERE value NOT IN ('std', 'core', 'alloc', 'crate', 'super')
3843 AND {target_path} LIKE '%/' || {like_value} || '.%' ESCAPE '\\'
3844 )
3845 OR ({rust_path})
3846 )
3847 )
3848 )
3849 )"
3850 )
3851}
3852
3853fn rust_path_rule(target: &str, ns: &str) -> String {
3860 let target_path = format!("('/' || replace({target}.path, '\\', '/'))");
3861 let caller_path = "('/' || replace(p.path, '\\', '/'))";
3862 let skipped = "'std', 'core', 'alloc', 'crate', 'self', 'super', 'Self'";
3863 let like_segment = "replace(replace(replace(seg.value, '\\', '\\\\'), '%', '\\%'), '_', '\\_')";
3864 let in_module_path = format!(
3865 "(module_path.value LIKE '%/' || {like_segment} || '.%' ESCAPE '\\'
3866 OR module_path.value LIKE '%/' || {like_segment} || '/%' ESCAPE '\\')"
3867 );
3868 format!(
3869 "s_from.language = 'rust'
3870 AND {target}.language = 'rust'
3871 AND EXISTS (
3872 SELECT 1
3873 FROM json_each(json_array(replace({target_path}, '/src/', char(1)))) marked
3874 CROSS JOIN json_each(json_array(
3875 CASE WHEN instr(marked.value, char(1)) > 0
3876 THEN length({target_path})
3877 - length(replace(marked.value, rtrim(marked.value, replace(marked.value, char(1), '')), ''))
3878 ELSE 0 END)) src
3879 CROSS JOIN json_each(json_array(
3880 CASE WHEN src.value > 0 THEN replace(substr({target_path}, src.value), '-', '_') ELSE '' END)) module_path
3881 WHERE (
3882 EXISTS (
3883 SELECT 1 FROM {ns} head
3884 WHERE head.key = 0
3885 AND (head.value IN ('crate', 'self', 'super')
3886 AND ({target}.path = p.path
3887 OR src.value > 0
3888 AND substr({caller_path}, 1, src.value) = substr({target_path}, 1, src.value))
3889 OR head.value NOT IN ({skipped})
3890 AND src.value >= length(head.value) + 6
3891 AND replace(substr({target_path}, src.value - length(head.value) - 5, length(head.value) + 6), '-', '_')
3892 = '/' || head.value || '/src/')
3893 )
3894 AND NOT EXISTS (
3895 SELECT 1 FROM {ns} seg
3896 WHERE seg.key > 0 AND seg.value NOT IN ({skipped}) AND NOT {in_module_path}
3897 )
3898 OR src.value > 0
3899 AND EXISTS (SELECT 1 FROM {ns} head WHERE head.key = 0 AND head.value NOT IN ({skipped}))
3900 AND NOT EXISTS (
3901 SELECT 1 FROM {ns} seg
3902 WHERE seg.value NOT IN ({skipped}) AND NOT {in_module_path}
3903 )
3904 )
3905 )"
3906 )
3907}
3908
3909pub(crate) const DOCUMENTATION_LANGUAGES: &[&str] = &[
3912 "markdown", "yaml", "toml", "json", "html", "css", "xml", "ini", "text",
3913];
3914
3915pub(crate) fn documentation_language_list() -> String {
3916 DOCUMENTATION_LANGUAGES
3917 .iter()
3918 .map(|l| format!("'{l}'"))
3919 .collect::<Vec<_>>()
3920 .join(", ")
3921}
3922
3923fn not_documentation(conn: &Connection, alias: &str) -> String {
3924 let has_content_type: bool = conn
3925 .query_row(
3926 "SELECT 1 FROM pragma_table_info('symbols') WHERE name = 'content_type'",
3927 [],
3928 |_| Ok(true),
3929 )
3930 .unwrap_or(false);
3931 if has_content_type {
3932 format!("({alias}.content_type IS NULL OR {alias}.content_type != 'documentation')")
3933 } else {
3934 "1 = 1".to_string()
3935 }
3936}
3937
3938pub(crate) fn has_column(conn: &Connection, table: &str, column: &str) -> bool {
3939 conn.query_row(
3940 "SELECT 1 FROM pragma_table_info(?1) WHERE name = ?2",
3941 [table, column],
3942 |_| Ok(true),
3943 )
3944 .unwrap_or(false)
3945}
3946
3947pub(crate) fn has_table(conn: &Connection, name: &str) -> bool {
3948 conn.query_row(
3949 "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1",
3950 [name],
3951 |_| Ok(true),
3952 )
3953 .unwrap_or(false)
3954}
3955
3956fn has_pending_namespace_column(conn: &Connection) -> bool {
3957 let has_ns: bool = conn
3958 .query_row(
3959 "SELECT 1 FROM pragma_table_info('pending_relationships') WHERE name = 'target_namespace_json'",
3960 [],
3961 |_| Ok(true),
3962 )
3963 .unwrap_or(false);
3964 let has_display: bool = conn
3965 .query_row(
3966 "SELECT 1 FROM pragma_table_info('pending_relationships') WHERE name = 'target_display_name'",
3967 [],
3968 |_| Ok(true),
3969 )
3970 .unwrap_or(false);
3971 let has_receiver: bool = conn
3972 .query_row(
3973 "SELECT 1 FROM pragma_table_info('pending_relationships') WHERE name = 'target_receiver'",
3974 [],
3975 |_| Ok(true),
3976 )
3977 .unwrap_or(false);
3978 has_ns && has_display && has_receiver
3979}
3980
3981fn find_references_internal(
3982 conn: &Connection,
3983 symbol_name: &str,
3984 direction: &str,
3985 limit: usize,
3986 symbol_id: Option<&str>,
3987 include_external: bool,
3988) -> Result<Vec<ReferenceSite>, QueryError> {
3989 let mut sites = find_direct_references(
3990 conn,
3991 symbol_name,
3992 direction,
3993 limit,
3994 symbol_id,
3995 include_external,
3996 )?;
3997 let class = match symbol_id.filter(|_| direction == "callers") {
3998 Some(id) => constructed_class(conn, id)?,
3999 None => None,
4000 };
4001 if let Some((class_id, class_name)) = class.filter(|_| sites.len() < limit) {
4002 let mut builds = find_direct_references(
4003 conn,
4004 &class_name,
4005 "callers",
4006 limit - sites.len(),
4007 Some(&class_id),
4008 include_external,
4009 )?;
4010 builds.retain(|site| site.kind == "calls");
4011 sites.extend(builds);
4012 sites.sort_by(|a, b| {
4013 a.path
4014 .cmp(&b.path)
4015 .then(a.start_line.cmp(&b.start_line))
4016 .then(a.start_column.cmp(&b.start_column))
4017 });
4018 }
4019 if direction == "callers" {
4020 populate_lambda_enclosing_names(conn, &mut sites)?;
4021 }
4022 Ok(sites)
4023}
4024
4025pub(crate) fn populate_lambda_enclosing_names(
4026 conn: &Connection,
4027 sites: &mut [ReferenceSite],
4028) -> Result<(), QueryError> {
4029 if !sites
4030 .iter()
4031 .any(|site| has_generated_lambda_name(&site.from_symbol_name))
4032 {
4033 return Ok(());
4034 }
4035 let mut stmt = conn.prepare(
4036 "SELECT caller.language, caller.kind, caller.signature, parent.name
4037 FROM symbols caller
4038 LEFT JOIN symbols parent ON parent.symbol_id = caller.parent_symbol_id
4039 WHERE caller.symbol_id = ?1",
4040 )?;
4041 for site in sites
4042 .iter_mut()
4043 .filter(|site| has_generated_lambda_name(&site.from_symbol_name))
4044 {
4045 let details = stmt
4046 .query_row([&site.from_symbol_id], |row| {
4047 Ok((
4048 row.get::<_, String>(0)?,
4049 row.get::<_, String>(1)?,
4050 row.get::<_, Option<String>>(2)?,
4051 row.get::<_, Option<String>>(3)?,
4052 ))
4053 })
4054 .optional()?;
4055 if let Some((language, kind, signature, enclosing_symbol_name)) = details
4056 && is_generated_lambda_name(
4057 &site.from_symbol_name,
4058 &language,
4059 &kind,
4060 signature.as_deref(),
4061 )
4062 {
4063 site.enclosing_symbol_name = enclosing_symbol_name;
4064 }
4065 }
4066 Ok(())
4067}
4068
4069pub(crate) fn constructed_class(
4071 conn: &Connection,
4072 symbol_id: &str,
4073) -> Result<Option<(String, String)>, QueryError> {
4074 Ok(conn
4075 .query_row(
4076 "SELECT class.symbol_id, class.name
4077 FROM symbols ctor JOIN symbols class ON class.symbol_id = ctor.parent_symbol_id
4078 WHERE ctor.symbol_id = ?1 AND ctor.kind = 'constructor'
4079 AND class.kind IN ('class', 'struct', 'record')",
4080 params![symbol_id],
4081 |row| Ok((row.get(0)?, row.get(1)?)),
4082 )
4083 .optional()?)
4084}
4085
4086pub(crate) struct DirectIdentifier {
4088 pub identifier_id: String,
4089 pub site: ReferenceSite,
4090}
4091
4092pub(crate) fn direct_identifier_rows(
4095 conn: &Connection,
4096 symbol_name: &str,
4097 symbol_id: Option<&str>,
4098 remaining: usize,
4099) -> Result<Vec<DirectIdentifier>, QueryError> {
4100 let receiver_match = member_receiver_match(conn);
4101 let type_usage_shadow = format!(
4104 "i.kind = 'type_usage'
4105 AND ?2 IS NOT NULL
4106 AND selected_target.kind IN ({NESTED_TYPE_KINDS})
4107 AND EXISTS (
4108 WITH RECURSIVE lexical(symbol_id, depth) AS (
4109 SELECT s_from.symbol_id, 0
4110 UNION ALL
4111 SELECT scope.parent_symbol_id, lexical.depth + 1
4112 FROM symbols scope JOIN lexical ON scope.symbol_id = lexical.symbol_id
4113 WHERE scope.parent_symbol_id IS NOT NULL AND lexical.depth < 32
4114 ), visible_types(symbol_id, depth) AS (
4115 SELECT shadow.symbol_id, lexical.depth
4116 FROM lexical
4117 JOIN symbols shadow ON shadow.parent_symbol_id = lexical.symbol_id
4118 WHERE shadow.name = selected_target.name
4119 AND shadow.kind IN ({NESTED_TYPE_KINDS})
4120 AND shadow.language = i.language
4121 UNION ALL
4122 SELECT shadow.symbol_id, 100
4123 FROM symbols shadow
4124 WHERE shadow.name = selected_target.name
4125 AND shadow.kind IN ({NESTED_TYPE_KINDS})
4126 AND shadow.language = i.language
4127 AND shadow.parent_symbol_id IS NULL
4128 AND replace(shadow.path, '\\', '/') = replace(i.path, '\\', '/')
4129 )
4130 SELECT 1 FROM visible_types nearest
4131 WHERE nearest.symbol_id != selected_target.symbol_id
4132 AND nearest.depth = (SELECT MIN(depth) FROM visible_types)
4133 )"
4134 );
4135 let receiver_match = receiver_match.replace("?3", "?1");
4136 let language_match = identifier_language_matches("i", "selected_target");
4137 let mut receiver_stmt = conn.prepare(&format!(
4138 "SELECT {receiver_match}
4139 FROM (SELECT name, kind, path, start_line, start_column,
4140 containing_symbol_id AS from_symbol_id,
4141 CASE WHEN json_valid(metadata_json) THEN metadata_json ELSE '{{}}' END AS metadata_json,
4142 COALESCE(json_valid(metadata_json), 0) AS has_metadata,
4143 language,
4144 CASE WHEN json_valid(metadata_json) THEN json_extract(metadata_json, '$.receiver') END AS target_receiver,
4145 '[]' AS target_namespace_json
4146 FROM identifiers WHERE identifier_id = ?2) p
4147 LEFT JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id"
4148 ))?;
4149 let mut ident_stmt = conn.prepare(&format!(
4150 "WITH raw AS MATERIALIZED (
4151 SELECT identifier_id, name, kind, path, start_line, start_column,
4152 containing_symbol_id, language,
4153 CASE WHEN json_valid(metadata_json) THEN metadata_json ELSE '{{}}' END AS metadata_json
4154 FROM identifiers WHERE name = ?1 AND kind IN ('type_usage', 'member_access')
4155 )
4156 SELECT i.identifier_id,
4157 COALESCE(s_from.name, ''),
4158 COALESCE(i.containing_symbol_id, ''),
4159 i.name,
4160 CASE WHEN json_valid(i.metadata_json)
4161 AND json_extract(i.metadata_json, '$.role') = 'signal_handler'
4162 THEN CASE WHEN json_extract(i.metadata_json, '$.receiver') = (
4163 SELECT CASE WHEN t.kind IN ('class', 'struct', 'enum', 'interface', 'trait', 'module', 'namespace')
4164 THEN t.name ELSE tp.name END
4165 FROM symbols t
4166 LEFT JOIN symbols tp ON tp.symbol_id = t.parent_symbol_id
4167 WHERE t.symbol_id = ?2)
4168 THEN 'handler' ELSE 'handler (candidate)' END
4169 ELSE i.kind END,
4170 i.path,
4171 i.start_line,
4172 i.start_column
4173 FROM raw i
4174 LEFT JOIN symbols s_from ON i.containing_symbol_id = s_from.symbol_id
4175 LEFT JOIN symbols selected_target ON selected_target.symbol_id = ?2
4176 WHERE i.name = ?1 AND i.kind IN ('type_usage', 'member_access')
4177 AND COALESCE(s_from.kind, '') != 'import'
4178 AND (?2 IS NULL OR {language_match})
4179 AND NOT ({type_usage_shadow})
4180 AND NOT EXISTS (
4181 SELECT 1 FROM relationships covered
4182 JOIN symbols covered_to ON covered.to_symbol_id = covered_to.symbol_id
4183 WHERE covered_to.name = i.name
4184 AND covered.path = i.path
4185 AND covered.start_line = i.start_line
4186 AND (?2 IS NULL OR covered.to_symbol_id = ?2)
4187 )
4188 AND NOT EXISTS (
4189 SELECT 1 FROM pending_relationships covered
4190 WHERE covered.target_terminal_name = i.name
4191 AND covered.path = i.path
4192 AND covered.start_line = i.start_line
4193 )
4194 AND (?2 IS NULL OR NOT EXISTS (
4195 SELECT 1 FROM symbols owner
4196 JOIN symbols member ON member.parent_symbol_id = owner.symbol_id
4197 WHERE owner.name = CASE WHEN json_valid(i.metadata_json) THEN json_extract(i.metadata_json, '$.receiver') END
4198 AND member.name = i.name
4199 AND owner.name IS NOT (SELECT parent.name FROM symbols target
4200 JOIN symbols parent ON parent.symbol_id = target.parent_symbol_id
4201 WHERE target.symbol_id = ?2)
4202 ))
4203 AND NOT (i.kind = 'member_access' AND EXISTS (
4204 SELECT 1 FROM symbols target
4205 LEFT JOIN symbols scope ON scope.symbol_id = target.parent_symbol_id
4206 WHERE target.symbol_id = ?2
4207 AND (scope.kind IN ('function', 'method', 'constructor')
4208 OR (scope.symbol_id IS NULL
4209 AND target.path != i.path
4210 AND target.language IN ('javascript', 'typescript', 'tsx')
4211 AND COALESCE(target.visibility, 'private') = 'private'))
4212 ))
4213 AND NOT COALESCE((i.kind = 'member_access'
4214 AND json_valid(i.metadata_json)
4215 AND json_extract(i.metadata_json, '$.role') IS NOT 'signal_handler'
4216 AND json_extract(i.metadata_json, '$.receiver') GLOB '[A-Z]*'
4217 AND NOT EXISTS (SELECT 1 FROM symbols known
4218 WHERE known.name = json_extract(i.metadata_json, '$.receiver')
4219 AND known.kind NOT IN ('variable', 'parameter', 'method'))), 0)
4220 ORDER BY i.path, i.start_line, i.start_column, i.identifier_id"
4221 ))?;
4222 let mut supported = Vec::new();
4223 let mut candidates = Vec::new();
4224 let mut supported_keys = HashSet::new();
4225 let mut candidate_keys = HashSet::new();
4226 let site_key = |site: &ReferenceSite| {
4227 (
4228 site.from_symbol_id.clone(),
4229 site.to_symbol_name.clone(),
4230 site.kind.clone(),
4231 site.path.clone(),
4232 site.start_line,
4233 )
4234 };
4235 let mut ident_rows = ident_stmt.query(params![symbol_name, symbol_id])?;
4236 while let Some(row) = ident_rows.next()? {
4237 let identifier_id: String = row.get(0)?;
4238 let mut site = ReferenceSite {
4239 from_symbol_name: row.get(1)?,
4240 from_symbol_id: row.get(2)?,
4241 to_symbol_name: row.get(3)?,
4242 kind: row.get(4)?,
4243 path: row.get::<_, String>(5)?.replace('\\', "/"),
4244 start_line: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4245 start_column: row.get::<_, Option<i64>>(7)?.map(|v| v as usize),
4246 occurrences: None,
4247 target: None,
4248 enclosing_symbol_name: None,
4249 };
4250 let receiver_match: i64 =
4251 receiver_stmt.query_row(params![symbol_id, identifier_id], |row| row.get(0))?;
4252 if receiver_match < 0 {
4253 continue;
4254 }
4255 if receiver_match == 0 && site.kind == "member_access" {
4256 site.kind = "member_access (candidate)".to_string();
4257 }
4258 if site.kind == "member_access (candidate)" {
4259 if candidates.len() < remaining && candidate_keys.insert(site_key(&site)) {
4260 candidates.push(DirectIdentifier {
4261 identifier_id,
4262 site,
4263 });
4264 }
4265 } else if supported_keys.insert(site_key(&site)) {
4266 supported.push(DirectIdentifier {
4267 identifier_id,
4268 site,
4269 });
4270 if supported.len() >= remaining {
4271 break;
4272 }
4273 }
4274 }
4275
4276 let site_order = |a: &DirectIdentifier, b: &DirectIdentifier| {
4277 a.site
4278 .path
4279 .cmp(&b.site.path)
4280 .then(a.site.start_line.cmp(&b.site.start_line))
4281 .then(a.site.start_column.cmp(&b.site.start_column))
4282 };
4283 supported.sort_by(site_order);
4284 supported.truncate(remaining);
4285 candidates.sort_by(site_order);
4286 candidates.truncate(remaining - supported.len());
4287 let mut rows = supported;
4288 rows.extend(candidates);
4289 Ok(rows)
4290}
4291
4292pub(crate) fn grouped_member_access_rows(
4295 conn: &Connection,
4296 symbol_id: &str,
4297 remaining: usize,
4298) -> Result<Vec<ReferenceSite>, QueryError> {
4299 let language_match = identifier_language_matches("i", "target");
4300 let mut receiver_stmt = conn.prepare(&format!(
4301 "SELECT COALESCE(s.name, ''),
4302 COALESCE(i.containing_symbol_id, ''),
4303 i.name,
4304 i.kind,
4305 i.path,
4306 MIN(i.start_line),
4307 i.start_column,
4308 COUNT(*)
4309 FROM identifiers i
4310 LEFT JOIN symbols s ON i.containing_symbol_id = s.symbol_id
4311 JOIN symbols target ON target.symbol_id = ?2
4312 LEFT JOIN symbols target_parent ON target_parent.symbol_id = target.parent_symbol_id
4313 WHERE i.kind = 'member_access'
4314 AND i.name != target.name
4315 AND COALESCE(s.kind, '') != 'import'
4316 AND target.kind IN ('class', 'struct', 'enum', 'interface', 'trait', 'module', 'namespace')
4317 AND json_valid(i.metadata_json)
4318 AND json_extract(i.metadata_json, '$.receiver') = target.name
4319 AND (json_extract(i.metadata_json, '$.receiver_qualifier') IS NULL
4320 OR json_extract(i.metadata_json, '$.receiver_qualifier') = target_parent.name)
4321 AND {language_match}
4322 AND NOT EXISTS (
4323 SELECT 1 FROM symbols nearer
4324 WHERE nearer.name = target.name
4325 AND nearer.path = i.path
4326 AND nearer.symbol_id != target.symbol_id
4327 AND nearer.kind IN ({NESTED_TYPE_KINDS}, 'module', 'namespace')
4328 )
4329 GROUP BY i.path
4330 ORDER BY i.path, i.start_line
4331 LIMIT ?1"
4332 ))?;
4333 let rows = receiver_stmt.query_map(params![remaining as i64, symbol_id], |row| {
4334 Ok(ReferenceSite {
4335 from_symbol_name: row.get(0)?,
4336 from_symbol_id: row.get(1)?,
4337 to_symbol_name: row.get(2)?,
4338 kind: row.get(3)?,
4339 path: row.get::<_, String>(4)?.replace('\\', "/"),
4340 start_line: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
4341 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4342 occurrences: Some(row.get::<_, i64>(7)? as usize),
4343 target: None,
4344 enclosing_symbol_name: None,
4345 })
4346 })?;
4347 Ok(rows.collect::<Result<_, _>>()?)
4348}
4349
4350fn find_direct_references(
4351 conn: &Connection,
4352 symbol_name: &str,
4353 direction: &str,
4354 limit: usize,
4355 symbol_id: Option<&str>,
4356 include_external: bool,
4357) -> Result<Vec<ReferenceSite>, QueryError> {
4358 let mut results = Vec::new();
4359
4360 if direction == "callers" {
4361 let mut stmt = conn.prepare(
4363 "SELECT s_from.name AS from_name,
4364 r.from_symbol_id,
4365 s_to.name AS to_name,
4366 r.kind,
4367 r.path,
4368 r.start_line,
4369 r.start_column
4370 FROM relationships r
4371 JOIN symbols s_from ON r.from_symbol_id = s_from.symbol_id
4372 JOIN symbols s_to ON r.to_symbol_id = s_to.symbol_id
4373 WHERE s_to.name = ?1 AND (?3 IS NULL OR r.to_symbol_id = ?3)
4374 LIMIT ?2",
4375 )?;
4376
4377 let rows = stmt.query_map(params![symbol_name, limit as i64, symbol_id], |row| {
4378 Ok(ReferenceSite {
4379 from_symbol_name: row.get(0)?,
4380 from_symbol_id: row.get(1)?,
4381 to_symbol_name: row.get(2)?,
4382 kind: row.get(3)?,
4383 path: row.get::<_, String>(4)?.replace('\\', "/"),
4384 start_line: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
4385 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4386 occurrences: None,
4387 target: None,
4388 enclosing_symbol_name: None,
4389 })
4390 })?;
4391
4392 for r in rows {
4393 results.push(r?);
4394 }
4395
4396 if results.len() < limit {
4398 let remaining = limit - results.len();
4399 if has_pending_namespace_column(conn) {
4400 if let Some(sid) = symbol_id {
4401 let mut pending_stmt = conn.prepare(
4402 &format!("SELECT s_from.name AS from_name,
4403 p.from_symbol_id,
4404 p.target_terminal_name AS to_name,
4405 p.kind,
4406 p.path,
4407 p.start_line,
4408 p.start_column
4409 FROM pending_relationships p
4410 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4411 JOIN symbols s_target ON s_target.symbol_id = ?3
4412 LEFT JOIN symbols s_target_parent ON s_target.parent_symbol_id = s_target_parent.symbol_id
4413 WHERE p.target_terminal_name = ?1
4414 AND {pred}
4415 LIMIT ?2", pred = pending_target_predicate(conn, "s_target", "s_target_parent")),
4416 )?;
4417
4418 let p_rows = pending_stmt.query_map(
4419 params![symbol_name, remaining as i64, sid],
4420 |row| {
4421 Ok(ReferenceSite {
4422 from_symbol_name: row.get(0)?,
4423 from_symbol_id: row.get(1)?,
4424 to_symbol_name: row.get(2)?,
4425 kind: row.get(3)?,
4426 path: row.get::<_, String>(4)?.replace('\\', "/"),
4427 start_line: Some(row.get::<_, i64>(5)? as usize),
4428 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4429 occurrences: None,
4430 target: None,
4431 enclosing_symbol_name: None,
4432 })
4433 },
4434 )?;
4435 for r in p_rows {
4436 results.push(r?);
4437 }
4438 } else {
4439 let mut pending_stmt = conn.prepare(
4440 "SELECT s_from.name AS from_name,
4441 p.from_symbol_id,
4442 p.target_terminal_name AS to_name,
4443 p.kind,
4444 p.path,
4445 p.start_line,
4446 p.start_column
4447 FROM pending_relationships p
4448 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4449 WHERE p.target_terminal_name = ?1
4450 AND (
4451 (p.target_namespace_json IS NULL OR p.target_namespace_json = '[]')
4452 OR EXISTS (
4453 SELECT 1 FROM symbols s_any
4454 JOIN symbols s_any_parent ON s_any.parent_symbol_id = s_any_parent.symbol_id
4455 WHERE s_any.name = p.target_terminal_name
4456 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)
4457 )
4458 )
4459 LIMIT ?2",
4460 )?;
4461
4462 let p_rows =
4463 pending_stmt.query_map(params![symbol_name, remaining as i64], |row| {
4464 Ok(ReferenceSite {
4465 from_symbol_name: row.get(0)?,
4466 from_symbol_id: row.get(1)?,
4467 to_symbol_name: row.get(2)?,
4468 kind: row.get(3)?,
4469 path: row.get::<_, String>(4)?.replace('\\', "/"),
4470 start_line: Some(row.get::<_, i64>(5)? as usize),
4471 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4472 occurrences: None,
4473 target: None,
4474 enclosing_symbol_name: None,
4475 })
4476 })?;
4477 for r in p_rows {
4478 results.push(r?);
4479 }
4480 }
4481 } else {
4482 let is_nested = if let Some(sid) = symbol_id {
4483 conn.query_row(
4484 "SELECT 1 FROM symbols WHERE symbol_id = ?1 AND parent_symbol_id IS NOT NULL",
4485 params![sid],
4486 |_| Ok(true),
4487 )
4488 .unwrap_or(false)
4489 } else {
4490 false
4491 };
4492
4493 if !is_nested {
4494 let mut pending_stmt = conn.prepare(
4495 "SELECT s_from.name AS from_name,
4496 p.from_symbol_id,
4497 p.target_terminal_name AS to_name,
4498 p.kind,
4499 p.path,
4500 p.start_line,
4501 p.start_column
4502 FROM pending_relationships p
4503 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4504 WHERE p.target_terminal_name = ?1
4505 LIMIT ?2",
4506 )?;
4507
4508 let p_rows =
4509 pending_stmt.query_map(params![symbol_name, remaining as i64], |row| {
4510 Ok(ReferenceSite {
4511 from_symbol_name: row.get(0)?,
4512 from_symbol_id: row.get(1)?,
4513 to_symbol_name: row.get(2)?,
4514 kind: row.get(3)?,
4515 path: row.get::<_, String>(4)?.replace('\\', "/"),
4516 start_line: Some(row.get::<_, i64>(5)? as usize),
4517 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4518 occurrences: None,
4519 target: None,
4520 enclosing_symbol_name: None,
4521 })
4522 })?;
4523
4524 for r in p_rows {
4525 results.push(r?);
4526 }
4527 }
4528 }
4529 }
4530
4531 if results.len() < limit && has_table(conn, "identifiers") {
4532 let remaining = limit - results.len();
4533 let identifiers = direct_identifier_rows(conn, symbol_name, symbol_id, remaining)?;
4534 results.extend(identifiers.into_iter().map(|row| row.site));
4535 if let Some(sid) = symbol_id.filter(|_| results.len() < limit) {
4536 let remaining = limit - results.len();
4537 results.extend(grouped_member_access_rows(conn, sid, remaining)?);
4538 }
4539 }
4540 } else {
4541 let mut stmt = conn.prepare(
4543 "SELECT s_from.name AS from_name,
4544 r.from_symbol_id,
4545 s_to.name AS to_name,
4546 r.kind,
4547 r.path,
4548 r.start_line,
4549 r.start_column
4550 FROM relationships r
4551 JOIN symbols s_from ON r.from_symbol_id = s_from.symbol_id
4552 JOIN symbols s_to ON r.to_symbol_id = s_to.symbol_id
4553 WHERE s_from.name = ?1 AND (?3 IS NULL OR r.from_symbol_id = ?3)
4554 LIMIT ?2",
4555 )?;
4556
4557 let rows = stmt.query_map(params![symbol_name, limit as i64, symbol_id], |row| {
4558 Ok(ReferenceSite {
4559 from_symbol_name: row.get(0)?,
4560 from_symbol_id: row.get(1)?,
4561 to_symbol_name: row.get(2)?,
4562 kind: row.get(3)?,
4563 path: row.get::<_, String>(4)?.replace('\\', "/"),
4564 start_line: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
4565 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4566 occurrences: None,
4567 target: None,
4568 enclosing_symbol_name: None,
4569 })
4570 })?;
4571
4572 for r in rows {
4573 results.push(r?);
4574 }
4575
4576 if results.len() < limit {
4578 let remaining = limit - results.len();
4579 let p_rows: Vec<ReferenceSite> = if has_pending_namespace_column(conn) {
4580 let sql = if include_external {
4581 String::from("SELECT DISTINCT s_from.name AS from_name,
4582 p.from_symbol_id,
4583 COALESCE(NULLIF(p.target_display_name, ''), p.target_terminal_name) AS to_name,
4584 p.kind,
4585 p.path,
4586 p.start_line,
4587 p.start_column
4588 FROM pending_relationships p
4589 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4590 WHERE s_from.name = ?1 AND (?3 IS NULL OR p.from_symbol_id = ?3)
4591 LIMIT ?2")
4592 } else {
4593 format!("SELECT DISTINCT s_from.name AS from_name,
4594 p.from_symbol_id,
4595 p.target_terminal_name AS to_name,
4596 p.kind,
4597 p.path,
4598 p.start_line,
4599 p.start_column
4600 FROM pending_relationships p
4601 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4602 WHERE s_from.name = ?1 AND (?3 IS NULL OR p.from_symbol_id = ?3)
4603 AND EXISTS (
4604 SELECT 1 FROM symbols s_to
4605 LEFT JOIN symbols s_to_parent ON s_to.parent_symbol_id = s_to_parent.symbol_id
4606 WHERE s_to.name = p.target_terminal_name
4607 AND {kind}
4608 AND {pred}
4609 )
4610 LIMIT ?2", kind = callee_target_kind("s_to_parent"), pred = pending_target_predicate(conn, "s_to", "s_to_parent"))
4611 };
4612 let mut pending_stmt = conn.prepare(&sql)?;
4613 let rows = pending_stmt.query_map(
4614 params![symbol_name, remaining as i64, symbol_id],
4615 |row| {
4616 Ok(ReferenceSite {
4617 from_symbol_name: row.get(0)?,
4618 from_symbol_id: row.get(1)?,
4619 to_symbol_name: row.get(2)?,
4620 kind: row.get(3)?,
4621 path: row.get::<_, String>(4)?.replace('\\', "/"),
4622 start_line: Some(row.get::<_, i64>(5)? as usize),
4623 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4624 occurrences: None,
4625 target: None,
4626 enclosing_symbol_name: None,
4627 })
4628 },
4629 )?;
4630 let mut out = Vec::new();
4631 for r in rows {
4632 out.push(r?);
4633 }
4634 out
4635 } else {
4636 let sql = if include_external {
4637 "SELECT DISTINCT s_from.name AS from_name,
4638 p.from_symbol_id,
4639 p.target_terminal_name AS to_name,
4640 p.kind,
4641 p.path,
4642 p.start_line,
4643 p.start_column
4644 FROM pending_relationships p
4645 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4646 WHERE s_from.name = ?1 AND (?3 IS NULL OR p.from_symbol_id = ?3)
4647 LIMIT ?2"
4648 } else {
4649 "SELECT DISTINCT s_from.name AS from_name,
4650 p.from_symbol_id,
4651 p.target_terminal_name AS to_name,
4652 p.kind,
4653 p.path,
4654 p.start_line,
4655 p.start_column
4656 FROM pending_relationships p
4657 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4658 WHERE s_from.name = ?1 AND (?3 IS NULL OR p.from_symbol_id = ?3)
4659 AND EXISTS (SELECT 1 FROM symbols s_to WHERE s_to.name = p.target_terminal_name)
4660 LIMIT ?2"
4661 };
4662 let mut pending_stmt = conn.prepare(sql)?;
4663
4664 let rows = pending_stmt.query_map(
4665 params![symbol_name, remaining as i64, symbol_id],
4666 |row| {
4667 Ok(ReferenceSite {
4668 from_symbol_name: row.get(0)?,
4669 from_symbol_id: row.get(1)?,
4670 to_symbol_name: row.get(2)?,
4671 kind: row.get(3)?,
4672 path: row.get::<_, String>(4)?.replace('\\', "/"),
4673 start_line: Some(row.get::<_, i64>(5)? as usize),
4674 start_column: row.get::<_, Option<i64>>(6)?.map(|v| v as usize),
4675 occurrences: None,
4676 target: None,
4677 enclosing_symbol_name: None,
4678 })
4679 },
4680 )?;
4681 let mut out = Vec::new();
4682 for r in rows {
4683 out.push(r?);
4684 }
4685 out
4686 };
4687
4688 for r in p_rows {
4689 results.push(r);
4690 }
4691 }
4692 results.sort_by(|a, b| {
4693 a.path
4694 .cmp(&b.path)
4695 .then(a.start_line.cmp(&b.start_line))
4696 .then(a.start_column.cmp(&b.start_column))
4697 });
4698 let mut sites = merge_same_site(results);
4699 describe_callee_targets(conn, &mut sites)?;
4700 return Ok(sites);
4701 }
4702
4703 Ok(merge_same_site(results))
4704}
4705
4706fn callee_target_kind(parent: &str) -> String {
4709 format!(
4710 "(s_to.kind NOT IN ('import', 'variable', 'parameter', 'field', 'property', 'module', 'namespace')
4711 OR p.kind = 'calls' AND s_to.kind IN ('variable', 'field')
4712 AND {parent}.kind IN ('class', 'struct', 'interface', 'trait', 'record', 'object', 'protocol'))"
4713 )
4714}
4715
4716fn describe_callee_targets(
4719 conn: &Connection,
4720 sites: &mut [ReferenceSite],
4721) -> Result<(), QueryError> {
4722 let owner = "CASE WHEN par.kind IN ('class', 'struct', 'interface', 'trait', 'enum', 'record', 'object', 'protocol', 'union')
4723 THEN par.name || '.' ELSE '' END";
4724 let pending = if has_pending_namespace_column(conn) {
4725 format!(
4726 "UNION
4727 SELECT {owner} || s_to.name, replace(s_to.path, '\\', '/'), s_to.start_line
4728 FROM pending_relationships p
4729 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4730 JOIN symbols s_to ON s_to.name = p.target_terminal_name
4731 LEFT JOIN symbols par ON s_to.parent_symbol_id = par.symbol_id
4732 LEFT JOIN symbols s_to_parent ON s_to.parent_symbol_id = s_to_parent.symbol_id
4733 WHERE p.from_symbol_id = ?1 AND p.start_line = ?2
4734 AND (p.target_terminal_name = ?3 OR p.target_display_name = ?3)
4735 AND {kind}
4736 AND {pred}",
4737 kind = callee_target_kind("s_to_parent"),
4738 pred = pending_target_predicate(conn, "s_to", "s_to_parent")
4739 )
4740 } else {
4741 String::new()
4742 };
4743 let mut stmt = conn.prepare(&format!(
4744 "SELECT {owner} || s_to.name, replace(s_to.path, '\\', '/'), s_to.start_line
4745 FROM relationships r
4746 JOIN symbols s_to ON r.to_symbol_id = s_to.symbol_id
4747 LEFT JOIN symbols par ON s_to.parent_symbol_id = par.symbol_id
4748 WHERE r.from_symbol_id = ?1 AND r.start_line = ?2 AND s_to.name = ?3
4749 {pending}
4750 ORDER BY 2, 3
4751 LIMIT 4"
4752 ))?;
4753 for site in sites.iter_mut() {
4754 let Some(line) = site.start_line else {
4755 continue;
4756 };
4757 let targets = stmt
4758 .query_map(
4759 params![site.from_symbol_id, line as i64, site.to_symbol_name],
4760 |row| {
4761 Ok(format!(
4762 "`{}` ({}:{})",
4763 row.get::<_, String>(0)?,
4764 row.get::<_, String>(1)?,
4765 row.get::<_, i64>(2)?
4766 ))
4767 },
4768 )?
4769 .collect::<Result<Vec<_>, _>>()?;
4770 site.target = match targets.len() {
4771 0 => None,
4772 1 => targets.into_iter().next(),
4773 2 | 3 => Some(format!("one of {}", targets.join(", "))),
4774 _ => Some(format!(
4775 "more than 3 definitions named `{}`",
4776 site.to_symbol_name
4777 )),
4778 };
4779 }
4780 Ok(())
4781}
4782
4783pub fn import_sites(
4787 conn: &Connection,
4788 name: &str,
4789 path_filter: Option<&str>,
4790) -> Result<Vec<(String, usize)>, QueryError> {
4791 import_sites_from(conn, name, path_filter, false)
4792}
4793
4794pub fn import_sites_of_absent(
4797 conn: &Connection,
4798 name: &str,
4799 path: &str,
4800) -> Result<Vec<(String, usize)>, QueryError> {
4801 let mut sites = import_sites_from(conn, name, Some(path), true)?;
4802 sites.extend(rust_use_lines(conn, name, path)?);
4803 sites.sort();
4804 sites.dedup();
4805 Ok(sites)
4806}
4807
4808fn rust_use_lines(
4812 conn: &Connection,
4813 name: &str,
4814 path: &str,
4815) -> Result<Vec<(String, usize)>, QueryError> {
4816 let mut stmt = conn.prepare(
4817 "SELECT DISTINCT replace(pr.path, '\\', '/'), pr.start_line
4818 FROM pending_relationships pr
4819 JOIN files f ON f.file_id = pr.file_id AND f.language = 'rust'
4820 WHERE pr.kind = 'imports' AND pr.target_terminal_name = ?1
4821 AND pr.start_line IS NOT NULL AND json_valid(pr.target_namespace_json)
4822 AND json_extract(pr.target_namespace_json, '$[#-1]') NOT IN ('crate', 'self', 'super')
4823 AND (?2 GLOB '*/' || json_extract(pr.target_namespace_json, '$[#-1]') || '.rs'
4824 OR ?2 GLOB '*/' || json_extract(pr.target_namespace_json, '$[#-1]') || '/mod.rs')",
4825 )?;
4826 let target_path = format!("/{}", path.replace('\\', "/"));
4827 let rows = stmt.query_map(params![name, target_path], |row| {
4828 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize))
4829 })?;
4830 Ok(rows.collect::<Result<_, _>>()?)
4831}
4832
4833fn import_sites_from(
4834 conn: &Connection,
4835 name: &str,
4836 path_filter: Option<&str>,
4837 absent: bool,
4838) -> Result<Vec<(String, usize)>, QueryError> {
4839 if !has_column(conn, "symbols", "metadata_json") {
4840 return Ok(Vec::new());
4841 }
4842 let targets = if absent {
4843 "(SELECT ?1 AS name, ?2 AS path, NULL AS parent_symbol_id, 'function' AS kind)"
4844 } else {
4845 "symbols"
4846 };
4847 let dotted = "replace(module_path.value, '/', '.')";
4848 let segment = format!("replace({dotted}, rtrim({dotted}, replace({dotted}, '.', '')), '')");
4849 let target_path = "('/' || replace(target.path, '\\', '/'))";
4850 let dir = |path: &str| {
4852 format!("rtrim(replace({path}, '\\', '/'), replace(replace({path}, '\\', '/'), '/', ''))")
4853 };
4854 let (import_dir, target_dir) = (dir("i.path"), dir("target.path"));
4855 let mut stmt = conn.prepare(&format!(
4856 "SELECT DISTINCT replace(i.path, '\\', '/'), i.start_line
4857 FROM {targets} target
4858 JOIN symbols i ON i.name = target.name AND i.kind = 'import'
4859 CROSS JOIN json_each(json_array(json_extract(i.metadata_json, '$.source'))) module_path
4860 WHERE target.name = ?1
4861 AND target.parent_symbol_id IS NULL
4862 AND target.kind NOT IN ({LOW_SIGNAL_KINDS_SQL})
4863 AND (?2 IS NULL OR replace(target.path, '\\', '/') = ?2
4864 OR replace(target.path, '\\', '/') LIKE ?3 ESCAPE '\\')
4865 AND json_valid(i.metadata_json)
4866 AND i.language NOT IN ({docs})
4867 AND (
4868 {segment} != ''
4869 AND ({target_path} GLOB '*/' || {segment} || '/*'
4870 OR {target_path} GLOB '*/' || {segment} || '.*')
4871 OR module_path.value != '' AND replace(module_path.value, '.', '') = ''
4872 AND substr({import_dir}, 1, length({target_dir})) = {target_dir}
4873 )
4874 ORDER BY 1, 2",
4875 docs = documentation_language_list()
4876 ))?;
4877 let norm = path_filter.map(|p| p.replace('\\', "/").trim_matches('/').to_string());
4878 let prefix = norm.as_ref().map(|p| format!("{}/%", escape_like(p)));
4879 let rows = stmt.query_map(params![name, norm, prefix], |row| {
4880 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize))
4881 })?;
4882 Ok(rows.collect::<Result<_, _>>()?)
4883}
4884
4885fn merge_same_site(sites: Vec<ReferenceSite>) -> Vec<ReferenceSite> {
4888 let mut merged: Vec<ReferenceSite> = Vec::with_capacity(sites.len());
4889 let mut index = HashMap::new();
4890 for site in sites {
4891 let key = (
4892 site.from_symbol_id.clone(),
4893 site.to_symbol_name.clone(),
4894 site.kind.clone(),
4895 site.path.clone(),
4896 site.start_line,
4897 );
4898 match index.get(&key) {
4899 Some(&at) => {
4900 let kept: &mut ReferenceSite = &mut merged[at];
4901 kept.start_column = kept.start_column.min(site.start_column);
4902 }
4903 None => {
4904 index.insert(key, merged.len());
4905 merged.push(site);
4906 }
4907 }
4908 }
4909 merged
4910}
4911
4912pub fn find_callee_signatures(
4915 conn: &Connection,
4916 symbol_name: &str,
4917 symbol_id: &str,
4918 limit: usize,
4919 include_external: bool,
4920) -> Result<Vec<String>, QueryError> {
4921 let mut stmt = conn.prepare(
4922 "SELECT DISTINCT s_to.name, s_to.signature, s_to.path, s_to.start_line, s_to.kind
4923 FROM relationships r
4924 JOIN symbols s_from ON r.from_symbol_id = s_from.symbol_id
4925 JOIN symbols s_to ON r.to_symbol_id = s_to.symbol_id
4926 WHERE s_from.name = ?1 AND r.from_symbol_id = ?2
4927 LIMIT ?3",
4928 )?;
4929
4930 let rows = stmt.query_map(params![symbol_name, symbol_id, (limit * 2) as i64], |row| {
4931 Ok((
4932 row.get::<_, String>(0)?,
4933 row.get::<_, Option<String>>(1)?,
4934 row.get::<_, String>(2)?.replace('\\', "/"),
4935 row.get::<_, Option<i64>>(3)?.unwrap_or(1) as usize,
4936 row.get::<_, String>(4)?,
4937 ))
4938 })?;
4939
4940 let mut signatures = Vec::new();
4941 let mut variants = Vec::new();
4942
4943 for r in rows.flatten() {
4944 let (name, sig_opt, path, line, kind) = r;
4945 let sig = sig_opt.unwrap_or(name);
4946 let entry = format!("{sig} ({path}:{line})");
4947 if kind == "variant" {
4948 if !variants.contains(&entry) {
4949 variants.push(entry);
4950 }
4951 } else if !signatures.contains(&entry) {
4952 signatures.push(entry);
4953 }
4954 }
4955
4956 if signatures.len() < limit {
4957 let remaining = (limit - signatures.len()) * 2;
4958 let p_rows: Vec<(String, Option<String>, String, usize, String)> =
4959 if has_pending_namespace_column(conn) {
4960 let mut p_stmt = conn.prepare(
4961 &format!("SELECT DISTINCT s_to.name, s_to.signature, s_to.path, s_to.start_line, s_to.kind
4962 FROM pending_relationships p
4963 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4964 JOIN symbols s_to ON s_to.name = p.target_terminal_name
4965 LEFT JOIN symbols s_parent ON s_to.parent_symbol_id = s_parent.symbol_id
4966 WHERE s_from.name = ?1 AND p.from_symbol_id = ?2
4967 AND {kind}
4968 AND {pred}
4969 LIMIT ?3", kind = callee_target_kind("s_parent"), pred = pending_target_predicate(conn, "s_to", "s_parent")),
4970 )?;
4971
4972 let rows =
4973 p_stmt.query_map(params![symbol_name, symbol_id, remaining as i64], |row| {
4974 Ok((
4975 row.get::<_, String>(0)?,
4976 row.get::<_, Option<String>>(1)?,
4977 row.get::<_, String>(2)?.replace('\\', "/"),
4978 row.get::<_, Option<i64>>(3)?.unwrap_or(1) as usize,
4979 row.get::<_, String>(4)?,
4980 ))
4981 })?;
4982 rows.flatten().collect()
4983 } else {
4984 let mut p_stmt = conn.prepare(
4985 "SELECT DISTINCT s_to.name, s_to.signature, s_to.path, s_to.start_line, s_to.kind
4986 FROM pending_relationships p
4987 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
4988 JOIN symbols s_to ON s_to.name = p.target_terminal_name
4989 WHERE s_from.name = ?1 AND p.from_symbol_id = ?2
4990 AND s_to.kind NOT IN ('import', 'variable', 'parameter', 'field', 'property', 'module', 'namespace')
4991 LIMIT ?3",
4992 )?;
4993
4994 let rows =
4995 p_stmt.query_map(params![symbol_name, symbol_id, remaining as i64], |row| {
4996 Ok((
4997 row.get::<_, String>(0)?,
4998 row.get::<_, Option<String>>(1)?,
4999 row.get::<_, String>(2)?.replace('\\', "/"),
5000 row.get::<_, Option<i64>>(3)?.unwrap_or(1) as usize,
5001 row.get::<_, String>(4)?,
5002 ))
5003 })?;
5004 rows.flatten().collect()
5005 };
5006
5007 for r in p_rows {
5008 let (name, sig_opt, path, line, kind) = r;
5009 let sig = sig_opt.unwrap_or(name);
5010 let entry = format!("{sig} ({path}:{line})");
5011 if kind == "variant" {
5012 if !variants.contains(&entry) {
5013 variants.push(entry);
5014 }
5015 } else if !signatures.contains(&entry) {
5016 signatures.push(entry);
5017 }
5018 }
5019 }
5020
5021 if include_external && signatures.len() < limit {
5022 let remaining = (limit - signatures.len()) * 2;
5023 let ext_rows: Vec<(String, String, usize)> = if has_pending_namespace_column(conn) {
5024 let mut ext_stmt = conn.prepare(
5025 &format!("SELECT DISTINCT COALESCE(NULLIF(p.target_display_name, ''), p.target_terminal_name), p.path, p.start_line
5026 FROM pending_relationships p
5027 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
5028 WHERE s_from.name = ?1 AND p.from_symbol_id = ?2
5029 AND NOT EXISTS (
5030 SELECT 1 FROM symbols s_to
5031 LEFT JOIN symbols s_parent ON s_to.parent_symbol_id = s_parent.symbol_id
5032 WHERE s_to.name = p.target_terminal_name
5033 AND {kind}
5034 AND {pred}
5035 )
5036 LIMIT ?3", kind = callee_target_kind("s_parent"), pred = pending_target_predicate(conn, "s_to", "s_parent")),
5037 )?;
5038
5039 let rows =
5040 ext_stmt.query_map(params![symbol_name, symbol_id, remaining as i64], |row| {
5041 Ok((
5042 row.get::<_, String>(0)?,
5043 row.get::<_, String>(1)?.replace('\\', "/"),
5044 row.get::<_, Option<i64>>(2)?.unwrap_or(1) as usize,
5045 ))
5046 })?;
5047 rows.flatten().collect()
5048 } else {
5049 let mut ext_stmt = conn.prepare(
5050 "SELECT DISTINCT p.target_terminal_name, p.path, p.start_line
5051 FROM pending_relationships p
5052 JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
5053 WHERE s_from.name = ?1 AND p.from_symbol_id = ?2
5054 AND NOT EXISTS (
5055 SELECT 1 FROM symbols s_to
5056 WHERE s_to.name = p.target_terminal_name
5057 AND s_to.kind NOT IN ('import', 'variable', 'parameter', 'field', 'property', 'module', 'namespace')
5058 )
5059 LIMIT ?3",
5060 )?;
5061
5062 let rows =
5063 ext_stmt.query_map(params![symbol_name, symbol_id, remaining as i64], |row| {
5064 Ok((
5065 row.get::<_, String>(0)?,
5066 row.get::<_, String>(1)?.replace('\\', "/"),
5067 row.get::<_, Option<i64>>(2)?.unwrap_or(1) as usize,
5068 ))
5069 })?;
5070 rows.flatten().collect()
5071 };
5072
5073 for r in ext_rows {
5074 let (name, path, line) = r;
5075 let entry = format!("{name} ({path}:{line})");
5076 if !signatures.contains(&entry) {
5077 signatures.push(entry);
5078 }
5079 }
5080 }
5081
5082 for v in variants {
5083 if signatures.len() >= limit {
5084 break;
5085 }
5086 if !signatures.contains(&v) {
5087 signatures.push(v);
5088 }
5089 }
5090
5091 signatures.truncate(limit);
5092 Ok(signatures)
5093}
5094
5095const SQL_FAMILIES: &[&str] = &["sql."];
5096
5097pub(crate) const ROUTE_FAMILIES: &[&str] = &[
5098 ".route.",
5099 ".attribute_route.",
5100 ".scope_route.",
5101 ".file_route.",
5102 ".route_handler.",
5103 ".route_reference.",
5104 ".route_group.",
5105 ".route_prefix.",
5106 ".resource_route.",
5107 ".router_mount.",
5108 ".include_router.",
5109 ".server_route.",
5110 ".route_definition.",
5111];
5112
5113const CONFIG_FAMILIES: &[&str] = &["toml.", "yaml.", "json.", "env."];
5114
5115const MODEL_FAMILIES: &[&str] = &[
5116 "sql.table_definition.v1",
5117 "sql.column_definition.v1",
5118 "sql.constraint.v1",
5119 "sql.foreign_key.v1",
5120 "sql.index_definition.v1",
5121 "json.schema.v1",
5122];
5123
5124const SIGNAL_FAMILIES: &[&str] = &[".signal_declaration."];
5125
5126const IMPORT_FAMILIES: &[&str] = &[".import_statement.", ".import."];
5127
5128const BINDING_FAMILIES: &[&str] = &[".binding."];
5129
5130const COMPONENT_FAMILIES: &[&str] = &[".object_instantiation.", ".object_type."];
5131
5132const MODULE_FAMILIES: &[&str] = &[".module."];
5133
5134const PRAGMA_FAMILIES: &[&str] = &[".pragma.", "javascript.qml_directive.v1"];
5135
5136const PROPERTY_FAMILIES: &[&str] = &[".property_declaration.", ".qt_property."];
5137
5138pub const CATEGORY_ALIASES: &[(&str, &[&str])] = &[
5143 ("sql", SQL_FAMILIES),
5144 ("query", SQL_FAMILIES),
5145 ("queries", SQL_FAMILIES),
5146 ("route", ROUTE_FAMILIES),
5147 ("routes", ROUTE_FAMILIES),
5148 ("config", CONFIG_FAMILIES),
5149 ("model", MODEL_FAMILIES),
5150 ("models", MODEL_FAMILIES),
5151 ("signal", SIGNAL_FAMILIES),
5152 ("signals", SIGNAL_FAMILIES),
5153 ("import", IMPORT_FAMILIES),
5154 ("imports", IMPORT_FAMILIES),
5155 ("binding", BINDING_FAMILIES),
5156 ("bindings", BINDING_FAMILIES),
5157 ("component", COMPONENT_FAMILIES),
5158 ("components", COMPONENT_FAMILIES),
5159 ("module", MODULE_FAMILIES),
5160 ("modules", MODULE_FAMILIES),
5161 ("pragma", PRAGMA_FAMILIES),
5162 ("property", PROPERTY_FAMILIES),
5163 ("properties", PROPERTY_FAMILIES),
5164];
5165
5166fn category_families(category: &str) -> Option<&'static [&'static str]> {
5167 let wanted = category.trim().to_ascii_lowercase();
5168 CATEGORY_ALIASES
5169 .iter()
5170 .find(|(alias, _)| *alias == wanted)
5171 .map(|(_, families)| *families)
5172}
5173
5174pub(crate) fn matches_family(pattern_id: &str, rule: &str) -> bool {
5175 if rule.starts_with('.') {
5176 pattern_id.contains(rule)
5177 } else if rule.ends_with('.') {
5178 pattern_id.starts_with(rule)
5179 } else {
5180 pattern_id == rule
5181 }
5182}
5183
5184fn family_clause(column: &str, families: &[&str]) -> String {
5185 let alternatives: Vec<String> = families
5186 .iter()
5187 .map(|rule| {
5188 let escaped = escape_like(rule);
5189 if rule.starts_with('.') {
5190 format!("{column} LIKE '%{escaped}%' ESCAPE '\\'")
5191 } else if rule.ends_with('.') {
5192 format!("{column} LIKE '{escaped}%' ESCAPE '\\'")
5193 } else {
5194 format!("{column} = '{rule}'")
5195 }
5196 })
5197 .collect();
5198 format!("({})", alternatives.join(" OR "))
5199}
5200
5201pub fn is_category_alias(category: &str) -> bool {
5203 category_families(category).is_some()
5204}
5205
5206pub fn alias_fact_counts(categories: &[(String, usize)]) -> Vec<(&'static str, usize, usize)> {
5210 let mut reported: Vec<&[&str]> = Vec::new();
5211 let mut counts = Vec::new();
5212 for (alias, families) in CATEGORY_ALIASES {
5213 if reported.contains(families) {
5214 continue;
5215 }
5216 reported.push(families);
5217 let mut patterns = 0;
5218 let mut facts = 0;
5219 for (pattern_id, count) in categories {
5220 if families.iter().any(|rule| matches_family(pattern_id, rule)) {
5221 patterns += 1;
5222 facts += count;
5223 }
5224 }
5225 if facts > 0 {
5226 counts.push((*alias, patterns, facts));
5227 }
5228 }
5229 counts
5230}
5231
5232pub fn find_structural_facts_scoped(
5234 conn: &Connection,
5235 category: &str,
5236 path_filter: Option<&str>,
5237 limit: usize,
5238) -> Result<Vec<StructuralFact>, QueryError> {
5239 validate_result_limit(limit)?;
5240 let norm_path = path_filter
5241 .map(|p| {
5242 p.replace('\\', "/")
5243 .trim_start_matches("./")
5244 .trim_matches('/')
5245 .to_string()
5246 })
5247 .filter(|p| !p.is_empty());
5248 let dir_prefix = norm_path
5249 .as_deref()
5250 .map(|p| format!("{}/%", escape_like(p)));
5251 let cat_pattern = format!("%{}%", escape_like(category));
5252
5253 let cat_clause = match category_families(category) {
5254 Some(families) => family_clause("sf.pattern_id", families),
5255 None => {
5256 "(sf.pattern_id LIKE :cat ESCAPE '\\' OR sf.capture_name LIKE :cat ESCAPE '\\' OR sf.node_kind LIKE :cat ESCAPE '\\')"
5257 .to_string()
5258 }
5259 };
5260
5261 let sql = format!(
5262 "SELECT sf.structural_fact_id, sf.path, sf.language, sf.pattern_id,
5263 sf.capture_name, sf.node_kind, s.name AS containing_symbol_name,
5264 sf.start_line, sf.end_line, sf.confidence,
5265 COALESCE(
5266 CASE WHEN json_extract(sf.metadata_json, '$.key_path') LIKE '$.%'
5267 THEN substr(json_extract(sf.metadata_json, '$.key_path'), 3)
5268 ELSE json_extract(sf.metadata_json, '$.key_path') END,
5269 json_extract(sf.metadata_json, '$.key'),
5270 CASE WHEN json_extract(sf.metadata_json, '$.normalized_route_template') IS NOT NULL
5271 THEN COALESCE(json_extract(sf.metadata_json, '$.verb') || ' ', '')
5272 || json_extract(sf.metadata_json, '$.normalized_route_template') END,
5273 CASE WHEN sf.pattern_id = 'cpp.qt_property.v1'
5274 THEN json_extract(sf.metadata_json, '$.name') END
5275 ) AS display_key,
5276 sf.metadata_json
5277 FROM structural_facts sf
5278 LEFT JOIN symbols s ON sf.containing_symbol_id = s.symbol_id
5279 WHERE (:cat IS NOT NULL AND {cat_clause})
5280 AND (:path IS NULL OR replace(sf.path, '\\', '/') = :path COLLATE NOCASE OR replace(sf.path, '\\', '/') LIKE :dir_prefix ESCAPE '\\')
5281 ORDER BY sf.path ASC, sf.start_line ASC,
5282 COALESCE(
5283 json_extract(sf.metadata_json, '$.effective_route_template'),
5284 json_extract(sf.metadata_json, '$.route_template'),
5285 json_extract(sf.metadata_json, '$.normalized_route_template'),
5286 ''
5287 ) ASC,
5288 COALESCE(json_extract(sf.metadata_json, '$.verb'), '') ASC,
5289 sf.structural_fact_id ASC
5290 LIMIT :limit"
5291 );
5292
5293 let mut stmt = conn.prepare(&sql)?;
5294 let rows = stmt.query_map(
5295 rusqlite::named_params! {
5296 ":cat": cat_pattern,
5297 ":path": norm_path.as_deref(),
5298 ":dir_prefix": dir_prefix.as_deref(),
5299 ":limit": limit as i64,
5300 },
5301 |row| {
5302 Ok(StructuralFact {
5303 structural_fact_id: row.get(0)?,
5304 path: row.get::<_, String>(1)?.replace('\\', "/"),
5305 language: row.get(2)?,
5306 pattern_id: row.get(3)?,
5307 capture_name: row.get(4)?,
5308 node_kind: row.get(5)?,
5309 key: row.get(10)?,
5310 metadata: row
5311 .get::<_, Option<String>>(11)?
5312 .map(|json| serde_json::from_str(&json))
5313 .transpose()
5314 .map_err(|err| {
5315 rusqlite::Error::FromSqlConversionFailure(
5316 11,
5317 rusqlite::types::Type::Text,
5318 Box::new(err),
5319 )
5320 })?,
5321 containing_symbol_name: row.get(6)?,
5322 start_line: row.get::<_, i64>(7)? as usize,
5323 end_line: row.get::<_, i64>(8)? as usize,
5324 confidence: row.get(9)?,
5325 })
5326 },
5327 )?;
5328
5329 let mut results = Vec::new();
5330 for r in rows {
5331 let mut fact = r?;
5332 let display = fact.metadata.as_ref().and_then(|metadata| {
5333 route_display(metadata).or_else(|| mount_display(&fact.capture_name, metadata))
5334 });
5335 if let Some(display) = display {
5336 fact.key = Some(display);
5337 }
5338 results.push(fact);
5339 }
5340 Ok(results)
5341}
5342
5343fn route_display(metadata: &serde_json::Value) -> Option<String> {
5347 let text = |key: &str| metadata.get(key).and_then(|value| value.as_str());
5348 let normalized = text("normalized_route_template")?;
5349 let template = match (text("effective_route_template"), text("route_template")) {
5350 (Some(effective), _) => effective,
5351 (None, Some(raw)) if route_shape(raw) == route_shape(normalized) => raw,
5352 _ => normalized,
5353 };
5354 Some(match text("verb") {
5355 Some(verb) => format!("{verb} {template}"),
5356 None => template.to_string(),
5357 })
5358}
5359
5360fn mount_display(capture: &str, metadata: &serde_json::Value) -> Option<String> {
5363 let text = |key: &str| metadata.get(key).and_then(|value| value.as_str());
5364 if text("query_family") != Some("framework") {
5365 return None;
5366 }
5367 let target = text("mount_target")?;
5368 Some(
5369 match text("normalized_mount_path").or_else(|| text("mount_path")) {
5370 Some(path) => format!("{capture} `{target}` at {path}"),
5371 None => format!("{capture} `{target}`"),
5372 },
5373 )
5374}
5375
5376fn route_shape(template: &str) -> String {
5378 let mut shape = String::new();
5379 let mut chars = template.chars().peekable();
5380 while let Some(c) = chars.next() {
5381 match c {
5382 '<' | '{' => {
5383 let close = if c == '<' { '>' } else { '}' };
5384 for inner in chars.by_ref() {
5385 if inner == close {
5386 break;
5387 }
5388 }
5389 shape.push('*');
5390 }
5391 ':' => {
5392 while chars.peek().is_some_and(|next| *next != '/') {
5393 chars.next();
5394 }
5395 shape.push('*');
5396 }
5397 _ => shape.push(c),
5398 }
5399 }
5400 shape
5401}
5402
5403pub fn find_structural_facts(
5405 conn: &Connection,
5406 category: &str,
5407 limit: usize,
5408) -> Result<Vec<StructuralFact>, QueryError> {
5409 find_structural_facts_scoped(conn, category, None, limit)
5410}
5411
5412pub fn find_literals_scoped(
5414 conn: &Connection,
5415 category: &str,
5416 path_filter: Option<&str>,
5417 limit: usize,
5418) -> Result<Vec<LiteralFact>, QueryError> {
5419 validate_result_limit(limit)?;
5420 let norm_path = path_filter
5421 .map(|p| {
5422 p.replace('\\', "/")
5423 .trim_start_matches("./")
5424 .trim_matches('/')
5425 .to_string()
5426 })
5427 .filter(|p| !p.is_empty());
5428 let dir_prefix = norm_path
5429 .as_deref()
5430 .map(|p| format!("{}/%", escape_like(p)));
5431 let cat_pattern = format!("%{}%", escape_like(category));
5432
5433 let cat_lower = category.trim().to_ascii_lowercase();
5434 let cat_clause = match cat_lower.as_str() {
5435 "config" => {
5436 "(l.kind LIKE '%config%' OR l.kind LIKE '%toml%' OR l.kind LIKE '%json%' OR l.kind LIKE '%yaml%')"
5437 }
5438 "route" | "routes" => "l.kind LIKE '%route%'",
5439 "query" | "queries" | "sql" => "(l.kind LIKE '%sql%' OR l.kind LIKE '%query%')",
5440 "model" | "models" => "l.kind LIKE '%model%'",
5441 _ => "(l.kind LIKE :cat ESCAPE '\\' OR l.literal_text LIKE :cat ESCAPE '\\')",
5442 };
5443
5444 let sql = format!(
5445 "SELECT l.literal_id, l.path, l.literal_text, l.kind, l.carrier,
5446 l.start_line, s.name AS containing_symbol_name
5447 FROM literals l
5448 LEFT JOIN symbols s ON l.containing_symbol_id = s.symbol_id
5449 WHERE (:cat IS NOT NULL AND {cat_clause})
5450 AND (:path IS NULL OR replace(l.path, '\\', '/') = :path COLLATE NOCASE OR replace(l.path, '\\', '/') LIKE :dir_prefix ESCAPE '\\')
5451 ORDER BY l.path ASC, l.start_line ASC
5452 LIMIT :limit"
5453 );
5454
5455 let mut stmt = conn.prepare(&sql)?;
5456 let rows = stmt.query_map(
5457 rusqlite::named_params! {
5458 ":cat": cat_pattern,
5459 ":path": norm_path.as_deref(),
5460 ":dir_prefix": dir_prefix.as_deref(),
5461 ":limit": limit as i64,
5462 },
5463 |row| {
5464 Ok(LiteralFact {
5465 literal_id: row.get(0)?,
5466 path: row.get::<_, String>(1)?.replace('\\', "/"),
5467 literal_text: row.get(2)?,
5468 kind: row.get(3)?,
5469 carrier: row.get(4)?,
5470 start_line: row.get::<_, i64>(5)? as usize,
5471 containing_symbol_name: row.get(6)?,
5472 })
5473 },
5474 )?;
5475
5476 let mut results = Vec::new();
5477 for r in rows {
5478 results.push(r?);
5479 }
5480 Ok(results)
5481}
5482
5483pub fn find_literals(
5485 conn: &Connection,
5486 category: &str,
5487 limit: usize,
5488) -> Result<Vec<LiteralFact>, QueryError> {
5489 find_literals_scoped(conn, category, None, limit)
5490}
5491
5492pub fn list_structural_fact_categories_scoped(
5494 conn: &Connection,
5495 path_filter: Option<&str>,
5496) -> Result<Vec<(String, usize)>, QueryError> {
5497 let norm_path = path_filter
5498 .map(|p| {
5499 p.replace('\\', "/")
5500 .trim_start_matches("./")
5501 .trim_matches('/')
5502 .to_string()
5503 })
5504 .filter(|p| !p.is_empty());
5505 let dir_prefix = norm_path
5506 .as_deref()
5507 .map(|p| format!("{}/%", escape_like(p)));
5508
5509 let mut categories = Vec::new();
5510
5511 let sql = "SELECT pattern_id, COUNT(*) AS cnt FROM structural_facts
5512 WHERE (:path IS NULL OR replace(path, '\\', '/') = :path COLLATE NOCASE OR replace(path, '\\', '/') LIKE :dir_prefix ESCAPE '\\')
5513 GROUP BY pattern_id ORDER BY cnt DESC";
5514 let mut stmt = conn.prepare(sql)?;
5515 let rows = stmt.query_map(
5516 rusqlite::named_params! {
5517 ":path": norm_path.as_deref(),
5518 ":dir_prefix": dir_prefix.as_deref(),
5519 },
5520 |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize)),
5521 )?;
5522 for r in rows {
5523 categories.push(r?);
5524 }
5525
5526 let lit_sql = "SELECT kind, COUNT(*) AS cnt FROM literals
5527 WHERE (:path IS NULL OR replace(path, '\\', '/') = :path COLLATE NOCASE OR replace(path, '\\', '/') LIKE :dir_prefix ESCAPE '\\')
5528 GROUP BY kind ORDER BY cnt DESC";
5529 let mut lit_stmt = conn.prepare(lit_sql)?;
5530 let lit_rows = lit_stmt.query_map(
5531 rusqlite::named_params! {
5532 ":path": norm_path.as_deref(),
5533 ":dir_prefix": dir_prefix.as_deref(),
5534 },
5535 |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize)),
5536 )?;
5537 for r in lit_rows {
5538 categories.push(r?);
5539 }
5540
5541 Ok(categories)
5542}
5543
5544pub fn list_structural_fact_categories(
5546 conn: &Connection,
5547) -> Result<Vec<(String, usize)>, QueryError> {
5548 list_structural_fact_categories_scoped(conn, None)
5549}
5550
5551pub fn find_type_facts(conn: &Connection, symbol_id: &str) -> Result<Vec<TypeFact>, QueryError> {
5553 let has_table: bool = conn
5554 .query_row(
5555 "SELECT 1 FROM sqlite_master WHERE type='table' AND name='type_facts'",
5556 [],
5557 |_| Ok(true),
5558 )
5559 .unwrap_or(false);
5560 if !has_table {
5561 return Ok(Vec::new());
5562 }
5563
5564 let mut stmt = conn.prepare(
5565 "SELECT type_fact_id, symbol_id, language, resolved_type, generic_params_json
5566 FROM type_facts
5567 WHERE symbol_id = ?1",
5568 )?;
5569
5570 let rows = stmt.query_map(params![symbol_id], |row| {
5571 Ok(TypeFact {
5572 type_fact_id: row.get(0)?,
5573 symbol_id: row.get(1)?,
5574 language: row.get(2)?,
5575 resolved_type: row.get(3)?,
5576 generic_params: row.get(4)?,
5577 })
5578 })?;
5579
5580 let mut results = Vec::new();
5581 for r in rows {
5582 results.push(r?);
5583 }
5584 Ok(results)
5585}
5586
5587pub fn find_related_types(
5591 conn: &Connection,
5592 target: &Symbol,
5593 limit: usize,
5594) -> Result<Vec<String>, QueryError> {
5595 if !has_table(conn, "type_facts") {
5596 return Ok(Vec::new());
5597 }
5598 let mut stmt = conn.prepare(
5599 "SELECT d.name, d.kind, d.signature, d.path, d.start_line
5600 FROM symbols d
5601 WHERE d.kind IN ('struct', 'enum', 'trait', 'class', 'interface', 'type', 'union')
5602 AND d.name IN (
5603 SELECT t.resolved_type FROM type_facts t
5604 JOIN symbols s ON s.symbol_id = t.symbol_id
5605 WHERE s.symbol_id = ?1 OR s.parent_symbol_id = ?1
5606 )
5607 ORDER BY d.path = ?2 DESC, length(d.path), d.path, d.start_line",
5608 )?;
5609 let rows = stmt.query_map(params![target.symbol_id, target.path], |row| {
5610 Ok((
5611 row.get::<_, String>(0)?,
5612 row.get::<_, String>(1)?,
5613 row.get::<_, Option<String>>(2)?,
5614 row.get::<_, String>(3)?,
5615 row.get::<_, i64>(4)?,
5616 ))
5617 })?;
5618
5619 let mut seen = std::collections::HashSet::new();
5620 let mut types = Vec::new();
5621 for row in rows {
5622 let (name, kind, signature, path, line) = row?;
5623 if types.len() >= limit {
5624 break;
5625 }
5626 if !seen.insert(name.clone()) {
5627 continue;
5628 }
5629 let signature = signature.unwrap_or_else(|| format!("{kind} {name}"));
5630 types.push(format!(
5631 "{} ({path}:{line})",
5632 type_declaration_line(&signature)
5633 ));
5634 }
5635 Ok(types)
5636}
5637
5638fn type_declaration_line(signature: &str) -> String {
5640 let mut declaration = signature;
5641 while declaration.starts_with("#[")
5642 && let Some((_, rest)) = declaration.split_once("] ")
5643 {
5644 declaration = rest;
5645 }
5646 let line = declaration.split_whitespace().collect::<Vec<_>>().join(" ");
5647 if line.chars().count() <= 120 {
5648 return line;
5649 }
5650 line.chars().take(119).collect::<String>() + "…"
5651}
5652
5653fn names_a_test_sql(alias: &str) -> String {
5664 ["test%", "%test", "%tests", "%spec", "%specs"]
5665 .map(|pattern| format!("{alias}.name LIKE '{pattern}'"))
5666 .join(" OR ")
5667}
5668
5669fn enclosing_test(conn: &Connection, row: Symbol) -> Result<Symbol, QueryError> {
5672 if row.is_test || row.test_container || names_a_test(&row.name) {
5673 return Ok(row);
5674 }
5675 let parent = row
5676 .parent_symbol_id
5677 .as_deref()
5678 .map(|id| get_symbol_by_id(conn, id))
5679 .transpose()?
5680 .flatten();
5681 Ok(match parent {
5682 Some(parent) if matches!(parent.kind.as_str(), "function" | "method") => parent,
5683 _ => row,
5684 })
5685}
5686
5687pub(crate) fn names_a_test(name: &str) -> bool {
5688 let lowered = name.to_ascii_lowercase();
5689 lowered.starts_with("test")
5690 || ["test", "tests", "spec", "specs"]
5691 .iter()
5692 .any(|suffix| lowered.ends_with(suffix))
5693}
5694
5695pub fn is_test_path(path: &str) -> bool {
5696 let p = path.replace('\\', "/");
5697 let cut = p.rfind('/').map_or(0, |i| i + 1);
5698 let directories = format!("/{}/", p[..cut].to_lowercase());
5699 let file_name = &p[cut..];
5700 let lower_name = file_name.to_lowercase();
5701 directories.contains("/test/")
5702 || directories.contains("/tests/")
5703 || directories.contains("/autotests/")
5704 || directories.contains("/__tests__/")
5705 || lower_name.starts_with("tst_")
5706 || (lower_name.starts_with("test_")
5707 && (lower_name.ends_with(".py") || lower_name.ends_with(".rb")))
5708 || lower_name.contains("_test.")
5709 || lower_name.contains(".test.")
5710 || lower_name.contains(".spec.")
5711 || lower_name == "test.rs"
5712 || lower_name == "tests.rs"
5713 || file_name.ends_with("Tests.cs")
5714}
5715
5716pub(crate) fn test_path_predicate(alias: &str) -> String {
5725 let guard = format!(
5726 "(lower({alias}.path) LIKE '%test%' OR lower({alias}.path) LIKE '%spec%' OR lower({alias}.path) LIKE '%tst\\_%' ESCAPE '\\')"
5727 );
5728 let p = format!("replace({alias}.path, '\\', '/')");
5729 let directories = format!("'/' || lower(rtrim({p}, replace({p}, '/', ''))) || '/'");
5730 let file_name = format!("replace({p}, rtrim({p}, replace({p}, '/', '')), '')");
5731 let lower_name = format!("lower({file_name})");
5732 let like = |subject: &String, pattern: &str| format!("{subject} LIKE '{pattern}' ESCAPE '\\'");
5733 let clauses = [
5734 like(&directories, "%/test/%"),
5735 like(&directories, "%/tests/%"),
5736 like(&directories, "%/autotests/%"),
5737 like(&directories, "%/\\_\\_tests\\_\\_/%"),
5738 like(&lower_name, "tst\\_%"),
5739 like(&lower_name, "test\\_%.py"),
5740 like(&lower_name, "test\\_%.rb"),
5741 like(&lower_name, "%\\_test.%"),
5742 like(&lower_name, "%.test.%"),
5743 like(&lower_name, "%.spec.%"),
5744 format!("{lower_name} = 'test.rs'"),
5745 format!("{lower_name} = 'tests.rs'"),
5746 format!("{file_name} GLOB '*Tests.cs'"),
5747 ]
5748 .join(" OR ");
5749 format!("({guard} AND ({clauses}))")
5750}
5751
5752pub fn compute_blast_radius_scoped(
5755 conn: &Connection,
5756 seed_symbols: &[&str],
5757 symbol_path_filter: Option<&str>,
5758 seed_paths: &[&str],
5759 max_depth: usize,
5760 limit: usize,
5761) -> Result<BlastRadiusResult, QueryError> {
5762 compute_blast_radius_scoped_with_ids(
5763 conn,
5764 seed_symbols,
5765 &[],
5766 symbol_path_filter,
5767 seed_paths,
5768 max_depth,
5769 limit,
5770 )
5771}
5772
5773const MAX_BLAST_DEPTH: usize = 5;
5774
5775pub fn compute_blast_radius_scoped_with_ids(
5777 conn: &Connection,
5778 seed_symbols: &[&str],
5779 seed_ids: &[&str],
5780 symbol_path_filter: Option<&str>,
5781 seed_paths: &[&str],
5782 max_depth: usize,
5783 limit: usize,
5784) -> Result<BlastRadiusResult, QueryError> {
5785 validate_result_limit(limit)?;
5786 let max_depth = max_depth.min(MAX_BLAST_DEPTH);
5787 let mut resolved_seed_symbols = seed_symbols
5788 .iter()
5789 .map(|name| {
5790 get_symbol_by_name(conn, name, symbol_path_filter)?.ok_or_else(|| {
5791 let (workspace, hint) = symbol_not_found_parts(conn, name, symbol_path_filter);
5792 QueryError::SymbolNotFound {
5793 name: (*name).to_string(),
5794 workspace,
5795 hint,
5796 }
5797 })
5798 })
5799 .collect::<Result<Vec<_>, _>>()?;
5800 for id in seed_ids {
5801 let symbol = get_symbol_by_id(conn, id)?.ok_or_else(|| {
5802 let workspace = workspace_name(conn);
5803 QueryError::SymbolNotFound {
5804 name: format!("symbol id {id}"),
5805 workspace,
5806 hint: "Run lookup_symbol again and select a current id.".to_string(),
5807 }
5808 })?;
5809 resolved_seed_symbols.push(symbol);
5810 }
5811 let mut seeds = Vec::new();
5812 let seed_type = if (!seed_symbols.is_empty() || !seed_ids.is_empty()) && !seed_paths.is_empty()
5813 {
5814 for s in seed_symbols {
5815 seeds.push(s.to_string());
5816 }
5817 for id in seed_ids {
5818 seeds.push(id.to_string());
5819 }
5820 for p in seed_paths {
5821 seeds.push(p.to_string());
5822 }
5823 "mixed".to_string()
5824 } else if !seed_symbols.is_empty() || !seed_ids.is_empty() {
5825 for s in seed_symbols {
5826 seeds.push(s.to_string());
5827 }
5828 for id in seed_ids {
5829 seeds.push(id.to_string());
5830 }
5831 "symbol".to_string()
5832 } else if !seed_paths.is_empty() {
5833 for p in seed_paths {
5834 seeds.push(p.to_string());
5835 }
5836 "file".to_string()
5837 } else {
5838 return Ok(BlastRadiusResult {
5839 seed_type: "none".to_string(),
5840 seeds: Vec::new(),
5841 likely_tests: Vec::new(),
5842 impacted_symbols: Vec::new(),
5843 likely_tests_truncated: false,
5844 impacted_symbols_truncated: false,
5845 traversal_ceiling_reached: false,
5846 limit_at_maximum: false,
5847 likely_tests_found: 0,
5848 impacted_symbols_found: 0,
5849 stopped_at_depth: None,
5850 });
5851 };
5852
5853 let mut where_clauses = Vec::new();
5854 let mut params_vec: Vec<rusqlite::types::Value> = Vec::new();
5855
5856 if !resolved_seed_symbols.is_empty() {
5857 let placeholders: Vec<String> = (1..=resolved_seed_symbols.len())
5858 .map(|i| format!("?{}", i))
5859 .collect();
5860 where_clauses.push(format!("symbol_id IN ({})", placeholders.join(", ")));
5861 for symbol in &resolved_seed_symbols {
5862 params_vec.push(rusqlite::types::Value::Text(symbol.symbol_id.clone()));
5863 }
5864 }
5865
5866 if !seed_paths.is_empty() {
5867 let mut path_conds = Vec::new();
5868 for p in seed_paths.iter() {
5869 let raw = p
5870 .replace('\\', "/")
5871 .trim_start_matches("./")
5872 .trim_matches('/')
5873 .to_string();
5874 let exact_idx = params_vec.len() + 1;
5875 params_vec.push(rusqlite::types::Value::Text(raw.clone()));
5876 let dir_pattern = format!("{}/%", escape_like(&raw));
5877 let like_idx = params_vec.len() + 1;
5878 params_vec.push(rusqlite::types::Value::Text(dir_pattern));
5879 path_conds.push(format!(
5880 "replace(path, '\\', '/') = ?{exact_idx} COLLATE NOCASE OR replace(path, '\\', '/') LIKE ?{like_idx} ESCAPE '\\'"
5881 ));
5882 }
5883 where_clauses.push(format!("({})", path_conds.join(" OR ")));
5884 }
5885
5886 let seed_condition = where_clauses.join(" OR ");
5887 let max_depth_idx = params_vec.len() + 1;
5888 let walk_depth = if max_depth < MAX_BLAST_DEPTH {
5890 max_depth + 1
5891 } else {
5892 max_depth
5893 };
5894 params_vec.push(rusqlite::types::Value::Integer(walk_depth as i64));
5895
5896 let mut traversal_ceiling_reached = false;
5897 let mut callers_beyond_depth = false;
5898
5899 let has_relationships: bool = conn
5900 .query_row(
5901 "SELECT 1 FROM sqlite_master WHERE type='table' AND name='relationships'",
5902 [],
5903 |_| Ok(true),
5904 )
5905 .unwrap_or(false);
5906
5907 let has_pending: bool = conn
5908 .query_row(
5909 "SELECT 1 FROM sqlite_master WHERE type='table' AND name='pending_relationships'",
5910 [],
5911 |_| Ok(true),
5912 )
5913 .unwrap_or(false);
5914
5915 let mut likely_tests = Vec::new();
5916 let mut impacted_symbols = Vec::new();
5917 let mut seen_test_keys = HashSet::new();
5918
5919 let mut recursive_branches = Vec::new();
5920
5921 if has_relationships {
5922 recursive_branches.push(format!(
5923 "SELECT r.from_symbol_id, iw.depth + 1, iw.via
5924 FROM relationships r
5925 JOIN impact_walk iw ON r.to_symbol_id = iw.symbol_id
5926 JOIN symbols s_from ON r.from_symbol_id = s_from.symbol_id
5927 WHERE iw.depth < ?{max_depth_idx}
5928 AND s_from.kind NOT IN ({LOW_SIGNAL_KINDS_SQL})"
5929 ));
5930 }
5931
5932 if has_pending {
5933 let (parent_join, ns_condition) = if conn
5934 .query_row(
5935 "SELECT 1 FROM pragma_table_info('pending_relationships') WHERE name='target_namespace_json'",
5936 [],
5937 |_| Ok(true),
5938 )
5939 .unwrap_or(false)
5940 {
5941 (
5942 "LEFT JOIN symbols s_target_parent ON s_target.parent_symbol_id = s_target_parent.symbol_id
5943 LEFT JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id",
5944 format!("AND {pred}", pred = pending_target_predicate(conn, "s_target", "s_target_parent")),
5945 )
5946 } else {
5947 ("", String::new())
5948 };
5949
5950 recursive_branches.push(format!(
5951 "SELECT p.from_symbol_id, iw.depth + 1, iw.via
5952 FROM pending_relationships p
5953 JOIN symbols s_target ON p.target_terminal_name = s_target.name
5954 JOIN impact_walk iw ON s_target.symbol_id = iw.symbol_id
5955 {parent_join}
5956 WHERE iw.depth < ?{max_depth_idx}
5957 AND s_target.kind NOT IN ({LOW_SIGNAL_KINDS_SQL})
5958 {ns_condition}"
5959 ));
5960 }
5961
5962 if has_relationships {
5965 recursive_branches.push(format!(
5966 "SELECT r.from_symbol_id, iw.depth + 1, 1
5967 FROM impact_walk iw
5968 CROSS JOIN symbols ctor ON ctor.symbol_id = iw.symbol_id
5969 JOIN relationships r ON r.to_symbol_id = ctor.parent_symbol_id
5970 WHERE iw.depth < ?{max_depth_idx}
5971 AND ctor.kind = 'constructor'
5972 AND +r.kind = 'calls'"
5973 ));
5974 }
5975 if has_pending && has_pending_namespace_column(conn) {
5976 recursive_branches.push(format!(
5977 "SELECT p.from_symbol_id, iw.depth + 1, 1
5978 FROM impact_walk iw
5979 CROSS JOIN symbols ctor ON ctor.symbol_id = iw.symbol_id
5980 CROSS JOIN symbols s_target ON s_target.symbol_id = ctor.parent_symbol_id
5981 JOIN pending_relationships p ON p.target_terminal_name = s_target.name
5982 LEFT JOIN symbols s_target_parent ON s_target.parent_symbol_id = s_target_parent.symbol_id
5983 LEFT JOIN symbols s_from ON p.from_symbol_id = s_from.symbol_id
5984 WHERE iw.depth < ?{max_depth_idx}
5985 AND ctor.kind = 'constructor'
5986 AND s_target.kind IN ('class', 'struct', 'record')
5987 AND +p.kind = 'calls'
5988 AND {pred}",
5989 pred = pending_target_predicate(conn, "s_target", "s_target_parent")
5990 ));
5991 }
5992
5993 let lifecycle = if has_column(conn, "symbols", "test_lifecycle") {
5994 "COALESCE(s.test_lifecycle, 0) != 0"
5995 } else {
5996 "0"
5997 };
5998 let mut fixtures = Vec::new();
5999 let mut setups = Vec::new();
6000 let mut constructor_only_test_keys = HashSet::new();
6001 let mut non_constructor_test_keys = HashSet::new();
6002 if !recursive_branches.is_empty() {
6003 let recursive_sql = recursive_branches.join("\n UNION \n");
6004 let not_documentation = not_documentation(conn, "s");
6005 let sql = format!(
6006 "WITH RECURSIVE impact_walk(symbol_id, depth, via) AS (
6007 SELECT symbol_id, 0, 0
6008 FROM symbols
6009 WHERE ({seed_condition})
6010 AND kind NOT IN ({LOW_SIGNAL_KINDS_SQL})
6011
6012 UNION
6013
6014 {recursive_sql}
6015 )
6016 SELECT s.name, s.kind, s.path, s.start_line, s.is_test, s.test_container,
6017 MIN(iw.depth) as min_depth, MIN(iw.via) as min_via,
6018 {lifecycle} AS is_fixture, s.parent_symbol_id,
6019 EXISTS (SELECT 1 FROM symbols owner
6020 WHERE owner.symbol_id = s.parent_symbol_id
6021 AND COALESCE(owner.test_container, 0) != 0) AS in_test_class,
6022 s.language, s.signature
6023 FROM impact_walk iw
6024 CROSS JOIN symbols s ON iw.symbol_id = s.symbol_id
6025 WHERE s.kind NOT IN ({LOW_SIGNAL_KINDS_SQL})
6026 AND {not_documentation}
6027 GROUP BY s.symbol_id, s.name, s.kind, s.path, s.start_line, s.is_test, s.test_container
6028 HAVING MIN(iw.depth) > 0
6029 ORDER BY MIN(iw.depth) > {max_depth} ASC, MIN(iw.via) ASC, min_depth ASC, s.path ASC, s.name ASC
6030 LIMIT 201"
6031 );
6032
6033 let mut stmt = conn.prepare(&sql)?;
6034 let param_refs: Vec<&dyn rusqlite::ToSql> = params_vec
6035 .iter()
6036 .map(|v| v as &dyn rusqlite::ToSql)
6037 .collect();
6038
6039 let rows = stmt.query_map(param_refs.as_slice(), |row| {
6040 Ok((
6041 row.get::<_, String>(0)?,
6042 row.get::<_, String>(1)?,
6043 row.get::<_, String>(2)?,
6044 row.get::<_, i64>(3)? as usize,
6045 row.get::<_, bool>(4)?,
6046 row.get::<_, bool>(5)?,
6047 row.get::<_, i64>(6)? as usize,
6048 row.get::<_, i64>(7)? as usize,
6049 row.get::<_, bool>(8)?,
6050 row.get::<_, Option<String>>(9)?,
6051 row.get::<_, bool>(10)?,
6052 row.get::<_, String>(11)?,
6053 row.get::<_, Option<String>>(12)?,
6054 ))
6055 })?;
6056
6057 for (index, r) in rows.enumerate() {
6058 let (
6059 name,
6060 kind,
6061 raw_path,
6062 line,
6063 is_test,
6064 test_container,
6065 depth,
6066 min_via,
6067 is_fixture,
6068 parent,
6069 in_test_class,
6070 language,
6071 signature,
6072 ) = r?;
6073 if depth > max_depth {
6074 callers_beyond_depth = true;
6075 break;
6076 }
6077 if index == 200 {
6078 traversal_ceiling_reached = true;
6079 break;
6080 }
6081 let path = raw_path.replace('\\', "/");
6082 let is_test_target = is_test
6083 || test_container
6084 || is_fixture
6085 || (is_test_path(&path) && (in_test_class || names_a_test(&name)));
6086 let constructor_only = min_via > 0;
6087 if is_test_target {
6088 let key = format!("{}:{}", path, line);
6089 let test_key = (path.clone(), line);
6090 if constructor_only {
6091 constructor_only_test_keys.insert(test_key);
6092 } else {
6093 non_constructor_test_keys.insert(test_key);
6094 }
6095 if seen_test_keys.insert(key) {
6096 let lowered = name.to_ascii_lowercase();
6097 let pytest_fixture = path.ends_with(".py")
6098 && !["setup", "teardown", "asyncsetup", "asyncteardown"]
6099 .iter()
6100 .any(|prefix| lowered.starts_with(prefix));
6101 let caller = if depth == 1 {
6102 "direct caller".to_string()
6103 } else {
6104 format!("indirect caller [depth {depth}]")
6105 };
6106 let reason = if is_fixture && pytest_fixture {
6107 fixtures.push((name.clone(), path.clone(), line, constructor_only));
6108 format!("fixture ({caller})")
6109 } else if is_fixture {
6110 if let Some(class_id) = parent {
6111 setups.push((
6112 name.clone(),
6113 class_id,
6114 path.clone(),
6115 line,
6116 constructor_only,
6117 ));
6118 }
6119 format!("setup ({caller})")
6120 } else {
6121 caller
6122 };
6123 likely_tests.push(TestTarget {
6124 name,
6125 path,
6126 line,
6127 reason,
6128 });
6129 }
6130 } else {
6131 let enclosing_symbol_name =
6132 if is_generated_lambda_name(&name, &language, &kind, signature.as_deref()) {
6133 match parent.as_deref() {
6134 Some(parent_id) => {
6135 get_symbol_by_id(conn, parent_id)?.map(|parent| parent.name)
6136 }
6137 None => None,
6138 }
6139 } else {
6140 None
6141 };
6142 impacted_symbols.push(ImpactedSymbol {
6143 name,
6144 kind,
6145 path,
6146 line,
6147 depth,
6148 enclosing_symbol_name,
6149 });
6150 }
6151 }
6152 }
6153
6154 let mut replaced = HashSet::new();
6156 for (fixture, fixture_path, line, constructor_only) in &fixtures {
6157 let shown = fixture_name(conn, fixture, fixture_path);
6158 let users = fixture_users(conn, fixture, fixture_path)?;
6159 if !users.is_empty() {
6160 replaced.insert((fixture_path.clone(), *line));
6161 }
6162 for test in users {
6163 let test_key = (test.path.clone(), test.line);
6164 if *constructor_only {
6165 constructor_only_test_keys.insert(test_key);
6166 } else {
6167 non_constructor_test_keys.insert(test_key);
6168 }
6169 if seen_test_keys.insert(format!("{}:{}", test.path, test.line)) {
6170 likely_tests.push(TestTarget {
6171 reason: format!("uses fixture `{shown}`"),
6172 ..test
6173 });
6174 }
6175 }
6176 }
6177
6178 for (setup, class_id, setup_path, line, constructor_only) in &setups {
6179 let tests = tests_in_class(conn, class_id)?;
6180 if !tests.is_empty() {
6181 replaced.insert((setup_path.clone(), *line));
6182 }
6183 for test in tests {
6184 let test_key = (test.path.clone(), test.line);
6185 if *constructor_only {
6186 constructor_only_test_keys.insert(test_key);
6187 } else {
6188 non_constructor_test_keys.insert(test_key);
6189 }
6190 if seen_test_keys.insert(format!("{}:{}", test.path, test.line)) {
6191 likely_tests.push(TestTarget {
6192 reason: format!("setup `{setup}` runs before it"),
6193 ..test
6194 });
6195 }
6196 }
6197 }
6198 likely_tests.retain(|test| !replaced.contains(&(test.path.clone(), test.line)));
6199 constructor_only_test_keys.retain(|key| !non_constructor_test_keys.contains(key));
6200
6201 qualify_test_methods(conn, &mut likely_tests)?;
6202 if seed_type == "file" {
6205 let mut constructor_tests_by_file =
6206 std::collections::BTreeMap::<String, Vec<TestTarget>>::new();
6207 let mut other_tests = Vec::with_capacity(likely_tests.len());
6208 for test in likely_tests.drain(..) {
6209 if constructor_only_test_keys.contains(&(test.path.clone(), test.line)) {
6210 constructor_tests_by_file
6211 .entry(test.path.clone())
6212 .or_default()
6213 .push(test);
6214 } else {
6215 other_tests.push(test);
6216 }
6217 }
6218 for (path, mut tests) in constructor_tests_by_file {
6219 if tests.len() > 1 {
6220 let count = tests.len();
6221 other_tests.push(TestTarget {
6222 name: path.clone(),
6223 path,
6224 line: 1,
6225 reason: format!("constructor-only callers ({count} targets)"),
6226 });
6227 } else if let Some(test) = tests.pop() {
6228 other_tests.push(test);
6229 }
6230 }
6231 likely_tests = other_tests;
6232 }
6233 let likely_tests_found = likely_tests.len();
6234 let impacted_symbols_found = impacted_symbols.len();
6235 let likely_tests_truncated = likely_tests.len() > limit;
6236 let impacted_symbols_truncated = impacted_symbols.len() > limit;
6237 if likely_tests.len() > limit {
6238 likely_tests.truncate(limit);
6239 }
6240 if impacted_symbols.len() > limit {
6241 impacted_symbols.truncate(limit);
6242 }
6243
6244 Ok(BlastRadiusResult {
6245 seed_type,
6246 seeds,
6247 likely_tests,
6248 impacted_symbols,
6249 likely_tests_truncated,
6250 impacted_symbols_truncated,
6251 traversal_ceiling_reached,
6252 limit_at_maximum: limit >= MAX_RESULT_LIMIT,
6253 likely_tests_found,
6254 impacted_symbols_found,
6255 stopped_at_depth: callers_beyond_depth.then_some(max_depth),
6256 })
6257}
6258
6259fn qualify_test_methods(conn: &Connection, tests: &mut [TestTarget]) -> Result<(), QueryError> {
6262 let mut stmt = conn.prepare(
6263 "SELECT class.name FROM symbols t
6264 JOIN symbols class ON class.symbol_id = t.parent_symbol_id
6265 WHERE replace(t.path, '\\', '/') = ?1 AND t.start_line = ?2 AND t.name = ?3
6266 AND class.kind = 'class'
6267 LIMIT 1",
6268 )?;
6269 for test in tests.iter_mut().filter(|test| test.name != test.path) {
6270 if let Some(class) = stmt
6271 .query_row(params![test.path, test.line as i64, test.name], |row| {
6272 row.get::<_, String>(0)
6273 })
6274 .optional()?
6275 {
6276 test.name = format!("{class}::{}", test.name);
6277 }
6278 }
6279 Ok(())
6280}
6281
6282fn fixture_name(conn: &Connection, function: &str, path: &str) -> String {
6285 let signature: Option<String> = conn
6286 .query_row(
6287 "SELECT signature FROM symbols
6288 WHERE name = ?1 AND replace(path, '\\', '/') = ?2 AND signature LIKE '%fixture(%'
6289 LIMIT 1",
6290 params![function, path],
6291 |row| row.get(0),
6292 )
6293 .optional()
6294 .ok()
6295 .flatten();
6296 signature
6297 .as_deref()
6298 .and_then(declared_fixture_name)
6299 .unwrap_or_else(|| function.to_string())
6300}
6301
6302fn declared_fixture_name(signature: &str) -> Option<String> {
6304 let args = &signature[signature.find("fixture(")? + "fixture(".len()..];
6305 let args = &args[..args.find(')').unwrap_or(args.len())];
6306 let value = args[args.find("name=")? + "name=".len()..].trim_start();
6307 let quote = value.chars().next().filter(|c| *c == '"' || *c == '\'')?;
6308 let end = value[1..].find(quote)?;
6309 Some(value[1..1 + end].to_string())
6310}
6311
6312fn fixture_users(
6316 conn: &Connection,
6317 name: &str,
6318 fixture_path: &str,
6319) -> Result<Vec<TestTarget>, QueryError> {
6320 let lifecycle = if has_column(conn, "symbols", "test_lifecycle") {
6321 "COALESCE(t.test_lifecycle, 0) != 0"
6322 } else {
6323 "0"
6324 };
6325 let mut stmt = conn.prepare(&format!(
6326 "SELECT DISTINCT t.name, t.path, t.start_line, {lifecycle}
6327 FROM symbols parameter
6328 JOIN symbols t ON t.symbol_id = parameter.parent_symbol_id
6329 WHERE parameter.name = ?1
6330 AND (parameter.kind = 'parameter'
6331 OR (parameter.kind = 'variable' AND parameter.start_byte < t.body_start_byte))
6332 AND t.is_test = 1
6333 AND replace(t.path, '\\', '/') LIKE ?2 ESCAPE '\\'
6334 ORDER BY t.path, t.start_line
6335 LIMIT 2000"
6336 ))?;
6337 let mut users = Vec::new();
6340 let mut listed = HashSet::new();
6341 let mut visited = HashSet::new();
6342 let mut pending = vec![(name.to_string(), fixture_path.to_string())];
6343 while let Some((function, fixture_path)) = pending.pop() {
6344 if !visited.insert((function.clone(), fixture_path.clone())) {
6345 continue;
6346 }
6347 let name = fixture_name(conn, &function, &fixture_path);
6348 let scope = match fixture_path.rsplit_once('/') {
6349 Some((dir, "conftest.py")) => format!("{}/%", escape_like(dir)),
6350 None if fixture_path == "conftest.py" => "%".to_string(),
6351 _ => escape_like(&fixture_path),
6352 };
6353 let rows = stmt.query_map(params![name, scope], |row| {
6354 Ok((
6355 TestTarget {
6356 name: row.get(0)?,
6357 path: row.get::<_, String>(1)?.replace('\\', "/"),
6358 line: row.get::<_, i64>(2)? as usize,
6359 reason: String::new(),
6360 },
6361 row.get::<_, bool>(3)?,
6362 ))
6363 })?;
6364 for row in rows {
6365 let (user, is_fixture) = row?;
6366 if is_fixture {
6367 pending.push((user.name, user.path));
6368 } else if listed.insert((user.path.clone(), user.line)) {
6369 users.push(user);
6370 }
6371 }
6372 }
6373 Ok(users)
6374}
6375
6376fn tests_in_class(conn: &Connection, class_id: &str) -> Result<Vec<TestTarget>, QueryError> {
6378 let not_lifecycle = if has_column(conn, "symbols", "test_lifecycle") {
6379 "COALESCE(test_lifecycle, 0) = 0"
6380 } else {
6381 "1"
6382 };
6383 let mut stmt = conn.prepare(&format!(
6384 "SELECT name, path, start_line FROM symbols
6385 WHERE parent_symbol_id = ?1 AND is_test = 1 AND {not_lifecycle}
6386 ORDER BY start_line"
6387 ))?;
6388 let rows = stmt.query_map([class_id], |row| {
6389 Ok(TestTarget {
6390 name: row.get(0)?,
6391 path: row.get::<_, String>(1)?.replace('\\', "/"),
6392 line: row.get::<_, i64>(2)? as usize,
6393 reason: String::new(),
6394 })
6395 })?;
6396 Ok(rows.collect::<Result<_, _>>()?)
6397}
6398
6399pub fn compute_blast_radius(
6401 conn: &Connection,
6402 seed_symbols: &[&str],
6403 seed_paths: &[&str],
6404 max_depth: usize,
6405 limit: usize,
6406) -> Result<BlastRadiusResult, QueryError> {
6407 compute_blast_radius_scoped(conn, seed_symbols, None, seed_paths, max_depth, limit)
6408}
6409
6410#[cfg(test)]
6411mod tests {
6412 #[test]
6413 fn a_type_declaration_drops_attributes_and_fits_on_one_short_line() {
6414 assert_eq!(
6415 super::type_declaration_line(
6416 "#[derive(Debug)] #[serde(tag = \"t\")] pub struct Workspace"
6417 ),
6418 "pub struct Workspace"
6419 );
6420 let record = format!(
6421 "public sealed record Node(\n{})",
6422 " string Id,\n".repeat(20)
6423 );
6424 let line = super::type_declaration_line(&record);
6425 assert!(
6426 line.starts_with("public sealed record Node( string Id, string Id,"),
6427 "{line}"
6428 );
6429 assert_eq!(line.chars().count(), 120);
6430 assert!(line.ends_with('…'));
6431 }
6432
6433 #[test]
6434 fn a_mount_names_what_it_mounts_and_where() {
6435 let mount = |capture: &str, json: &str| {
6436 super::mount_display(capture, &serde_json::from_str(json).unwrap())
6437 };
6438 assert_eq!(
6439 mount(
6440 "blueprint_registration",
6441 r#"{"framework":"flask","mount_target":"auth.bp","query_family":"framework"}"#
6442 )
6443 .as_deref(),
6444 Some("blueprint_registration `auth.bp`")
6445 );
6446 assert_eq!(
6447 mount(
6448 "mount",
6449 r#"{"mount_path":"/jobs","mount_target":"Sidekiq::Web","normalized_mount_path":"/admin/jobs","query_family":"framework"}"#
6450 )
6451 .as_deref(),
6452 Some("mount `Sidekiq::Web` at /admin/jobs")
6453 );
6454 assert_eq!(
6455 mount(
6456 "property",
6457 r#"{"mount_target":"x","query_family":"metadata"}"#
6458 ),
6459 None
6460 );
6461 }
6462
6463 #[test]
6464 fn a_route_shows_its_source_template_and_keeps_a_joined_prefix() {
6465 let route = |json: &str| super::route_display(&serde_json::from_str(json).unwrap());
6466 assert_eq!(
6467 route(
6468 r#"{"verb":"GET","route_template":"/<int:id>/update","normalized_route_template":"/:id/update"}"#
6469 ),
6470 Some("GET /<int:id>/update".into())
6471 );
6472 assert_eq!(
6473 route(
6474 r#"{"verb":"POST","route_template":"/register","effective_route_template":"/auth/register","normalized_route_template":"/auth/register"}"#
6475 ),
6476 Some("POST /auth/register".into())
6477 );
6478 assert_eq!(
6479 route(
6480 r#"{"verb":"GET","route_template":"/users/{id}","normalized_route_template":"/api/v1/users/:id"}"#
6481 ),
6482 Some("GET /api/v1/users/:id".into())
6483 );
6484 assert_eq!(route(r#"{"key":"x"}"#), None);
6485 }
6486
6487 #[test]
6488 fn a_fixture_declared_with_a_name_is_used_by_that_name() {
6489 assert_eq!(
6490 super::declared_fixture_name("@pytest.fixture(name=\"async_app\") def _async_app()"),
6491 Some("async_app".into())
6492 );
6493 assert_eq!(
6494 super::declared_fixture_name(
6495 "@pytest.fixture(scope='session', name='db') def make_db()"
6496 ),
6497 Some("db".into())
6498 );
6499 assert_eq!(
6500 super::declared_fixture_name("@pytest.fixture def app()"),
6501 None
6502 );
6503 assert_eq!(
6504 super::declared_fixture_name("@pytest.fixture(params=[1]) def f(name='x')"),
6505 None
6506 );
6507 }
6508
6509 #[test]
6510 fn result_limit_rejects_values_above_the_shared_ceiling() {
6511 assert!(validate_result_limit(MAX_RESULT_LIMIT).is_ok());
6512 assert!(matches!(
6513 validate_result_limit(usize::MAX),
6514 Err(QueryError::InvalidResultLimit(usize::MAX))
6515 ));
6516 }
6517
6518 #[test]
6519 fn find_references_rejects_an_unbounded_limit_before_sql_execution() {
6520 let conn = Connection::open_in_memory().unwrap();
6521
6522 assert!(matches!(
6523 find_references_scoped(&conn, "target", "callers", usize::MAX, false, None),
6524 Err(QueryError::InvalidResultLimit(usize::MAX))
6525 ));
6526 }
6527
6528 use super::*;
6529 use crate::db::{ensure_fts_index, open_read_write};
6530
6531 #[test]
6532 fn count_parse_diagnostics_counts_rows_for_one_file() {
6533 let dir = crate::safe_tempdir();
6534 let conn = open_read_write(&dir.path().join("parse_diagnostics.db")).unwrap();
6535
6536 assert_eq!(count_parse_diagnostics(&conn, "src/lib.rs"), 0);
6537
6538 conn.execute_batch(
6539 "CREATE TABLE parse_diagnostics (
6540 diagnostic_id TEXT, file_id TEXT, path TEXT, language TEXT, kind TEXT
6541 );
6542 INSERT INTO parse_diagnostics VALUES ('d1', 'f1', 'src/lib.rs', 'rust', 'error');
6543 INSERT INTO parse_diagnostics VALUES ('d2', 'f1', 'src/lib.rs', 'rust', 'error');
6544 INSERT INTO parse_diagnostics VALUES ('d3', 'f2', 'src/other.rs', 'rust', 'error');",
6545 )
6546 .unwrap();
6547
6548 assert_eq!(count_parse_diagnostics(&conn, "src/lib.rs"), 2);
6549 assert_eq!(count_parse_diagnostics(&conn, "src\\lib.rs"), 2);
6550 assert_eq!(count_parse_diagnostics(&conn, "src/clean.rs"), 0);
6551 }
6552
6553 #[test]
6554 fn test_sanitize_fts5_query() {
6555 let (and_q, or_q) = sanitize_fts5_query("parse tokens");
6556 assert_eq!(and_q, "(\"parse\"* AND \"tokens\"*) OR \"parsetokens\"*");
6557 assert_eq!(or_q, "\"parse\"* OR \"tokens\"* OR \"parsetokens\"*");
6558
6559 let (and_q, or_q) = sanitize_fts5_query(" Option<T> ");
6560 assert_eq!(and_q, "(\"Option\"* AND \"T\") OR \"OptionT\"*");
6561 assert_eq!(or_q, "\"Option\"* OR \"T\" OR \"OptionT\"*");
6562
6563 let (and_q, or_q) = sanitize_fts5_query(" ");
6564 assert!(and_q.is_empty());
6565 assert!(or_q.is_empty());
6566 }
6567
6568 #[test]
6569 fn sanitize_splits_case_boundaries_and_drops_stop_words() {
6570 let (and_q, or_q) = sanitize_fts5_query("ValidateSyntax");
6571 assert_eq!(
6572 and_q,
6573 "((\"Validate\"* \"Syntax\"*) OR \"ValidateSyntax\"*)"
6574 );
6575 assert_eq!(or_q, "\"Validate\"* OR \"Syntax\"* OR \"ValidateSyntax\"*");
6576
6577 let (and_q, _) = sanitize_fts5_query("find tests related to a symbol");
6578 assert_eq!(
6579 and_q,
6580 "\"find\"* AND \"tests\"* AND \"related\"* AND \"symbol\"*"
6581 );
6582
6583 let (and_q, or_q) = sanitize_fts5_query("parseHTTPResponse2");
6584 assert_eq!(
6585 and_q,
6586 "((\"parse\"* \"HTTP\"* \"Response\"* \"2\") OR \"parseHTTPResponse2\"*)"
6587 );
6588 assert!(or_q.ends_with("OR \"parseHTTPResponse2\"*"));
6589
6590 let (and_q, _) = sanitize_fts5_query("validate_syntax");
6591 assert_eq!(
6592 and_q,
6593 "((\"validate\"* \"syntax\"*) OR \"validate_syntax\"*)"
6594 );
6595
6596 let (and_q, _) = sanitize_fts5_query("isReady");
6597 assert_eq!(and_q, "((\"Ready\"*) OR \"isReady\"*)");
6598
6599 let (and_q, _) = sanitize_fts5_query("before");
6600 assert_eq!(and_q, "\"before\"*");
6601
6602 let (and_q, _) = sanitize_fts5_query("fooBar quux");
6603 assert_eq!(
6604 and_q,
6605 "(((\"foo\"* \"Bar\"*) OR \"fooBar\"*) AND \"quux\"*) OR \"fooBarquux\"*"
6606 );
6607
6608 let (and_q, _) = sanitize_fts5_query("the for a");
6609 assert_eq!(and_q, "(\"the\"* AND \"for\"* AND \"a\") OR \"thefora\"*");
6610 }
6611
6612 fn search_fixture(rows: &str) -> Connection {
6613 let conn = Connection::open_in_memory().unwrap();
6614 conn.execute_batch(&format!(
6615 "CREATE TABLE symbols (
6616 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT,
6617 kind TEXT, signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
6618 start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
6619 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
6620 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
6621 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
6622 semantic_group TEXT, is_test INTEGER, test_container INTEGER, content_type TEXT
6623 );
6624 INSERT INTO symbols VALUES {rows};"
6625 ))
6626 .unwrap();
6627 ensure_fts_index(&conn).unwrap();
6628 conn
6629 }
6630
6631 fn code_row(id: &str, path: &str, language: &str, name: &str, doc: &str) -> String {
6632 format!(
6633 "('{id}', 'f_{id}', '{path}', '{language}', '{name}', 'function', 'fn {name}()', '{doc}', 'pub', NULL,
6634 10, 0, 20, 1, 100, 250, 12, 4, 19, 1, 120, 240, 'h_{id}', NULL, 0, 0, 'code')"
6635 )
6636 }
6637
6638 fn doc_row(id: &str, name: &str, doc: &str) -> String {
6639 format!(
6640 "('{id}', 'f_{id}', 'docs/{id}.md', 'markdown', '{name}', 'module', '{name}', '{doc}', NULL, NULL,
6641 3, 0, 3, 1, 10, 40, NULL, NULL, NULL, NULL, NULL, NULL, 'h_{id}', NULL, 0, 0, 'documentation')"
6642 )
6643 }
6644
6645 #[test]
6646 fn ranked_owner_context_recovers_members_and_enriches_existing_rows() {
6647 let mut rows = vec![
6648 code_row(
6649 "planner",
6650 "src/planner.rs",
6651 "rust",
6652 "EditPlanner",
6653 "Plans edit operations for a function body",
6654 ),
6655 code_row(
6656 "target",
6657 "src/planner.rs",
6658 "rust",
6659 "ReplaceBodyImplementation",
6660 "",
6661 ),
6662 code_row("existing", "src/planner.rs", "rust", "Body", ""),
6663 code_row(
6664 "unrelated",
6665 "src/planner.rs",
6666 "rust",
6667 "SaveConfiguration",
6668 "",
6669 ),
6670 ];
6671 rows.extend((0..200).map(|i| {
6673 code_row(
6674 &format!("noise{i}"),
6675 "src/noise.rs",
6676 "rust",
6677 &format!("body{i:03}"),
6678 "edit planner body",
6679 )
6680 }));
6681 let conn = search_fixture(&rows.join(","));
6682 conn.execute_batch(
6683 "UPDATE symbols SET kind = 'class' WHERE symbol_id = 'planner';
6684 UPDATE symbols SET kind = 'method', parent_symbol_id = 'planner'
6685 WHERE symbol_id IN ('target', 'existing', 'unrelated');",
6686 )
6687 .unwrap();
6688 let ordinary =
6689 collect_search_candidates(&conn, "edit planner body", None, None, false, 200).unwrap();
6690 assert!(
6691 !ordinary
6692 .iter()
6693 .any(|c| c.result.symbol.symbol_id == "target")
6694 );
6695 assert!(
6696 ordinary
6697 .iter()
6698 .any(|c| c.result.symbol.symbol_id == "existing")
6699 );
6700 let found =
6701 fts_search_symbols_explained(&conn, "edit planner body", None, None, false, 200, true)
6702 .unwrap();
6703 for id in ["target", "existing"] {
6704 let members: Vec<_> = found.iter().filter(|r| r.symbol.symbol_id == id).collect();
6705 assert_eq!(members.len(), 1);
6706 let explain = members[0].explain.as_ref().unwrap();
6707 assert!(explain.branches.iter().any(|b| b == "owner_context"));
6708 for word in ["edit", "planner"] {
6709 assert!(
6710 explain
6711 .terms
6712 .contains(&(word.into(), "owner_context".into(), 0.5))
6713 );
6714 }
6715 assert!(explain.terms.contains(&("body".into(), "name".into(), 3.0)));
6716 }
6717 assert!(!found.iter().any(|r| r.symbol.symbol_id == "unrelated"));
6718 for exact in ["EditPlanner", "Body"] {
6719 assert_eq!(search_names(&conn, exact)[0], exact);
6720 }
6721 let scoped = fts_search_symbols_scoped(
6722 &conn,
6723 "edit planner body",
6724 None,
6725 Some("src/noise.rs"),
6726 false,
6727 200,
6728 )
6729 .unwrap();
6730 assert!(scoped.iter().all(|r| r.symbol.path == "src/noise.rs"));
6731 let methods =
6732 fts_search_symbols_scoped(&conn, "edit planner body", Some("method"), None, false, 200)
6733 .unwrap();
6734 assert!(methods.iter().all(|r| r.symbol.kind == "method"));
6735 assert!(methods.iter().all(|r| r.explain.is_none()));
6737 }
6738
6739 #[test]
6740 fn ranked_owner_expansion_is_bounded_and_preserves_member_filters() {
6741 let mut rows = Vec::new();
6742 for owner in 0..4 {
6743 rows.push(code_row(
6744 &format!("o{owner}"),
6745 "src/lib.rs",
6746 "rust",
6747 &format!("Planner{owner}"),
6748 "body planner",
6749 ));
6750 for member in 0..41 {
6751 rows.push(code_row(
6752 &format!("o{owner}m{member:02}"),
6753 "src/lib.rs",
6754 "rust",
6755 &format!("Body{member:02}"),
6756 "",
6757 ));
6758 }
6759 }
6760 rows.push(doc_row("docowner", "PlannerDocs", "body planner"));
6761 rows.push(code_row(
6762 "docchild",
6763 "src/lib.rs",
6764 "rust",
6765 "BodyFromDocs",
6766 "",
6767 ));
6768 rows.push(code_row(
6769 "testchild",
6770 "tests/lib.rs",
6771 "rust",
6772 "BodyTest",
6773 "",
6774 ));
6775 rows.push(code_row(
6776 "linkchild",
6777 "docs/link.md",
6778 "markdown",
6779 "BodyLink",
6780 "",
6781 ));
6782 rows.push(code_row(
6783 "nestedchild",
6784 "src/lib.rs",
6785 "rust",
6786 "BodyNested",
6787 "",
6788 ));
6789 let conn = search_fixture(&rows.join(","));
6790 for owner in 0..4 {
6791 conn.execute(
6792 "UPDATE symbols SET kind = 'class' WHERE symbol_id = ?1",
6793 [format!("o{owner}")],
6794 )
6795 .unwrap();
6796 conn.execute(
6797 "UPDATE symbols SET kind = 'method', parent_symbol_id = ?1 WHERE symbol_id LIKE ?2",
6798 params![format!("o{owner}"), format!("o{owner}m%")],
6799 )
6800 .unwrap();
6801 }
6802 conn.execute_batch(
6803 "UPDATE symbols SET kind = 'class' WHERE symbol_id = 'docowner';
6804 UPDATE symbols SET parent_symbol_id = 'docowner' WHERE symbol_id = 'docchild';
6805 UPDATE symbols SET parent_symbol_id = 'o0' WHERE symbol_id IN ('testchild', 'linkchild');
6806 UPDATE symbols SET kind = 'import' WHERE symbol_id = 'linkchild';
6807 UPDATE symbols SET parent_symbol_id = 'o0m00' WHERE symbol_id = 'nestedchild';",
6808 ).unwrap();
6809 let mut candidates =
6810 collect_search_candidates(&conn, "planner body", None, None, false, 200).unwrap();
6811 candidates.retain(|c| c.result.symbol.parent_symbol_id.is_none());
6813 let ranked = rerank_with(candidates.clone(), "planner body", false, None);
6814 assert!(
6815 expand_ranked_owner_members(
6816 &conn,
6817 &mut candidates,
6818 &ranked,
6819 "planner body",
6820 None,
6821 None,
6822 false
6823 )
6824 .unwrap()
6825 );
6826 let expanded: Vec<_> = candidates
6827 .iter()
6828 .filter(|c| c.owner_context.is_some())
6829 .collect();
6830 assert_eq!(expanded.len(), 120);
6831 for owner in 0..3 {
6832 assert_eq!(
6833 expanded
6834 .iter()
6835 .filter(|c| c.result.symbol.parent_symbol_id.as_deref()
6836 == Some(&format!("o{owner}")))
6837 .count(),
6838 40
6839 );
6840 assert!(
6841 !expanded
6842 .iter()
6843 .any(|c| c.result.symbol.symbol_id == format!("o{owner}m40"))
6844 );
6845 }
6846 for excluded in ["docchild", "testchild", "linkchild", "nestedchild"] {
6847 assert!(
6848 !expanded
6849 .iter()
6850 .any(|c| c.result.symbol.symbol_id == excluded)
6851 );
6852 }
6853 let mut late_ranked = rerank_with(
6855 (0..20).map(|i| function(&format!("body{i}"))).collect(),
6856 "body",
6857 false,
6858 None,
6859 );
6860 late_ranked.push(ranked[0].clone());
6861 assert!(
6862 !expand_ranked_owner_members(
6863 &conn,
6864 &mut Vec::new(),
6865 &late_ranked,
6866 "planner body",
6867 None,
6868 None,
6869 false
6870 )
6871 .unwrap()
6872 );
6873 }
6874
6875 #[test]
6876 fn lookup_and_search_skip_documentation_links_recorded_as_imports() {
6877 let conn = search_fixture(
6878 &[
6879 "('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(),
6880 "('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(),
6881 ]
6882 .join(","),
6883 );
6884
6885 let lookup: Vec<String> = search_symbols(&conn, "Flask", Some("import"), true, 10)
6886 .unwrap()
6887 .into_iter()
6888 .map(|s| s.path)
6889 .collect();
6890 assert_eq!(lookup, vec!["src/app.py"]);
6891 let search: Vec<String> =
6892 fts_search_symbols_scoped(&conn, "Flask", Some("import"), None, true, 10)
6893 .unwrap()
6894 .into_iter()
6895 .map(|r| r.symbol.path)
6896 .collect();
6897 assert_eq!(search, vec!["src/app.py"]);
6898 }
6899
6900 #[test]
6901 fn lookup_ranks_a_prefix_match_above_a_substring_match() {
6902 let conn = search_fixture(
6903 &[
6904 code_row("c1", "tests/a.rs", "rust", "test_test_client", ""),
6905 code_row("c2", "tests/b.rs", "rust", "test_client_open_environ", ""),
6906 ]
6907 .join(","),
6908 );
6909
6910 let names: Vec<String> = search_symbols(&conn, "test_cli", None, true, 10)
6911 .unwrap()
6912 .into_iter()
6913 .map(|s| s.name)
6914 .collect();
6915
6916 assert_eq!(names, vec!["test_client_open_environ", "test_test_client"]);
6917 }
6918
6919 #[test]
6920 fn lookup_ranks_a_declared_property_above_a_self_assignment() {
6921 let property = |id: &str, path: &str, signature: &str| {
6922 format!(
6923 "('{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')"
6924 )
6925 };
6926 let conn = search_fixture(
6927 &[
6928 property("p1", "src/flask/app.py", "self.debug = get_debug_flag()"),
6929 property(
6930 "p2",
6931 "src/flask/sansio/app.py",
6932 "@property def debug(self) -> bool",
6933 ),
6934 ]
6935 .join(","),
6936 );
6937
6938 let paths: Vec<String> = search_symbols(&conn, "debug", None, false, 10)
6939 .unwrap()
6940 .into_iter()
6941 .map(|s| s.path)
6942 .collect();
6943
6944 assert_eq!(paths, vec!["src/flask/sansio/app.py", "src/flask/app.py"]);
6945 }
6946
6947 fn search_names(conn: &Connection, query: &str) -> Vec<String> {
6948 fts_search_symbols_scoped(conn, query, None, None, false, 10)
6949 .unwrap()
6950 .into_iter()
6951 .map(|r| r.symbol.name)
6952 .collect()
6953 }
6954
6955 const TEST_PATH_CASES: &[(&str, bool)] = &[
6956 ("tests/foo.py", true),
6957 ("tests/x.py", true),
6958 ("tests/tools/test_web.py", true),
6959 ("src/tests/x.rs", true),
6960 ("__tests__/a.ts", true),
6961 ("a/__tests__/b.ts", true),
6962 ("test/x.java", true),
6963 ("src/test/Helper.java", true),
6964 ("src/test_utils.py", true),
6965 ("lib/test_helper.rb", true),
6966 ("test_config.py", true),
6967 ("pkg/test_data/x.json", false),
6968 ("src/test_detection.rs", false),
6969 ("autotests/tst_pagerow.qml", true),
6970 ("autotests/helper.qml", true),
6971 ("src/autotests/columnview.cpp", true),
6972 ("tst_foo.qml", true),
6973 ("src/tst_columnview.qml", true),
6974 ("autotests\\tst_bar.qml", true),
6975 ("autotests_helper/x.rs", false),
6976 ("src/autotest.rs", false),
6977 ("src/tstamp.rs", false),
6978 ("src/tst.rs", false),
6979 ("crates/julie-index/src/analysis/test_quality.rs", false),
6980 ("x/foo_test.go", true),
6981 ("x/foo.test.ts", true),
6982 ("x/foo.spec.js", true),
6983 ("src/lib_test.rs", true),
6984 ("src/test.rs", true),
6985 ("tests.rs", true),
6986 ("src/tests.rs", true),
6987 ("Foo.Tests.cs", true),
6988 ("x/FooTests.cs", true),
6989 ("src/FooTests.cs", true),
6990 ("src/Foo.Tests.cs", true),
6991 ("tests/Foo.cs", true),
6992 ("x/parser.spec.ts", true),
6993 ("test_x.py", true),
6994 ("test.rs", true),
6995 ("tests\\x.py", true),
6996 ("src/protocol.spec.v1/parser.rs", false),
6997 ("pkg/test_support/runtime.py", false),
6998 ("src/Contests.cs", false),
6999 ("spec/x.rb", false),
7000 ("crates/x/src/impact/likely_tests.rs", false),
7001 ("x/foo_tests.rs", false),
7002 ("src/latest.rs", false),
7003 ("x/latest.go", false),
7004 ("x/manifest.rs", false),
7005 ("src/attest.rs", false),
7006 ("contest/x.py", false),
7007 ("src/testing.rs", false),
7008 ("src/main.rs", false),
7009 ("pkg/service.go", false),
7010 ];
7011
7012 #[test]
7013 fn test_path_rule_and_its_sql_mirror_agree_on_every_path() {
7014 let conn = Connection::open_in_memory().unwrap();
7015 let sql = format!(
7016 "SELECT {} FROM (SELECT :path AS path) s",
7017 test_path_predicate("s")
7018 );
7019 let mut stmt = conn.prepare(&sql).unwrap();
7020 for (path, expected) in TEST_PATH_CASES {
7021 assert_eq!(is_test_path(path), *expected, "rust rule: {path}");
7022 let from_sql: bool = stmt
7023 .query_row(rusqlite::named_params! { ":path": path }, |row| row.get(0))
7024 .unwrap();
7025 assert_eq!(from_sql, *expected, "sql mirror: {path}");
7026 }
7027 }
7028
7029 #[test]
7030 fn test_name_rule_and_its_sql_mirror_agree_on_every_name() {
7031 let conn = Connection::open_in_memory().unwrap();
7032 let sql = format!(
7033 "SELECT {} FROM (SELECT :name AS name) s",
7034 names_a_test_sql("s")
7035 );
7036 let mut stmt = conn.prepare(&sql).unwrap();
7037 for name in [
7038 "test_run",
7039 "TestRoutes",
7040 "RoutesTest",
7041 "RoutesTests",
7042 "runSpec",
7043 "Specs",
7044 "create_app",
7045 "attest_value",
7046 "inspection",
7047 ] {
7048 let from_sql: bool = stmt
7049 .query_row(rusqlite::named_params! { ":name": name }, |row| row.get(0))
7050 .unwrap();
7051 assert_eq!(from_sql, names_a_test(name), "sql mirror: {name}");
7052 }
7053 }
7054
7055 #[test]
7056 fn unflagged_test_file_rows_are_hidden_unless_tests_are_included() {
7057 let conn = search_fixture(
7058 &[
7059 code_row(
7060 "a",
7061 "src/parser.rs",
7062 "rust",
7063 "parse_sidecar",
7064 "Parse a sidecar.",
7065 ),
7066 code_row(
7067 "b",
7068 "src/tests/helpers.py",
7069 "python",
7070 "parse_sidecar_fixture",
7071 "Parse a sidecar.",
7072 ),
7073 ]
7074 .join(", "),
7075 );
7076
7077 let default_search: Vec<String> =
7078 fts_search_symbols_scoped(&conn, "parse sidecar", None, None, false, 10)
7079 .unwrap()
7080 .into_iter()
7081 .map(|r| r.symbol.name)
7082 .collect();
7083 assert_eq!(default_search, vec!["parse_sidecar".to_string()]);
7084
7085 let with_tests: Vec<String> =
7086 fts_search_symbols_scoped(&conn, "parse sidecar", None, None, true, 10)
7087 .unwrap()
7088 .into_iter()
7089 .map(|r| r.symbol.name)
7090 .collect();
7091 assert!(with_tests.contains(&"parse_sidecar_fixture".to_string()));
7092
7093 let default_lookup: Vec<String> =
7094 search_symbols_scoped(&conn, "parse_sidecar", None, None, false, 10)
7095 .unwrap()
7096 .into_iter()
7097 .map(|s| s.name)
7098 .collect();
7099 assert_eq!(default_lookup, vec!["parse_sidecar".to_string()]);
7100
7101 let lookup_with_tests: Vec<String> =
7102 search_symbols_scoped(&conn, "parse_sidecar", None, None, true, 10)
7103 .unwrap()
7104 .into_iter()
7105 .map(|s| s.name)
7106 .collect();
7107 assert!(lookup_with_tests.contains(&"parse_sidecar_fixture".to_string()));
7108 }
7109
7110 #[test]
7111 fn count_file_symbols_prefers_the_exact_case_path_like_the_loader() {
7112 let conn = search_fixture(
7113 &[
7114 code_row("a", "src/Foo.rs", "rust", "one", ""),
7115 code_row("b", "src/foo.rs", "rust", "two", ""),
7116 code_row("c", "src/foo.rs", "rust", "three", ""),
7117 ]
7118 .join(", "),
7119 );
7120 for path in ["src/Foo.rs", "src/foo.rs", "src/FOO.rs", "src\\foo.rs"] {
7121 assert_eq!(
7122 count_file_symbols(&conn, path),
7123 load_file_symbols(&conn, path).unwrap().len(),
7124 "{path}"
7125 );
7126 }
7127 assert_eq!(count_file_symbols(&conn, "src/Foo.rs"), 1);
7128 assert_eq!(count_file_symbols(&conn, "src/FOO.rs"), 3);
7129 }
7130
7131 #[test]
7132 fn lookup_statement_is_not_planned_as_a_multi_index_or() {
7133 let conn = search_fixture(&code_row("a", "src/lib.rs", "rust", "needle", ""));
7134 conn.execute_batch(
7135 "CREATE INDEX idx_symbols_name_kind ON symbols(name, kind);
7136 CREATE INDEX idx_symbols_test_container ON symbols(test_container);
7137 CREATE INDEX idx_symbols_is_test ON symbols(is_test);",
7138 )
7139 .unwrap();
7140 let sql = format!(
7141 "EXPLAIN QUERY PLAN {}",
7142 search_symbols_sql(false, false, 20)
7143 );
7144 let plan: Vec<String> = conn
7145 .prepare(&sql)
7146 .unwrap()
7147 .query_map(
7148 rusqlite::named_params! {
7149 ":query": "needle",
7150 ":pattern": "%needle%",
7151 ":kind": None::<&str>,
7152 ":path": None::<&str>,
7153 ":path_like": None::<&str>,
7154 },
7155 |row| row.get::<_, String>(3),
7156 )
7157 .unwrap()
7158 .collect::<Result<_, _>>()
7159 .unwrap();
7160 assert!(
7161 !plan.iter().any(|step| step.contains("MULTI-INDEX OR")),
7162 "{plan:?}"
7163 );
7164 }
7165
7166 #[test]
7167 fn qualified_lookup_in_a_test_file_returns_the_named_row() {
7168 let conn = search_fixture(
7169 &[
7170 "('c', 'f_c', 'src/tests/helpers.py', 'python', 'Helpers', 'class', 'class Helpers', '', 'pub', NULL,
7171 1, 0, 9, 1, 0, 90, 1, 0, 9, 1, 5, 88, 'h_c', NULL, 0, 0, 'code')".to_string(),
7172 "('d', 'f_d', 'src/tests/helpers.py', 'python', 'load_fixture', 'method', 'def load_fixture()', '', 'pub', 'c',
7173 10, 0, 20, 1, 100, 250, 12, 4, 19, 1, 120, 240, 'h_d', NULL, 0, 0, 'code')".to_string(),
7174 ]
7175 .join(", "),
7176 );
7177
7178 assert_eq!(
7179 search_symbols_scoped(&conn, "Helpers.load_fixture", None, None, false, 10)
7180 .unwrap()
7181 .len(),
7182 1
7183 );
7184 assert_eq!(
7185 search_symbols_scoped(&conn, "Helpers.load_fixture", None, None, true, 10)
7186 .unwrap()
7187 .len(),
7188 1
7189 );
7190 assert!(
7191 search_symbols_scoped(&conn, "load_fix", None, None, false, 10)
7192 .unwrap()
7193 .is_empty()
7194 );
7195 }
7196
7197 #[test]
7198 fn concept_query_prefers_partial_code_match_over_full_doc_match() {
7199 let conn = search_fixture(
7200 &[
7201 doc_row(
7202 "d1",
7203 "Safety guarantees",
7204 "Pre-flight syntax validation runs before the edit touches disk",
7205 ),
7206 doc_row(
7207 "d2",
7208 "Audit",
7209 "The syntax validation before an edit is the invariant",
7210 ),
7211 code_row(
7212 "c1",
7213 "src/syntax.rs",
7214 "rust",
7215 "validate_syntax",
7216 "Validate the syntax of a file",
7217 ),
7218 code_row(
7219 "c2",
7220 "src/edit.rs",
7221 "rust",
7222 "replace_symbol_body",
7223 "Atomic edit with validation",
7224 ),
7225 ]
7226 .join(","),
7227 );
7228
7229 let names = search_names(&conn, "syntax validation before edit");
7230
7231 assert_eq!(names[0], "validate_syntax");
7232 assert!(names.contains(&"replace_symbol_body".to_string()));
7233 assert!(names.contains(&"Safety guarantees".to_string()));
7234 }
7235
7236 #[test]
7237 fn camel_case_query_finds_snake_case_symbol_and_vice_versa() {
7238 let conn = search_fixture(
7239 &[
7240 code_row("c1", "src/syntax.rs", "rust", "validate_syntax", ""),
7241 code_row("c2", "src/syntax.ts", "typescript", "validateSyntax", ""),
7242 ]
7243 .join(","),
7244 );
7245
7246 let mut camel = search_names(&conn, "ValidateSyntax");
7247 camel.sort();
7248 assert_eq!(camel, vec!["validateSyntax", "validate_syntax"]);
7249 let mut words = search_names(&conn, "validate syntax");
7250 words.sort();
7251 assert_eq!(words, vec!["validateSyntax", "validate_syntax"]);
7252 }
7253
7254 #[test]
7255 fn stop_word_prefixed_camel_case_symbol_is_still_found() {
7256 let conn = search_fixture(
7257 &[
7258 code_row("c1", "src/state.ts", "typescript", "isReady", ""),
7259 code_row("c2", "src/hooks.rs", "rust", "before", ""),
7260 code_row(
7261 "c3",
7262 "src/x.rs",
7263 "rust",
7264 "fooBar",
7265 "has fooBar but not the other word",
7266 ),
7267 ]
7268 .join(","),
7269 );
7270
7271 assert_eq!(search_names(&conn, "isReady"), vec!["isReady"]);
7272 assert_eq!(search_names(&conn, "before"), vec!["before"]);
7273 }
7274
7275 #[test]
7276 fn related_tests_use_the_name_as_typed_without_splitting() {
7277 let conn = search_fixture(
7278 &[
7279 code_row("c1", "src/state.ts", "typescript", "isReady", ""),
7280 "('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(),
7281 "('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(),
7282 ]
7283 .join(","),
7284 );
7285 let target = get_symbol_by_name(&conn, "isReady", None).unwrap().unwrap();
7286
7287 let names: Vec<String> = find_related_tests(&conn, &target, 5)
7288 .unwrap()
7289 .into_iter()
7290 .map(|t| t.name)
7291 .collect();
7292
7293 assert_eq!(names, vec!["isReady_reports_true"]);
7294 }
7295
7296 #[test]
7297 fn related_tests_match_underscores_literally_and_skip_dunder_names() {
7298 let conn = search_fixture(
7299 &[
7300 code_row("c1", "src/app.py", "python", "wsgi_app", ""),
7301 code_row("c2", "src/app.py", "python", "__init__", ""),
7302 "('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(),
7303 "('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(),
7304 "('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(),
7305 ]
7306 .join(","),
7307 );
7308 let names = |name: &str| -> Vec<String> {
7309 let target = get_symbol_by_name(&conn, name, None).unwrap().unwrap();
7310 find_related_tests(&conn, &target, 5)
7311 .unwrap()
7312 .into_iter()
7313 .map(|t| t.name)
7314 .collect()
7315 };
7316
7317 assert_eq!(names("wsgi_app"), vec!["test_wsgi_app"]);
7318 assert!(names("__init__").is_empty());
7319 }
7320
7321 #[test]
7322 fn related_tests_admit_unflagged_test_names_in_test_paths_without_helpers_or_low_signal_rows() {
7323 let conn = search_fixture(
7324 "('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'),
7325 ('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'),
7326 ('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'),
7327 ('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'),
7328 ('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'),
7329 ('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'),
7330 ('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'),
7331 ('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'),
7332 ('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')",
7333 );
7334 conn.execute_batch(
7335 "CREATE TABLE relationships (from_symbol_id TEXT, to_symbol_id TEXT, kind TEXT, path TEXT, start_line INTEGER, start_column INTEGER);
7336 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);
7337 CREATE TABLE type_facts (type_fact_id TEXT, symbol_id TEXT, language TEXT, resolved_type TEXT, generic_params_json TEXT);
7338 INSERT INTO relationships VALUES ('direct', 'target', 'calls', 'tests/direct.rs', 1, 0);
7339 INSERT INTO pending_relationships VALUES ('pending', 'calculate', 'calls', 'autotests/tst_pending.qml', 1, 0, NULL, '[]', 'calculate');",
7340 )
7341 .unwrap();
7342 let target = get_symbol_by_name(&conn, "calculate", None)
7343 .unwrap()
7344 .unwrap();
7345
7346 let names: Vec<String> = find_related_tests(&conn, &target, 5)
7347 .unwrap()
7348 .into_iter()
7349 .map(|test| test.name)
7350 .collect();
7351
7352 assert_eq!(
7353 names,
7354 vec![
7355 "test_direct",
7356 "test_pending",
7357 "test_calculate_named",
7358 "test_fts"
7359 ]
7360 );
7361 }
7362
7363 #[test]
7364 fn exact_name_ranks_before_longer_names_with_the_same_tokens() {
7365 let conn = search_fixture(
7366 &[
7367 code_row(
7368 "c1",
7369 "src/queries.rs",
7370 "rust",
7371 "fts_search_symbols_scoped",
7372 "search symbols scoped with fts",
7373 ),
7374 code_row("c2", "src/queries.rs", "rust", "search_symbols_scoped", ""),
7375 ]
7376 .join(","),
7377 );
7378
7379 assert_eq!(
7380 search_names(&conn, "search_symbols_scoped")[0],
7381 "search_symbols_scoped"
7382 );
7383 }
7384
7385 fn sidecar_fixture() -> Connection {
7386 search_fixture(
7387 &[
7388 code_row("c1", "src/sidecar.rs", "rust", "parseSha256Sidecar", ""),
7389 code_row(
7390 "c2",
7391 "src/sidecar.rs",
7392 "rust",
7393 "parse_sidecar_file",
7394 "parse the sha256 sidecar file",
7395 ),
7396 ]
7397 .join(","),
7398 )
7399 }
7400
7401 fn candidate<'a>(candidates: &'a [Candidate], name: &str) -> &'a Candidate {
7402 candidates
7403 .iter()
7404 .find(|c| c.result.symbol.name == name)
7405 .unwrap_or_else(|| panic!("{name} is not a candidate"))
7406 }
7407
7408 #[test]
7409 fn name_substring_admits_a_symbol_the_word_branch_cannot_reach() {
7410 let conn = sidecar_fixture();
7411
7412 let candidates = collect_search_candidates(&conn, "sha256", None, None, false, 10).unwrap();
7413
7414 let target = candidate(&candidates, "parseSha256Sidecar");
7415 assert!(target.name_match);
7416 assert!(!target.word_match);
7417 assert!(!target.exact_name);
7418 assert!(target.name_terms.contains(&"sha256".to_string()));
7419 }
7420
7421 #[test]
7422 fn a_row_matching_every_word_does_not_hide_a_row_matching_some() {
7423 let conn = search_fixture(
7424 &[
7425 code_row(
7426 "c1",
7427 "examples/demo.rs",
7428 "rust",
7429 "demo",
7430 "restore offline state",
7431 ),
7432 code_row(
7433 "c2",
7434 "src/replay.rs",
7435 "rust",
7436 "replay",
7437 "restore offline records",
7438 ),
7439 ]
7440 .join(","),
7441 );
7442
7443 let candidates =
7444 collect_search_candidates(&conn, "restore offline state", None, None, false, 10)
7445 .unwrap();
7446
7447 assert!(candidate(&candidates, "demo").word_match);
7448 assert!(candidate(&candidates, "replay").word_match);
7449 assert_eq!(search_names(&conn, "restore offline state")[0], "replay");
7450 }
7451
7452 #[test]
7453 fn name_branch_admits_the_target_when_word_matches_exceed_the_cap() {
7454 let mut rows: Vec<String> = (1..=170)
7455 .map(|i| {
7456 code_row(
7457 &format!("h{i:03}"),
7458 "src/sidecar.rs",
7459 "rust",
7460 &format!("sidecar_helper_{i:03}"),
7461 "parse sidecar file",
7462 )
7463 })
7464 .collect();
7465 rows.push(code_row(
7466 "c1",
7467 "src/sidecar.rs",
7468 "rust",
7469 "parseSha256Sidecar",
7470 "",
7471 ));
7472 let conn = search_fixture(&rows.join(","));
7473
7474 let candidates = collect_search_candidates(
7475 &conn,
7476 "parse the sha256 sidecar file",
7477 None,
7478 None,
7479 false,
7480 40,
7481 )
7482 .unwrap();
7483
7484 assert!(candidate(&candidates, "parseSha256Sidecar").name_match);
7485 assert_eq!(candidates.iter().filter(|c| c.word_match).count(), 160);
7486 }
7487
7488 #[test]
7489 fn the_or_pass_fills_the_word_cap_but_never_exceeds_it() {
7490 let mut rows: Vec<String> = (1..=20)
7491 .map(|i| {
7492 code_row(
7493 &format!("a{i:02}"),
7494 "src/a.rs",
7495 "rust",
7496 &format!("both_{i:02}"),
7497 "restore offline",
7498 )
7499 })
7500 .collect();
7501 rows.extend((1..=50).map(|i| {
7502 code_row(
7503 &format!("p{i:02}"),
7504 "src/p.rs",
7505 "rust",
7506 &format!("partial_{i:02}"),
7507 "restore records",
7508 )
7509 }));
7510 let conn = search_fixture(&rows.join(","));
7511
7512 let candidates =
7513 collect_search_candidates(&conn, "restore offline", None, None, false, 10).unwrap();
7514
7515 let word_rows: Vec<&Candidate> = candidates.iter().filter(|c| c.word_match).collect();
7516 assert_eq!(word_rows.len(), 40);
7517 assert_eq!(
7518 word_rows
7519 .iter()
7520 .filter(|c| c.result.symbol.name.starts_with("both_"))
7521 .count(),
7522 20
7523 );
7524 }
7525
7526 #[test]
7527 fn exact_name_is_admitted_regardless_of_case() {
7528 let conn = search_fixture(&code_row("c1", "src/q.rs", "rust", "xyzzy_q", ""));
7529
7530 let candidates =
7531 collect_search_candidates(&conn, "XYZZY_Q", None, None, false, 10).unwrap();
7532 assert!(candidate(&candidates, "xyzzy_q").exact_name);
7533
7534 conn.execute_batch("DROP TABLE symbol_names_tri").unwrap();
7535 let candidates =
7536 collect_search_candidates(&conn, "xyzzy_q", None, None, false, 10).unwrap();
7537 assert!(candidate(&candidates, "xyzzy_q").exact_name);
7538 }
7539
7540 #[test]
7541 fn exact_name_with_a_quote_is_admitted_through_the_trigram_index() {
7542 let conn = search_fixture(&code_row(
7543 "c1",
7544 "src/say.js",
7545 "javascript",
7546 "say \"hi\"",
7547 "",
7548 ));
7549
7550 let candidates =
7551 collect_search_candidates(&conn, "say \"hi\"", None, None, false, 10).unwrap();
7552
7553 let target = candidate(&candidates, "say \"hi\"");
7554 assert!(target.exact_name && target.name_match);
7555 }
7556
7557 #[test]
7558 fn a_row_matched_by_every_branch_is_one_candidate_with_all_flags() {
7559 let conn = search_fixture(
7560 &[
7561 code_row("c1", "src/a.rs", "rust", "sidecar", ""),
7562 code_row("c2", "src/b.rs", "rust", "sidecar_helper", ""),
7563 ]
7564 .join(","),
7565 );
7566
7567 let candidates =
7568 collect_search_candidates(&conn, "sidecar", None, None, false, 10).unwrap();
7569
7570 assert_eq!(candidates.len(), 2);
7571 let target = candidate(&candidates, "sidecar");
7572 assert!(target.exact_name && target.word_match && target.name_match);
7573 assert!(target.bm25.is_some());
7574 let helper = candidate(&candidates, "sidecar_helper");
7575 assert!(!helper.exact_name && helper.word_match && helper.name_match);
7576 }
7577
7578 #[test]
7579 fn an_index_without_the_trigram_table_returns_word_rows_only() {
7580 let conn = sidecar_fixture();
7581 conn.execute_batch("DROP TABLE symbol_names_tri").unwrap();
7582
7583 let candidates = collect_search_candidates(&conn, "sha256", None, None, false, 10).unwrap();
7584
7585 let names: Vec<&str> = candidates
7586 .iter()
7587 .map(|c| c.result.symbol.name.as_str())
7588 .collect();
7589 assert_eq!(names, vec!["parse_sidecar_file"]);
7590 assert!(candidates.iter().all(|c| c.word_match && !c.name_match));
7591 assert_eq!(search_names(&conn, "sha256"), vec!["parse_sidecar_file"]);
7592 }
7593
7594 #[test]
7595 fn words_under_three_characters_skip_the_name_branch() {
7596 let conn = search_fixture(
7597 &[
7598 code_row("c1", "src/a.rs", "rust", "ab", ""),
7599 code_row("c2", "src/b.rs", "rust", "cab", ""),
7600 ]
7601 .join(","),
7602 );
7603
7604 let candidates = collect_search_candidates(&conn, "ab", None, None, false, 10).unwrap();
7605
7606 assert!(candidates.iter().all(|c| !c.name_match));
7607 assert!(candidate(&candidates, "ab").exact_name);
7608 }
7609
7610 #[test]
7611 fn trigram_terms_include_the_identifier_parts_of_each_word() {
7612 assert_eq!(
7613 trigram_name_terms("collapse_name"),
7614 vec!["collapse_name", "collapse", "name"]
7615 );
7616 assert_eq!(
7617 trigram_name_terms("parse the sha256 sidecar"),
7618 vec!["parse", "sha256", "sha", "256", "sidecar"]
7619 );
7620 assert_eq!(trigram_name_terms("isReady"), vec!["isready", "ready"]);
7621 assert_eq!(trigram_name_terms("the before"), vec!["the", "before"]);
7622 assert!(trigram_name_terms("ab").is_empty());
7623 }
7624
7625 #[test]
7626 fn snake_case_query_admits_a_pascal_case_name_through_the_name_branch() {
7627 let conn = search_fixture(
7628 &[
7629 code_row("c1", "src/collapse.rs", "rust", "CollapseName", ""),
7630 code_row("c2", "src/other.rs", "rust", "name_collapsed", ""),
7631 ]
7632 .join(","),
7633 );
7634
7635 let candidates =
7636 collect_search_candidates(&conn, "collapse_name", None, None, false, 10).unwrap();
7637
7638 let target = candidate(&candidates, "CollapseName");
7639 assert!(target.name_match);
7640 assert_eq!(target.name_terms, vec!["collapse", "name"]);
7641 assert_eq!(search_names(&conn, "collapse_name")[0], "CollapseName");
7642 }
7643
7644 fn plain_candidate(name: &str, kind: &str, path: &str) -> Candidate {
7645 Candidate {
7646 result: SymbolSearchResult {
7647 symbol: Symbol {
7648 symbol_id: format!("{path}:{name}"),
7649 file_id: "f".into(),
7650 path: path.into(),
7651 language: "rust".into(),
7652 name: name.into(),
7653 kind: kind.into(),
7654 signature: None,
7655 doc_comment: None,
7656 visibility: None,
7657 parent_symbol_id: None,
7658 start_line: 1,
7659 start_column: 0,
7660 end_line: 1,
7661 end_column: 0,
7662 start_byte: 0,
7663 end_byte: 0,
7664 body_start_line: None,
7665 body_start_column: None,
7666 body_end_line: None,
7667 body_end_column: None,
7668 body_start_byte: None,
7669 body_end_byte: None,
7670 body_hash: None,
7671 semantic_group: None,
7672 is_test: false,
7673 test_container: false,
7674 },
7675 score: 0.0,
7676 snippet: None,
7677 explain: None,
7678 },
7679 bm25: None,
7680 exact_name: false,
7681 word_match: false,
7682 name_match: false,
7683 owner_match: false,
7684 name_terms: Vec::new(),
7685 documentation: false,
7686 nested: false,
7687 owner: None,
7688 owner_context: None,
7689 }
7690 }
7691
7692 fn function(name: &str) -> Candidate {
7693 plain_candidate(name, "function", "src/lib.rs")
7694 }
7695
7696 fn ranked(candidates: Vec<Candidate>, query: &str) -> Vec<(SymbolSearchResult, SearchExplain)> {
7697 rerank_with(candidates, query, false, None)
7698 }
7699
7700 fn ranked_names(candidates: Vec<Candidate>, query: &str) -> Vec<String> {
7701 ranked(candidates, query)
7702 .into_iter()
7703 .map(|(r, _)| r.symbol.name)
7704 .collect()
7705 }
7706
7707 fn documented(name: &str, signature: Option<&str>, doc: &str) -> Candidate {
7708 let mut candidate = function(name);
7709 candidate.result.symbol.signature = signature.map(str::to_string);
7710 candidate.result.symbol.doc_comment = Some(doc.into());
7711 candidate
7712 }
7713
7714 fn strip_ansi_case() -> Vec<Candidate> {
7715 vec![
7716 documented("strip_ansi", None, "Remove ANSI escape sequences"),
7717 documented(
7718 "_strip_code_fences",
7719 Some("def _strip_code_fences(text: str)"),
7720 "The first fenced code block's body, or the stripped text",
7721 ),
7722 ]
7723 }
7724
7725 #[test]
7726 fn rerank_words_split_identifiers_and_drop_stop_words_only_beside_content_words() {
7727 assert_eq!(
7728 rerank_words("parse the sha256 sidecar file"),
7729 vec!["parse", "sha", "256", "sidecar", "file"]
7730 );
7731 assert_eq!(
7732 rerank_words("parse_sha256_sidecar"),
7733 vec!["parse", "sha", "256", "sidecar"]
7734 );
7735 assert_eq!(
7736 rerank_words("ParseHTTPResponse"),
7737 vec!["parse", "http", "response"]
7738 );
7739 assert_eq!(rerank_words("is_ok"), vec!["ok"]);
7740 assert_eq!(rerank_words("the before"), vec!["the", "before"]);
7741 }
7742
7743 #[test]
7744 fn stop_words_cover_english_function_words_but_not_identifier_directions() {
7745 for word in ["was", "whether", "another"] {
7746 assert!(is_stop_word(word), "{word} must be a stop word");
7747 }
7748 for word in ["down", "into", "run"] {
7749 assert!(!is_stop_word(word), "{word} must stay a content word");
7750 }
7751 assert_eq!(rerank_words("what was the file"), vec!["file"]);
7752 }
7753
7754 #[test]
7755 fn a_public_name_sorts_before_its_private_twin_at_an_equal_score() {
7756 let mut private = function("_create_skill");
7757 private.bm25 = Some(-9.0);
7758 let mut public = function("create_skill");
7759 public.bm25 = Some(-1.0);
7760
7761 let rows = ranked(vec![private, public], "create skill");
7762
7763 assert_eq!(rows[0].0.score, rows[1].0.score);
7764 assert_eq!(rows[0].1.name_strength, rows[1].1.name_strength);
7765 assert_eq!(
7766 rows.iter()
7767 .map(|(r, _)| r.symbol.name.as_str())
7768 .collect::<Vec<_>>(),
7769 vec!["create_skill", "_create_skill"]
7770 );
7771 }
7772
7773 #[test]
7774 fn a_whole_name_constant_yields_to_a_function_that_holds_the_word_with_context() {
7775 let rows = ranked(
7776 vec![
7777 plain_candidate("Glob", "constant", "src/glob.rs"),
7778 function("matches_glob_pattern"),
7779 ],
7780 "glob",
7781 );
7782
7783 assert_eq!(rows[0].0.symbol.name, "matches_glob_pattern");
7784 assert_eq!(rows[1].1.name_tier, "whole");
7785 assert_eq!(rows[1].1.name_bonus, W_NAME_ALL_WORDS);
7786 }
7787
7788 #[test]
7789 fn name_tiers_are_whole_then_all_words_then_partial_then_none() {
7790 let rows = ranked(
7791 vec![
7792 function("validate_everything"),
7793 function("validate_syntax_now"),
7794 function("validate_syntax"),
7795 function("unrelated"),
7796 ],
7797 "validate syntax",
7798 );
7799 let tiers: Vec<(&str, &str, f64)> = rows
7800 .iter()
7801 .map(|(r, e)| (r.symbol.name.as_str(), e.name_tier.as_str(), e.name_bonus))
7802 .collect();
7803
7804 assert_eq!(
7805 tiers,
7806 vec![
7807 ("validate_syntax", "whole", W_NAME_WHOLE),
7808 ("validate_syntax_now", "all", W_NAME_ALL_WORDS),
7809 ("validate_everything", "partial", 0.0),
7810 ("unrelated", "none", 0.0),
7811 ]
7812 );
7813 assert_eq!(rows[0].0.score, W_NAME_WHOLE + W_TERMS + W_KIND_DEFINITION);
7814 assert_eq!(
7815 rows[1].0.score,
7816 W_NAME_ALL_WORDS + W_TERMS + W_KIND_DEFINITION
7817 );
7818 assert_eq!(rows[2].0.score, W_TERMS / 2.0 + W_KIND_DEFINITION);
7819 assert_eq!(rows[3].0.score, W_KIND_DEFINITION);
7820 }
7821
7822 #[test]
7823 fn distinct_scoring_prefers_three_terms_covered_once_over_two_terms_repeated() {
7824 assert_eq!(
7825 ranked_names(strip_ansi_case(), "strip ansi escape codes"),
7826 vec!["strip_ansi", "_strip_code_fences"]
7827 );
7828 }
7829
7830 #[test]
7831 fn distinct_scoring_denies_the_all_words_bonus_to_a_substring_only_name() {
7832 let candidates = vec![
7833 function("execute_julie_extract"),
7834 documented("slice_bytes", None, "cut a byte range"),
7835 ];
7836
7837 let rows = ranked(candidates, "cut");
7838 let bonuses: Vec<(&str, &str, f64)> = rows
7839 .iter()
7840 .map(|(r, e)| (r.symbol.name.as_str(), e.name_tier.as_str(), e.name_bonus))
7841 .collect();
7842
7843 assert_eq!(
7844 bonuses,
7845 vec![
7846 ("execute_julie_extract", "partial", 0.0),
7847 ("slice_bytes", "none", 0.0),
7848 ]
7849 );
7850 assert_eq!(rows[0].0.score, rows[1].0.score);
7851 }
7852
7853 #[test]
7854 fn distinct_scoring_keeps_the_whole_name_and_all_words_tiers_in_order() {
7855 let candidates = vec![
7856 function("validate_everything"),
7857 function("validate_syntax_now"),
7858 function("validate_syntax"),
7859 function("unrelated"),
7860 ];
7861
7862 assert_eq!(
7863 ranked_names(candidates, "validate syntax"),
7864 vec![
7865 "validate_syntax",
7866 "validate_syntax_now",
7867 "validate_everything",
7868 "unrelated"
7869 ]
7870 );
7871 }
7872
7873 #[test]
7874 fn idf_weights_rank_a_term_in_one_row_above_a_term_in_most_rows() {
7875 let mut rows: Vec<String> = (0..10)
7876 .map(|i| {
7877 code_row(
7878 &format!("s{i}"),
7879 &format!("src/f{i}.rs"),
7880 "rust",
7881 &format!("search_{i}"),
7882 "searches the index",
7883 )
7884 })
7885 .collect();
7886 rows.push(code_row(
7887 "rare",
7888 "src/rare.rs",
7889 "rust",
7890 "sanitize_input",
7891 "sanitize the input",
7892 ));
7893 let conn = search_fixture(&rows.join(", "));
7894 let terms = vec!["sanitize".to_string(), "search".to_string()];
7895
7896 let weights = idf_weights(&conn, &terms);
7897 assert!(weights[0] > weights[1]);
7898 }
7899
7900 #[test]
7901 fn idf_weights_count_a_term_the_way_the_index_tokenizer_stems_it() {
7902 let conn = search_fixture(&code_row(
7903 "n",
7904 "src/news.rs",
7905 "rust",
7906 "fetch_news",
7907 "fetch the news feed",
7908 ));
7909 let terms = vec!["news".to_string(), "unseen".to_string()];
7910
7911 let weights = idf_weights(&conn, &terms);
7912
7913 assert!(weights[0] < weights[1]);
7914 }
7915
7916 #[test]
7917 fn a_signature_hit_past_the_head_byte_cap_does_not_credit_its_term() {
7918 let crediting_field = |padding: usize| {
7919 let mut row = function("handler");
7920 row.result.symbol.signature = Some(format!(
7921 "fn handler({}sidecar: u8)",
7922 "a: u8, ".repeat(padding)
7923 ));
7924 ranked(vec![row], "sidecar")[0].1.terms[0].1.clone()
7925 };
7926
7927 assert_eq!(crediting_field(4), "signature");
7928 assert_eq!(crediting_field(80), "none");
7929 }
7930
7931 #[test]
7932 fn explain_terms_name_the_crediting_field_of_every_query_term() {
7933 let rows = ranked(strip_ansi_case(), "strip ansi escape codes");
7934 let terms: Vec<(&str, &str, f64)> = rows[0]
7935 .1
7936 .terms
7937 .iter()
7938 .map(|(term, field, credit)| (term.as_str(), field.as_str(), *credit))
7939 .collect();
7940
7941 assert_eq!(rows[0].0.symbol.name, "strip_ansi");
7942 assert_eq!(
7943 terms,
7944 vec![
7945 ("strip", "name", 3.0),
7946 ("ansi", "name", 3.0),
7947 ("escape", "doc", TEXT_CREDIT),
7948 ("codes", "none", 0.0),
7949 ]
7950 );
7951 }
7952
7953 #[test]
7954 fn credited_fields_require_whole_name_multi_field_evidence_and_query_coverage() {
7955 let terms = vec![
7956 ("wsgi".into(), "name".into(), 3.0),
7957 ("response".into(), "signature".into(), TEXT_CREDIT),
7958 ("request".into(), "doc".into(), TEXT_CREDIT),
7959 ("error".into(), "doc".into(), TEXT_CREDIT),
7960 ];
7961 assert_eq!(credited_field_bonus(&terms), FIELD_DIVERSITY_BONUS);
7962 assert_eq!(credited_field_bonus(&terms[..2]), 0.0);
7963 assert_eq!(
7964 credited_field_bonus(&[
7965 ("wsgi".into(), "name".into(), 2.0),
7966 ("response".into(), "signature".into(), TEXT_CREDIT),
7967 ("request".into(), "doc".into(), TEXT_CREDIT),
7968 ("error".into(), "doc".into(), TEXT_CREDIT),
7969 ]),
7970 0.0
7971 );
7972 let sparse_terms = [
7973 ("terminal".into(), "name".into(), 3.0),
7974 ("docker".into(), "signature".into(), TEXT_CREDIT),
7975 ("local".into(), "doc".into(), TEXT_CREDIT),
7976 ("sandbox".into(), "doc".into(), TEXT_CREDIT),
7977 ("seven".into(), "none".into(), 0.0),
7978 ("ssh".into(), "none".into(), 0.0),
7979 ("singularity".into(), "none".into(), 0.0),
7980 ("modal".into(), "none".into(), 0.0),
7981 ("daytona".into(), "none".into(), 0.0),
7982 ("vercel".into(), "none".into(), 0.0),
7983 ];
7984 assert_eq!(credited_field_bonus(&sparse_terms), 0.0);
7985 assert_eq!(
7986 credited_field_bonus(&[
7987 ("response".into(), "name".into(), 3.0),
7988 ("request".into(), "name".into(), 3.0),
7989 ]),
7990 0.0
7991 );
7992 }
7993
7994 #[test]
7995 fn credited_field_bonus_requires_two_distinct_document_terms() {
7996 let terms = [
7997 ("strip".into(), "name".into(), 3.0),
7998 ("path".into(), "signature".into(), TEXT_CREDIT),
7999 ("windows".into(), "doc".into(), TEXT_CREDIT),
8000 ];
8001 assert_eq!(credited_field_bonus(&terms), 0.0);
8002 }
8003
8004 #[test]
8005 fn name_coverage_accepts_token_runs_substrings_and_stems() {
8006 let strengths = |name: &str, query: &str| {
8007 let stemmer = Stemmer::create(Algorithm::English);
8008 let words = stemmed_query_words(query, &stemmer);
8009 name_hits(name, &words, &stemmer)
8010 };
8011
8012 assert_eq!(strengths("parseSha256Sidecar", "sha 256"), vec![3, 3]);
8013 assert_eq!(strengths("parseSha256Sidecar", "sha256"), vec![3, 3]);
8014 assert_eq!(strengths("parseSha256Sidecar", "esha"), vec![1]);
8015 assert_eq!(strengths("validate_syntax", "validation"), vec![2]);
8016 assert_eq!(strengths("is_ok", "ok"), vec![3]);
8017 assert_eq!(strengths("isReady", "is"), vec![3]);
8018 assert_eq!(strengths("größe_berechnen", "größe"), vec![3]);
8019 assert_eq!(
8020 strengths("parseSha256Sidecar", "sidecar checksum"),
8021 vec![3, 0]
8022 );
8023 assert_eq!(
8024 strengths("parseSha256Sidecar", "checksum digest"),
8025 vec![0, 0]
8026 );
8027 }
8028
8029 #[test]
8030 fn a_rarer_term_moves_the_score_more_than_a_common_one() {
8031 let weights = [4.0, 1.0];
8032 let rows = rerank_with(
8033 vec![function("rare_helper"), function("common_helper")],
8034 "rare common",
8035 false,
8036 Some(&weights),
8037 );
8038
8039 assert_eq!(
8040 rows[0].1.word_weights,
8041 vec![("rare".to_string(), 4.0), ("common".to_string(), 1.0)]
8042 );
8043 assert_eq!(rows[0].0.symbol.name, "rare_helper");
8044 assert_eq!(rows[0].0.score, W_TERMS * 4.0 / 5.0 + W_KIND_DEFINITION);
8045 assert_eq!(rows[1].0.score, W_TERMS * 1.0 / 5.0 + W_KIND_DEFINITION);
8046 }
8047
8048 #[test]
8049 fn any_name_hit_outranks_a_zero_coverage_definition_for_long_queries() {
8050 let rows = ranked(
8051 vec![
8052 function("render_mode"),
8053 plain_candidate("retry_count", "constant", "src/scan.rs"),
8054 ],
8055 "how many times a failed download is tried again retry limit",
8056 );
8057
8058 assert_eq!(rows[0].0.symbol.name, "retry_count");
8059 assert_eq!(rows[0].1.name_tier, "partial");
8060 assert!(rows[0].0.score > rows[1].0.score);
8061 assert_eq!(rows[1].0.score, W_KIND_DEFINITION);
8062 }
8063
8064 #[test]
8065 fn a_doc_hit_past_the_head_byte_cap_does_not_credit_its_term() {
8066 let mut row = function("load");
8067 row.result.symbol.signature = Some("fn load(config: &Config) -> Loaded".into());
8068 row.result.symbol.doc_comment = Some(format!("{}settings", "é".repeat(321)));
8069 let (result, explain) = ranked(vec![row], "config settings").remove(0);
8070
8071 assert_eq!(
8072 explain.terms,
8073 vec![
8074 ("config".into(), "signature".into(), TEXT_CREDIT),
8075 ("settings".into(), "none".into(), 0.0),
8076 ]
8077 );
8078 assert_eq!(result.score, explain.term_score + W_KIND_DEFINITION);
8079 }
8080
8081 #[test]
8082 fn text_coverage_matches_whole_tokens_by_word_or_stem_prefix() {
8083 let crediting_field =
8084 |row: Candidate, query: &str| ranked(vec![row], query).remove(0).1.terms[0].1.clone();
8085 let doc_field = |doc: &str, query: &str| {
8086 let mut row = function("row");
8087 row.result.symbol.doc_comment = Some(doc.into());
8088 crediting_field(row, query)
8089 };
8090 let sig_field = |signature: &str, query: &str| {
8091 let mut row = function("row");
8092 row.result.symbol.signature = Some(signature.into());
8093 crediting_field(row, query)
8094 };
8095
8096 assert_eq!(doc_field("The system runs.", "stemming"), "none");
8097 assert_eq!(doc_field("The stemmer runs.", "stemming"), "doc");
8098 assert_eq!(doc_field("Compares stems.", "stemming"), "doc");
8099 assert_eq!(doc_field("An important port.", "porter"), "none");
8100 assert_eq!(
8101 sig_field("fn sha256sum(data: &[u8]) -> String", "sha256"),
8102 "signature"
8103 );
8104 assert_eq!(sig_field("fn is_ok()", "ok"), "signature");
8105 assert_eq!(sig_field("fn okay()", "ok"), "none");
8106 assert_eq!(
8107 sig_field("fn parseSha256Sidecar(text)", "sidecar"),
8108 "signature"
8109 );
8110 }
8111
8112 #[test]
8113 fn text_tokens_split_like_query_words_then_identifiers() {
8114 fn two_pass(text: &str) -> Vec<&str> {
8115 query_words(text)
8116 .into_iter()
8117 .flat_map(split_identifier)
8118 .collect()
8119 }
8120 fn one_pass(text: &str) -> Vec<&str> {
8121 let mut out = Vec::new();
8122 text_tokens_into(text, &mut out);
8123 out
8124 }
8125 let ascii = "fn parseHTTPResponse2(raw: &str, _id: u8) -> Vec<&str> // sha256_sum";
8126 let unicode = "Berechnet die Größe: größe_berechnen(pfad) -> ÜberGroß2x";
8127
8128 assert_eq!(one_pass(ascii), two_pass(ascii));
8129 assert_eq!(
8130 one_pass(ascii),
8131 vec![
8132 "fn", "parse", "HTTP", "Response", "2", "raw", "str", "id", "u", "8", "Vec", "str",
8133 "sha", "256", "sum",
8134 ]
8135 );
8136 assert_eq!(one_pass(unicode), two_pass(unicode));
8137 assert!(one_pass("").is_empty());
8138 assert!(one_pass("_ __ ...").is_empty());
8139 }
8140
8141 #[test]
8142 fn a_doc_credits_a_term_by_its_stem() {
8143 let mut row = function("check");
8144 row.result.symbol.doc_comment = Some("Validates the input.".into());
8145 let explain = ranked(vec![row], "validation").remove(0).1;
8146
8147 assert_eq!(
8148 explain.terms,
8149 vec![("validation".into(), "doc".into(), TEXT_CREDIT)]
8150 );
8151 }
8152
8153 #[test]
8154 fn kind_prior_orders_definitions_over_members_over_imports() {
8155 let rows = ranked(
8156 vec![
8157 plain_candidate("Scan", "import", "src/a.rs"),
8158 plain_candidate("Scan", "enum_member", "src/b.rs"),
8159 plain_candidate("Scan", "function", "src/c.rs"),
8160 ],
8161 "scan",
8162 );
8163 let order: Vec<(&str, f64)> = rows
8164 .iter()
8165 .map(|(r, e)| (r.symbol.path.as_str(), e.kind_prior))
8166 .collect();
8167
8168 assert_eq!(
8169 order,
8170 vec![
8171 ("src/c.rs", W_KIND_DEFINITION),
8172 ("src/b.rs", W_KIND_MEMBER),
8173 ("src/a.rs", W_KIND_IMPORT),
8174 ]
8175 );
8176 }
8177
8178 #[test]
8179 fn a_partial_name_match_on_a_member_beats_the_kind_prior_of_a_function() {
8180 let names = ranked_names(
8181 vec![
8182 function("RenderMode"),
8183 plain_candidate("MaxRetryCount", "constant", "pkg/scan.go"),
8184 ],
8185 "retry download limit timeout",
8186 );
8187
8188 assert_eq!(names[0], "MaxRetryCount");
8189 }
8190
8191 #[test]
8192 fn path_role_demotes_role_directories_unless_the_query_names_them() {
8193 let rows = |query: &str| {
8194 ranked(
8195 vec![
8196 plain_candidate("verifyChecksum", "function", "scripts/launcher.ts"),
8197 plain_candidate("verify_checksum", "function", "src/archive.rs"),
8198 ],
8199 query,
8200 )
8201 };
8202
8203 let plain = rows("verify checksum");
8204 assert_eq!(plain[0].0.symbol.path, "src/archive.rs");
8205 assert_eq!(plain[1].1.path_role, W_PATH_ROLE);
8206
8207 let named = rows("launcher script verify checksum");
8208 assert!(named.iter().all(|(_, e)| e.path_role == 0.0));
8209
8210 let only_launcher = rows("launcher verify checksum");
8211 assert_eq!(only_launcher[0].0.symbol.path, "src/archive.rs");
8212 assert_eq!(only_launcher[1].1.path_role, W_PATH_ROLE);
8213
8214 let windows = ranked(
8215 vec![plain_candidate(
8216 "verifyChecksum",
8217 "function",
8218 "scripts\\launcher.ts",
8219 )],
8220 "verify checksum",
8221 );
8222 assert_eq!(windows[0].1.path_role, W_PATH_ROLE);
8223 }
8224
8225 #[test]
8226 fn documentation_rows_sort_after_every_code_row() {
8227 let mut heading = plain_candidate("Verify checksum", "heading", "README.md");
8228 heading.documentation = true;
8229 heading.result.symbol.language = "markdown".into();
8230 heading.result.symbol.signature = Some("Verify checksum".into());
8231 heading.result.symbol.doc_comment = Some("Verify the checksum of the archive.".into());
8232 let rows = ranked(
8233 vec![
8234 heading,
8235 plain_candidate("unrelated", "variable", "src/a.rs"),
8236 ],
8237 "verify checksum",
8238 );
8239
8240 assert_eq!(rows[0].0.symbol.name, "unrelated");
8241 assert_eq!(rows[1].1.documentation, W_DOCUMENTATION_ROW);
8242 assert_eq!(rows[1].1.name_tier, "whole");
8243 assert!(rows[1].0.score < 0.0);
8244 }
8245
8246 #[test]
8247 fn a_helper_nested_in_a_function_ranks_below_an_equal_module_function() {
8248 let mut helper = plain_candidate("run_test", "function", "tests/test_basic.py");
8249 helper.nested = true;
8250 let module_function = plain_candidate("run_dotenv", "function", "tests/test_basic.py");
8251
8252 let rows = rerank_with(vec![helper, module_function], "run", true, None);
8253
8254 assert_eq!(rows[0].0.symbol.name, "run_dotenv");
8255 assert_eq!(rows[1].1.nested, W_NESTED);
8256 }
8257
8258 #[test]
8259 fn test_intent_boosts_test_rows_only_when_tests_are_included_and_named() {
8260 let rows = |query: &str, include_tests: bool| {
8261 let mut test_row = plain_candidate("payment_flow", "function", "tests/payment.rs");
8262 test_row.result.symbol.is_test = true;
8263 let plain_row = plain_candidate("payment_flow", "function", "src/payment.rs");
8264 rerank_with(vec![plain_row, test_row], query, include_tests, None)
8265 };
8266
8267 let boosted = rows("payment flow tests", true);
8268 assert_eq!(boosted[0].0.symbol.path, "tests/payment.rs");
8269 assert_eq!(boosted[0].1.test_intent, W_TEST_INTENT);
8270 assert_eq!(boosted[1].1.test_intent, 0.0);
8271
8272 assert!(
8273 rows("payment flow tests", false)
8274 .iter()
8275 .all(|(_, e)| e.test_intent == 0.0)
8276 );
8277 assert!(
8278 rows("payment flow", true)
8279 .iter()
8280 .all(|(_, e)| e.test_intent == 0.0)
8281 );
8282 }
8283
8284 #[test]
8285 fn ties_break_by_bm25_then_name_length_then_path() {
8286 let mut word_row = plain_candidate("payment", "function", "src/z.rs");
8287 word_row.word_match = true;
8288 word_row.bm25 = Some(-4.0);
8289 let mut weaker_word_row = plain_candidate("payment", "function", "src/a.rs");
8290 weaker_word_row.word_match = true;
8291 weaker_word_row.bm25 = Some(-2.0);
8292 let mut name_only = plain_candidate("payment", "function", "src/b.rs");
8293 name_only.name_match = true;
8294 let rows = ranked(
8295 vec![
8296 plain_candidate("payment", "function", "src/y.rs"),
8297 name_only,
8298 weaker_word_row,
8299 word_row,
8300 ],
8301 "payment",
8302 );
8303 let paths: Vec<&str> = rows.iter().map(|(r, _)| r.symbol.path.as_str()).collect();
8304
8305 assert_eq!(paths, vec!["src/z.rs", "src/a.rs", "src/b.rs", "src/y.rs"]);
8306
8307 let by_length = ranked_names(
8308 vec![
8309 function("payment_gateway_client"),
8310 function("payment_gateway"),
8311 ],
8312 "gateway",
8313 );
8314 assert_eq!(by_length, vec!["payment_gateway", "payment_gateway_client"]);
8315 }
8316
8317 #[test]
8318 fn a_whole_token_name_outranks_a_substring_name_with_better_bm25() {
8319 let mut token = function("csr");
8320 token.bm25 = Some(-1.0);
8321 let mut substring = function("action_csrf_token");
8322 substring.bm25 = Some(-5.0);
8323
8324 let rows = ranked(vec![substring, token], "csr adjacency");
8325
8326 assert!(rows[0].0.score > rows[1].0.score);
8327 assert_eq!(rows[0].0.symbol.name, "csr");
8328 }
8329
8330 #[test]
8331 fn an_acronym_token_outranks_a_name_that_only_contains_it() {
8332 let mut token = function("http_client");
8333 token.bm25 = Some(-1.0);
8334 let mut substring = function("shttpd_config");
8335 substring.bm25 = Some(-5.0);
8336
8337 let rows = ranked(vec![substring, token], "http");
8338
8339 assert!(rows[0].0.score > rows[1].0.score);
8340 assert_eq!(rows[0].0.symbol.name, "http_client");
8341 }
8342
8343 #[test]
8344 fn explain_reports_the_sum_of_the_name_strengths() {
8345 let rows = ranked(vec![function("action_csrf_token")], "csr token");
8346
8347 assert_eq!(rows[0].1.name_strength, 4);
8348 }
8349
8350 #[test]
8351 fn snippets_follow_the_admitting_branch() {
8352 let mut word_row = function("parse_sidecar_file");
8353 word_row.word_match = true;
8354 word_row.result.snippet = Some("parse the [sha256] sidecar file".into());
8355 let mut name_row = function("parseSha256Sidecar");
8356 name_row.name_match = true;
8357 name_row.name_terms = vec!["sha".into(), "sha256".into(), "256".into()];
8358 let mut exact_row = function("sha256");
8359 exact_row.exact_name = true;
8360 let rows = ranked(vec![word_row, name_row, exact_row], "sha256");
8361 let snippets: Vec<(&str, &str)> = rows
8362 .iter()
8363 .map(|(r, _)| (r.symbol.name.as_str(), r.snippet.as_deref().unwrap()))
8364 .collect();
8365
8366 assert_eq!(
8367 snippets,
8368 vec![
8369 ("sha256", "sha256"),
8370 ("parseSha256Sidecar", "parse[Sha256]Sidecar"),
8371 ("parse_sidecar_file", "parse the [sha256] sidecar file"),
8372 ]
8373 );
8374 assert_eq!(rows[1].1.branches, vec!["name"]);
8375 assert_eq!(rows[0].1.branches, vec!["exact"]);
8376 }
8377
8378 #[test]
8379 fn explain_is_attached_only_when_requested() {
8380 let conn = sidecar_fixture();
8381 let query = "sha256";
8382
8383 let silent = fts_search_symbols_scoped(&conn, query, None, None, false, 10).unwrap();
8384 assert!(silent.iter().all(|r| r.explain.is_none()));
8385 assert!(silent[0].score > 0.0);
8386 assert_eq!(
8387 serde_json::to_value(&silent[0]).unwrap().get("explain"),
8388 None
8389 );
8390
8391 let explained =
8392 fts_search_symbols_explained(&conn, query, None, None, false, 10, true).unwrap();
8393 let by_name = |name: &str| {
8394 explained
8395 .iter()
8396 .find(|r| r.symbol.name == name)
8397 .and_then(|r| r.explain.as_ref())
8398 .unwrap()
8399 };
8400 let name_only = by_name("parseSha256Sidecar");
8401 assert_eq!(name_only.candidates, 2);
8402 assert_eq!(name_only.branches, vec!["name"]);
8403 assert_eq!(name_only.bm25, None);
8404 let word_row = by_name("parse_sidecar_file");
8405 assert!(word_row.bm25.unwrap() < 0.0);
8406 assert_eq!(word_row.candidates, 2);
8407 assert!(
8408 serde_json::to_value(&explained[0])
8409 .unwrap()
8410 .get("explain")
8411 .is_some()
8412 );
8413 }
8414
8415 #[test]
8416 fn search_symbols_treats_like_wildcards_as_literals() {
8417 let dir = crate::safe_tempdir();
8418 let db_path = dir.path().join("search_symbols_treats_like_wildcards.db");
8419 let conn = open_read_write(&db_path).unwrap();
8420 conn.execute_batch(
8421 "CREATE TABLE symbols (
8422 symbol_id TEXT, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
8423 signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
8424 start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
8425 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
8426 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
8427 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
8428 semantic_group TEXT, is_test INTEGER, test_container INTEGER
8429 );
8430 INSERT INTO symbols VALUES (
8431 's', 'f', 'src/lib.rs', 'rust', 'ordinary', 'function', NULL, NULL, NULL, NULL,
8432 1, 0, 1, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0
8433 );
8434 INSERT INTO symbols VALUES (
8435 'p', 'f', 'src/lib.rs', 'rust', 'literal%name', 'function', NULL, NULL, NULL, NULL,
8436 1, 0, 1, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0
8437 );
8438 INSERT INTO symbols VALUES (
8439 'u', 'f', 'src/lib.rs', 'rust', 'literal_name', 'function', NULL, NULL, NULL, NULL,
8440 1, 0, 1, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0
8441 );
8442 CREATE TABLE files (
8443 file_id TEXT, path TEXT, language TEXT, content_hash TEXT,
8444 content_bytes INTEGER, line_count INTEGER, indexed_at TEXT
8445 );
8446 INSERT INTO files VALUES ('f1', 'src/literal_path/lib.rs', 'rust', 'hash', 0, 0, 'now');
8447 INSERT INTO files VALUES ('f2', 'src/literalXpath/lib.rs', 'rust', 'hash', 0, 0, 'now'
8448 );",
8449 )
8450 .unwrap();
8451
8452 assert_eq!(
8453 search_symbols(&conn, "%", None, false, 10).unwrap()[0].name,
8454 "literal%name"
8455 );
8456 assert_eq!(
8457 search_symbols(&conn, "_", None, false, 10).unwrap()[0].name,
8458 "literal_name"
8459 );
8460 assert_eq!(
8461 load_scoped_files(&conn, Some("src/literal_path"))
8462 .unwrap()
8463 .len(),
8464 1
8465 );
8466 }
8467
8468 #[test]
8469 fn find_references_for_symbol_limits_callees_by_symbol_id() {
8470 let dir = crate::safe_tempdir();
8471 let db_path = dir.path().join("find_references_for_symbol.db");
8472 let conn = open_read_write(&db_path).unwrap();
8473 conn.execute_batch(
8474 "CREATE TABLE symbols (
8475 symbol_id TEXT, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
8476 signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
8477 start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
8478 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
8479 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
8480 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
8481 semantic_group TEXT, is_test INTEGER, test_container INTEGER
8482 );
8483 CREATE TABLE relationships (
8484 from_symbol_id TEXT, to_symbol_id TEXT, kind TEXT, path TEXT,
8485 start_line INTEGER, start_column INTEGER
8486 );
8487 CREATE TABLE pending_relationships (
8488 from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT, path TEXT,
8489 start_line INTEGER, start_column INTEGER
8490 );
8491 INSERT INTO symbols VALUES
8492 ('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),
8493 ('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),
8494 ('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),
8495 ('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);
8496 INSERT INTO relationships VALUES
8497 ('other', 'other-callee', 'calls', 'b.rs', 1, 0),
8498 ('wanted', 'wanted-callee', 'calls', 'a.rs', 1, 0);",
8499 )
8500 .unwrap();
8501
8502 let references = find_references_for_symbol(&conn, "new", "callees", 1, "wanted").unwrap();
8503 assert_eq!(references.len(), 1);
8504 assert_eq!(references[0].to_symbol_name, "wanted_dep");
8505 }
8506
8507 #[test]
8508 fn test_fts_search_symbols_and_porter_stemming() {
8509 let dir = crate::safe_tempdir();
8510 let db_path = dir.path().join("fts_search_symbols.db");
8511 let conn = open_read_write(&db_path).unwrap();
8512
8513 conn.execute_batch(
8514 "CREATE TABLE symbols (
8515 symbol_id TEXT PRIMARY KEY,
8516 file_id TEXT,
8517 path TEXT,
8518 language TEXT,
8519 name TEXT,
8520 kind TEXT,
8521 signature TEXT,
8522 doc_comment TEXT,
8523 visibility TEXT,
8524 parent_symbol_id TEXT,
8525 start_line INTEGER,
8526 start_column INTEGER,
8527 end_line INTEGER,
8528 end_column INTEGER,
8529 start_byte INTEGER,
8530 end_byte INTEGER,
8531 body_start_line INTEGER,
8532 body_start_column INTEGER,
8533 body_end_line INTEGER,
8534 body_end_column INTEGER,
8535 body_start_byte INTEGER,
8536 body_end_byte INTEGER,
8537 body_hash TEXT,
8538 semantic_group TEXT,
8539 is_test INTEGER,
8540 test_container INTEGER
8541 );
8542 INSERT INTO symbols VALUES (
8543 's1', 'f1', 'src/payment.rs', 'rust', 'PaymentGateway', 'trait',
8544 'pub trait PaymentGateway', 'Core payment provider interface for transactions',
8545 'pub', NULL, 10, 0, 20, 1, 100, 250, 12, 4, 19, 1, 120, 240, 'hash1', 'type', 0, 0
8546 );
8547 INSERT INTO symbols VALUES (
8548 's2', 'f1', 'src/payment.rs', 'rust', 'StripeClient', 'struct',
8549 'pub struct StripeClient', 'Handles HTTP requests to stripe payment API',
8550 'pub', NULL, 25, 0, 35, 1, 300, 450, 27, 4, 34, 1, 320, 440, 'hash2', 'type', 0, 0
8551 );
8552 INSERT INTO symbols VALUES (
8553 's3', 'f2', 'src/parser.rs', 'rust', 'parse_tokens', 'function',
8554 'pub fn parse_tokens(stream: &TokenStream) -> Result<Vec<Token>>', 'Parses syntax tokens from stream',
8555 'pub', NULL, 5, 0, 15, 1, 50, 200, 7, 4, 14, 1, 70, 190, 'hash3', 'function', 0, 0
8556 );
8557 INSERT INTO symbols VALUES (
8558 's4', 'f3', 'tests/payment_test.rs', 'rust', 'test_payment_flow', 'function',
8559 'fn test_payment_flow()', 'Tests payment charge workflow',
8560 NULL, NULL, 5, 0, 15, 1, 50, 200, 7, 4, 14, 1, 70, 190, 'hash4', 'function', 1, 0
8561 );",
8562 )
8563 .unwrap();
8564
8565 ensure_fts_index(&conn).unwrap();
8566
8567 let results =
8569 fts_search_symbols_scoped(&conn, "parsing tokens", None, None, false, 10).unwrap();
8570 assert_eq!(results.len(), 1);
8571 assert_eq!(results[0].symbol.name, "parse_tokens");
8572 assert!(results[0].snippet.is_some());
8573
8574 let results =
8576 fts_search_symbols_scoped(&conn, "transactions", None, None, false, 10).unwrap();
8577 assert_eq!(results.len(), 1);
8578 assert_eq!(results[0].symbol.name, "PaymentGateway");
8579
8580 let results = fts_search_symbols_scoped(&conn, "payment", None, None, false, 10).unwrap();
8582 assert_eq!(results.len(), 2);
8583 assert!(results.iter().all(|r| !r.symbol.is_test));
8584
8585 let results = fts_search_symbols_scoped(&conn, "payment", None, None, true, 10).unwrap();
8587 assert_eq!(results.len(), 3);
8588
8589 let results =
8591 fts_search_symbols_scoped(&conn, "stripe kafka redis", None, None, false, 10).unwrap();
8592 assert_eq!(results.len(), 1);
8593 assert_eq!(results[0].symbol.name, "StripeClient");
8594 }
8595
8596 #[test]
8597 fn find_related_tests_returns_each_test_once_under_the_limit() {
8598 let dir = crate::safe_tempdir();
8599 let conn = open_read_write(&dir.path().join("related_tests_limit.db")).unwrap();
8600 conn.execute_batch(
8601 "CREATE TABLE symbols (
8602 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT,
8603 name TEXT, kind TEXT, signature TEXT, doc_comment TEXT,
8604 visibility TEXT, parent_symbol_id TEXT, start_line INTEGER,
8605 start_column INTEGER, end_line INTEGER, end_column INTEGER,
8606 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
8607 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
8608 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
8609 semantic_group TEXT, is_test INTEGER, test_container INTEGER
8610 );
8611 CREATE TABLE relationships (
8612 from_symbol_id TEXT, to_symbol_id TEXT, kind TEXT, path TEXT,
8613 start_line INTEGER, start_column INTEGER
8614 );
8615 CREATE TABLE pending_relationships (
8616 from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT, path TEXT,
8617 start_line INTEGER, start_column INTEGER,
8618 target_receiver TEXT, target_namespace_json TEXT, target_display_name TEXT
8619 );
8620 CREATE TABLE type_facts (
8621 type_fact_id TEXT, symbol_id TEXT, language TEXT, resolved_type TEXT, generic_params_json TEXT
8622 );
8623 INSERT INTO symbols VALUES
8624 ('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),
8625 ('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),
8626 ('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);
8627 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
8628 ('t_a', 'compute', 'calls', 'tests/a.rs', 3, 4, NULL, NULL, 'compute'),
8629 ('t_a', 'compute', 'calls', 'tests/a.rs', 5, 4, NULL, NULL, 'compute'),
8630 ('t_a', 'compute', 'calls', 'tests/a.rs', 7, 4, NULL, NULL, 'compute'),
8631 ('t_a', 'compute', 'calls', 'tests/a.rs', 9, 4, NULL, NULL, 'compute'),
8632 ('t_a', 'compute', 'calls', 'tests/a.rs', 11, 4, NULL, NULL, 'compute'),
8633 ('t_b', 'compute', 'calls', 'tests/b.rs', 3, 4, NULL, NULL, 'compute');",
8634 )
8635 .unwrap();
8636 let target = get_symbol_by_name(&conn, "compute", None).unwrap().unwrap();
8637
8638 let tests = find_related_tests(&conn, &target, 5).unwrap();
8639
8640 let mut names: Vec<&str> = tests.iter().map(|t| t.name.as_str()).collect();
8641 names.sort();
8642 assert_eq!(names, vec!["first_case", "second_case"]);
8643 }
8644
8645 #[test]
8646 fn documentation_rows_rank_after_code_in_search() {
8647 let dir = crate::safe_tempdir();
8648 let conn = open_read_write(&dir.path().join("doc_rank.db")).unwrap();
8649 conn.execute_batch(
8650 "CREATE TABLE symbols (
8651 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT,
8652 kind TEXT, signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
8653 start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
8654 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
8655 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
8656 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
8657 semantic_group TEXT, is_test INTEGER, test_container INTEGER, content_type TEXT
8658 );
8659 INSERT INTO symbols VALUES
8660 ('s_doc', 'f1', 'docs/plans/018.adoc', 'asciidoc', 'Reconcile offline edits',
8661 'heading', 'Reconcile offline edits', NULL, NULL, NULL,
8662 3, 0, 3, 1, 10, 40, 3, 0, 3, 1, 10, 40, 'hash_doc', NULL, 0, 0, 'documentation'),
8663 ('s_code', 'f2', 'src/sync.rs', 'rust', 'reconcile_offline_edits', 'function',
8664 'fn reconcile_offline_edits()', 'Reconcile offline edits at startup', 'pub', NULL,
8665 10, 0, 20, 1, 100, 250, 12, 4, 19, 1, 120, 240, 'hash_code', NULL, 0, 0, 'code');",
8666 )
8667 .unwrap();
8668 ensure_fts_index(&conn).unwrap();
8669
8670 let results =
8671 fts_search_symbols_scoped(&conn, "reconcile offline edits", None, None, false, 10)
8672 .unwrap();
8673
8674 assert_eq!(results.len(), 2);
8675 assert_eq!(results[0].symbol.name, "reconcile_offline_edits");
8676 assert_eq!(results[1].symbol.name, "Reconcile offline edits");
8677 }
8678
8679 #[test]
8680 fn test_queries_nocase_and_path_normalization() {
8681 let conn = Connection::open_in_memory().unwrap();
8682 conn.execute_batch(
8683 "CREATE TABLE files (
8684 file_id TEXT PRIMARY KEY,
8685 path TEXT NOT NULL,
8686 language TEXT,
8687 content_hash TEXT,
8688 content_bytes INTEGER,
8689 line_count INTEGER,
8690 indexed_at INTEGER
8691 );
8692 CREATE TABLE symbols (
8693 symbol_id TEXT PRIMARY KEY,
8694 file_id TEXT,
8695 path TEXT NOT NULL,
8696 language TEXT,
8697 name TEXT,
8698 kind TEXT,
8699 signature TEXT,
8700 doc_comment TEXT,
8701 visibility TEXT,
8702 parent_symbol_id TEXT,
8703 start_line INTEGER,
8704 start_column INTEGER,
8705 end_line INTEGER,
8706 end_column INTEGER,
8707 start_byte INTEGER,
8708 end_byte INTEGER,
8709 body_start_line INTEGER,
8710 body_start_column INTEGER,
8711 body_end_line INTEGER,
8712 body_end_column INTEGER,
8713 body_start_byte INTEGER,
8714 body_end_byte INTEGER,
8715 body_hash TEXT,
8716 semantic_group TEXT,
8717 is_test INTEGER,
8718 test_container INTEGER
8719 );
8720 -- Insert with backslashes and mixed casing to verify defensive normalization and COLLATE NOCASE
8721 INSERT INTO files VALUES ('f1', 'src\\Payment.rs', 'rust', 'hash1', 100, 10, '2026-09-14T00:00:00Z');
8722 INSERT INTO symbols VALUES (
8723 's1', 'f1', 'src\\Payment.rs', 'rust', 'ProcessPayment', 'function',
8724 'pub fn ProcessPayment()', NULL, 'pub', NULL, 1, 0, 5, 0, 0, 50,
8725 2, 4, 4, 1, 10, 45, 'bhash', 'function', 0, 0
8726 );",
8727 )
8728 .unwrap();
8729
8730 let file = get_file(&conn, "SRC/PAYMENT.RS")
8732 .unwrap()
8733 .expect("File should be found");
8734 assert_eq!(
8735 file.path, "src/Payment.rs",
8736 "Path should be normalized to forward slashes"
8737 );
8738
8739 let file2 = get_file(&conn, "src/payment.rs")
8740 .unwrap()
8741 .expect("File should be found");
8742 assert_eq!(file2.path, "src/Payment.rs");
8743
8744 let syms = load_file_symbols(&conn, "SRC/PAYMENT.RS").unwrap();
8746 assert_eq!(syms.len(), 1);
8747 assert_eq!(
8748 syms[0].path, "src/Payment.rs",
8749 "Symbol path should be normalized to forward slashes"
8750 );
8751
8752 let sym = get_symbol_by_name(&conn, "ProcessPayment", Some("SRC/PAYMENT.RS"))
8754 .unwrap()
8755 .expect("Symbol should be found with case-insensitive path filter");
8756 assert_eq!(sym.path, "src/Payment.rs");
8757 }
8758
8759 #[test]
8760 fn test_exact_case_prioritized_over_nocase() {
8761 let conn = Connection::open_in_memory().unwrap();
8762 conn.execute_batch(
8763 "CREATE TABLE files (
8764 file_id TEXT PRIMARY KEY,
8765 path TEXT NOT NULL,
8766 language TEXT,
8767 content_hash TEXT,
8768 content_bytes INTEGER,
8769 line_count INTEGER,
8770 indexed_at TEXT
8771 );
8772 CREATE TABLE symbols (
8773 symbol_id TEXT PRIMARY KEY,
8774 file_id TEXT,
8775 path TEXT NOT NULL,
8776 language TEXT,
8777 name TEXT NOT NULL,
8778 kind TEXT NOT NULL,
8779 signature TEXT,
8780 doc_comment TEXT,
8781 visibility TEXT,
8782 parent_symbol_id TEXT,
8783 start_line INTEGER,
8784 start_column INTEGER,
8785 end_line INTEGER,
8786 end_column INTEGER,
8787 start_byte INTEGER,
8788 end_byte INTEGER,
8789 body_start_line INTEGER,
8790 body_start_column INTEGER,
8791 body_end_line INTEGER,
8792 body_end_column INTEGER,
8793 body_start_byte INTEGER,
8794 body_end_byte INTEGER,
8795 body_hash TEXT,
8796 semantic_group TEXT,
8797 is_test INTEGER,
8798 test_container INTEGER
8799 );
8800 INSERT INTO files VALUES ('f1', 'src/Payment.rs', 'rust', 'h1', 100, 10, '2026-09-14T00:00:00Z');
8801 INSERT INTO files VALUES ('f2', 'src/payment.rs', 'rust', 'h2', 100, 10, '2026-09-14T00:00:00Z');
8802 INSERT INTO symbols VALUES (
8803 's1', 'f1', 'src/Payment.rs', 'rust', 'pay', 'function',
8804 'pub fn pay()', NULL, 'pub', NULL, 1, 0, 5, 0, 0, 50,
8805 2, 4, 4, 1, 10, 45, 'b1', 'function', 0, 0
8806 );
8807 INSERT INTO symbols VALUES (
8808 's2', 'f2', 'src/payment.rs', 'rust', 'pay', 'function',
8809 'pub fn pay()', NULL, 'pub', NULL, 1, 0, 5, 0, 0, 50,
8810 2, 4, 4, 1, 10, 45, 'b2', 'function', 0, 0
8811 );",
8812 )
8813 .unwrap();
8814
8815 let f_lower = get_file(&conn, "src/payment.rs").unwrap().unwrap();
8817 assert_eq!(f_lower.path, "src/payment.rs");
8818 assert_eq!(f_lower.file_id, "f2");
8819
8820 let f_upper = get_file(&conn, "src/Payment.rs").unwrap().unwrap();
8821 assert_eq!(f_upper.path, "src/Payment.rs");
8822 assert_eq!(f_upper.file_id, "f1");
8823
8824 let syms_lower = load_file_symbols(&conn, "src/payment.rs").unwrap();
8825 assert_eq!(syms_lower.len(), 1);
8826 assert_eq!(syms_lower[0].file_id, "f2");
8827
8828 let syms_upper = load_file_symbols(&conn, "src/Payment.rs").unwrap();
8829 assert_eq!(syms_upper.len(), 1);
8830 assert_eq!(syms_upper[0].file_id, "f1");
8831 }
8832
8833 #[test]
8834 fn test_conservative_pending_resolution_ignores_unmatched_namespace() {
8835 let dir = crate::safe_tempdir();
8836 let db_path = dir.path().join("conservative_resolution.db");
8837 let conn = open_read_write(&db_path).unwrap();
8838
8839 conn.execute_batch(
8840 "CREATE TABLE symbols (
8841 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT,
8842 name TEXT, kind TEXT, signature TEXT, doc_comment TEXT,
8843 visibility TEXT, parent_symbol_id TEXT, start_line INTEGER,
8844 start_column INTEGER, end_line INTEGER, end_column INTEGER,
8845 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
8846 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
8847 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
8848 semantic_group TEXT, is_test INTEGER, test_container INTEGER
8849 );
8850 CREATE TABLE relationships (
8851 from_symbol_id TEXT, to_symbol_id TEXT, kind TEXT, path TEXT,
8852 start_line INTEGER, start_column INTEGER
8853 );
8854 CREATE TABLE pending_relationships (
8855 from_symbol_id TEXT, target_terminal_name TEXT, kind TEXT, path TEXT,
8856 start_line INTEGER, start_column INTEGER,
8857 target_receiver TEXT, target_namespace_json TEXT, target_display_name TEXT
8858 );
8859 CREATE TABLE type_facts (
8860 type_fact_id TEXT, symbol_id TEXT, language TEXT, resolved_type TEXT, generic_params_json TEXT
8861 );
8862 -- Workspace struct Workspace and method Workspace::new
8863 INSERT INTO symbols VALUES
8864 ('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),
8865 ('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),
8866 ('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);
8867
8868 -- my_func calls Vec::new() (external namespace 'Vec')
8869 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
8870 ('s_caller', 'new', 'calls', 'src/caller.rs', 3, 8, NULL, '[\"Vec\"]', 'Vec::new');",
8871 )
8872 .unwrap();
8873
8874 let sigs = find_callee_signatures(&conn, "my_func", "s_caller", 10, false).unwrap();
8876 assert!(sigs.is_empty(), "Expected 0 signatures, got: {:?}", sigs);
8877
8878 let refs = find_references_for_symbol(&conn, "my_func", "callees", 10, "s_caller").unwrap();
8879 assert!(refs.is_empty(), "Expected 0 references, got: {:?}", refs);
8880
8881 let callers = find_references_for_symbol(&conn, "new", "callers", 10, "s_ws_new").unwrap();
8883 assert!(
8884 callers.is_empty(),
8885 "Expected 0 callers for Workspace::new, got: {:?}",
8886 callers
8887 );
8888
8889 let blast = compute_blast_radius(&conn, &["new"], &["src/workspace.rs"], 2, 20).unwrap();
8891 assert!(
8892 !blast.impacted_symbols.iter().any(|s| s.name == "my_func"),
8893 "my_func should not be impacted before calling Workspace::new: {:?}",
8894 blast.impacted_symbols
8895 );
8896
8897 conn.execute(
8899 "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')",
8900 [],
8901 )
8902 .unwrap();
8903
8904 let sigs2 = find_callee_signatures(&conn, "my_func", "s_caller", 10, false).unwrap();
8905 assert_eq!(
8906 sigs2.len(),
8907 1,
8908 "Expected 1 signature for Workspace::new, got: {:?}",
8909 sigs2
8910 );
8911 assert!(sigs2[0].contains("pub fn new() -> Workspace"));
8912
8913 let blast2 = compute_blast_radius(&conn, &["new"], &["src/workspace.rs"], 2, 20).unwrap();
8915 assert!(
8916 blast2.impacted_symbols.iter().any(|s| s.name == "my_func"),
8917 "my_func should be impacted after calling Workspace::new: {:?}",
8918 blast2.impacted_symbols
8919 );
8920
8921 conn.execute(
8923 "INSERT INTO symbols VALUES
8924 ('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);",
8925 [],
8926 )
8927 .unwrap();
8928 conn.execute(
8929 "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')",
8930 [],
8931 )
8932 .unwrap();
8933
8934 let sigs_other = find_callee_signatures(&conn, "other_func", "s_other", 10, false).unwrap();
8936 assert!(
8937 sigs_other.is_empty(),
8938 "Bare call to new() from outside Workspace should not resolve to Workspace::new: {:?}",
8939 sigs_other
8940 );
8941
8942 conn.execute(
8944 "INSERT INTO symbols VALUES
8945 ('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);",
8946 [],
8947 )
8948 .unwrap();
8949 conn.execute(
8950 "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')",
8951 [],
8952 )
8953 .unwrap();
8954
8955 let sigs_sibling =
8956 find_callee_signatures(&conn, "helper", "s_ws_helper", 10, false).unwrap();
8957 assert_eq!(
8958 sigs_sibling.len(),
8959 1,
8960 "Sibling method calling bare new() should resolve to Workspace::new: {:?}",
8961 sigs_sibling
8962 );
8963
8964 let ext_sigs = find_callee_signatures(&conn, "my_func", "s_caller", 10, true).unwrap();
8966 assert!(
8967 ext_sigs.iter().any(|s| s.contains("Vec")),
8968 "include_external: true should include external Vec::new: {:?}",
8969 ext_sigs
8970 );
8971 }
8972
8973 #[test]
8974 fn test_find_structural_facts_and_literals_scoped() {
8975 let dir = crate::safe_tempdir();
8976 let db_path = dir.path().join("facts_test.db");
8977 let conn = open_read_write(&db_path).unwrap();
8978 conn.execute_batch(
8979 "CREATE TABLE symbols (
8980 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT, kind TEXT,
8981 signature TEXT, doc_comment TEXT, visibility TEXT, parent_symbol_id TEXT,
8982 start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
8983 start_byte INTEGER, end_byte INTEGER, body_start_line INTEGER,
8984 body_start_column INTEGER, body_end_line INTEGER, body_end_column INTEGER,
8985 body_start_byte INTEGER, body_end_byte INTEGER, body_hash TEXT,
8986 semantic_group TEXT, is_test INTEGER, test_container INTEGER
8987 );
8988 CREATE TABLE structural_facts (
8989 structural_fact_id TEXT PRIMARY KEY, file_id TEXT, path TEXT NOT NULL, language TEXT,
8990 pattern_id TEXT, capture_name TEXT, node_kind TEXT, containing_symbol_id TEXT,
8991 start_line INTEGER, end_line INTEGER, confidence REAL, metadata_json TEXT
8992 );
8993 CREATE TABLE literals (
8994 literal_id TEXT PRIMARY KEY, file_id TEXT, path TEXT NOT NULL, language TEXT,
8995 kind TEXT, literal_text TEXT, carrier TEXT, containing_symbol_id TEXT,
8996 start_line INTEGER, start_column INTEGER, end_line INTEGER, end_column INTEGER,
8997 start_byte INTEGER, end_byte INTEGER
8998 );
8999 INSERT INTO structural_facts VALUES
9000 ('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\"}'),
9001 ('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\"}'),
9002 ('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\"}'),
9003 ('sf_sql', 'f3', 'src/db/queries.rs', 'rust', 'sql.select_query.v1', 'select_users', 'function', NULL, 30, 40, 1.0, NULL),
9004 ('sf_model', 'f4', 'src/models/user.rs', 'rust', 'sql.table_definition.v1', 'User', 'struct', NULL, 50, 60, 1.0, NULL),
9005 ('sf_css', 'f7', 'web/site.css', 'css', 'css.media_query.v1', 'media', 'media_statement', NULL, 1, 1, 1.0, NULL),
9006 ('sf_custom', 'f5', 'src/custom.rs', 'rust', 'my_custom_pattern', 'custom_name', 'item', NULL, 70, 80, 1.0, NULL);
9007 INSERT INTO literals VALUES
9008 ('lit_toml', 'f1', 'Cargo.toml', 'toml', 'toml_key', '\"version\"', 'key', NULL, 3, 0, 3, 9, 20, 29),
9009 ('lit_route', 'f2', 'src/routes/api.rs', 'rust', 'http_route', '\"/api/v1/users\"', 'string', NULL, 12, 0, 12, 15, 100, 115),
9010 ('lit_sql', 'f3', 'src/db/queries.rs', 'rust', 'sql_query', '\"SELECT * FROM users\"', 'string', NULL, 32, 0, 32, 21, 200, 221),
9011 ('lit_model', 'f4', 'src/models/user.rs', 'rust', 'model_table', '\"users_table\"', 'string', NULL, 52, 0, 52, 13, 300, 313);",
9012 )
9013 .unwrap();
9014
9015 let facts_config = find_structural_facts_scoped(&conn, "config", None, 10).unwrap();
9017 assert_eq!(facts_config.len(), 2);
9018 assert_eq!(facts_config[0].pattern_id, "yaml.key_value.v1");
9019 assert_eq!(facts_config[0].key.as_deref(), Some("on.name"));
9020 assert_eq!(facts_config[1].pattern_id, "toml.key_value.v1");
9021 assert_eq!(
9022 facts_config[1].key.as_deref(),
9023 Some("mcp_servers.code-kb.command")
9024 );
9025 let lits_config = find_literals_scoped(&conn, "config", None, 10).unwrap();
9026 assert_eq!(lits_config.len(), 1);
9027 assert_eq!(lits_config[0].kind, "toml_key");
9028
9029 let facts_route = find_structural_facts_scoped(&conn, "route", None, 10).unwrap();
9031 assert_eq!(facts_route.len(), 1);
9032 assert_eq!(facts_route[0].pattern_id, "axum.route.v1");
9033 assert_eq!(
9034 facts_route[0].key.as_deref(),
9035 Some("GET /api/v1/users/{id}")
9036 );
9037 let facts_routes = find_structural_facts_scoped(&conn, "routes", None, 10).unwrap();
9038 assert_eq!(facts_routes.len(), 1);
9039 let lits_route = find_literals_scoped(&conn, "route", None, 10).unwrap();
9040 assert_eq!(lits_route.len(), 1);
9041 assert_eq!(lits_route[0].kind, "http_route");
9042
9043 for q in &["query", "queries", "sql"] {
9045 let facts = find_structural_facts_scoped(&conn, q, None, 10).unwrap();
9046 assert_eq!(facts.len(), 2, "Failed for {}", q);
9047 assert!(facts.iter().all(|f| f.pattern_id.starts_with("sql.")));
9048 let lits = find_literals_scoped(&conn, q, None, 10).unwrap();
9049 assert_eq!(lits.len(), 1, "Failed for {}", q);
9050 assert_eq!(lits[0].kind, "sql_query");
9051 }
9052
9053 for m in &["model", "models"] {
9055 let facts = find_structural_facts_scoped(&conn, m, None, 10).unwrap();
9056 assert_eq!(facts.len(), 1, "Failed for {}", m);
9057 assert_eq!(facts[0].pattern_id, "sql.table_definition.v1");
9058 let lits = find_literals_scoped(&conn, m, None, 10).unwrap();
9059 assert_eq!(lits.len(), 1, "Failed for {}", m);
9060 assert_eq!(lits[0].kind, "model_table");
9061 }
9062
9063 let facts_custom = find_structural_facts_scoped(&conn, "custom_pattern", None, 10).unwrap();
9065 assert_eq!(facts_custom.len(), 1);
9066 assert_eq!(facts_custom[0].pattern_id, "my_custom_pattern");
9067 assert_eq!(facts_custom[0].key, None);
9068
9069 let facts_exact =
9071 find_structural_facts_scoped(&conn, "config", Some("Cargo.toml"), 10).unwrap();
9072 assert_eq!(facts_exact.len(), 1);
9073 let facts_miss =
9074 find_structural_facts_scoped(&conn, "config", Some("src/routes/api.rs"), 10).unwrap();
9075 assert_eq!(facts_miss.len(), 0);
9076
9077 let facts_dir =
9079 find_structural_facts_scoped(&conn, "route", Some("src/routes"), 10).unwrap();
9080 assert_eq!(facts_dir.len(), 1);
9081 let facts_dir_miss =
9082 find_structural_facts_scoped(&conn, "route", Some("src/db"), 10).unwrap();
9083 assert_eq!(facts_dir_miss.len(), 0);
9084
9085 let f_del = find_structural_facts(&conn, "config", 10).unwrap();
9087 assert_eq!(f_del.len(), 2);
9088 let l_del = find_literals(&conn, "config", 10).unwrap();
9089 assert_eq!(l_del.len(), 1);
9090 }
9091
9092 fn local_variable_fixture() -> Connection {
9093 let conn = Connection::open_in_memory().unwrap();
9094 conn.execute_batch(
9095 "CREATE TABLE symbols (
9096 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT,
9097 kind TEXT, signature TEXT, doc_comment TEXT, visibility TEXT,
9098 parent_symbol_id TEXT, start_line INTEGER, start_column INTEGER,
9099 end_line INTEGER, end_column INTEGER, start_byte INTEGER, end_byte INTEGER,
9100 body_start_line INTEGER, body_start_column INTEGER, body_end_line INTEGER,
9101 body_end_column INTEGER, body_start_byte INTEGER, body_end_byte INTEGER,
9102 body_hash TEXT, semantic_group TEXT, is_test INTEGER, test_container INTEGER
9103 );
9104 INSERT INTO symbols (symbol_id, file_id, path, language, name, kind, signature,
9105 parent_symbol_id, start_line, start_column, end_line, end_column,
9106 start_byte, end_byte, is_test, test_container)
9107 VALUES
9108 ('func', 'f1', 'src/db.rs', 'rust', 'open_conn', 'function',
9109 'fn open_conn() -> sqlite Connection', NULL, 1, 0, 9, 1, 0, 100, 0, 0),
9110 ('local', 'f1', 'src/db.rs', 'rust', 'conn', 'variable',
9111 'let conn: sqlite Connection', 'func', 2, 4, 2, 30, 10, 40, 0, 0),
9112 ('pool', 'f1', 'src/db.rs', 'rust', 'Pool', 'struct',
9113 'struct Pool sqlite', NULL, 12, 0, 16, 1, 120, 200, 0, 0),
9114 ('field', 'f1', 'src/db.rs', 'rust', 'conn', 'variable',
9115 'conn: sqlite Connection', 'pool', 13, 4, 13, 28, 130, 160, 0, 0),
9116 ('global', 'f1', 'src/db.rs', 'rust', 'conn', 'variable',
9117 'static conn: sqlite Connection', NULL, 20, 0, 20, 30, 210, 240, 0, 0),
9118 ('closure', 'f1', 'src/db.rs', 'rust', 'with_conn', 'variable',
9119 'let with_conn = |c: sqlite Connection|', 'func', 4, 4, 6, 5, 50, 90, 0, 0),
9120 ('nested', 'f1', 'src/db.rs', 'rust', 'conn', 'variable',
9121 'let conn = c sqlite', 'closure', 5, 8, 5, 24, 60, 80, 0, 0);",
9122 )
9123 .unwrap();
9124 conn
9125 }
9126
9127 fn matched_symbol_ids(conn: &Connection, query: &str) -> Vec<String> {
9128 let mut stmt = conn
9129 .prepare(
9130 "SELECT s.symbol_id FROM symbols_fts f
9131 JOIN symbols s ON s.rowid = f.rowid
9132 WHERE f.symbols_fts MATCH ?1 ORDER BY s.symbol_id",
9133 )
9134 .unwrap();
9135 let mut ids = stmt
9136 .query_map(params![query], |row| row.get::<_, String>(0))
9137 .unwrap()
9138 .collect::<Result<Vec<_>, _>>()
9139 .unwrap();
9140 ids.sort();
9141 ids
9142 }
9143
9144 #[test]
9145 fn fts_index_excludes_locals_and_rebuilds_a_stale_index() {
9146 let conn = local_variable_fixture();
9147 conn.execute_batch(
9148 "CREATE VIRTUAL TABLE symbols_fts USING fts5(
9149 name, signature, doc_comment,
9150 content='symbols', content_rowid='rowid', tokenize='porter unicode61'
9151 );
9152 INSERT INTO symbols_fts(rowid, name, signature, doc_comment)
9153 SELECT rowid, name, signature, doc_comment FROM symbols;",
9154 )
9155 .unwrap();
9156
9157 ensure_fts_index(&conn).unwrap();
9158
9159 assert_eq!(
9160 matched_symbol_ids(&conn, "sqlite"),
9161 vec!["field", "func", "global", "pool"]
9162 );
9163 }
9164
9165 #[test]
9166 fn lookup_excludes_locals_and_parameters() {
9167 let conn = local_variable_fixture();
9168
9169 let ids: Vec<String> = search_symbols_scoped(&conn, "conn", None, None, false, 10)
9170 .unwrap()
9171 .into_iter()
9172 .map(|s| s.symbol_id)
9173 .collect();
9174
9175 assert!(!ids.contains(&"local".to_string()));
9176 assert!(!ids.contains(&"nested".to_string()));
9177 assert!(ids.contains(&"field".to_string()));
9178 assert!(ids.contains(&"global".to_string()));
9179 }
9180
9181 #[test]
9182 fn search_excludes_locals_and_parameters() {
9183 let conn = local_variable_fixture();
9184 ensure_fts_index(&conn).unwrap();
9185
9186 let ids: Vec<String> = fts_search_symbols_scoped(&conn, "sqlite", None, None, false, 10)
9187 .unwrap()
9188 .into_iter()
9189 .map(|r| r.symbol.symbol_id)
9190 .collect();
9191
9192 assert!(!ids.contains(&"local".to_string()));
9193 assert!(ids.contains(&"func".to_string()));
9194 }
9195
9196 #[test]
9197 fn variable_kind_search_keeps_full_text_matching() {
9198 let conn = local_variable_fixture();
9199 ensure_fts_index(&conn).unwrap();
9200
9201 let ids: Vec<String> = fts_search_symbols_scoped(
9202 &conn,
9203 "sqlite connection",
9204 Some("variable"),
9205 None,
9206 false,
9207 10,
9208 )
9209 .unwrap()
9210 .into_iter()
9211 .map(|r| r.symbol.symbol_id)
9212 .collect();
9213
9214 assert!(ids.contains(&"global".to_string()));
9215 assert!(ids.contains(&"field".to_string()));
9216 }
9217
9218 #[test]
9219 fn qualified_lookup_returns_the_named_local_variable() {
9220 let conn = local_variable_fixture();
9221
9222 let ids: Vec<String> =
9223 search_symbols_scoped(&conn, "open_conn::conn", None, None, false, 10)
9224 .unwrap()
9225 .into_iter()
9226 .map(|s| s.symbol_id)
9227 .collect();
9228
9229 assert_eq!(ids, vec!["local".to_string()]);
9230 }
9231
9232 #[test]
9233 fn exact_local_variable_outranks_a_partial_global_match_within_the_limit() {
9234 let conn = Connection::open_in_memory().unwrap();
9235 conn.execute_batch(
9236 "CREATE TABLE symbols (
9237 symbol_id TEXT PRIMARY KEY, file_id TEXT, path TEXT, language TEXT, name TEXT,
9238 kind TEXT, signature TEXT, doc_comment TEXT, visibility TEXT,
9239 parent_symbol_id TEXT, start_line INTEGER, start_column INTEGER,
9240 end_line INTEGER, end_column INTEGER, start_byte INTEGER, end_byte INTEGER,
9241 body_start_line INTEGER, body_start_column INTEGER, body_end_line INTEGER,
9242 body_end_column INTEGER, body_start_byte INTEGER, body_end_byte INTEGER,
9243 body_hash TEXT, semantic_group TEXT, is_test INTEGER, test_container INTEGER
9244 );
9245 INSERT INTO symbols (symbol_id, file_id, path, language, name, kind, signature,
9246 parent_symbol_id, start_line, start_column, end_line, end_column,
9247 start_byte, end_byte, is_test, test_container)
9248 VALUES
9249 ('func', 'f1', 'src/sum.rs', 'rust', 'digest', 'function',
9250 'fn digest()', NULL, 1, 0, 9, 1, 0, 100, 0, 0),
9251 ('local', 'f1', 'src/sum.rs', 'rust', 'checksum', 'variable',
9252 'let checksum', 'func', 2, 4, 2, 30, 10, 40, 0, 0),
9253 ('global', 'f1', 'src/sum.rs', 'rust', 'getChecksum', 'variable',
9254 'const getChecksum', NULL, 20, 0, 20, 30, 210, 240, 0, 0);",
9255 )
9256 .unwrap();
9257 ensure_fts_index(&conn).unwrap();
9258
9259 let rows =
9260 fts_search_symbols_explained(&conn, "checksum", Some("variable"), None, false, 1, true)
9261 .unwrap();
9262
9263 assert_eq!(rows.len(), 1);
9264 assert_eq!(rows[0].symbol.symbol_id, "local");
9265 let explain = rows[0].explain.as_ref().unwrap();
9266 assert_eq!(explain.name_tier, "whole");
9267 assert_eq!(explain.branches, vec!["exact", "name"]);
9268 assert_eq!(explain.candidates, 2);
9269 }
9270
9271 #[test]
9272 fn variable_kind_filter_returns_locals_and_parameters() {
9273 let conn = local_variable_fixture();
9274 ensure_fts_index(&conn).unwrap();
9275
9276 let lookup_ids: Vec<String> =
9277 search_symbols_scoped(&conn, "conn", Some("variable"), None, false, 10)
9278 .unwrap()
9279 .into_iter()
9280 .map(|s| s.symbol_id)
9281 .collect();
9282 assert!(lookup_ids.contains(&"local".to_string()));
9283 assert!(lookup_ids.contains(&"nested".to_string()));
9284
9285 let search_ids: Vec<String> =
9286 fts_search_symbols_scoped(&conn, "conn", Some("variable"), None, false, 10)
9287 .unwrap()
9288 .into_iter()
9289 .map(|r| r.symbol.symbol_id)
9290 .collect();
9291 assert!(search_ids.contains(&"local".to_string()));
9292 }
9293}