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 let confidence = normalize_confidence(&confidence).to_string();
386 Ok(GraphHop {
387 edge_id: row.get("edge_id")?,
388 from_symbol: row.get("from_symbol")?,
389 to_symbol: row.get("to_symbol")?,
390 edge_kind: edge_kind.clone(),
391 confidence: confidence.clone(),
392 edge_confidence: confidence,
393 target: row.get("target")?,
394 target_qualified_name: row.get("target_qualified_name")?,
395 evidence: row.get("evidence")?,
396 receiver_hint: row.get("receiver_hint")?,
397 resolution,
398 verified_target_symbol,
399 shown_by_default: CALL_EDGE_KINDS.contains(&edge_kind.as_str()),
400 callsite: Some(Callsite {
401 path: callsite_path,
402 line: callsite_start,
403 span: [callsite_start, callsite_end],
404 }),
405 })
406 })?;
407 let mut hops = Vec::new();
408 for row in rows {
409 hops.push(row?);
410 }
411 dedupe_hops(&mut hops);
412 Ok(hops)
413}
414
415fn dedupe_hops(hops: &mut Vec<GraphHop>) {
416 let mut seen = BTreeSet::new();
417 hops.retain(|hop| {
418 let callsite = hop.callsite.as_ref();
419 seen.insert((
420 hop.from_symbol.clone(),
421 hop.to_symbol.clone(),
422 hop.edge_id,
423 hop.edge_kind.clone(),
424 hop.target.clone(),
425 hop.target_qualified_name.clone(),
426 hop.receiver_hint.clone(),
427 callsite.map(|value| value.path.clone()),
428 callsite.map(|value| value.span),
429 ))
430 });
431}
432
433pub fn traversal_summary(
434 conn: &Connection,
435 symbol: &str,
436 reverse: bool,
437 limit: u32,
438 options: &GraphTraversalOptions,
439 returned_count: usize,
440) -> anyhow::Result<GraphTraversalSummary> {
441 let edge_kinds =
442 if reverse { options.caller_edge_kinds()? } else { options.callee_edge_kinds()? };
443 let quoted = quoted_placeholders(edge_kinds.len());
444 let unique_short_name = unique_symbol_name(conn, short_name(symbol))?;
445 let mode = options.resolution_mode;
446 let sql = if reverse {
447 let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
448 format!(
449 "
450 SELECT
451 COUNT(*),
452 SUM(CASE WHEN edges.to_symbol_id IS NOT NULL THEN 1 ELSE 0 END),
453 SUM(CASE WHEN edges.confidence = 'Syntactic' THEN 1 ELSE 0 END),
454 SUM(CASE WHEN edges.confidence = 'NameOnly' THEN 1 ELSE 0 END),
455 SUM(CASE WHEN edges.confidence = 'Ambiguous' THEN 1 ELSE 0 END),
456 SUM(CASE WHEN edges.to_symbol_id IS NULL THEN 1 ELSE 0 END)
457 FROM edges
458 LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
459 WHERE edges.edge_kind IN ({quoted})
460 AND ({predicate})
461 "
462 )
463 } else {
464 let predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
465 let target_filter = forward_target_filter(mode, options);
466 let visibility_filter = forward_visibility_filter(options);
467 format!(
468 "
469 SELECT
470 COUNT(*),
471 SUM(CASE WHEN edges.to_symbol_id IS NOT NULL THEN 1 ELSE 0 END),
472 SUM(CASE WHEN edges.confidence = 'Syntactic' THEN 1 ELSE 0 END),
473 SUM(CASE WHEN edges.confidence = 'NameOnly' THEN 1 ELSE 0 END),
474 SUM(CASE WHEN edges.confidence = 'Ambiguous' THEN 1 ELSE 0 END),
475 SUM(CASE WHEN edges.to_symbol_id IS NULL THEN 1 ELSE 0 END)
476 FROM edges
477 LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
478 WHERE edges.edge_kind IN ({quoted})
479 AND ({predicate})
480 AND ({target_filter})
481 AND ({visibility_filter})
482 AND ?4 IN ('true', 'false')
483 "
484 )
485 };
486 let params = traversal_params(
487 symbol,
488 limit,
489 &edge_kinds,
490 options.symbol_id,
491 options.logical_symbol_id,
492 unique_short_name,
493 );
494 let mut summary = conn.query_row(&sql, params_from_iter(params), |row| {
495 Ok(GraphTraversalSummary {
496 returned_count: u64::try_from(returned_count).unwrap_or(u64::MAX),
497 total_matching_edges: count_col(row, 0)?,
498 truncated: false,
499 exact_verified: count_col(row, 1)?,
500 syntactic: count_col(row, 2)?,
501 name_only: count_col(row, 3)?,
502 ambiguous: count_col(row, 4)?,
503 unresolved: count_col(row, 5)?,
504 false_positive_risk: String::new(),
505 completeness_risk: String::new(),
506 })
507 })?;
508 let hidden_unresolved = hidden_unresolved_candidate_count(
509 conn,
510 symbol,
511 reverse,
512 &edge_kinds,
513 options,
514 unique_short_name,
515 )?;
516 summary.total_matching_edges = summary.total_matching_edges.saturating_add(hidden_unresolved);
517 summary.unresolved = summary.unresolved.saturating_add(hidden_unresolved);
518 summary.truncated = summary.total_matching_edges > u64::from(limit);
519 summary.false_positive_risk = false_positive_risk(&summary, mode).to_string();
520 summary.completeness_risk = completeness_risk(&summary).to_string();
521 Ok(summary)
522}
523
524fn count_col(row: &rusqlite::Row<'_>, index: usize) -> rusqlite::Result<u64> {
525 let value = row.get::<_, Option<i64>>(index)?.unwrap_or(0);
526 Ok(u64::try_from(value).unwrap_or(0))
527}
528
529pub(crate) fn normalize_confidence(value: &str) -> &'static str {
533 match value {
534 "Exact" => "exact",
535 "Syntactic" => "syntactic",
536 "NameOnly" => "name_only",
537 "Ambiguous" => "ambiguous",
538 _ => "name_only",
539 }
540}
541
542fn false_positive_risk(summary: &GraphTraversalSummary, mode: GraphResolutionMode) -> &'static str {
543 let has_unverified = summary.exact_verified < summary.total_matching_edges;
548 if summary.ambiguous > 0 || mode == GraphResolutionMode::Fuzzy {
549 "high"
550 } else if summary.name_only > 0
551 || summary.unresolved > 0
552 || (mode == GraphResolutionMode::Syntactic && has_unverified)
553 {
554 "medium"
555 } else {
556 "low"
557 }
558}
559
560fn completeness_risk(summary: &GraphTraversalSummary) -> &'static str {
561 if summary.truncated
562 || summary.unresolved > summary.exact_verified.saturating_add(summary.syntactic)
563 {
564 "high"
565 } else if summary.unresolved > 0 || summary.name_only > 0 || summary.ambiguous > 0 {
566 "medium"
567 } else {
568 "low"
569 }
570}
571
572fn hidden_unresolved_candidate_count(
573 conn: &Connection,
574 symbol: &str,
575 reverse: bool,
576 edge_kinds: &[String],
577 options: &GraphTraversalOptions,
578 unique_short_name: bool,
579) -> anyhow::Result<u64> {
580 let mode = options.resolution_mode;
581 let quoted = quoted_placeholders(edge_kinds.len());
582 let sql = if reverse {
583 let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
584 format!(
585 "
586 SELECT COUNT(*)
587 FROM edges
588 LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
589 WHERE edges.edge_kind IN ({quoted})
590 AND edges.to_symbol_id IS NULL
591 AND NOT ({predicate})
592 AND (
593 edges.target_qualified_name = ?1
594 OR edges.target_qualified_name LIKE ?2
595 OR edges.to_name = ?3
596 )
597 "
598 )
599 } else {
600 let source_predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
601 let target_filter = forward_target_filter(mode, options);
602 let visibility_filter = forward_visibility_filter(options);
603 format!(
604 "
605 SELECT COUNT(*)
606 FROM edges
607 LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
608 WHERE edges.edge_kind IN ({quoted})
609 AND ({source_predicate})
610 AND edges.to_symbol_id IS NULL
611 AND NOT (({target_filter}) AND ({visibility_filter}))
612 AND ?4 IN ('true', 'false')
613 "
614 )
615 };
616 let params = traversal_params(
617 symbol,
618 0,
619 edge_kinds,
620 options.symbol_id,
621 options.logical_symbol_id,
622 unique_short_name,
623 );
624 let count = conn.query_row(&sql, params_from_iter(params), |row| count_col(row, 0))?;
625 Ok(count)
626}
627
628fn validate_edge_kinds(edge_kinds: &[String]) -> anyhow::Result<()> {
629 for edge_kind in edge_kinds {
630 if !OPTIONAL_EDGE_KINDS.contains(&edge_kind.as_str()) {
631 anyhow::bail!("unknown graph edge kind `{edge_kind}`");
632 }
633 }
634 Ok(())
635}
636
637fn traversal_params(
638 symbol: &str,
639 limit: u32,
640 edge_kinds: &[String],
641 symbol_id: Option<i64>,
642 logical_symbol_id: Option<i64>,
643 unique_short_name: bool,
644) -> Vec<String> {
645 let qualified = symbol.to_string();
646 let short = short_name(symbol).to_string();
647 let fuzzy_qualified = format!("%::{qualified}");
648 let allow_name_fallback = (!is_qualified_symbol(symbol)).to_string();
649 let mut params = vec![
650 qualified,
651 fuzzy_qualified,
652 short,
653 allow_name_fallback,
654 limit.to_string(),
655 symbol_id.unwrap_or(-1).to_string(),
656 unique_short_name.to_string(),
657 logical_symbol_id.unwrap_or(-1).to_string(),
658 ];
659 params.extend(edge_kinds.iter().cloned());
660 params
661}
662
663fn quoted_placeholders(count: usize) -> String {
664 (0..count).map(|index| format!("?{}", index + 9)).collect::<Vec<_>>().join(", ")
665}
666
667fn reverse_predicate(mode: GraphResolutionMode, logical: bool) -> &'static str {
668 if logical {
669 return match mode {
670 GraphResolutionMode::Exact => {
671 "edges.to_symbol_id IS NOT NULL
672 AND edges.to_symbol_id IN (
673 SELECT symbol_id
674 FROM logical_symbol_members
675 WHERE logical_symbol_id = ?8
676 )"
677 },
678 GraphResolutionMode::Syntactic => {
679 "(edges.to_symbol_id IN (
680 SELECT symbol_id
681 FROM logical_symbol_members
682 WHERE logical_symbol_id = ?8
683 )
684 OR edges.target_qualified_name = ?1)"
685 },
686 GraphResolutionMode::Fuzzy => {
687 "edges.to_symbol_id IN (
688 SELECT symbol_id
689 FROM logical_symbol_members
690 WHERE logical_symbol_id = ?8
691 )
692 OR to_symbols.name = ?3
693 OR to_symbols.qualified_name = ?1
694 OR to_symbols.qualified_name LIKE ?2
695 OR edges.target_qualified_name = ?1
696 OR edges.target_qualified_name LIKE ?2
697 OR (?4 = 'true' AND edges.to_name = ?3)"
698 },
699 };
700 }
701 match mode {
702 GraphResolutionMode::Exact => {
703 "edges.to_symbol_id IS NOT NULL
704 AND (edges.to_symbol_id = ?6 OR to_symbols.qualified_name = ?1)"
705 },
706 GraphResolutionMode::Syntactic => {
707 "(edges.to_symbol_id = ?6
708 OR to_symbols.qualified_name = ?1
709 OR (?7 = 'true' AND to_symbols.name = ?3)
710 OR edges.target_qualified_name = ?1)"
711 },
712 GraphResolutionMode::Fuzzy => {
713 "to_symbols.name = ?3
714 OR to_symbols.qualified_name = ?1
715 OR to_symbols.qualified_name LIKE ?2
716 OR edges.target_qualified_name = ?1
717 OR edges.target_qualified_name LIKE ?2
718 OR (?4 = 'true' AND edges.to_name = ?3)"
719 },
720 }
721}
722
723fn reverse_tier(mode: GraphResolutionMode) -> &'static str {
724 match mode {
725 GraphResolutionMode::Exact => "0",
726 GraphResolutionMode::Syntactic => {
727 "CASE
728 WHEN edges.to_symbol_id IS NOT NULL THEN 0
729 WHEN edges.target_qualified_name = ?1 THEN 1
730 ELSE 4
731 END"
732 },
733 GraphResolutionMode::Fuzzy => {
734 "CASE
735 WHEN edges.to_symbol_id IS NOT NULL THEN 0
736 WHEN edges.target_qualified_name = ?1 OR edges.target_qualified_name LIKE ?2 THEN 1
737 WHEN ?4 = 'true' AND edges.to_name = ?3 THEN 2
738 ELSE 4
739 END"
740 },
741 }
742}
743
744fn forward_source_predicate(mode: GraphResolutionMode, logical: bool) -> &'static str {
745 if logical {
746 return match mode {
747 GraphResolutionMode::Exact => {
748 "from_symbols.id IS NOT NULL
749 AND from_symbols.id IN (
750 SELECT symbol_id
751 FROM logical_symbol_members
752 WHERE logical_symbol_id = ?8
753 )"
754 },
755 GraphResolutionMode::Syntactic => {
756 "from_symbols.id IN (
757 SELECT symbol_id
758 FROM logical_symbol_members
759 WHERE logical_symbol_id = ?8
760 )
761 OR edges.from_name = ?1"
762 },
763 GraphResolutionMode::Fuzzy => {
764 "from_symbols.id IN (
765 SELECT symbol_id
766 FROM logical_symbol_members
767 WHERE logical_symbol_id = ?8
768 )
769 OR from_symbols.name = ?3
770 OR from_symbols.qualified_name = ?1
771 OR from_symbols.qualified_name LIKE ?2
772 OR edges.from_name = ?1
773 OR edges.from_name LIKE ?2"
774 },
775 };
776 }
777 match mode {
778 GraphResolutionMode::Exact => {
779 "from_symbols.id IS NOT NULL
780 AND (from_symbols.id = ?6 OR from_symbols.qualified_name = ?1)"
781 },
782 GraphResolutionMode::Syntactic => {
783 "from_symbols.id = ?6
784 OR from_symbols.qualified_name = ?1
785 OR (?7 = 'true' AND from_symbols.name = ?3)
786 OR edges.from_name = ?1"
787 },
788 GraphResolutionMode::Fuzzy => {
789 "from_symbols.name = ?3
790 OR from_symbols.qualified_name = ?1
791 OR from_symbols.qualified_name LIKE ?2
792 OR edges.from_name = ?1
793 OR edges.from_name LIKE ?2"
794 },
795 }
796}
797
798fn forward_target_filter(
799 mode: GraphResolutionMode,
800 options: &GraphTraversalOptions,
801) -> &'static str {
802 match mode {
803 GraphResolutionMode::Exact => "edges.to_symbol_id IS NOT NULL",
804 GraphResolutionMode::Syntactic => {
805 if options.include_unresolved {
806 "1 = 1"
807 } else if options.include_macros {
808 "
809 edges.to_symbol_id IS NOT NULL
810 OR edges.target_qualified_name IS NOT NULL
811 OR edges.edge_kind = 'uses_macro'
812 "
813 } else {
814 "edges.to_symbol_id IS NOT NULL OR edges.target_qualified_name IS NOT NULL"
815 }
816 },
817 GraphResolutionMode::Fuzzy => "1 = 1",
818 }
819}
820
821fn forward_visibility_filter(options: &GraphTraversalOptions) -> &'static str {
822 match (
823 options.include_unresolved,
824 options.include_macros,
825 options.include_common_methods,
826 ) {
827 (true, true, true) => "1 = 1",
828 (true, true, false) => {
829 "
830 (
831 edges.edge_kind != 'calls_name'
832 OR edges.to_name NOT IN (
833 'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
834 'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
835 'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
836 'expect', 'ok', 'err'
837 )
838 OR edges.to_symbol_id IS NOT NULL
839 )
840 "
841 },
842 (true, false, true) => "edges.edge_kind != 'uses_macro'",
843 (true, false, false) => {
844 "
845 edges.edge_kind != 'uses_macro'
846 AND (
847 edges.edge_kind != 'calls_name'
848 OR edges.to_name NOT IN (
849 'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
850 'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
851 'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
852 'expect', 'ok', 'err'
853 )
854 OR edges.to_symbol_id IS NOT NULL
855 )
856 "
857 },
858 (false, true, true) => {
859 "
860 (
861 edges.edge_kind = 'calls_name'
862 AND (
863 edges.to_symbol_id IS NOT NULL
864 OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
865 )
866 )
867 OR (
868 edges.edge_kind = 'constructs'
869 AND edges.to_symbol_id IS NOT NULL
870 )
871 OR edges.edge_kind = 'uses_macro'
872 OR edges.edge_kind NOT IN ('calls_name', 'constructs')
873 "
874 },
875 (false, true, false) => {
876 "
877 (
878 edges.edge_kind = 'calls_name'
879 AND (
880 edges.to_symbol_id IS NOT NULL
881 OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
882 )
883 AND (
884 edges.to_name NOT IN (
885 'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
886 'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
887 'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
888 'expect', 'ok', 'err'
889 )
890 OR edges.to_symbol_id 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 = 'uses_macro'
898 OR edges.edge_kind NOT IN ('calls_name', 'constructs')
899 "
900 },
901 (false, false, true) => {
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 )
912 OR (
913 edges.edge_kind = 'constructs'
914 AND edges.to_symbol_id IS NOT NULL
915 )
916 OR edges.edge_kind NOT IN ('calls_name', 'constructs')
917 )
918 "
919 },
920 (false, false, false) => {
921 "
922 edges.edge_kind != 'uses_macro'
923 AND (
924 (
925 edges.edge_kind = 'calls_name'
926 AND (
927 edges.to_symbol_id IS NOT NULL
928 OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
929 )
930 AND (
931 edges.to_name NOT IN (
932 'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
933 'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
934 'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
935 'expect', 'ok', 'err'
936 )
937 OR edges.to_symbol_id IS NOT NULL
938 )
939 )
940 OR (
941 edges.edge_kind = 'constructs'
942 AND edges.to_symbol_id IS NOT NULL
943 )
944 OR edges.edge_kind NOT IN ('calls_name', 'constructs')
945 )
946 "
947 },
948 }
949}
950
951fn unique_symbol_name(conn: &Connection, name: &str) -> anyhow::Result<bool> {
952 let count: i64 = conn.query_row(
953 "SELECT COUNT(*) AS symbol_count FROM symbols WHERE name = ?1",
954 [name],
955 |row| row.get("symbol_count"),
956 )?;
957 Ok(count == 1)
958}
959
960fn resolution_label(
961 mode: GraphResolutionMode,
962 stored: String,
963 tier: i64,
964 verified_target_symbol: bool,
965) -> String {
966 if mode == GraphResolutionMode::Exact && verified_target_symbol {
967 return "exact".to_string();
968 }
969 if stored != "unresolved" {
970 return stored;
971 }
972 match tier {
973 1 => "target_qualified_suffix".to_string(),
974 2 => "target_name_fallback".to_string(),
975 _ => stored,
976 }
977}
978
979fn short_name(symbol: &str) -> &str {
980 symbol.rsplit([':', '.', '#', '/']).find(|part| !part.is_empty()).unwrap_or(symbol)
981}
982
983fn is_qualified_symbol(symbol: &str) -> bool {
984 symbol.contains("::")
985 || symbol.contains(".rs:")
986 || symbol.contains(".ts:")
987 || symbol.contains(".tsx:")
988 || symbol.contains(".kt:")
989 || symbol.contains('/')
990}