use super::*;
pub(crate) fn index_file_edges(
conn: &Connection,
file_id: i64,
path: &Path,
language: Language,
text: &str,
) -> anyhow::Result<()> {
let symbols = symbols_for_file(conn, file_id)?;
let candidates = edge_candidates(path, language, text, &symbols)?;
insert_candidates(conn, file_id, candidates)
}
pub(crate) fn edge_candidates(
path: &Path,
language: Language,
text: &str,
symbols: &[IndexedSymbol],
) -> anyhow::Result<Vec<EdgeCandidate>> {
if language == Language::Markdown {
return Ok(Vec::new());
}
let mut candidates = contains_edges(symbols);
candidates.extend(syntactic_edges(path, language, text, symbols)?);
Ok(candidates)
}
pub(crate) fn edge_candidates_from_root(
path: &Path,
language: Language,
text: &str,
root: Node<'_>,
symbols: &[IndexedSymbol],
) -> Vec<EdgeCandidate> {
if language == Language::Markdown {
return Vec::new();
}
let mut candidates = contains_edges(symbols);
collect_edges(language, text, root, symbols, path, &mut candidates);
candidates
}
pub(crate) fn qn_tail(qualified_name: &str) -> &str {
qualified_name.rsplit("::").next().unwrap_or(qualified_name)
}
pub(crate) fn contains_edges(symbols: &[IndexedSymbol]) -> Vec<EdgeCandidate> {
let mut out = Vec::new();
for child in symbols {
let parent = symbols
.iter()
.filter(|candidate| {
candidate.id != child.id
&& candidate.start_byte <= child.start_byte
&& candidate.end_byte >= child.end_byte
})
.min_by_key(|candidate| candidate.end_byte.saturating_sub(candidate.start_byte));
if let Some(parent) = parent {
out.push(EdgeCandidate {
from_symbol_id: Some(parent.id),
from_name: Some(parent.qualified_name.clone()),
to_name: child.qualified_name.clone(),
target_qualified_name: Some(child.qualified_name.clone()),
evidence: Some(child.qualified_name.clone()),
receiver_hint: None,
source_span: child.span(),
edge_kind: EdgeKind::Contains,
confidence: EdgeConfidence::Exact,
});
}
}
out
}
pub(crate) fn syntactic_edges(
path: &Path,
language: Language,
text: &str,
symbols: &[IndexedSymbol],
) -> anyhow::Result<Vec<EdgeCandidate>> {
let grammar = match parser::parser_kind(path, language) {
ParserKind::Rust => tree_sitter_rust::LANGUAGE.into(),
ParserKind::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
ParserKind::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
ParserKind::Kotlin => tree_sitter_kotlin::LANGUAGE.into(),
ParserKind::C => tree_sitter_c::LANGUAGE.into(),
ParserKind::Cpp => tree_sitter_cpp::LANGUAGE.into(),
ParserKind::Markdown => return Ok(Vec::new()),
};
let mut parser = tree_sitter::Parser::new();
parser.set_language(&grammar)?;
let Some(tree) = parser.parse(text, None) else {
return Ok(Vec::new());
};
let mut out = Vec::new();
collect_edges(language, text, tree.root_node(), symbols, path, &mut out);
Ok(out)
}
pub(crate) fn collect_edges(
language: Language,
text: &str,
node: Node<'_>,
symbols: &[IndexedSymbol],
path: &Path,
out: &mut Vec<EdgeCandidate>,
) {
if node.is_error() || node.is_missing() {
return;
}
match language {
Language::Rust => rust_edges(text, node, symbols, path, out),
Language::TypeScript => typescript_edges(text, node, symbols, path, out),
Language::Kotlin => kotlin_edges(text, node, symbols, path, out),
Language::C | Language::Cpp => c_like_edges(text, node, symbols, path, out),
Language::Markdown => {},
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
collect_edges(language, text, child, symbols, path, out);
}
}
pub(crate) fn rust_edges(
text: &str,
node: Node<'_>,
symbols: &[IndexedSymbol],
path: &Path,
out: &mut Vec<EdgeCandidate>,
) {
match node.kind() {
"use_declaration" => {
let names = identifiers_under(node, text);
let is_reexport = node_text(node, text).trim_start().starts_with("pub use ");
for name in names {
if !is_rust_path_keyword(&name) {
out.push(file_edge(
path,
node,
text,
name,
EdgeKind::Imports,
EdgeConfidence::NameOnly,
));
}
}
if is_reexport {
for name in identifiers_under(node, text) {
if !is_rust_path_keyword(&name) {
out.push(file_edge(
path,
node,
text,
name,
EdgeKind::Exports,
EdgeConfidence::NameOnly,
));
}
}
}
},
"mod_item" =>
if let Some(name) = child_name_text(node, text) {
out.push(file_edge(
path,
node,
text,
name,
EdgeKind::Imports,
EdgeConfidence::NameOnly,
));
},
"call_expression" => {
if let Some(name) = call_target_name(node, text) {
out.push(symbol_edge_with_context(
symbols,
node,
text,
name,
EdgeKind::CallsName,
EdgeConfidence::NameOnly,
EdgeContext {
target_qualified_name: target_qualified_name(node, text),
receiver_hint: scoped_receiver_name(node, text),
},
));
}
if let Some(receiver) = scoped_receiver_name(node, text)
&& receiver.chars().next().is_some_and(char::is_uppercase)
{
out.push(symbol_edge(
symbols,
node,
receiver,
EdgeKind::ReferencesType,
EdgeConfidence::NameOnly,
));
}
},
"macro_invocation" =>
if let Some(name) = first_identifier_text(node, text) {
out.push(symbol_edge_with_context(
symbols,
node,
text,
name,
EdgeKind::UsesMacro,
EdgeConfidence::NameOnly,
EdgeContext::default(),
));
},
"impl_item" => rust_impl_edges(text, node, symbols, out),
"type_identifier" | "scoped_type_identifier" | "generic_type" => {
if let Some(name) = last_identifier_text(node, text) {
out.push(symbol_edge(
symbols,
node,
name,
EdgeKind::ReferencesType,
EdgeConfidence::NameOnly,
));
}
},
_ => {},
}
}
pub(crate) fn rust_impl_edges(
text: &str,
node: Node<'_>,
symbols: &[IndexedSymbol],
out: &mut Vec<EdgeCandidate>,
) {
let node_text = node_text(node, text);
let header = node_text.split('{').next().unwrap_or_default();
let type_names = header
.split(|ch: char| !ch.is_alphanumeric() && ch != '_')
.filter(|part| !part.is_empty())
.filter(|part| !matches!(*part, "impl" | "for" | "where"))
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
if node_text.contains(" for ") && type_names.len() >= 2 {
let trait_name = type_names.first().cloned().unwrap_or_default();
let type_name = type_names.last().cloned().unwrap_or_default();
out.push(EdgeCandidate {
from_symbol_id: containing_symbol(symbols, node.start_byte()).map(|symbol| symbol.id),
from_name: Some(type_name),
to_name: trait_name,
target_qualified_name: None,
evidence: Some(edge_evidence(node, text)),
receiver_hint: None,
source_span: span_for_node(node),
edge_kind: EdgeKind::Implements,
confidence: EdgeConfidence::NameOnly,
});
} else if let Some(type_name) = type_names.first() {
out.push(symbol_edge(
symbols,
node,
type_name.clone(),
EdgeKind::ReferencesType,
EdgeConfidence::NameOnly,
));
}
}
pub(crate) fn typescript_edges(
text: &str,
node: Node<'_>,
symbols: &[IndexedSymbol],
path: &Path,
out: &mut Vec<EdgeCandidate>,
) {
match node.kind() {
"import_statement" =>
for name in identifiers_under(node, text) {
out.push(file_edge(
path,
node,
text,
name,
EdgeKind::Imports,
EdgeConfidence::NameOnly,
));
},
"export_statement" =>
for name in identifiers_under(node, text) {
out.push(file_edge(
path,
node,
text,
name,
EdgeKind::Exports,
EdgeConfidence::NameOnly,
));
},
"call_expression" | "new_expression" => {
let identifiers =
identifiers_under(node.child_by_field_name("function").unwrap_or(node), text);
if let Some(name) = identifiers.last().cloned().or_else(|| call_target_name(node, text))
{
let edge_kind = if node.kind() == "new_expression" {
EdgeKind::Constructs
} else {
EdgeKind::CallsName
};
out.push(symbol_edge_with_context(
symbols,
node,
text,
name,
edge_kind,
EdgeConfidence::NameOnly,
EdgeContext {
target_qualified_name: dotted_qualified_name(&identifiers),
receiver_hint: identifiers
.first()
.filter(|_| identifiers.len() > 1)
.cloned(),
},
));
}
if let Some(receiver) = identifiers.first().filter(|_| identifiers.len() > 1).cloned() {
out.push(symbol_edge(
symbols,
node,
receiver,
EdgeKind::ReferencesType,
EdgeConfidence::NameOnly,
));
}
},
"jsx_opening_element" | "jsx_self_closing_element" => {
if let Some(name) = first_identifier_text(node, text) {
out.push(symbol_edge(
symbols,
node,
name,
EdgeKind::ReferencesType,
EdgeConfidence::NameOnly,
));
}
},
"type_identifier" => {
if let Some(name) = node.utf8_text(text.as_bytes()).ok().map(ToOwned::to_owned) {
out.push(symbol_edge(
symbols,
node,
name,
EdgeKind::ReferencesType,
EdgeConfidence::NameOnly,
));
}
},
_ => {},
}
}
pub(crate) fn kotlin_edges(
text: &str,
node: Node<'_>,
symbols: &[IndexedSymbol],
path: &Path,
out: &mut Vec<EdgeCandidate>,
) {
match node.kind() {
"import" | "import_header" | "import_directive" => {
for name in identifiers_under(node, text) {
out.push(file_edge(
path,
node,
text,
name,
EdgeKind::Imports,
EdgeConfidence::NameOnly,
));
}
},
"call_expression" => {
let identifiers = identifiers_under(node, text);
if let Some(name) =
identifiers.last().cloned().or_else(|| first_identifier_text(node, text))
{
out.push(symbol_edge_with_context(
symbols,
node,
text,
name,
EdgeKind::CallsName,
EdgeConfidence::NameOnly,
EdgeContext {
target_qualified_name: dotted_qualified_name(&identifiers),
receiver_hint: identifiers
.first()
.filter(|_| identifiers.len() > 1)
.cloned(),
},
));
}
if let Some(receiver) = identifiers.first().filter(|_| identifiers.len() > 1).cloned() {
out.push(symbol_edge(
symbols,
node,
receiver,
EdgeKind::ReferencesType,
EdgeConfidence::NameOnly,
));
}
if let Some(constructor) =
identifiers.first().filter(|name| looks_like_type_name(name)).cloned()
{
out.push(symbol_edge(
symbols,
node,
constructor.clone(),
EdgeKind::ReferencesType,
EdgeConfidence::NameOnly,
));
out.push(symbol_edge_with_context(
symbols,
node,
text,
constructor,
EdgeKind::Constructs,
EdgeConfidence::NameOnly,
EdgeContext::default(),
));
}
},
"user_type" | "type_identifier" =>
if let Some(name) = last_identifier_text(node, text) {
out.push(symbol_edge(
symbols,
node,
name,
EdgeKind::ReferencesType,
EdgeConfidence::NameOnly,
));
},
"delegation_specifier" | "supertype" | "super_type" => {
if let Some(name) = last_identifier_text(node, text) {
out.push(symbol_edge(
symbols,
node,
name,
EdgeKind::Implements,
EdgeConfidence::NameOnly,
));
}
},
_ => {},
}
}
pub(crate) fn c_like_edges(
text: &str,
node: Node<'_>,
symbols: &[IndexedSymbol],
path: &Path,
out: &mut Vec<EdgeCandidate>,
) {
match node.kind() {
"preproc_include" => {
let include = node_text(node, text)
.trim()
.trim_start_matches("#include")
.trim()
.trim_matches(['<', '>', '"'])
.to_string();
if !include.is_empty() {
out.push(file_edge(
path,
node,
text,
include,
EdgeKind::Imports,
EdgeConfidence::NameOnly,
));
}
},
"call_expression" => {
let identifiers =
identifiers_under(node.child_by_field_name("function").unwrap_or(node), text);
if let Some(name) = identifiers.last().cloned().or_else(|| call_target_name(node, text))
{
out.push(symbol_edge_with_context(
symbols,
node,
text,
name,
EdgeKind::CallsName,
EdgeConfidence::NameOnly,
EdgeContext {
target_qualified_name: c_like_qualified_name(&identifiers),
receiver_hint: identifiers
.first()
.filter(|_| identifiers.len() > 1)
.cloned(),
},
));
}
},
"type_identifier" | "qualified_identifier" | "namespace_identifier" => {
if let Some(name) = last_identifier_text(node, text) {
out.push(symbol_edge(
symbols,
node,
name,
EdgeKind::ReferencesType,
EdgeConfidence::NameOnly,
));
}
},
_ => {},
}
}
pub(crate) fn file_edge(
path: &Path,
node: Node<'_>,
text: &str,
to_name: String,
edge_kind: EdgeKind,
confidence: EdgeConfidence,
) -> EdgeCandidate {
EdgeCandidate {
from_symbol_id: None,
from_name: Some(path.to_string_lossy().replace('\\', "/")),
to_name,
target_qualified_name: None,
evidence: Some(edge_evidence(node, text)),
receiver_hint: None,
source_span: span_for_node(node),
edge_kind,
confidence,
}
}
pub(crate) fn symbol_edge(
symbols: &[IndexedSymbol],
node: Node<'_>,
to_name: String,
edge_kind: EdgeKind,
confidence: EdgeConfidence,
) -> EdgeCandidate {
symbol_edge_with_context(
symbols,
node,
"",
to_name,
edge_kind,
confidence,
EdgeContext::default(),
)
}
pub(crate) fn symbol_edge_with_context(
symbols: &[IndexedSymbol],
node: Node<'_>,
text: &str,
to_name: String,
edge_kind: EdgeKind,
confidence: EdgeConfidence,
context: EdgeContext,
) -> EdgeCandidate {
let byte = node.start_byte();
let source = containing_symbol(symbols, byte);
EdgeCandidate {
from_symbol_id: source.map(|symbol| symbol.id),
from_name: source.map(|symbol| symbol.qualified_name.clone()),
to_name,
target_qualified_name: context.target_qualified_name,
evidence: (!text.is_empty()).then(|| edge_evidence(node, text)),
receiver_hint: context.receiver_hint,
source_span: span_for_node(node),
edge_kind,
confidence,
}
}