use std::collections::BTreeSet;
use rusqlite::{Connection, params_from_iter};
use serde::Serialize;
const CALL_EDGE_KINDS: &[&str] = &["calls_name", "constructs"];
const MACRO_EDGE_KINDS: &[&str] = &["uses_macro"];
const REFERENCE_EDGE_KINDS: &[&str] =
&["references_type", "imports", "exports", "contains", "implements"];
const OPTIONAL_EDGE_KINDS: &[&str] = &[
"calls_name",
"constructs",
"uses_macro",
"references_type",
"imports",
"exports",
"contains",
"implements",
];
#[derive(Debug, Clone, Default)]
pub struct GraphTraversalOptions {
pub include_references: bool,
pub include_unresolved: bool,
pub include_macros: bool,
pub include_common_methods: bool,
pub edge_kinds: Option<Vec<String>>,
pub resolution_mode: GraphResolutionMode,
pub symbol_id: Option<i64>,
pub logical_symbol_id: Option<i64>,
}
#[derive(Debug, Serialize)]
pub struct GraphTraversalReport {
pub query: GraphTraversalQuery,
#[serde(skip_serializing_if = "Option::is_none")]
pub logical_symbol: Option<LogicalSymbol>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub variants: Vec<LogicalSymbolVariant>,
pub summary: GraphTraversalSummary,
pub coverage: GraphCoverage,
pub results: Vec<GraphHop>,
}
#[derive(Debug, Serialize)]
pub struct GraphTraversalQuery {
pub tool: String,
pub symbol_id: Option<i64>,
pub logical_symbol_id: Option<i64>,
pub symbol_path: String,
pub resolution: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct LogicalSymbol {
pub logical_symbol_id: i64,
pub qualified_name: String,
pub variant_count: u64,
pub group_reason: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct LogicalSymbolVariant {
pub symbol_id: i64,
pub cfg_expr: Option<String>,
pub signature_hash: Option<String>,
pub start_line: i64,
pub end_line: i64,
}
#[derive(Debug, Default, Serialize)]
pub struct GraphTraversalSummary {
pub returned_count: u64,
pub total_matching_edges: u64,
pub truncated: bool,
pub exact_verified: u64,
pub syntactic: u64,
pub name_only: u64,
pub ambiguous: u64,
pub unresolved: u64,
pub false_positive_risk: String,
pub completeness_risk: String,
}
#[derive(Debug, Default, Serialize)]
pub struct GraphCoverage {
pub indexed_files: u64,
pub parser_failures: u64,
pub stale_files: u64,
pub known_index_gaps: Vec<String>,
pub parser_coverage_for_paths: Vec<GraphPathCoverage>,
}
#[derive(Debug, Serialize)]
pub struct GraphPathCoverage {
pub path: String,
pub language: String,
pub parser_status: String,
pub graph_status: String,
pub last_indexed_revision: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum GraphResolutionMode {
Exact,
#[default]
Syntactic,
Fuzzy,
}
impl GraphResolutionMode {
pub fn parse(value: Option<&str>) -> anyhow::Result<Self> {
match value.unwrap_or("syntactic") {
"exact" => Ok(Self::Exact),
"syntactic" => Ok(Self::Syntactic),
"fuzzy" => Ok(Self::Fuzzy),
other => anyhow::bail!(
"unknown graph resolution mode `{other}`; expected exact, syntactic, or fuzzy"
),
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Exact => "exact",
Self::Syntactic => "syntactic",
Self::Fuzzy => "fuzzy",
}
}
}
impl GraphTraversalOptions {
pub fn callee_edge_kinds(&self) -> anyhow::Result<Vec<String>> {
if let Some(edge_kinds) = &self.edge_kinds {
validate_edge_kinds(edge_kinds)?;
return Ok(edge_kinds.clone());
}
let mut edge_kinds =
CALL_EDGE_KINDS.iter().map(|value| (*value).to_string()).collect::<Vec<_>>();
if self.include_macros {
edge_kinds.extend(MACRO_EDGE_KINDS.iter().map(|value| (*value).to_string()));
}
if self.include_references {
edge_kinds.extend(REFERENCE_EDGE_KINDS.iter().map(|value| (*value).to_string()));
}
Ok(edge_kinds)
}
pub fn caller_edge_kinds(&self) -> anyhow::Result<Vec<String>> {
self.callee_edge_kinds()
}
}
#[derive(Debug, Serialize)]
pub struct CompareGraphTextReport {
pub query: CompareGraphTextQuery,
#[serde(skip_serializing_if = "Option::is_none")]
pub logical_symbol: Option<LogicalSymbol>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub variants: Vec<LogicalSymbolVariant>,
pub summary: CompareGraphTextSummary,
pub coverage: GraphCoverage,
pub matched_hits: Vec<MatchedGraphTextHit>,
pub text_only_hits: Vec<TextOnlyHit>,
pub graph_only_edges: Vec<GraphOnlyEdge>,
pub likely_parser_gaps: Vec<TextOnlyHit>,
pub likely_false_positives: Vec<GraphOnlyEdge>,
}
#[derive(Debug, Serialize)]
pub struct CompareGraphTextQuery {
pub symbol_id: Option<i64>,
pub logical_symbol_id: Option<i64>,
pub symbol_path: String,
pub pattern: String,
pub resolution: String,
pub include_tests: bool,
}
#[derive(Debug, Default, Serialize)]
pub struct CompareGraphTextSummary {
pub graph_hits: u64,
pub graph_edges: u64,
pub text_hits: u64,
pub matched: u64,
pub graph_only: u64,
pub text_only: u64,
pub text_mentions: u64,
pub likely_parser_gaps: u64,
pub likely_false_positives: u64,
pub likely_index_gaps: u64,
pub complete: bool,
pub recommended_fallback: String,
pub pattern_match_mode: String,
pub warnings: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct MatchedGraphTextHit {
pub path: String,
pub line: i64,
pub text: String,
pub target: Option<String>,
pub edge_kind: String,
pub confidence: String,
pub resolution: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct TextOnlyHit {
pub path: String,
pub line: i64,
pub text: String,
pub reason: String,
pub likely_gap: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct GraphOnlyEdge {
pub path: String,
pub line: i64,
pub target: Option<String>,
pub edge_kind: String,
pub confidence: String,
pub resolution: String,
pub evidence: Option<String>,
pub reason: String,
pub likely_reason: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct GraphHop {
pub edge_id: i64,
pub from_symbol: Option<String>,
pub to_symbol: Option<String>,
pub edge_kind: String,
pub confidence: String,
pub edge_confidence: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_qualified_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub evidence: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub receiver_hint: Option<String>,
pub resolution: String,
pub verified_target_symbol: bool,
pub shown_by_default: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub callsite: Option<Callsite>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Callsite {
pub path: String,
pub line: i64,
pub span: [i64; 2],
}
pub fn traverse(
conn: &Connection,
symbol: &str,
reverse: bool,
limit: u32,
) -> anyhow::Result<Vec<GraphHop>> {
traverse_with_options(conn, symbol, reverse, limit, &GraphTraversalOptions::default())
}
pub fn traverse_with_options(
conn: &Connection,
symbol: &str,
reverse: bool,
limit: u32,
options: &GraphTraversalOptions,
) -> anyhow::Result<Vec<GraphHop>> {
let edge_kinds =
if reverse { options.caller_edge_kinds()? } else { options.callee_edge_kinds()? };
let quoted = quoted_placeholders(edge_kinds.len());
let unique_short_name = unique_symbol_name(conn, short_name(symbol))?;
let mode = options.resolution_mode;
let sql = if reverse {
let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
let tier = reverse_tier(mode);
format!(
"
SELECT COALESCE(from_symbols.qualified_name, edges.from_name) AS from_symbol,
COALESCE(to_symbols.qualified_name, edges.to_name) AS to_symbol,
edges.id AS edge_id,
edges.edge_kind AS edge_kind,
edges.confidence AS confidence,
edges.to_name AS target,
edges.target_qualified_name AS target_qualified_name,
edges.evidence AS evidence,
edges.receiver_hint AS receiver_hint,
edges.resolution AS edge_resolution,
edges.to_symbol_id IS NOT NULL AS verified_target_symbol,
source_files.path AS callsite_path,
COALESCE(NULLIF(edges.source_start_line, 0), 1) AS callsite_start_line,
COALESCE(NULLIF(edges.source_end_line, 0), NULLIF(edges.source_start_line, 0), 1) AS callsite_end_line,
{tier} AS match_tier
FROM edges
JOIN files source_files ON source_files.id = edges.source_file_id
LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
WHERE edges.edge_kind IN ({quoted})
AND ({predicate})
ORDER BY match_tier,
CASE edges.confidence
WHEN 'Exact' THEN 0
WHEN 'Syntactic' THEN 1
WHEN 'NameOnly' THEN 2
ELSE 3
END,
edges.edge_kind,
edges.from_name
LIMIT ?5
"
)
} else {
let predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
let target_filter = forward_target_filter(mode, options);
let visibility_filter = forward_visibility_filter(options);
format!(
"
SELECT COALESCE(from_symbols.qualified_name, edges.from_name) AS from_symbol,
COALESCE(to_symbols.qualified_name, edges.to_name) AS to_symbol,
edges.id AS edge_id,
edges.edge_kind AS edge_kind,
edges.confidence AS confidence,
edges.to_name AS target,
edges.target_qualified_name AS target_qualified_name,
edges.evidence AS evidence,
edges.receiver_hint AS receiver_hint,
edges.resolution AS edge_resolution,
edges.to_symbol_id IS NOT NULL AS verified_target_symbol,
source_files.path AS callsite_path,
COALESCE(NULLIF(edges.source_start_line, 0), 1) AS callsite_start_line,
COALESCE(NULLIF(edges.source_end_line, 0), NULLIF(edges.source_start_line, 0), 1) AS callsite_end_line,
0 AS match_tier
FROM edges
JOIN files source_files ON source_files.id = edges.source_file_id
LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
WHERE edges.edge_kind IN ({quoted})
AND ({predicate})
AND ({target_filter})
AND ({visibility_filter})
AND ?4 IN ('true', 'false')
ORDER BY
CASE edges.confidence
WHEN 'Exact' THEN 0
WHEN 'Syntactic' THEN 1
WHEN 'NameOnly' THEN 2
ELSE 3
END,
edges.edge_kind,
edges.to_name
LIMIT ?5
"
)
};
let params = traversal_params(
symbol,
limit,
&edge_kinds,
options.symbol_id,
options.logical_symbol_id,
unique_short_name,
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map(params_from_iter(params), |row| {
let edge_kind: String = row.get("edge_kind")?;
let confidence: String = row.get("confidence")?;
let verified_target_symbol = row.get("verified_target_symbol")?;
let resolution = resolution_label(
mode,
row.get::<_, String>("edge_resolution")?,
row.get("match_tier")?,
verified_target_symbol,
);
let callsite_path: String = row.get("callsite_path")?;
let callsite_start = row.get("callsite_start_line")?;
let callsite_end = row.get("callsite_end_line")?;
Ok(GraphHop {
edge_id: row.get("edge_id")?,
from_symbol: row.get("from_symbol")?,
to_symbol: row.get("to_symbol")?,
edge_kind: edge_kind.clone(),
confidence: confidence.clone(),
edge_confidence: confidence,
target: row.get("target")?,
target_qualified_name: row.get("target_qualified_name")?,
evidence: row.get("evidence")?,
receiver_hint: row.get("receiver_hint")?,
resolution,
verified_target_symbol,
shown_by_default: CALL_EDGE_KINDS.contains(&edge_kind.as_str()),
callsite: Some(Callsite {
path: callsite_path,
line: callsite_start,
span: [callsite_start, callsite_end],
}),
})
})?;
let mut hops = Vec::new();
for row in rows {
hops.push(row?);
}
dedupe_hops(&mut hops);
Ok(hops)
}
fn dedupe_hops(hops: &mut Vec<GraphHop>) {
let mut seen = BTreeSet::new();
hops.retain(|hop| {
let callsite = hop.callsite.as_ref();
seen.insert((
hop.from_symbol.clone(),
hop.to_symbol.clone(),
hop.edge_id,
hop.edge_kind.clone(),
hop.target.clone(),
hop.target_qualified_name.clone(),
hop.receiver_hint.clone(),
callsite.map(|value| value.path.clone()),
callsite.map(|value| value.span),
))
});
}
pub fn traversal_summary(
conn: &Connection,
symbol: &str,
reverse: bool,
limit: u32,
options: &GraphTraversalOptions,
returned_count: usize,
) -> anyhow::Result<GraphTraversalSummary> {
let edge_kinds =
if reverse { options.caller_edge_kinds()? } else { options.callee_edge_kinds()? };
let quoted = quoted_placeholders(edge_kinds.len());
let unique_short_name = unique_symbol_name(conn, short_name(symbol))?;
let mode = options.resolution_mode;
let sql = if reverse {
let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
format!(
"
SELECT
COUNT(*),
SUM(CASE WHEN edges.to_symbol_id IS NOT NULL THEN 1 ELSE 0 END),
SUM(CASE WHEN edges.confidence = 'Syntactic' THEN 1 ELSE 0 END),
SUM(CASE WHEN edges.confidence = 'NameOnly' THEN 1 ELSE 0 END),
SUM(CASE WHEN edges.confidence = 'Ambiguous' THEN 1 ELSE 0 END),
SUM(CASE WHEN edges.to_symbol_id IS NULL THEN 1 ELSE 0 END)
FROM edges
LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
WHERE edges.edge_kind IN ({quoted})
AND ({predicate})
"
)
} else {
let predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
let target_filter = forward_target_filter(mode, options);
let visibility_filter = forward_visibility_filter(options);
format!(
"
SELECT
COUNT(*),
SUM(CASE WHEN edges.to_symbol_id IS NOT NULL THEN 1 ELSE 0 END),
SUM(CASE WHEN edges.confidence = 'Syntactic' THEN 1 ELSE 0 END),
SUM(CASE WHEN edges.confidence = 'NameOnly' THEN 1 ELSE 0 END),
SUM(CASE WHEN edges.confidence = 'Ambiguous' THEN 1 ELSE 0 END),
SUM(CASE WHEN edges.to_symbol_id IS NULL THEN 1 ELSE 0 END)
FROM edges
LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
WHERE edges.edge_kind IN ({quoted})
AND ({predicate})
AND ({target_filter})
AND ({visibility_filter})
AND ?4 IN ('true', 'false')
"
)
};
let params = traversal_params(
symbol,
limit,
&edge_kinds,
options.symbol_id,
options.logical_symbol_id,
unique_short_name,
);
let mut summary = conn.query_row(&sql, params_from_iter(params), |row| {
Ok(GraphTraversalSummary {
returned_count: u64::try_from(returned_count).unwrap_or(u64::MAX),
total_matching_edges: count_col(row, 0)?,
truncated: false,
exact_verified: count_col(row, 1)?,
syntactic: count_col(row, 2)?,
name_only: count_col(row, 3)?,
ambiguous: count_col(row, 4)?,
unresolved: count_col(row, 5)?,
false_positive_risk: String::new(),
completeness_risk: String::new(),
})
})?;
let hidden_unresolved = hidden_unresolved_candidate_count(
conn,
symbol,
reverse,
&edge_kinds,
options,
unique_short_name,
)?;
summary.total_matching_edges = summary.total_matching_edges.saturating_add(hidden_unresolved);
summary.unresolved = summary.unresolved.saturating_add(hidden_unresolved);
summary.truncated = summary.total_matching_edges > u64::from(limit);
summary.false_positive_risk = false_positive_risk(&summary, mode).to_string();
summary.completeness_risk = completeness_risk(&summary).to_string();
Ok(summary)
}
fn count_col(row: &rusqlite::Row<'_>, index: usize) -> rusqlite::Result<u64> {
let value = row.get::<_, Option<i64>>(index)?.unwrap_or(0);
Ok(u64::try_from(value).unwrap_or(0))
}
fn false_positive_risk(summary: &GraphTraversalSummary, mode: GraphResolutionMode) -> &'static str {
if summary.ambiguous > 0 || mode == GraphResolutionMode::Fuzzy {
"high"
} else if summary.name_only > 0
|| summary.unresolved > 0
|| mode == GraphResolutionMode::Syntactic
{
"medium"
} else {
"low"
}
}
fn completeness_risk(summary: &GraphTraversalSummary) -> &'static str {
if summary.truncated
|| summary.unresolved > summary.exact_verified.saturating_add(summary.syntactic)
{
"high"
} else if summary.unresolved > 0 || summary.name_only > 0 || summary.ambiguous > 0 {
"medium"
} else {
"low"
}
}
fn hidden_unresolved_candidate_count(
conn: &Connection,
symbol: &str,
reverse: bool,
edge_kinds: &[String],
options: &GraphTraversalOptions,
unique_short_name: bool,
) -> anyhow::Result<u64> {
let mode = options.resolution_mode;
let quoted = quoted_placeholders(edge_kinds.len());
let sql = if reverse {
let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
format!(
"
SELECT COUNT(*)
FROM edges
LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
WHERE edges.edge_kind IN ({quoted})
AND edges.to_symbol_id IS NULL
AND NOT ({predicate})
AND (
edges.target_qualified_name = ?1
OR edges.target_qualified_name LIKE ?2
OR edges.to_name = ?3
)
"
)
} else {
let source_predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
let target_filter = forward_target_filter(mode, options);
let visibility_filter = forward_visibility_filter(options);
format!(
"
SELECT COUNT(*)
FROM edges
LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
WHERE edges.edge_kind IN ({quoted})
AND ({source_predicate})
AND edges.to_symbol_id IS NULL
AND NOT (({target_filter}) AND ({visibility_filter}))
AND ?4 IN ('true', 'false')
"
)
};
let params = traversal_params(
symbol,
0,
edge_kinds,
options.symbol_id,
options.logical_symbol_id,
unique_short_name,
);
let count = conn.query_row(&sql, params_from_iter(params), |row| count_col(row, 0))?;
Ok(count)
}
fn validate_edge_kinds(edge_kinds: &[String]) -> anyhow::Result<()> {
for edge_kind in edge_kinds {
if !OPTIONAL_EDGE_KINDS.contains(&edge_kind.as_str()) {
anyhow::bail!("unknown graph edge kind `{edge_kind}`");
}
}
Ok(())
}
fn traversal_params(
symbol: &str,
limit: u32,
edge_kinds: &[String],
symbol_id: Option<i64>,
logical_symbol_id: Option<i64>,
unique_short_name: bool,
) -> Vec<String> {
let qualified = symbol.to_string();
let short = short_name(symbol).to_string();
let fuzzy_qualified = format!("%::{qualified}");
let allow_name_fallback = (!is_qualified_symbol(symbol)).to_string();
let mut params = vec![
qualified,
fuzzy_qualified,
short,
allow_name_fallback,
limit.to_string(),
symbol_id.unwrap_or(-1).to_string(),
unique_short_name.to_string(),
logical_symbol_id.unwrap_or(-1).to_string(),
];
params.extend(edge_kinds.iter().cloned());
params
}
fn quoted_placeholders(count: usize) -> String {
(0..count).map(|index| format!("?{}", index + 9)).collect::<Vec<_>>().join(", ")
}
fn reverse_predicate(mode: GraphResolutionMode, logical: bool) -> &'static str {
if logical {
return match mode {
GraphResolutionMode::Exact => {
"edges.to_symbol_id IS NOT NULL
AND edges.to_symbol_id IN (
SELECT symbol_id
FROM logical_symbol_members
WHERE logical_symbol_id = ?8
)"
},
GraphResolutionMode::Syntactic => {
"(edges.to_symbol_id IN (
SELECT symbol_id
FROM logical_symbol_members
WHERE logical_symbol_id = ?8
)
OR edges.target_qualified_name = ?1)"
},
GraphResolutionMode::Fuzzy => {
"edges.to_symbol_id IN (
SELECT symbol_id
FROM logical_symbol_members
WHERE logical_symbol_id = ?8
)
OR to_symbols.name = ?3
OR to_symbols.qualified_name = ?1
OR to_symbols.qualified_name LIKE ?2
OR edges.target_qualified_name = ?1
OR edges.target_qualified_name LIKE ?2
OR (?4 = 'true' AND edges.to_name = ?3)"
},
};
}
match mode {
GraphResolutionMode::Exact => {
"edges.to_symbol_id IS NOT NULL
AND (edges.to_symbol_id = ?6 OR to_symbols.qualified_name = ?1)"
},
GraphResolutionMode::Syntactic => {
"(edges.to_symbol_id = ?6
OR to_symbols.qualified_name = ?1
OR (?7 = 'true' AND to_symbols.name = ?3)
OR edges.target_qualified_name = ?1)"
},
GraphResolutionMode::Fuzzy => {
"to_symbols.name = ?3
OR to_symbols.qualified_name = ?1
OR to_symbols.qualified_name LIKE ?2
OR edges.target_qualified_name = ?1
OR edges.target_qualified_name LIKE ?2
OR (?4 = 'true' AND edges.to_name = ?3)"
},
}
}
fn reverse_tier(mode: GraphResolutionMode) -> &'static str {
match mode {
GraphResolutionMode::Exact => "0",
GraphResolutionMode::Syntactic => {
"CASE
WHEN edges.to_symbol_id IS NOT NULL THEN 0
WHEN edges.target_qualified_name = ?1 THEN 1
ELSE 4
END"
},
GraphResolutionMode::Fuzzy => {
"CASE
WHEN edges.to_symbol_id IS NOT NULL THEN 0
WHEN edges.target_qualified_name = ?1 OR edges.target_qualified_name LIKE ?2 THEN 1
WHEN ?4 = 'true' AND edges.to_name = ?3 THEN 2
ELSE 4
END"
},
}
}
fn forward_source_predicate(mode: GraphResolutionMode, logical: bool) -> &'static str {
if logical {
return match mode {
GraphResolutionMode::Exact => {
"from_symbols.id IS NOT NULL
AND from_symbols.id IN (
SELECT symbol_id
FROM logical_symbol_members
WHERE logical_symbol_id = ?8
)"
},
GraphResolutionMode::Syntactic => {
"from_symbols.id IN (
SELECT symbol_id
FROM logical_symbol_members
WHERE logical_symbol_id = ?8
)
OR edges.from_name = ?1"
},
GraphResolutionMode::Fuzzy => {
"from_symbols.id IN (
SELECT symbol_id
FROM logical_symbol_members
WHERE logical_symbol_id = ?8
)
OR from_symbols.name = ?3
OR from_symbols.qualified_name = ?1
OR from_symbols.qualified_name LIKE ?2
OR edges.from_name = ?1
OR edges.from_name LIKE ?2"
},
};
}
match mode {
GraphResolutionMode::Exact => {
"from_symbols.id IS NOT NULL
AND (from_symbols.id = ?6 OR from_symbols.qualified_name = ?1)"
},
GraphResolutionMode::Syntactic => {
"from_symbols.id = ?6
OR from_symbols.qualified_name = ?1
OR (?7 = 'true' AND from_symbols.name = ?3)
OR edges.from_name = ?1"
},
GraphResolutionMode::Fuzzy => {
"from_symbols.name = ?3
OR from_symbols.qualified_name = ?1
OR from_symbols.qualified_name LIKE ?2
OR edges.from_name = ?1
OR edges.from_name LIKE ?2"
},
}
}
fn forward_target_filter(
mode: GraphResolutionMode,
options: &GraphTraversalOptions,
) -> &'static str {
match mode {
GraphResolutionMode::Exact => "edges.to_symbol_id IS NOT NULL",
GraphResolutionMode::Syntactic => {
if options.include_unresolved {
"1 = 1"
} else if options.include_macros {
"
edges.to_symbol_id IS NOT NULL
OR edges.target_qualified_name IS NOT NULL
OR edges.edge_kind = 'uses_macro'
"
} else {
"edges.to_symbol_id IS NOT NULL OR edges.target_qualified_name IS NOT NULL"
}
},
GraphResolutionMode::Fuzzy => "1 = 1",
}
}
fn forward_visibility_filter(options: &GraphTraversalOptions) -> &'static str {
match (
options.include_unresolved,
options.include_macros,
options.include_common_methods,
) {
(true, true, true) => "1 = 1",
(true, true, false) => {
"
(
edges.edge_kind != 'calls_name'
OR edges.to_name NOT IN (
'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
'expect', 'ok', 'err'
)
OR edges.to_symbol_id IS NOT NULL
)
"
},
(true, false, true) => "edges.edge_kind != 'uses_macro'",
(true, false, false) => {
"
edges.edge_kind != 'uses_macro'
AND (
edges.edge_kind != 'calls_name'
OR edges.to_name NOT IN (
'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
'expect', 'ok', 'err'
)
OR edges.to_symbol_id IS NOT NULL
)
"
},
(false, true, true) => {
"
(
edges.edge_kind = 'calls_name'
AND (
edges.to_symbol_id IS NOT NULL
OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
)
)
OR (
edges.edge_kind = 'constructs'
AND edges.to_symbol_id IS NOT NULL
)
OR edges.edge_kind = 'uses_macro'
OR edges.edge_kind NOT IN ('calls_name', 'constructs')
"
},
(false, true, false) => {
"
(
edges.edge_kind = 'calls_name'
AND (
edges.to_symbol_id IS NOT NULL
OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
)
AND (
edges.to_name NOT IN (
'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
'expect', 'ok', 'err'
)
OR edges.to_symbol_id IS NOT NULL
)
)
OR (
edges.edge_kind = 'constructs'
AND edges.to_symbol_id IS NOT NULL
)
OR edges.edge_kind = 'uses_macro'
OR edges.edge_kind NOT IN ('calls_name', 'constructs')
"
},
(false, false, true) => {
"
edges.edge_kind != 'uses_macro'
AND (
(
edges.edge_kind = 'calls_name'
AND (
edges.to_symbol_id IS NOT NULL
OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
)
)
OR (
edges.edge_kind = 'constructs'
AND edges.to_symbol_id IS NOT NULL
)
OR edges.edge_kind NOT IN ('calls_name', 'constructs')
)
"
},
(false, false, false) => {
"
edges.edge_kind != 'uses_macro'
AND (
(
edges.edge_kind = 'calls_name'
AND (
edges.to_symbol_id IS NOT NULL
OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
)
AND (
edges.to_name NOT IN (
'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
'expect', 'ok', 'err'
)
OR edges.to_symbol_id IS NOT NULL
)
)
OR (
edges.edge_kind = 'constructs'
AND edges.to_symbol_id IS NOT NULL
)
OR edges.edge_kind NOT IN ('calls_name', 'constructs')
)
"
},
}
}
fn unique_symbol_name(conn: &Connection, name: &str) -> anyhow::Result<bool> {
let count: i64 = conn.query_row(
"SELECT COUNT(*) AS symbol_count FROM symbols WHERE name = ?1",
[name],
|row| row.get("symbol_count"),
)?;
Ok(count == 1)
}
fn resolution_label(
mode: GraphResolutionMode,
stored: String,
tier: i64,
verified_target_symbol: bool,
) -> String {
if mode == GraphResolutionMode::Exact && verified_target_symbol {
return "exact".to_string();
}
if stored != "unresolved" {
return stored;
}
match tier {
1 => "target_qualified_suffix".to_string(),
2 => "target_name_fallback".to_string(),
_ => stored,
}
}
fn short_name(symbol: &str) -> &str {
symbol.rsplit([':', '.', '#', '/']).find(|part| !part.is_empty()).unwrap_or(symbol)
}
fn is_qualified_symbol(symbol: &str) -> bool {
symbol.contains("::")
|| symbol.contains(".rs:")
|| symbol.contains(".ts:")
|| symbol.contains(".tsx:")
|| symbol.contains(".kt:")
|| symbol.contains('/')
}