use tree_sitter::Node;
mod bash;
mod cpp;
#[cfg(test)]
mod cpp_test;
mod css;
mod go;
mod java;
mod javascript;
mod json;
mod lua;
mod markdown;
mod php;
#[cfg(test)]
mod php_test;
mod python;
pub mod resolution_utils;
mod ruby;
mod rust;
mod svelte;
mod swift;
#[cfg(test)]
mod swift_test;
mod typescript;
pub use bash::Bash;
pub use cpp::Cpp;
pub use css::Css;
pub use go::Go;
pub use java::Java;
pub use javascript::JavaScript;
pub use json::Json;
pub use lua::Lua;
pub use markdown::Markdown;
pub use php::Php;
pub use python::Python;
pub use ruby::Ruby;
pub use rust::Rust;
pub use svelte::Svelte;
pub use swift::Swift;
pub use typescript::TypeScript;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TypeRelationKind {
Extends,
Implements,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CallTarget {
pub name: String,
pub qualifier: Option<String>,
}
#[derive(Debug, Clone)]
pub struct EmbeddedSource {
pub language: &'static str,
pub contents: String,
pub start_line: u32,
}
pub trait Language: Send + Sync {
fn name(&self) -> &'static str;
fn get_ts_language(&self) -> tree_sitter::Language;
fn get_meaningful_kinds(&self) -> Vec<&'static str>;
fn get_symbol_kinds(&self) -> Vec<&'static str> {
self.get_meaningful_kinds()
}
fn extract_symbols(&self, node: Node, contents: &str) -> Vec<String>;
fn extract_identifiers(&self, node: Node, contents: &str, symbols: &mut Vec<String>);
fn extract_imports_exports(&self, node: Node, contents: &str) -> (Vec<String>, Vec<String>) {
let _ = (node, contents);
(Vec::new(), Vec::new())
}
fn extract_function_calls(&self, node: Node, contents: &str) -> Vec<CallTarget> {
let _ = (node, contents);
Vec::new()
}
fn extract_symbol_owner(&self, node: Node, contents: &str) -> Option<String> {
find_graph_symbol_owner(node, contents)
}
fn extract_embedded_sources(&self, root: Node, contents: &str) -> Vec<EmbeddedSource> {
let _ = (root, contents);
Vec::new()
}
fn extract_type_relations(
&self,
node: Node,
contents: &str,
) -> Vec<(TypeRelationKind, String)> {
let _ = (node, contents);
Vec::new()
}
fn extract_type_relation_source(&self, node: Node, contents: &str) -> Option<String> {
self.extract_declaration_name(node, contents)
}
fn extract_declaration_name(&self, node: Node, contents: &str) -> Option<String> {
extract_symbol_by_kinds(node, contents, &["identifier", "name", "type_identifier"])
}
fn are_node_types_equivalent(&self, type1: &str, type2: &str) -> bool {
type1 == type2
}
fn get_node_type_description(&self, node_type: &str) -> &'static str {
match node_type {
t if t.contains("function") => "function declarations",
t if t.contains("method") => "function declarations",
t if t.contains("class") => "class/interface declarations",
t if t.contains("struct") => "type definitions",
t if t.contains("enum") => "type definitions",
t if t.contains("mod") || t.contains("module") => "module declarations",
t if t.contains("const") => "constant declarations",
t if t.contains("var") || t.contains("let") => "variable declarations",
t if t.contains("type") => "type declarations",
t if t.contains("trait") => "trait declarations",
t if t.contains("impl") => "implementation blocks",
t if t.contains("macro") => "macro definitions",
t if t.contains("namespace") => "namespace declarations",
t if t.contains("comment") => "comments",
_ => "declarations",
}
}
fn resolve_import(
&self,
import_path: &str,
source_file: &str,
all_files: &[String],
) -> Option<String>;
fn get_file_extensions(&self) -> Vec<&'static str>;
}
pub fn get_language(name: &str) -> Option<Box<dyn Language>> {
match name {
"rust" => Some(Box::new(Rust {})),
"javascript" => Some(Box::new(JavaScript {})),
"typescript" => Some(Box::new(TypeScript {})),
"python" => Some(Box::new(Python {})),
"go" => Some(Box::new(Go {})),
"java" => Some(Box::new(Java {})),
"cpp" => Some(Box::new(Cpp {})),
"php" => Some(Box::new(Php {})),
"bash" => Some(Box::new(Bash {})),
"ruby" => Some(Box::new(Ruby {})),
"lua" => Some(Box::new(Lua {})),
"json" => Some(Box::new(Json {})),
"svelte" => Some(Box::new(Svelte {})),
"swift" => Some(Box::new(Swift {})),
"css" => Some(Box::new(Css {})),
"markdown" => Some(Box::new(Markdown {})),
_ => None,
}
}
pub fn deduplicate_symbols(symbols: &mut Vec<String>) {
symbols.sort();
symbols.dedup();
}
pub fn extract_identifiers_default<F>(
node: Node,
contents: &str,
symbols: &mut Vec<String>,
should_include: F,
) where
F: Fn(&str, &str) -> bool + Copy,
{
let kind = node.kind();
if let Ok(text) = node.utf8_text(contents.as_bytes()) {
let trimmed = text.trim();
if !trimmed.is_empty()
&& should_include(kind, trimmed)
&& !symbols.iter().any(|s| s.as_str() == trimmed)
{
symbols.push(trimmed.to_string());
}
}
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
extract_identifiers_default(cursor.node(), contents, symbols, should_include);
if !cursor.goto_next_sibling() {
break;
}
}
}
}
pub fn check_semantic_groups(type1: &str, type2: &str, semantic_groups: &[&[&str]]) -> bool {
if type1 == type2 {
return true;
}
for group in semantic_groups {
let contains_type1 = group.contains(&type1);
let contains_type2 = group.contains(&type2);
if contains_type1 && contains_type2 {
return true;
}
}
false
}
pub fn extract_symbol_by_kind(node: Node, contents: &str, target_kind: &str) -> Option<String> {
for child in node.children(&mut node.walk()) {
if child.kind() == target_kind {
if let Ok(text) = child.utf8_text(contents.as_bytes()) {
return Some(text.to_string());
}
}
}
None
}
pub fn simple_type_name(text: &str) -> Option<String> {
let stripped = text.split('<').next().unwrap_or(text);
let after_colons = stripped.rsplit("::").next().unwrap_or(stripped);
let after_dots = after_colons.rsplit('.').next().unwrap_or(after_colons);
let trimmed = after_dots
.trim()
.trim_matches(|character: char| !character.is_alphanumeric() && character != '_');
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
pub fn extract_call_target(text: &str) -> Option<CallTarget> {
let mut trimmed = text.trim().trim_start_matches('&').trim_start_matches('*');
if trimmed.is_empty() {
return None;
}
let mut without_generics = String::with_capacity(trimmed.len());
let mut generic_depth = 0u32;
for character in trimmed.chars() {
match character {
'<' => generic_depth += 1,
'>' if generic_depth > 0 => generic_depth -= 1,
_ if generic_depth == 0 => without_generics.push(character),
_ => {}
}
}
trimmed = without_generics.trim();
let normalized = trimmed.replace("?.", ".").replace("->", ".");
let segments: Vec<&str> = normalized
.split(['.', ':'])
.map(str::trim)
.filter(|segment| !segment.is_empty())
.collect();
let (name, qualifier_segments) = segments.split_last()?;
let name =
name.trim_matches(|character: char| !character.is_alphanumeric() && character != '_');
if name.is_empty()
|| !name
.chars()
.all(|character| character.is_alphanumeric() || character == '_')
{
return None;
}
let qualifier = if qualifier_segments.is_empty() {
None
} else {
if qualifier_segments.iter().any(|segment| {
!segment.chars().all(|character| {
character.is_alphanumeric() || matches!(character, '_' | '$' | '@' | '#')
})
}) {
return None;
}
Some(qualifier_segments.join("::"))
};
Some(CallTarget {
name: name.to_string(),
qualifier,
})
}
pub fn find_graph_symbol_owner(node: Node, contents: &str) -> Option<String> {
let mut current = node.parent();
while let Some(parent) = current {
let kind = parent.kind();
let is_owner = !kind.contains("body")
&& (kind.contains("class")
|| kind.contains("struct")
|| kind.contains("interface")
|| kind.contains("trait")
|| kind.contains("module")
|| kind.contains("namespace")
|| kind.contains("extension")
|| kind == "impl_item");
if is_owner {
for field in ["type", "name"] {
if let Some(name_node) = parent.child_by_field_name(field) {
if let Ok(text) = name_node.utf8_text(contents.as_bytes()) {
if let Some(name) = simple_type_name(text) {
return Some(name);
}
}
}
}
return extract_symbol_by_kinds(
parent,
contents,
&["identifier", "name", "type_identifier", "constant"],
)
.and_then(|name| simple_type_name(&name));
}
current = parent.parent();
}
None
}
pub fn extract_symbol_by_kinds(
node: Node,
contents: &str,
target_kinds: &[&str],
) -> Option<String> {
for child in node.children(&mut node.walk()) {
if target_kinds.contains(&child.kind())
|| target_kinds.iter().any(|k| child.kind().contains(k))
{
if let Ok(text) = child.utf8_text(contents.as_bytes()) {
return Some(text.to_string());
}
}
}
None
}
pub fn find_enclosing_container_name(
node: Node,
contents: &str,
container_kinds: &[&str],
name_kinds: &[&str],
) -> Option<String> {
let mut cur = node.parent();
while let Some(parent) = cur {
if container_kinds.contains(&parent.kind()) {
if let Some(type_field) = parent.child_by_field_name("type") {
if let Ok(text) = type_field.utf8_text(contents.as_bytes()) {
if let Some(name) = simple_type_name(text) {
return Some(name);
}
}
}
for child in parent.children(&mut parent.walk()) {
if name_kinds.iter().any(|k| child.kind() == *k) {
if let Ok(text) = child.utf8_text(contents.as_bytes()) {
if let Some(name) = simple_type_name(text) {
return Some(name);
}
}
}
}
return None;
}
cur = parent.parent();
}
None
}
#[cfg(test)]
mod graph_extraction_tests {
use super::*;
#[test]
fn structured_callee_preserves_terminal_name_and_qualifier() {
for (input, expected_name, expected_qualifier) in [
("helper", "helper", None),
("service.run", "run", Some("service")),
("Service::new", "new", Some("Service")),
("std::vector<Item>::make", "make", Some("std::vector")),
("ptr->flush", "flush", Some("ptr")),
("client?.send", "send", Some("client")),
] {
let target = extract_call_target(input).expect("callee should parse");
assert_eq!(target.name, expected_name);
assert_eq!(target.qualifier.as_deref(), expected_qualifier);
}
}
#[test]
fn dynamic_callee_is_dropped_instead_of_inventing_a_symbol() {
assert!(extract_call_target("obj[method]").is_none());
assert!(extract_call_target("condition ? first : second").is_none());
}
}