use super::node::{NodeId, Span};
use crate::relations::CallIdentityMetadata;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct EdgeId(u64);
impl EdgeId {
pub fn new() -> Self {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
Self(COUNTER.fetch_add(1, Ordering::SeqCst))
}
#[must_use]
pub const fn as_u64(&self) -> u64 {
self.0
}
#[must_use]
pub const fn from_u64(id: u64) -> Self {
Self(id)
}
}
impl Default for EdgeId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for EdgeId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "edge#{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum EdgeKind {
Call {
argument_count: usize,
is_async: bool,
},
Import {
alias: Option<String>,
is_wildcard: bool,
},
Inherits,
Implements,
HTTPRequest {
method: String,
endpoint: String,
},
FFICall {
ffi_type: FFIType,
},
FieldAccess {
field_name: String,
},
TableRead {
table_name: String,
schema: Option<String>,
},
TableWrite {
table_name: String,
schema: Option<String>,
operation: TableWriteOp,
},
TriggeredBy {
trigger_name: String,
schema: Option<String>,
},
ChannelInvoke {
channel_name: String,
method: String,
},
WidgetChild {
widget_type: String,
},
Export {
kind: ExportKind,
symbol: Option<String>,
alias: Option<String>,
from_module: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TableWriteOp {
Insert,
Update,
Delete,
Merge,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ExportKind {
Named,
NamedTypeOnly,
Default,
Namespace,
NamespaceTypeOnly,
AllFromModule,
AllFromModuleTypeOnly,
Assignment,
GlobalNamespace,
}
impl fmt::Display for ExportKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ExportKind::Named => write!(f, "named"),
ExportKind::NamedTypeOnly => write!(f, "named-type-only"),
ExportKind::Default => write!(f, "default"),
ExportKind::Namespace => write!(f, "namespace"),
ExportKind::NamespaceTypeOnly => write!(f, "namespace-type-only"),
ExportKind::AllFromModule => write!(f, "all-from-module"),
ExportKind::AllFromModuleTypeOnly => write!(f, "all-from-module-type-only"),
ExportKind::Assignment => write!(f, "assignment"),
ExportKind::GlobalNamespace => write!(f, "global-namespace"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum FFIType {
NodeFFI,
Ctypes,
CFFI,
RustExtern,
JNI,
ElixirNIF,
RDotCall,
Rcpp,
Other(String),
}
impl fmt::Display for FFIType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
FFIType::NodeFFI => write!(f, "node-ffi"),
FFIType::Ctypes => write!(f, "ctypes"),
FFIType::CFFI => write!(f, "cffi"),
FFIType::RustExtern => write!(f, "extern-C"),
FFIType::JNI => write!(f, "JNI"),
FFIType::ElixirNIF => write!(f, "elixir-nif"),
FFIType::RDotCall => write!(f, "r-dotcall"),
FFIType::Rcpp => write!(f, "rcpp"),
FFIType::Other(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DetectionMethod {
ASTAnalysis,
TypeInference,
Heuristic,
Manual,
#[default]
Unknown,
}
#[derive(Debug, Clone)]
pub struct EdgeMetadata {
pub span: Option<Span>,
pub confidence: f32,
pub detection_method: DetectionMethod,
pub reason: Option<String>,
pub caller_identity: Option<CallIdentityMetadata>,
pub callee_identity: Option<CallIdentityMetadata>,
}
impl Default for EdgeMetadata {
fn default() -> Self {
Self {
span: None,
confidence: 1.0,
detection_method: DetectionMethod::Unknown,
reason: None,
caller_identity: None,
callee_identity: None,
}
}
}
#[derive(Debug, Clone)]
pub struct CodeEdge {
pub id: EdgeId,
pub from: NodeId,
pub to: NodeId,
pub kind: EdgeKind,
pub metadata: EdgeMetadata,
}
impl CodeEdge {
#[must_use]
pub fn new(from: NodeId, to: NodeId, kind: EdgeKind) -> Self {
Self {
id: EdgeId::new(),
from,
to,
kind,
metadata: EdgeMetadata::default(),
}
}
#[must_use]
pub fn with_metadata(from: NodeId, to: NodeId, kind: EdgeKind, metadata: EdgeMetadata) -> Self {
Self {
id: EdgeId::new(),
from,
to,
kind,
metadata,
}
}
#[must_use]
pub fn is_cross_language(&self) -> bool {
if self.from.language != self.to.language {
return true;
}
if matches!(self.kind, EdgeKind::HTTPRequest { .. }) {
return true;
}
if matches!(self.kind, EdgeKind::FFICall { .. }) {
return true;
}
false
}
#[must_use]
pub fn http_method(&self) -> Option<&str> {
match &self.kind {
EdgeKind::HTTPRequest { method, .. } => Some(method),
_ => None,
}
}
#[must_use]
pub fn http_endpoint(&self) -> Option<&str> {
match &self.kind {
EdgeKind::HTTPRequest { endpoint, .. } => Some(endpoint),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::node::Language;
use approx::assert_abs_diff_eq;
#[test]
fn test_edge_id_unique() {
let id1 = EdgeId::new();
let id2 = EdgeId::new();
let id3 = EdgeId::new();
assert_ne!(id1, id2);
assert_ne!(id2, id3);
assert_ne!(id1, id3);
}
#[test]
fn test_edge_creation() {
let from = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
let to = NodeId::new(Language::Python, "api.py", "get_users");
let edge = CodeEdge::new(
from.clone(),
to.clone(),
EdgeKind::HTTPRequest {
method: "GET".to_string(),
endpoint: "/api/users".to_string(),
},
);
assert_eq!(edge.from, from);
assert_eq!(edge.to, to);
assert!(edge.is_cross_language());
}
#[test]
fn test_cross_language_detection() {
let js_node = NodeId::new(Language::JavaScript, "api.js", "fetch");
let py_node = NodeId::new(Language::Python, "api.py", "handler");
let js_node2 = NodeId::new(Language::JavaScript, "utils.js", "helper");
let cross_language = CodeEdge::new(
js_node.clone(),
py_node,
EdgeKind::HTTPRequest {
method: "POST".to_string(),
endpoint: "/api/data".to_string(),
},
);
let same_lang = CodeEdge::new(
js_node,
js_node2,
EdgeKind::Call {
argument_count: 2,
is_async: true,
},
);
assert!(cross_language.is_cross_language());
assert!(!same_lang.is_cross_language());
}
#[test]
fn test_http_requests_are_cross_language() {
let from = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
let to = NodeId::new(Language::JavaScript, "api.js", "httpGet");
let http_edge = CodeEdge::new(
from.clone(),
to.clone(),
EdgeKind::HTTPRequest {
method: "GET".to_string(),
endpoint: "/api/users".to_string(),
},
);
assert!(http_edge.is_cross_language());
let call_edge = CodeEdge::new(
from,
to,
EdgeKind::Call {
argument_count: 1,
is_async: true,
},
);
assert!(!call_edge.is_cross_language());
}
#[test]
fn test_ffi_calls_are_cross_language() {
let from = NodeId::new(Language::Python, "api.py", "authenticate");
let to = NodeId::new(Language::Python, "api.py", "validate_token");
let ffi_edge = CodeEdge::new(
from.clone(),
to.clone(),
EdgeKind::FFICall {
ffi_type: FFIType::Ctypes,
},
);
assert!(ffi_edge.is_cross_language());
let call_edge = CodeEdge::new(
from,
to,
EdgeKind::Call {
argument_count: 1,
is_async: false,
},
);
assert!(!call_edge.is_cross_language());
}
#[test]
fn test_http_helpers() {
let from = NodeId::new(Language::JavaScript, "api.js", "fetch");
let to = NodeId::new(Language::Http, "api", "/users");
let edge = CodeEdge::new(
from,
to,
EdgeKind::HTTPRequest {
method: "GET".to_string(),
endpoint: "/api/users".to_string(),
},
);
assert_eq!(edge.http_method(), Some("GET"));
assert_eq!(edge.http_endpoint(), Some("/api/users"));
}
#[test]
fn test_edge_metadata() {
let from = NodeId::new(Language::JavaScript, "api.js", "fetch");
let to = NodeId::new(Language::Http, "api", "/users");
let metadata = EdgeMetadata {
span: None,
confidence: 0.8,
detection_method: DetectionMethod::Heuristic,
reason: Some("Template literal with interpolation".to_string()),
..Default::default()
};
let edge = CodeEdge::with_metadata(
from,
to,
EdgeKind::HTTPRequest {
method: "GET".to_string(),
endpoint: "/api/users/${id}".to_string(),
},
metadata,
);
assert_abs_diff_eq!(edge.metadata.confidence, 0.8, epsilon = 1e-10);
assert!(edge.metadata.reason.is_some());
assert!(edge.metadata.span.is_none());
}
#[test]
fn test_ffi_type_display() {
assert_eq!(FFIType::NodeFFI.to_string(), "node-ffi");
assert_eq!(FFIType::Ctypes.to_string(), "ctypes");
assert_eq!(FFIType::RustExtern.to_string(), "extern-C");
assert_eq!(FFIType::Other("custom".to_string()).to_string(), "custom");
}
}