use crate::symbol::SymbolId;
pub const FILE_FACTS_SCHEMA_VERSION: u32 = 1;
pub const CODE_GRAPH_SCHEMA_VERSION: u32 = 1;
#[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, PartialOrd, Ord, 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, PartialOrd, Ord, 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, PartialEq, Eq, Hash)]
pub struct EdgeKey {
pub from: SymbolId,
pub to: SymbolId,
pub role: RefRole,
pub occurrence_file: String,
pub occurrence_byte: usize,
pub provenance: Provenance,
}
impl Ord for EdgeKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(
&self.from,
&self.to,
self.role,
&self.occurrence_file,
self.occurrence_byte,
self.provenance,
)
.cmp(&(
&other.from,
&other.to,
other.role,
&other.occurrence_file,
other.occurrence_byte,
other.provenance,
))
}
}
impl PartialOrd for EdgeKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[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,
}
impl Edge {
pub fn key(&self) -> EdgeKey {
EdgeKey {
from: self.from.clone(),
to: self.to.clone(),
role: self.role,
occurrence_file: self.occ.file.clone(),
occurrence_byte: self.occ.byte,
provenance: self.provenance,
}
}
}
#[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 schema_tests {
use super::*;
#[test]
fn cache_schema_epochs_are_stable() {
assert_eq!(FILE_FACTS_SCHEMA_VERSION, 1);
assert_eq!(CODE_GRAPH_SCHEMA_VERSION, 1);
}
}
#[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 edge_key_uses_every_identity_dimension_except_confidence() {
let edge = make_edge("from", "to", Confidence::NameOnly);
let key = edge.key();
assert_eq!(key.from, edge.from);
assert_eq!(key.to, edge.to);
assert_eq!(key.role, edge.role);
assert_eq!(key.occurrence_file, edge.occ.file);
assert_eq!(key.occurrence_byte, edge.occ.byte);
assert_eq!(key.provenance, edge.provenance);
let mut changed = edge.clone();
changed.confidence = Confidence::Exact;
assert_eq!(changed.key(), key, "confidence is not edge identity");
changed.from = SymbolId::global(
"python",
vec![
Descriptor::Namespace("pkg".into()),
Descriptor::Term("from".into()),
],
);
assert_eq!(changed.from.to_scip_string(), edge.from.to_scip_string());
assert_ne!(
changed.key(),
key,
"structural SymbolId coordinates are identity"
);
let mut changed = edge.clone();
changed.to = make_id("other");
assert_ne!(changed.key(), key);
changed = edge.clone();
changed.role = RefRole::Read;
assert_ne!(changed.key(), key);
changed = edge.clone();
changed.occ.file = "src/b.rs".into();
assert_ne!(changed.key(), key);
changed = edge.clone();
changed.occ.byte = 1;
assert_ne!(changed.key(), key);
changed = edge.clone();
changed.provenance = Provenance::ScopeGraph;
assert_ne!(changed.key(), key);
}
#[test]
fn edge_key_component_enum_orders_are_stable() {
let roles = [
RefRole::Call,
RefRole::IsImplementation,
RefRole::Import,
RefRole::ModuleRef,
RefRole::TypeRef,
RefRole::Read,
RefRole::Write,
];
assert!(roles.windows(2).all(|pair| pair[0] < pair[1]));
let provenances = [
Provenance::SymbolTable,
Provenance::ScopeGraph,
Provenance::FfiBridge,
Provenance::Conformance,
Provenance::NormalizedName,
Provenance::External,
];
assert!(provenances.windows(2).all(|pair| pair[0] < pair[1]));
}
#[test]
fn edge_key_ordering_uses_all_key_dimensions_and_ignores_confidence() {
use std::collections::BTreeSet;
let base = make_edge("from", "to", Confidence::NameOnly);
let same_key_different_confidence = make_edge("from", "to", Confidence::Exact);
assert_eq!(base.key(), same_key_different_confidence.key());
let mut changed_from = base.clone();
changed_from.from = make_id("other-from");
let mut changed_to = base.clone();
changed_to.to = make_id("other-to");
let mut changed_role = base.clone();
changed_role.role = RefRole::Read;
let mut changed_file = base.clone();
changed_file.occ.file = "src/b.rs".into();
let mut changed_byte = base.clone();
changed_byte.occ.byte = 1;
let mut changed_provenance = base.clone();
changed_provenance.provenance = Provenance::ScopeGraph;
let keys = [
base.key(),
changed_from.key(),
changed_to.key(),
changed_role.key(),
changed_file.key(),
changed_byte.key(),
changed_provenance.key(),
];
let forward: BTreeSet<_> = keys.iter().cloned().collect();
let reverse: BTreeSet<_> = keys.iter().rev().cloned().collect();
assert_eq!(
forward.len(),
keys.len(),
"every key dimension participates"
);
assert_eq!(
forward, reverse,
"ordering is independent of insertion order"
);
assert_eq!(
forward.into_iter().collect::<Vec<_>>(),
reverse.into_iter().collect::<Vec<_>>(),
"ordered output is stable across insertion permutations"
);
}
#[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_lossless_scip_wire_object() {
let id = make_symbol_id();
let json = serde_json::to_value(&id).expect("serialize SymbolId");
assert_eq!(json["version"], 1);
assert_eq!(json["scip"], id.to_scip_string());
assert_eq!(json["lang"], "rust");
}
#[test]
fn symbol_id_serde_preserves_complete_structural_identity() {
let id = SymbolId::global(
"python",
vec![
Descriptor::Namespace("auth".into()),
Descriptor::Term("validate".into()),
],
);
let json = serde_json::to_string(&id).expect("serialize");
let restored: SymbolId = serde_json::from_str(&json).expect("deserialize");
assert_eq!(
restored, id,
"serde must preserve every identity coordinate, including language"
);
}
#[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, id2, "the versioned wire format is lossless");
}
#[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);
}
}