use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SymbolKind {
Function,
Method,
Class,
Struct,
Interface,
Trait,
Enum,
Module,
Variable,
Constant,
Parameter,
Field,
Import,
Export,
EnumVariant,
TypeAlias,
Unknown,
}
impl SymbolKind {
pub fn from_ast_kind(kind: &str) -> Self {
match kind {
"function_item" | "function_definition" | "function_declaration" | "function_expression" | "arrow_function" | "decorated_definition" => Self::Function,
"method_definition" | "method_declaration" | "method" | "singleton_method" | "constructor_declaration" => Self::Method,
"impl_item" | "class_definition" | "class_declaration" | "class_specifier" | "class" => Self::Class,
"struct_item" | "struct_specifier" | "struct_declaration" => Self::Struct,
"interface_declaration" | "protocol_declaration" => Self::Interface,
"trait_item" | "trait_declaration" => Self::Trait,
"enum_item" | "enum_declaration" | "enum_specifier" => Self::Enum,
"mod_item" | "module" | "namespace_definition" | "namespace_declaration" => Self::Module,
"static_item" | "variable_declaration" | "lexical_declaration" => Self::Variable,
"const_item" => Self::Constant,
"type_item" | "type_alias_declaration" | "type_declaration" => Self::TypeAlias,
_ => Self::Unknown,
}
}
pub fn display_name(&self) -> &'static str {
match self {
Self::Function => "function",
Self::Method => "method",
Self::Class => "class",
Self::Struct => "struct",
Self::Interface => "interface",
Self::Trait => "trait",
Self::Enum => "enum",
Self::Module => "module",
Self::Variable => "variable",
Self::Constant => "constant",
Self::Parameter => "parameter",
Self::Field => "field",
Self::Import => "import",
Self::Export => "export",
Self::EnumVariant => "enum variant",
Self::TypeAlias => "type alias",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum Visibility {
Public,
#[default]
Private,
Protected,
Internal,
}
impl Visibility {
pub fn from_keywords(text: &str) -> Self {
let lower = text.to_lowercase();
if lower.contains("pub ") || lower.contains("public ") || lower.contains("export ") {
Self::Public
} else if lower.contains("protected ") {
Self::Protected
} else if lower.contains("internal ") || lower.contains("package ") {
Self::Internal
} else {
Self::Private
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ReferenceKind {
Call,
Read,
Write,
Import,
TypeReference,
Inheritance,
Instantiation,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SymbolId {
pub file_path: String,
pub name: String,
pub kind: SymbolKind,
pub start_line: usize,
pub start_col: usize,
}
impl SymbolId {
pub fn new(
file_path: impl Into<String>,
name: impl Into<String>,
kind: SymbolKind,
start_line: usize,
start_col: usize,
) -> Self {
Self {
file_path: file_path.into(),
name: name.into(),
kind,
start_line,
start_col,
}
}
pub fn to_storage_id(&self) -> String {
format!(
"{}:{}:{}:{}",
self.file_path, self.name, self.start_line, self.start_col
)
}
pub fn from_storage_id(id: &str) -> Option<Self> {
let parts: Vec<&str> = id.rsplitn(4, ':').collect();
if parts.len() != 4 {
return None;
}
let start_col = parts[0].parse().ok()?;
let start_line = parts[1].parse().ok()?;
let name = parts[2].to_string();
let file_path = parts[3].to_string();
Some(Self {
file_path,
name,
kind: SymbolKind::Unknown, start_line,
start_col,
})
}
}
impl PartialEq for SymbolId {
fn eq(&self, other: &Self) -> bool {
self.file_path == other.file_path
&& self.name == other.name
&& self.start_line == other.start_line
&& self.start_col == other.start_col
}
}
impl Eq for SymbolId {}
impl Hash for SymbolId {
fn hash<H: Hasher>(&self, state: &mut H) {
self.file_path.hash(state);
self.name.hash(state);
self.start_line.hash(state);
self.start_col.hash(state);
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Definition {
pub symbol_id: SymbolId,
pub root_path: Option<String>,
pub project: Option<String>,
pub end_line: usize,
pub end_col: usize,
pub signature: String,
pub doc_comment: Option<String>,
pub visibility: Visibility,
pub parent_id: Option<String>,
pub indexed_at: i64,
}
impl Definition {
pub fn to_storage_id(&self) -> String {
format!(
"def:{}:{}:{}",
self.symbol_id.file_path, self.symbol_id.name, self.symbol_id.start_line
)
}
pub fn file_path(&self) -> &str {
&self.symbol_id.file_path
}
pub fn name(&self) -> &str {
&self.symbol_id.name
}
pub fn kind(&self) -> SymbolKind {
self.symbol_id.kind
}
pub fn start_line(&self) -> usize {
self.symbol_id.start_line
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Reference {
pub file_path: String,
pub root_path: Option<String>,
pub project: Option<String>,
pub start_line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
pub target_symbol_id: String,
pub reference_kind: ReferenceKind,
pub indexed_at: i64,
}
impl Reference {
pub fn to_storage_id(&self) -> String {
format!(
"ref:{}:{}:{}",
self.file_path, self.start_line, self.start_col
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CallEdge {
pub caller_id: String,
pub callee_id: String,
pub call_site_file: String,
pub call_site_line: usize,
pub call_site_col: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum PrecisionLevel {
High,
Medium,
Low,
}
impl PrecisionLevel {
pub fn description(&self) -> &'static str {
match self {
Self::High => "high (stack-graphs)",
Self::Medium => "medium (AST-based)",
Self::Low => "low (text-based)",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DefinitionResult {
pub file_path: String,
pub name: String,
pub kind: SymbolKind,
pub start_line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
pub signature: String,
pub doc_comment: Option<String>,
}
impl From<&Definition> for DefinitionResult {
fn from(def: &Definition) -> Self {
Self {
file_path: def.symbol_id.file_path.clone(),
name: def.symbol_id.name.clone(),
kind: def.symbol_id.kind,
start_line: def.symbol_id.start_line,
end_line: def.end_line,
start_col: def.symbol_id.start_col,
end_col: def.end_col,
signature: def.signature.clone(),
doc_comment: def.doc_comment.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ReferenceResult {
pub file_path: String,
pub start_line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
pub reference_kind: ReferenceKind,
pub preview: Option<String>,
}
impl From<&Reference> for ReferenceResult {
fn from(r: &Reference) -> Self {
Self {
file_path: r.file_path.clone(),
start_line: r.start_line,
end_line: r.end_line,
start_col: r.start_col,
end_col: r.end_col,
reference_kind: r.reference_kind,
preview: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CallGraphNode {
pub name: String,
pub kind: SymbolKind,
pub file_path: String,
pub line: usize,
pub children: Vec<CallGraphNode>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SymbolInfo {
pub name: String,
pub kind: SymbolKind,
pub file_path: String,
pub start_line: usize,
pub end_line: usize,
pub signature: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_symbol_kind_from_ast_kind() {
assert_eq!(SymbolKind::from_ast_kind("function_item"), SymbolKind::Function);
assert_eq!(SymbolKind::from_ast_kind("class_definition"), SymbolKind::Class);
assert_eq!(SymbolKind::from_ast_kind("method_definition"), SymbolKind::Method);
assert_eq!(SymbolKind::from_ast_kind("unknown_node"), SymbolKind::Unknown);
}
#[test]
fn test_symbol_kind_display_name() {
assert_eq!(SymbolKind::Function.display_name(), "function");
assert_eq!(SymbolKind::Class.display_name(), "class");
assert_eq!(SymbolKind::Unknown.display_name(), "unknown");
}
#[test]
fn test_visibility_from_keywords() {
assert_eq!(Visibility::from_keywords("pub fn foo"), Visibility::Public);
assert_eq!(Visibility::from_keywords("public void bar"), Visibility::Public);
assert_eq!(Visibility::from_keywords("protected int x"), Visibility::Protected);
assert_eq!(Visibility::from_keywords("fn private_func"), Visibility::Private);
}
#[test]
fn test_symbol_id_equality() {
let id1 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 0);
let id2 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 0);
let id3 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 20, 0);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_symbol_id_hash() {
use std::collections::HashSet;
let id1 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 0);
let id2 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 0);
let mut set = HashSet::new();
set.insert(id1);
assert!(set.contains(&id2));
}
#[test]
fn test_symbol_id_storage_id() {
let id = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 5);
let storage_id = id.to_storage_id();
assert_eq!(storage_id, "src/main.rs:foo:10:5");
}
#[test]
fn test_definition_storage_id() {
let def = Definition {
symbol_id: SymbolId::new("src/lib.rs", "MyClass", SymbolKind::Class, 15, 0),
root_path: Some("/project".to_string()),
project: Some("test".to_string()),
end_line: 50,
end_col: 1,
signature: "class MyClass".to_string(),
doc_comment: None,
visibility: Visibility::Public,
parent_id: None,
indexed_at: 12345,
};
assert_eq!(def.to_storage_id(), "def:src/lib.rs:MyClass:15");
assert_eq!(def.file_path(), "src/lib.rs");
assert_eq!(def.name(), "MyClass");
assert_eq!(def.kind(), SymbolKind::Class);
}
#[test]
fn test_reference_storage_id() {
let reference = Reference {
file_path: "src/consumer.rs".to_string(),
root_path: None,
project: None,
start_line: 25,
end_line: 25,
start_col: 10,
end_col: 20,
target_symbol_id: "def:src/lib.rs:foo:10".to_string(),
reference_kind: ReferenceKind::Call,
indexed_at: 12345,
};
assert_eq!(reference.to_storage_id(), "ref:src/consumer.rs:25:10");
}
#[test]
fn test_precision_level_description() {
assert_eq!(PrecisionLevel::High.description(), "high (stack-graphs)");
assert_eq!(PrecisionLevel::Medium.description(), "medium (AST-based)");
assert_eq!(PrecisionLevel::Low.description(), "low (text-based)");
}
#[test]
fn test_definition_result_from_definition() {
let def = Definition {
symbol_id: SymbolId::new("src/lib.rs", "my_func", SymbolKind::Function, 10, 0),
root_path: None,
project: None,
end_line: 20,
end_col: 1,
signature: "fn my_func()".to_string(),
doc_comment: Some("Does stuff".to_string()),
visibility: Visibility::Public,
parent_id: None,
indexed_at: 0,
};
let result = DefinitionResult::from(&def);
assert_eq!(result.file_path, "src/lib.rs");
assert_eq!(result.name, "my_func");
assert_eq!(result.kind, SymbolKind::Function);
assert_eq!(result.start_line, 10);
assert_eq!(result.end_line, 20);
assert_eq!(result.doc_comment, Some("Does stuff".to_string()));
}
#[test]
fn test_serialization() {
let id = SymbolId::new("src/main.rs", "test", SymbolKind::Function, 1, 0);
let json = serde_json::to_string(&id).unwrap();
let deserialized: SymbolId = serde_json::from_str(&json).unwrap();
assert_eq!(id, deserialized);
}
#[test]
fn test_reference_kind_serialization() {
let kind = ReferenceKind::Call;
let json = serde_json::to_string(&kind).unwrap();
assert_eq!(json, "\"call\"");
let deserialized: ReferenceKind = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, ReferenceKind::Call);
}
}