1use std::collections::BTreeSet;
2
3use rusqlite::{Connection, params_from_iter};
4use serde::Serialize;
5
6const CALL_EDGE_KINDS: &[&str] = &["calls_name", "constructs"];
7const MACRO_EDGE_KINDS: &[&str] = &["uses_macro"];
8const REFERENCE_EDGE_KINDS: &[&str] =
9 &["references_type", "imports", "exports", "contains", "implements"];
10const OPTIONAL_EDGE_KINDS: &[&str] = &[
11 "calls_name",
12 "constructs",
13 "uses_macro",
14 "references_type",
15 "imports",
16 "exports",
17 "contains",
18 "implements",
19];
20
21#[derive(Debug, Clone, Default)]
22pub struct GraphTraversalOptions {
23 pub include_references: bool,
24 pub include_unresolved: bool,
25 pub include_macros: bool,
26 pub include_common_methods: bool,
27 pub edge_kinds: Option<Vec<String>>,
28 pub resolution_mode: GraphResolutionMode,
29 pub symbol_id: Option<i64>,
30 pub logical_symbol_id: Option<i64>,
31}
32
33#[derive(Debug, Serialize)]
34pub struct GraphTraversalReport {
35 pub query: GraphTraversalQuery,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub logical_symbol: Option<LogicalSymbol>,
38 #[serde(skip_serializing_if = "Vec::is_empty")]
39 pub variants: Vec<LogicalSymbolVariant>,
40 pub summary: GraphTraversalSummary,
41 pub coverage: GraphCoverage,
42 pub results: Vec<GraphHop>,
43}
44
45#[derive(Debug, Serialize)]
46pub struct GraphTraversalQuery {
47 pub tool: String,
48 pub symbol_id: Option<i64>,
49 pub logical_symbol_id: Option<i64>,
50 pub symbol_path: String,
51 pub resolution: String,
52}
53
54#[derive(Debug, Clone, Serialize)]
55pub struct LogicalSymbol {
56 pub logical_symbol_id: i64,
57 pub qualified_name: String,
58 pub variant_count: u64,
59 pub group_reason: String,
60}
61
62#[derive(Debug, Clone, Serialize)]
63pub struct LogicalSymbolVariant {
64 pub symbol_id: i64,
65 pub cfg_expr: Option<String>,
66 pub signature_hash: Option<String>,
67 pub start_line: i64,
68 pub end_line: i64,
69}
70
71#[derive(Debug, Default, Serialize)]
72pub struct GraphTraversalSummary {
73 pub returned_count: u64,
74 pub total_matching_edges: u64,
75 pub truncated: bool,
76 pub exact_verified: u64,
77 pub syntactic: u64,
78 pub name_only: u64,
79 pub ambiguous: u64,
80 pub unresolved: u64,
81 pub false_positive_risk: String,
82 pub completeness_risk: String,
83}
84
85#[derive(Debug, Default, Serialize)]
86pub struct GraphCoverage {
87 pub indexed_files: u64,
88 pub parser_failures: u64,
89 pub stale_files: u64,
90 pub known_index_gaps: Vec<String>,
91 pub parser_coverage_for_paths: Vec<GraphPathCoverage>,
92}
93
94#[derive(Debug, Serialize)]
95pub struct GraphPathCoverage {
96 pub path: String,
97 pub language: String,
98 pub parser_status: String,
99 pub graph_status: String,
100 pub last_indexed_revision: Option<String>,
101}
102
103#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
104pub enum GraphResolutionMode {
105 Exact,
106 #[default]
107 Syntactic,
108 Fuzzy,
109}
110
111impl GraphResolutionMode {
112 pub fn parse(value: Option<&str>) -> anyhow::Result<Self> {
113 match value.unwrap_or("syntactic") {
114 "exact" => Ok(Self::Exact),
115 "syntactic" => Ok(Self::Syntactic),
116 "fuzzy" => Ok(Self::Fuzzy),
117 other => anyhow::bail!(
118 "unknown graph resolution mode `{other}`; expected exact, syntactic, or fuzzy"
119 ),
120 }
121 }
122
123 pub fn as_str(self) -> &'static str {
124 match self {
125 Self::Exact => "exact",
126 Self::Syntactic => "syntactic",
127 Self::Fuzzy => "fuzzy",
128 }
129 }
130}
131
132impl GraphTraversalOptions {
133 pub fn callee_edge_kinds(&self) -> anyhow::Result<Vec<String>> {
134 if let Some(edge_kinds) = &self.edge_kinds {
135 validate_edge_kinds(edge_kinds)?;
136 return Ok(edge_kinds.clone());
137 }
138 let mut edge_kinds =
139 CALL_EDGE_KINDS.iter().map(|value| (*value).to_string()).collect::<Vec<_>>();
140 if self.include_macros {
141 edge_kinds.extend(MACRO_EDGE_KINDS.iter().map(|value| (*value).to_string()));
142 }
143 if self.include_references {
144 edge_kinds.extend(REFERENCE_EDGE_KINDS.iter().map(|value| (*value).to_string()));
145 }
146 Ok(edge_kinds)
147 }
148
149 pub fn caller_edge_kinds(&self) -> anyhow::Result<Vec<String>> {
150 self.callee_edge_kinds()
151 }
152}
153
154#[derive(Debug, Serialize)]
155pub struct CompareGraphTextReport {
156 pub query: CompareGraphTextQuery,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub logical_symbol: Option<LogicalSymbol>,
159 #[serde(skip_serializing_if = "Vec::is_empty")]
160 pub variants: Vec<LogicalSymbolVariant>,
161 pub summary: CompareGraphTextSummary,
162 pub coverage: GraphCoverage,
163 pub matched_hits: Vec<MatchedGraphTextHit>,
164 pub text_only_hits: Vec<TextOnlyHit>,
165 pub graph_only_edges: Vec<GraphOnlyEdge>,
166 pub likely_parser_gaps: Vec<TextOnlyHit>,
167 pub likely_false_positives: Vec<GraphOnlyEdge>,
168}
169
170#[derive(Debug, Serialize)]
171pub struct CompareGraphTextQuery {
172 pub symbol_id: Option<i64>,
173 pub logical_symbol_id: Option<i64>,
174 pub symbol_path: String,
175 pub pattern: String,
176 pub resolution: String,
177 pub include_tests: bool,
178}
179
180#[derive(Debug, Default, Serialize)]
181pub struct CompareGraphTextSummary {
182 pub graph_hits: u64,
183 pub graph_edges: u64,
184 pub text_hits: u64,
185 pub matched: u64,
186 pub graph_only: u64,
187 pub text_only: u64,
188 pub text_mentions: u64,
189 pub likely_parser_gaps: u64,
190 pub likely_false_positives: u64,
191 pub likely_index_gaps: u64,
192 pub complete: bool,
193 pub recommended_fallback: String,
194 pub pattern_match_mode: String,
195 pub warnings: Vec<String>,
196}
197
198#[derive(Debug, Serialize)]
199pub struct MatchedGraphTextHit {
200 pub path: String,
201 pub line: i64,
202 pub text: String,
203 pub target: Option<String>,
204 pub edge_kind: String,
205 pub confidence: String,
206 pub resolution: String,
207}
208
209#[derive(Debug, Clone, Serialize)]
210pub struct TextOnlyHit {
211 pub path: String,
212 pub line: i64,
213 pub text: String,
214 pub reason: String,
215 pub likely_gap: String,
216}
217
218#[derive(Debug, Clone, Serialize)]
219pub struct GraphOnlyEdge {
220 pub path: String,
221 pub line: i64,
222 pub target: Option<String>,
223 pub edge_kind: String,
224 pub confidence: String,
225 pub resolution: String,
226 pub evidence: Option<String>,
227 pub reason: String,
228 pub likely_reason: String,
229}
230
231#[derive(Debug, Clone, Serialize)]
232pub struct GraphHop {
233 pub edge_id: i64,
234 pub from_symbol: Option<String>,
235 pub to_symbol: Option<String>,
236 pub edge_kind: String,
237 pub confidence: String,
238 pub edge_confidence: String,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub target: Option<String>,
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub target_qualified_name: Option<String>,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub evidence: Option<String>,
245 #[serde(skip_serializing_if = "Option::is_none")]
246 pub receiver_hint: Option<String>,
247 pub resolution: String,
248 pub verified_target_symbol: bool,
249 pub shown_by_default: bool,
250 #[serde(skip_serializing_if = "Option::is_none")]
251 pub callsite: Option<Callsite>,
252}
253
254#[derive(Debug, Clone, Serialize)]
255pub struct Callsite {
256 pub path: String,
257 pub line: i64,
258 pub span: [i64; 2],
259}
260
261pub fn traverse(
262 conn: &Connection,
263 symbol: &str,
264 reverse: bool,
265 limit: u32,
266) -> anyhow::Result<Vec<GraphHop>> {
267 traverse_with_options(conn, symbol, reverse, limit, &GraphTraversalOptions::default())
268}
269
270pub fn traverse_with_options(
271 conn: &Connection,
272 symbol: &str,
273 reverse: bool,
274 limit: u32,
275 options: &GraphTraversalOptions,
276) -> anyhow::Result<Vec<GraphHop>> {
277 let edge_kinds =
278 if reverse { options.caller_edge_kinds()? } else { options.callee_edge_kinds()? };
279 let quoted = quoted_placeholders(edge_kinds.len());
280 let unique_short_name = unique_symbol_name(conn, short_name(symbol))?;
281 let mode = options.resolution_mode;
282 let sql = if reverse {
283 let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
284 let tier = reverse_tier(mode);
285 format!(
286 "
287 SELECT COALESCE(from_symbols.qualified_name, edges.from_name) AS from_symbol,
288 COALESCE(to_symbols.qualified_name, edges.to_name) AS to_symbol,
289 edges.id AS edge_id,
290 edges.edge_kind AS edge_kind,
291 edges.confidence AS confidence,
292 edges.to_name AS target,
293 edges.target_qualified_name AS target_qualified_name,
294 edges.evidence AS evidence,
295 edges.receiver_hint AS receiver_hint,
296 edges.resolution AS edge_resolution,
297 edges.to_symbol_id IS NOT NULL AS verified_target_symbol,
298 source_files.path AS callsite_path,
299 COALESCE(NULLIF(edges.source_start_line, 0), 1) AS callsite_start_line,
300 COALESCE(NULLIF(edges.source_end_line, 0), NULLIF(edges.source_start_line, 0), 1) AS callsite_end_line,
301 {tier} AS match_tier
302 FROM edges
303 JOIN files source_files ON source_files.id = edges.source_file_id
304 LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
305 LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
306 WHERE edges.edge_kind IN ({quoted})
307 AND ({predicate})
308 ORDER BY match_tier,
309 CASE edges.confidence
310 WHEN 'Exact' THEN 0
311 WHEN 'Syntactic' THEN 1
312 WHEN 'NameOnly' THEN 2
313 ELSE 3
314 END,
315 edges.edge_kind,
316 edges.from_name
317 LIMIT ?5
318 "
319 )
320 } else {
321 let predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
322 let target_filter = forward_target_filter(mode, options);
323 let visibility_filter = forward_visibility_filter(options);
324 format!(
325 "
326 SELECT COALESCE(from_symbols.qualified_name, edges.from_name) AS from_symbol,
327 COALESCE(to_symbols.qualified_name, edges.to_name) AS to_symbol,
328 edges.id AS edge_id,
329 edges.edge_kind AS edge_kind,
330 edges.confidence AS confidence,
331 edges.to_name AS target,
332 edges.target_qualified_name AS target_qualified_name,
333 edges.evidence AS evidence,
334 edges.receiver_hint AS receiver_hint,
335 edges.resolution AS edge_resolution,
336 edges.to_symbol_id IS NOT NULL AS verified_target_symbol,
337 source_files.path AS callsite_path,
338 COALESCE(NULLIF(edges.source_start_line, 0), 1) AS callsite_start_line,
339 COALESCE(NULLIF(edges.source_end_line, 0), NULLIF(edges.source_start_line, 0), 1) AS callsite_end_line,
340 0 AS match_tier
341 FROM edges
342 JOIN files source_files ON source_files.id = edges.source_file_id
343 LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
344 LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
345 WHERE edges.edge_kind IN ({quoted})
346 AND ({predicate})
347 AND ({target_filter})
348 AND ({visibility_filter})
349 AND ?4 IN ('true', 'false')
350 ORDER BY
351 CASE edges.confidence
352 WHEN 'Exact' THEN 0
353 WHEN 'Syntactic' THEN 1
354 WHEN 'NameOnly' THEN 2
355 ELSE 3
356 END,
357 edges.edge_kind,
358 edges.to_name
359 LIMIT ?5
360 "
361 )
362 };
363 let params = traversal_params(
364 symbol,
365 limit,
366 &edge_kinds,
367 options.symbol_id,
368 options.logical_symbol_id,
369 unique_short_name,
370 );
371 let mut stmt = conn.prepare(&sql)?;
372 let rows = stmt.query_map(params_from_iter(params), |row| {
373 let edge_kind: String = row.get("edge_kind")?;
374 let confidence: String = row.get("confidence")?;
375 let verified_target_symbol = row.get("verified_target_symbol")?;
376 let resolution = resolution_label(
377 mode,
378 row.get::<_, String>("edge_resolution")?,
379 row.get("match_tier")?,
380 verified_target_symbol,
381 );
382 let callsite_path: String = row.get("callsite_path")?;
383 let callsite_start = row.get("callsite_start_line")?;
384 let callsite_end = row.get("callsite_end_line")?;
385 Ok(GraphHop {
386 edge_id: row.get("edge_id")?,
387 from_symbol: row.get("from_symbol")?,
388 to_symbol: row.get("to_symbol")?,
389 edge_kind: edge_kind.clone(),
390 confidence: confidence.clone(),
391 edge_confidence: confidence,
392 target: row.get("target")?,
393 target_qualified_name: row.get("target_qualified_name")?,
394 evidence: row.get("evidence")?,
395 receiver_hint: row.get("receiver_hint")?,
396 resolution,
397 verified_target_symbol,
398 shown_by_default: CALL_EDGE_KINDS.contains(&edge_kind.as_str()),
399 callsite: Some(Callsite {
400 path: callsite_path,
401 line: callsite_start,
402 span: [callsite_start, callsite_end],
403 }),
404 })
405 })?;
406 let mut hops = Vec::new();
407 for row in rows {
408 hops.push(row?);
409 }
410 dedupe_hops(&mut hops);
411 Ok(hops)
412}
413
414fn dedupe_hops(hops: &mut Vec<GraphHop>) {
415 let mut seen = BTreeSet::new();
416 hops.retain(|hop| {
417 let callsite = hop.callsite.as_ref();
418 seen.insert((
419 hop.from_symbol.clone(),
420 hop.to_symbol.clone(),
421 hop.edge_id,
422 hop.edge_kind.clone(),
423 hop.target.clone(),
424 hop.target_qualified_name.clone(),
425 hop.receiver_hint.clone(),
426 callsite.map(|value| value.path.clone()),
427 callsite.map(|value| value.span),
428 ))
429 });
430}
431
432pub fn traversal_summary(
433 conn: &Connection,
434 symbol: &str,
435 reverse: bool,
436 limit: u32,
437 options: &GraphTraversalOptions,
438 returned_count: usize,
439) -> anyhow::Result<GraphTraversalSummary> {
440 let edge_kinds =
441 if reverse { options.caller_edge_kinds()? } else { options.callee_edge_kinds()? };
442 let quoted = quoted_placeholders(edge_kinds.len());
443 let unique_short_name = unique_symbol_name(conn, short_name(symbol))?;
444 let mode = options.resolution_mode;
445 let sql = if reverse {
446 let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
447 format!(
448 "
449 SELECT
450 COUNT(*),
451 SUM(CASE WHEN edges.to_symbol_id IS NOT NULL THEN 1 ELSE 0 END),
452 SUM(CASE WHEN edges.confidence = 'Syntactic' THEN 1 ELSE 0 END),
453 SUM(CASE WHEN edges.confidence = 'NameOnly' THEN 1 ELSE 0 END),
454 SUM(CASE WHEN edges.confidence = 'Ambiguous' THEN 1 ELSE 0 END),
455 SUM(CASE WHEN edges.to_symbol_id IS NULL THEN 1 ELSE 0 END)
456 FROM edges
457 LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
458 WHERE edges.edge_kind IN ({quoted})
459 AND ({predicate})
460 "
461 )
462 } else {
463 let predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
464 let target_filter = forward_target_filter(mode, options);
465 let visibility_filter = forward_visibility_filter(options);
466 format!(
467 "
468 SELECT
469 COUNT(*),
470 SUM(CASE WHEN edges.to_symbol_id IS NOT NULL THEN 1 ELSE 0 END),
471 SUM(CASE WHEN edges.confidence = 'Syntactic' THEN 1 ELSE 0 END),
472 SUM(CASE WHEN edges.confidence = 'NameOnly' THEN 1 ELSE 0 END),
473 SUM(CASE WHEN edges.confidence = 'Ambiguous' THEN 1 ELSE 0 END),
474 SUM(CASE WHEN edges.to_symbol_id IS NULL THEN 1 ELSE 0 END)
475 FROM edges
476 LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
477 WHERE edges.edge_kind IN ({quoted})
478 AND ({predicate})
479 AND ({target_filter})
480 AND ({visibility_filter})
481 AND ?4 IN ('true', 'false')
482 "
483 )
484 };
485 let params = traversal_params(
486 symbol,
487 limit,
488 &edge_kinds,
489 options.symbol_id,
490 options.logical_symbol_id,
491 unique_short_name,
492 );
493 let mut summary = conn.query_row(&sql, params_from_iter(params), |row| {
494 Ok(GraphTraversalSummary {
495 returned_count: u64::try_from(returned_count).unwrap_or(u64::MAX),
496 total_matching_edges: count_col(row, 0)?,
497 truncated: false,
498 exact_verified: count_col(row, 1)?,
499 syntactic: count_col(row, 2)?,
500 name_only: count_col(row, 3)?,
501 ambiguous: count_col(row, 4)?,
502 unresolved: count_col(row, 5)?,
503 false_positive_risk: String::new(),
504 completeness_risk: String::new(),
505 })
506 })?;
507 let hidden_unresolved = hidden_unresolved_candidate_count(
508 conn,
509 symbol,
510 reverse,
511 &edge_kinds,
512 options,
513 unique_short_name,
514 )?;
515 summary.total_matching_edges = summary.total_matching_edges.saturating_add(hidden_unresolved);
516 summary.unresolved = summary.unresolved.saturating_add(hidden_unresolved);
517 summary.truncated = summary.total_matching_edges > u64::from(limit);
518 summary.false_positive_risk = false_positive_risk(&summary, mode).to_string();
519 summary.completeness_risk = completeness_risk(&summary).to_string();
520 Ok(summary)
521}
522
523fn count_col(row: &rusqlite::Row<'_>, index: usize) -> rusqlite::Result<u64> {
524 let value = row.get::<_, Option<i64>>(index)?.unwrap_or(0);
525 Ok(u64::try_from(value).unwrap_or(0))
526}
527
528fn false_positive_risk(summary: &GraphTraversalSummary, mode: GraphResolutionMode) -> &'static str {
529 if summary.ambiguous > 0 || mode == GraphResolutionMode::Fuzzy {
530 "high"
531 } else if summary.name_only > 0
532 || summary.unresolved > 0
533 || mode == GraphResolutionMode::Syntactic
534 {
535 "medium"
536 } else {
537 "low"
538 }
539}
540
541fn completeness_risk(summary: &GraphTraversalSummary) -> &'static str {
542 if summary.truncated
543 || summary.unresolved > summary.exact_verified.saturating_add(summary.syntactic)
544 {
545 "high"
546 } else if summary.unresolved > 0 || summary.name_only > 0 || summary.ambiguous > 0 {
547 "medium"
548 } else {
549 "low"
550 }
551}
552
553fn hidden_unresolved_candidate_count(
554 conn: &Connection,
555 symbol: &str,
556 reverse: bool,
557 edge_kinds: &[String],
558 options: &GraphTraversalOptions,
559 unique_short_name: bool,
560) -> anyhow::Result<u64> {
561 let mode = options.resolution_mode;
562 let quoted = quoted_placeholders(edge_kinds.len());
563 let sql = if reverse {
564 let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
565 format!(
566 "
567 SELECT COUNT(*)
568 FROM edges
569 LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
570 WHERE edges.edge_kind IN ({quoted})
571 AND edges.to_symbol_id IS NULL
572 AND NOT ({predicate})
573 AND (
574 edges.target_qualified_name = ?1
575 OR edges.target_qualified_name LIKE ?2
576 OR edges.to_name = ?3
577 )
578 "
579 )
580 } else {
581 let source_predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
582 let target_filter = forward_target_filter(mode, options);
583 let visibility_filter = forward_visibility_filter(options);
584 format!(
585 "
586 SELECT COUNT(*)
587 FROM edges
588 LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
589 WHERE edges.edge_kind IN ({quoted})
590 AND ({source_predicate})
591 AND edges.to_symbol_id IS NULL
592 AND NOT (({target_filter}) AND ({visibility_filter}))
593 AND ?4 IN ('true', 'false')
594 "
595 )
596 };
597 let params = traversal_params(
598 symbol,
599 0,
600 edge_kinds,
601 options.symbol_id,
602 options.logical_symbol_id,
603 unique_short_name,
604 );
605 let count = conn.query_row(&sql, params_from_iter(params), |row| count_col(row, 0))?;
606 Ok(count)
607}
608
609fn validate_edge_kinds(edge_kinds: &[String]) -> anyhow::Result<()> {
610 for edge_kind in edge_kinds {
611 if !OPTIONAL_EDGE_KINDS.contains(&edge_kind.as_str()) {
612 anyhow::bail!("unknown graph edge kind `{edge_kind}`");
613 }
614 }
615 Ok(())
616}
617
618fn traversal_params(
619 symbol: &str,
620 limit: u32,
621 edge_kinds: &[String],
622 symbol_id: Option<i64>,
623 logical_symbol_id: Option<i64>,
624 unique_short_name: bool,
625) -> Vec<String> {
626 let qualified = symbol.to_string();
627 let short = short_name(symbol).to_string();
628 let fuzzy_qualified = format!("%::{qualified}");
629 let allow_name_fallback = (!is_qualified_symbol(symbol)).to_string();
630 let mut params = vec![
631 qualified,
632 fuzzy_qualified,
633 short,
634 allow_name_fallback,
635 limit.to_string(),
636 symbol_id.unwrap_or(-1).to_string(),
637 unique_short_name.to_string(),
638 logical_symbol_id.unwrap_or(-1).to_string(),
639 ];
640 params.extend(edge_kinds.iter().cloned());
641 params
642}
643
644fn quoted_placeholders(count: usize) -> String {
645 (0..count).map(|index| format!("?{}", index + 9)).collect::<Vec<_>>().join(", ")
646}
647
648fn reverse_predicate(mode: GraphResolutionMode, logical: bool) -> &'static str {
649 if logical {
650 return match mode {
651 GraphResolutionMode::Exact => {
652 "edges.to_symbol_id IS NOT NULL
653 AND edges.to_symbol_id IN (
654 SELECT symbol_id
655 FROM logical_symbol_members
656 WHERE logical_symbol_id = ?8
657 )"
658 },
659 GraphResolutionMode::Syntactic => {
660 "(edges.to_symbol_id IN (
661 SELECT symbol_id
662 FROM logical_symbol_members
663 WHERE logical_symbol_id = ?8
664 )
665 OR edges.target_qualified_name = ?1)"
666 },
667 GraphResolutionMode::Fuzzy => {
668 "edges.to_symbol_id IN (
669 SELECT symbol_id
670 FROM logical_symbol_members
671 WHERE logical_symbol_id = ?8
672 )
673 OR to_symbols.name = ?3
674 OR to_symbols.qualified_name = ?1
675 OR to_symbols.qualified_name LIKE ?2
676 OR edges.target_qualified_name = ?1
677 OR edges.target_qualified_name LIKE ?2
678 OR (?4 = 'true' AND edges.to_name = ?3)"
679 },
680 };
681 }
682 match mode {
683 GraphResolutionMode::Exact => {
684 "edges.to_symbol_id IS NOT NULL
685 AND (edges.to_symbol_id = ?6 OR to_symbols.qualified_name = ?1)"
686 },
687 GraphResolutionMode::Syntactic => {
688 "(edges.to_symbol_id = ?6
689 OR to_symbols.qualified_name = ?1
690 OR (?7 = 'true' AND to_symbols.name = ?3)
691 OR edges.target_qualified_name = ?1)"
692 },
693 GraphResolutionMode::Fuzzy => {
694 "to_symbols.name = ?3
695 OR to_symbols.qualified_name = ?1
696 OR to_symbols.qualified_name LIKE ?2
697 OR edges.target_qualified_name = ?1
698 OR edges.target_qualified_name LIKE ?2
699 OR (?4 = 'true' AND edges.to_name = ?3)"
700 },
701 }
702}
703
704fn reverse_tier(mode: GraphResolutionMode) -> &'static str {
705 match mode {
706 GraphResolutionMode::Exact => "0",
707 GraphResolutionMode::Syntactic => {
708 "CASE
709 WHEN edges.to_symbol_id IS NOT NULL THEN 0
710 WHEN edges.target_qualified_name = ?1 THEN 1
711 ELSE 4
712 END"
713 },
714 GraphResolutionMode::Fuzzy => {
715 "CASE
716 WHEN edges.to_symbol_id IS NOT NULL THEN 0
717 WHEN edges.target_qualified_name = ?1 OR edges.target_qualified_name LIKE ?2 THEN 1
718 WHEN ?4 = 'true' AND edges.to_name = ?3 THEN 2
719 ELSE 4
720 END"
721 },
722 }
723}
724
725fn forward_source_predicate(mode: GraphResolutionMode, logical: bool) -> &'static str {
726 if logical {
727 return match mode {
728 GraphResolutionMode::Exact => {
729 "from_symbols.id IS NOT NULL
730 AND from_symbols.id IN (
731 SELECT symbol_id
732 FROM logical_symbol_members
733 WHERE logical_symbol_id = ?8
734 )"
735 },
736 GraphResolutionMode::Syntactic => {
737 "from_symbols.id IN (
738 SELECT symbol_id
739 FROM logical_symbol_members
740 WHERE logical_symbol_id = ?8
741 )
742 OR edges.from_name = ?1"
743 },
744 GraphResolutionMode::Fuzzy => {
745 "from_symbols.id IN (
746 SELECT symbol_id
747 FROM logical_symbol_members
748 WHERE logical_symbol_id = ?8
749 )
750 OR from_symbols.name = ?3
751 OR from_symbols.qualified_name = ?1
752 OR from_symbols.qualified_name LIKE ?2
753 OR edges.from_name = ?1
754 OR edges.from_name LIKE ?2"
755 },
756 };
757 }
758 match mode {
759 GraphResolutionMode::Exact => {
760 "from_symbols.id IS NOT NULL
761 AND (from_symbols.id = ?6 OR from_symbols.qualified_name = ?1)"
762 },
763 GraphResolutionMode::Syntactic => {
764 "from_symbols.id = ?6
765 OR from_symbols.qualified_name = ?1
766 OR (?7 = 'true' AND from_symbols.name = ?3)
767 OR edges.from_name = ?1"
768 },
769 GraphResolutionMode::Fuzzy => {
770 "from_symbols.name = ?3
771 OR from_symbols.qualified_name = ?1
772 OR from_symbols.qualified_name LIKE ?2
773 OR edges.from_name = ?1
774 OR edges.from_name LIKE ?2"
775 },
776 }
777}
778
779fn forward_target_filter(
780 mode: GraphResolutionMode,
781 options: &GraphTraversalOptions,
782) -> &'static str {
783 match mode {
784 GraphResolutionMode::Exact => "edges.to_symbol_id IS NOT NULL",
785 GraphResolutionMode::Syntactic => {
786 if options.include_unresolved {
787 "1 = 1"
788 } else if options.include_macros {
789 "
790 edges.to_symbol_id IS NOT NULL
791 OR edges.target_qualified_name IS NOT NULL
792 OR edges.edge_kind = 'uses_macro'
793 "
794 } else {
795 "edges.to_symbol_id IS NOT NULL OR edges.target_qualified_name IS NOT NULL"
796 }
797 },
798 GraphResolutionMode::Fuzzy => "1 = 1",
799 }
800}
801
802fn forward_visibility_filter(options: &GraphTraversalOptions) -> &'static str {
803 match (
804 options.include_unresolved,
805 options.include_macros,
806 options.include_common_methods,
807 ) {
808 (true, true, true) => "1 = 1",
809 (true, true, false) => {
810 "
811 (
812 edges.edge_kind != 'calls_name'
813 OR edges.to_name NOT IN (
814 'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
815 'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
816 'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
817 'expect', 'ok', 'err'
818 )
819 OR edges.to_symbol_id IS NOT NULL
820 )
821 "
822 },
823 (true, false, true) => "edges.edge_kind != 'uses_macro'",
824 (true, false, false) => {
825 "
826 edges.edge_kind != 'uses_macro'
827 AND (
828 edges.edge_kind != 'calls_name'
829 OR edges.to_name NOT IN (
830 'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
831 'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
832 'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
833 'expect', 'ok', 'err'
834 )
835 OR edges.to_symbol_id IS NOT NULL
836 )
837 "
838 },
839 (false, true, true) => {
840 "
841 (
842 edges.edge_kind = 'calls_name'
843 AND (
844 edges.to_symbol_id IS NOT NULL
845 OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
846 )
847 )
848 OR (
849 edges.edge_kind = 'constructs'
850 AND edges.to_symbol_id IS NOT NULL
851 )
852 OR edges.edge_kind = 'uses_macro'
853 OR edges.edge_kind NOT IN ('calls_name', 'constructs')
854 "
855 },
856 (false, true, false) => {
857 "
858 (
859 edges.edge_kind = 'calls_name'
860 AND (
861 edges.to_symbol_id IS NOT NULL
862 OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
863 )
864 AND (
865 edges.to_name NOT IN (
866 'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
867 'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
868 'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
869 'expect', 'ok', 'err'
870 )
871 OR edges.to_symbol_id IS NOT NULL
872 )
873 )
874 OR (
875 edges.edge_kind = 'constructs'
876 AND edges.to_symbol_id IS NOT NULL
877 )
878 OR edges.edge_kind = 'uses_macro'
879 OR edges.edge_kind NOT IN ('calls_name', 'constructs')
880 "
881 },
882 (false, false, true) => {
883 "
884 edges.edge_kind != 'uses_macro'
885 AND (
886 (
887 edges.edge_kind = 'calls_name'
888 AND (
889 edges.to_symbol_id IS NOT NULL
890 OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
891 )
892 )
893 OR (
894 edges.edge_kind = 'constructs'
895 AND edges.to_symbol_id IS NOT NULL
896 )
897 OR edges.edge_kind NOT IN ('calls_name', 'constructs')
898 )
899 "
900 },
901 (false, false, false) => {
902 "
903 edges.edge_kind != 'uses_macro'
904 AND (
905 (
906 edges.edge_kind = 'calls_name'
907 AND (
908 edges.to_symbol_id IS NOT NULL
909 OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
910 )
911 AND (
912 edges.to_name NOT IN (
913 'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
914 'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
915 'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
916 'expect', 'ok', 'err'
917 )
918 OR edges.to_symbol_id IS NOT NULL
919 )
920 )
921 OR (
922 edges.edge_kind = 'constructs'
923 AND edges.to_symbol_id IS NOT NULL
924 )
925 OR edges.edge_kind NOT IN ('calls_name', 'constructs')
926 )
927 "
928 },
929 }
930}
931
932fn unique_symbol_name(conn: &Connection, name: &str) -> anyhow::Result<bool> {
933 let count: i64 = conn.query_row(
934 "SELECT COUNT(*) AS symbol_count FROM symbols WHERE name = ?1",
935 [name],
936 |row| row.get("symbol_count"),
937 )?;
938 Ok(count == 1)
939}
940
941fn resolution_label(
942 mode: GraphResolutionMode,
943 stored: String,
944 tier: i64,
945 verified_target_symbol: bool,
946) -> String {
947 if mode == GraphResolutionMode::Exact && verified_target_symbol {
948 return "exact".to_string();
949 }
950 if stored != "unresolved" {
951 return stored;
952 }
953 match tier {
954 1 => "target_qualified_suffix".to_string(),
955 2 => "target_name_fallback".to_string(),
956 _ => stored,
957 }
958}
959
960fn short_name(symbol: &str) -> &str {
961 symbol.rsplit([':', '.', '#', '/']).find(|part| !part.is_empty()).unwrap_or(symbol)
962}
963
964fn is_qualified_symbol(symbol: &str) -> bool {
965 symbol.contains("::")
966 || symbol.contains(".rs:")
967 || symbol.contains(".ts:")
968 || symbol.contains(".tsx:")
969 || symbol.contains(".kt:")
970 || symbol.contains('/')
971}