use crate::symbol::SymbolId;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ByteSpan {
pub start: usize,
pub end: usize,
}
impl ByteSpan {
pub fn contains(&self, byte: usize) -> bool {
self.start <= byte && byte < self.end
}
pub fn len(&self) -> usize {
self.end.saturating_sub(self.start)
}
pub fn is_empty(&self) -> bool {
self.end <= self.start
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Occurrence {
pub file: String,
pub line: u32,
pub col: u32,
pub byte: usize,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SymbolKind {
Function,
Method,
Struct,
Enum,
Trait,
Interface,
Class,
TypeAlias,
Const,
Static,
Module,
Impl,
Table,
View,
Column,
Resource,
Other,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub enum EntryPoint {
Main,
HttpRoute(String),
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Visibility {
Public,
Internal,
Protected,
Private,
Unknown,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct Symbol {
pub id: SymbolId,
pub name: String,
pub kind: SymbolKind,
pub visibility: Visibility,
pub entry_points: Vec<EntryPoint>,
pub file: String,
pub line: u32,
pub span: ByteSpan,
pub signature: String,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RefRole {
Call,
IsImplementation,
Import,
ModuleRef,
TypeRef,
Read,
Write,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TypeRefContext {
ParameterType,
ReturnType,
Field,
GenericArg,
Attribute,
Other,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct Reference {
pub name: String,
pub occ: Occurrence,
pub role: RefRole,
pub source_module: Option<String>,
pub from_path: Option<String>,
pub qualifier: Option<String>,
pub scope: Option<ScopeId>,
pub type_ref_ctx: Option<TypeRefContext>,
}
pub type ScopeId = usize;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScopeKind {
Module,
Function,
Block,
Type,
Other,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Scope {
pub parent: Option<ScopeId>,
pub span: ByteSpan,
pub kind: ScopeKind,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BindingKind {
Local,
Param,
Import,
Definition,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BindingTarget {
Local,
Import(String),
Def(SymbolId),
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Binding {
pub scope: ScopeId,
pub name: String,
pub intro: usize,
pub kind: BindingKind,
pub target: BindingTarget,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Confidence {
Heuristic,
NameOnly,
Scoped,
Exact,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Provenance {
SymbolTable,
ScopeGraph,
FfiBridge,
Conformance,
NormalizedName,
External,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FfiAbi {
C,
Python,
Wasm,
NodeApi,
Jni,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FfiExport {
pub symbol: SymbolId,
pub abi: FfiAbi,
pub export_name: String,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct Edge {
pub from: SymbolId,
pub to: SymbolId,
pub role: RefRole,
pub confidence: Confidence,
pub provenance: Provenance,
pub occ: Occurrence,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct FileFacts {
pub file: String,
pub lang: String,
pub symbols: Vec<Symbol>,
pub references: Vec<Reference>,
pub scopes: Vec<Scope>,
pub bindings: Vec<Binding>,
pub ffi_exports: Vec<FfiExport>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct CodeGraph {
pub symbols: Vec<Symbol>,
pub edges: Vec<Edge>,
}
impl CodeGraph {
pub fn edges_min_confidence(&self, threshold: Confidence) -> impl Iterator<Item = &Edge> {
self.edges.iter().filter(move |e| e.confidence >= threshold)
}
pub fn min_confidence(&self, threshold: Confidence) -> CodeGraph {
CodeGraph {
symbols: self.symbols.clone(),
edges: self.edges_min_confidence(threshold).cloned().collect(),
}
}
}
#[cfg(test)]
mod confidence_tests {
use super::*;
use crate::symbol::{Descriptor, SymbolId};
fn make_id(name: &str) -> SymbolId {
SymbolId::global(
"rust",
vec![
Descriptor::Namespace("pkg".into()),
Descriptor::Term(name.into()),
],
)
}
fn make_edge(from: &str, to: &str, confidence: Confidence) -> Edge {
Edge {
from: make_id(from),
to: make_id(to),
role: RefRole::Call,
confidence,
provenance: Provenance::SymbolTable,
occ: Occurrence {
file: "src/a.rs".into(),
line: 1,
col: 0,
byte: 0,
},
}
}
fn make_graph_with_one_of_each() -> (CodeGraph, Vec<Symbol>) {
let symbols = vec![Symbol {
id: make_id("sym"),
name: "sym".into(),
kind: SymbolKind::Function,
visibility: Visibility::Public,
entry_points: Vec::new(),
file: "src/a.rs".into(),
line: 1,
span: ByteSpan { start: 0, end: 10 },
signature: "pub fn sym()".into(),
}];
let graph = CodeGraph {
symbols: symbols.clone(),
edges: vec![
make_edge("a", "b", Confidence::NameOnly),
make_edge("c", "d", Confidence::Scoped),
make_edge("e", "f", Confidence::Exact),
],
};
(graph, symbols)
}
#[test]
fn confidence_ordering_exact_gt_scoped() {
assert!(Confidence::Exact > Confidence::Scoped);
}
#[test]
fn confidence_ordering_scoped_gt_name_only() {
assert!(Confidence::Scoped > Confidence::NameOnly);
}
#[test]
fn confidence_ordering_exact_gt_name_only() {
assert!(Confidence::Exact > Confidence::NameOnly);
}
#[test]
fn edges_min_confidence_scoped_yields_two() {
let (graph, _) = make_graph_with_one_of_each();
let result: Vec<&Edge> = graph.edges_min_confidence(Confidence::Scoped).collect();
assert_eq!(result.len(), 2);
assert!(result.iter().all(|e| e.confidence >= Confidence::Scoped));
assert!(result.iter().any(|e| e.confidence == Confidence::Scoped));
assert!(result.iter().any(|e| e.confidence == Confidence::Exact));
}
#[test]
fn min_confidence_exact_keeps_one_edge_and_all_symbols() {
let (graph, symbols) = make_graph_with_one_of_each();
let filtered = graph.min_confidence(Confidence::Exact);
assert_eq!(filtered.edges.len(), 1);
assert_eq!(filtered.edges[0].confidence, Confidence::Exact);
assert_eq!(filtered.symbols.len(), symbols.len());
}
}
#[cfg(all(test, feature = "serde"))]
mod serde_tests {
use super::*;
use crate::symbol::{Descriptor, SymbolId};
fn make_symbol_id() -> SymbolId {
SymbolId::global(
"rust",
vec![
Descriptor::Namespace("auth".into()),
Descriptor::Term("validate".into()),
],
)
}
#[test]
fn symbol_id_serializes_as_scip_string() {
let id = make_symbol_id();
let json = serde_json::to_string(&id).expect("serialize SymbolId");
let expected = format!("\"{}\"", id.to_scip_string());
assert_eq!(json, expected);
}
#[test]
fn symbol_id_round_trips() {
let id = make_symbol_id();
let json = serde_json::to_string(&id).expect("serialize");
let id2: SymbolId = serde_json::from_str(&json).expect("deserialize");
assert_eq!(id.to_scip_string(), id2.to_scip_string());
}
#[test]
fn entry_point_variants_round_trip() {
let id = make_symbol_id();
let sym = Symbol {
id,
name: "handler".into(),
kind: SymbolKind::Function,
visibility: Visibility::Public,
entry_points: vec![EntryPoint::Main, EntryPoint::HttpRoute("app.route".into())],
file: "src/main.rs".into(),
line: 1,
span: ByteSpan { start: 0, end: 10 },
signature: "pub fn handler()".into(),
};
let json = serde_json::to_string(&sym).expect("serialize Symbol");
let sym2: Symbol = serde_json::from_str(&json).expect("deserialize Symbol");
let json2 = serde_json::to_string(&sym2).expect("re-serialize Symbol");
assert_eq!(json, json2);
}
#[test]
fn file_facts_round_trips_via_json() {
let id = make_symbol_id();
let facts = FileFacts {
file: "src/auth.rs".into(),
lang: "rust".into(),
symbols: vec![Symbol {
id: id.clone(),
name: "validate".into(),
kind: SymbolKind::Function,
visibility: Visibility::Public,
entry_points: Vec::new(),
file: "src/auth.rs".into(),
line: 1,
span: ByteSpan { start: 0, end: 20 },
signature: "pub fn validate()".into(),
}],
references: vec![Reference {
name: "validate".into(),
occ: Occurrence {
file: "src/main.rs".into(),
line: 5,
col: 4,
byte: 80,
},
role: RefRole::Call,
source_module: None,
from_path: None,
qualifier: None,
scope: None,
type_ref_ctx: None,
}],
scopes: vec![],
bindings: vec![],
ffi_exports: vec![],
};
let json = serde_json::to_string(&facts).expect("serialize FileFacts");
let facts2: FileFacts = serde_json::from_str(&json).expect("deserialize FileFacts");
let json2 = serde_json::to_string(&facts2).expect("re-serialize FileFacts");
assert_eq!(json, json2);
}
}