use std::path::PathBuf;
use crate::language::LangId;
use crate::model::{DataNodeId, FileId, SnapshotId, SourceRange, SymbolId, SymbolKind, Visibility};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum NodeData {
File(FileNode),
Symbol(SymbolNode),
External(ExternalNode),
Data(DataGraphNode),
}
impl NodeData {
pub fn kind_str(&self) -> &'static str {
match self {
NodeData::File(_) => "file",
NodeData::Symbol(_) => "symbol",
NodeData::External(_) => "external",
NodeData::Data(_) => "data",
}
}
pub fn as_file(&self) -> Option<&FileNode> {
if let NodeData::File(f) = self {
Some(f)
} else {
None
}
}
pub fn as_symbol(&self) -> Option<&SymbolNode> {
if let NodeData::Symbol(s) = self {
Some(s)
} else {
None
}
}
pub fn as_external(&self) -> Option<&ExternalNode> {
if let NodeData::External(e) = self {
Some(e)
} else {
None
}
}
pub fn as_data(&self) -> Option<&DataGraphNode> {
if let NodeData::Data(d) = self {
Some(d)
} else {
None
}
}
pub fn file_path(&self) -> Option<&PathBuf> {
self.as_file().map(|f| &f.path)
}
pub fn symbol_name(&self) -> Option<&str> {
self.as_symbol().map(|s| s.name.as_str())
}
}
#[derive(Debug, Clone)]
pub struct FileNode {
pub id: FileId,
pub path: PathBuf,
pub language: LangId,
pub snapshot_id: SnapshotId,
}
#[derive(Debug, Clone)]
pub struct ExternalNode {
pub raw_path: String,
pub language: LangId,
pub classification: Option<ExternalClassification>,
}
#[derive(Debug, Clone, serde::Serialize)]
#[non_exhaustive]
pub enum ExternalClassification {
Classified {
package_name: String,
version: Option<String>,
language: LangId,
source: DependencySource,
},
Unresolved { raw_path: String, reason: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[non_exhaustive]
pub enum DependencySource {
Lockfile,
Manifest,
}
#[derive(Debug, Clone)]
pub struct SymbolNode {
pub id: SymbolId,
pub name: String,
pub kind: SymbolKind,
pub file_id: FileId,
pub visibility: Option<Visibility>,
pub source_range: SourceRange,
}
#[derive(Debug, Clone)]
pub struct DataGraphNode {
pub id: DataNodeId,
pub symbol_id: Option<SymbolId>,
pub name: Option<String>,
pub scope: crate::model::DataScope,
pub type_hint: Option<String>,
pub source_range: SourceRange,
}
impl FileNode {
pub fn new(id: FileId, path: PathBuf, language: LangId, snapshot_id: SnapshotId) -> Self {
Self {
id,
path,
language,
snapshot_id,
}
}
pub fn file_name(&self) -> Option<&str> {
self.path.file_name().and_then(|n| n.to_str())
}
pub fn extension(&self) -> Option<&str> {
self.path.extension().and_then(|e| e.to_str())
}
}
impl SymbolNode {
pub fn from_symbol(symbol: &crate::model::Symbol, file_id: FileId) -> Self {
Self {
id: symbol.id,
name: symbol.name.clone(),
kind: symbol.kind,
file_id,
visibility: symbol.visibility,
source_range: symbol.source_range.clone(),
}
}
}
impl DataGraphNode {
pub fn from_data_node(data: &crate::model::DataNode) -> Self {
Self {
id: data.id,
symbol_id: data.symbol_id,
name: data.name.clone(),
scope: data.scope,
type_hint: data.type_hint.clone(),
source_range: data.source_range.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{LineColumn, SourceRange};
fn test_path() -> PathBuf {
PathBuf::from("src/main.rs")
}
fn test_source_range() -> SourceRange {
SourceRange {
byte_start: 0,
byte_end: 10,
start: LineColumn { line: 0, column: 0 },
end: LineColumn {
line: 0,
column: 10,
},
}
}
#[test]
fn file_node_creation() {
let file_id = FileId::new(1).unwrap();
let snapshot_id = SnapshotId::new(1).unwrap();
let node = FileNode::new(file_id, test_path(), LangId::Rust, snapshot_id);
assert_eq!(node.id, file_id);
assert_eq!(node.path, test_path());
assert_eq!(node.language, LangId::Rust);
assert_eq!(node.snapshot_id, snapshot_id);
}
#[test]
fn file_node_file_name() {
let node = FileNode::new(
FileId::new(1).unwrap(),
PathBuf::from("src/main.rs"),
LangId::Rust,
SnapshotId::new(1).unwrap(),
);
assert_eq!(node.file_name(), Some("main.rs"));
}
#[test]
fn file_node_extension() {
let node = FileNode::new(
FileId::new(1).unwrap(),
PathBuf::from("test.py"),
LangId::Python,
SnapshotId::new(1).unwrap(),
);
assert_eq!(node.extension(), Some("py"));
}
#[test]
fn symbol_node_creation() {
let symbol = crate::model::Symbol {
id: SymbolId::new(42).unwrap(),
name: "test_function".to_string(),
kind: SymbolKind::Function,
language: LangId::Rust,
file_path: test_path(),
source_range: test_source_range(),
name_range: None,
visibility: Some(Visibility::Public),
signature: None,
docstring: None,
is_async: false,
};
let file_id = FileId::new(7).unwrap();
let node = SymbolNode::from_symbol(&symbol, file_id);
assert_eq!(node.id, SymbolId::new(42).unwrap());
assert_eq!(node.name, "test_function");
assert_eq!(node.kind, SymbolKind::Function);
assert_eq!(node.file_id, file_id);
assert_eq!(node.visibility, Some(Visibility::Public));
}
#[test]
fn node_data_file_variant() {
let file_node = FileNode::new(
FileId::new(1).unwrap(),
test_path(),
LangId::Rust,
SnapshotId::new(1).unwrap(),
);
let node_data = NodeData::File(file_node);
assert_eq!(node_data.kind_str(), "file");
assert!(node_data.as_file().is_some());
assert!(node_data.as_symbol().is_none());
assert_eq!(node_data.file_path(), Some(&test_path()));
assert_eq!(node_data.symbol_name(), None);
}
#[test]
fn node_data_symbol_variant() {
let symbol_node = SymbolNode {
id: SymbolId::new(1).unwrap(),
name: "my_func".to_string(),
kind: SymbolKind::Function,
file_id: FileId::new(1).unwrap(),
visibility: None,
source_range: test_source_range(),
};
let node_data = NodeData::Symbol(symbol_node);
assert_eq!(node_data.kind_str(), "symbol");
assert!(node_data.as_symbol().is_some());
assert!(node_data.as_file().is_none());
assert_eq!(node_data.file_path(), None);
assert_eq!(node_data.symbol_name(), Some("my_func"));
}
#[test]
fn data_graph_node_from_model() {
use crate::model::{DataNode, DataNodeId, DataScope};
let data = DataNode {
id: DataNodeId::new(5).unwrap(),
symbol_id: None,
name: Some("var_x".into()),
scope: DataScope::Local,
type_hint: Some("int".into()),
source_range: test_source_range(),
};
let gnode = DataGraphNode::from_data_node(&data);
assert_eq!(gnode.id, DataNodeId::new(5).unwrap());
assert_eq!(gnode.name.as_deref(), Some("var_x"));
assert_eq!(gnode.scope, DataScope::Local);
assert_eq!(gnode.type_hint.as_deref(), Some("int"));
assert!(gnode.symbol_id.is_none());
}
#[test]
fn node_data_data_variant() {
let dnode = DataGraphNode {
id: crate::model::DataNodeId::new(1).unwrap(),
symbol_id: Some(SymbolId::new(3).unwrap()),
name: Some("param".into()),
scope: crate::model::DataScope::Parameter,
type_hint: Some("str".into()),
source_range: test_source_range(),
};
let node_data = NodeData::Data(dnode);
assert_eq!(node_data.kind_str(), "data");
assert!(node_data.as_data().is_some());
assert!(node_data.as_file().is_none());
assert!(node_data.as_symbol().is_none());
assert_eq!(node_data.as_data().unwrap().name.as_deref(), Some("param"));
}
}