use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::super::node::id::NodeId;
use super::dispatch_tables::DispatchTables;
use super::framework_routes::{FrameworkRouteMetadata, FrameworkRoutesMap};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct MacroNodeMetadata {
pub macro_generated: Option<bool>,
pub macro_source: Option<String>,
pub cfg_condition: Option<String>,
pub cfg_active: Option<bool>,
pub proc_macro_kind: Option<ProcMacroFunctionKind>,
pub expansion_cached: Option<bool>,
pub unresolved_attributes: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProcMacroFunctionKind {
Derive,
Attribute,
FunctionLike,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClasspathNodeMetadata {
pub coordinates: Option<String>,
pub jar_path: String,
pub fqn: String,
pub is_direct_dependency: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypedMetadata {
Macro(MacroNodeMetadata),
Classpath(ClasspathNodeMetadata),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(transparent)]
pub struct NodeFlags(u8);
impl NodeFlags {
pub const SYNTHETIC: NodeFlags = NodeFlags(1 << 0);
pub const ADDRESS_TAKEN: NodeFlags = NodeFlags(1 << 1);
pub const CALLSITE_PROMISCUOUS: NodeFlags = NodeFlags(1 << 2);
pub const EMPTY: NodeFlags = NodeFlags(0);
#[must_use]
pub const fn contains(self, other: NodeFlags) -> bool {
(self.0 & other.0) == other.0
}
pub fn insert(&mut self, other: NodeFlags) {
self.0 |= other.0;
}
pub fn remove(&mut self, other: NodeFlags) {
self.0 &= !other.0;
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn bits(self) -> u8 {
self.0
}
#[must_use]
pub const fn from_bits(bits: u8) -> NodeFlags {
NodeFlags(bits)
}
}
impl std::ops::BitOr for NodeFlags {
type Output = NodeFlags;
fn bitor(self, rhs: NodeFlags) -> NodeFlags {
NodeFlags(self.0 | rhs.0)
}
}
impl std::ops::BitOrAssign for NodeFlags {
fn bitor_assign(&mut self, rhs: NodeFlags) {
self.0 |= rhs.0;
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StoredEntry {
pub typed: Option<TypedMetadata>,
pub flags: NodeFlags,
}
impl StoredEntry {
#[must_use]
pub fn with_typed(typed: TypedMetadata) -> StoredEntry {
StoredEntry {
typed: Some(typed),
flags: NodeFlags::EMPTY,
}
}
#[must_use]
pub fn with_flags(flags: NodeFlags) -> StoredEntry {
StoredEntry { typed: None, flags }
}
#[must_use]
pub fn is_vacant(&self) -> bool {
self.typed.is_none() && self.flags.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct NodeMetadataStore {
entries: HashMap<(u32, u64), StoredEntry>,
framework_routes: FrameworkRoutesMap,
dispatch_tables: DispatchTables,
}
const TYPED_KIND_NONE: u8 = 0;
const TYPED_KIND_MACRO: u8 = 1;
const TYPED_KIND_CLASSPATH: u8 = 2;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct NodeMetadataEntryV11 {
index: u32,
generation: u64,
kind: u8,
macro_data: Option<MacroNodeMetadata>,
classpath_data: Option<ClasspathNodeMetadata>,
flags: u8,
}
impl Serialize for NodeMetadataStore {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let entries: Vec<NodeMetadataEntryV11> = self
.entries
.iter()
.map(|(&(index, generation), stored)| {
let (kind, macro_data, classpath_data) = match &stored.typed {
None => (TYPED_KIND_NONE, None, None),
Some(TypedMetadata::Macro(m)) => (TYPED_KIND_MACRO, Some(m.clone()), None),
Some(TypedMetadata::Classpath(c)) => {
(TYPED_KIND_CLASSPATH, None, Some(c.clone()))
}
};
NodeMetadataEntryV11 {
index,
generation,
kind,
macro_data,
classpath_data,
flags: stored.flags.bits(),
}
})
.collect();
entries.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for NodeMetadataStore {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let entries: Vec<NodeMetadataEntryV11> = Vec::deserialize(deserializer)?;
let mut map = HashMap::with_capacity(entries.len());
for e in entries {
let typed = match e.kind {
TYPED_KIND_NONE => None,
TYPED_KIND_MACRO => Some(TypedMetadata::Macro(e.macro_data.unwrap_or_default())),
TYPED_KIND_CLASSPATH => {
let data = e.classpath_data.ok_or_else(|| {
serde::de::Error::custom(
"missing classpath_data for Classpath typed metadata entry",
)
})?;
Some(TypedMetadata::Classpath(data))
}
other => {
return Err(serde::de::Error::custom(format!(
"unknown typed-metadata kind discriminant {other}"
)));
}
};
let stored = StoredEntry {
typed,
flags: NodeFlags::from_bits(e.flags),
};
map.insert((e.index, e.generation), stored);
}
Ok(Self {
entries: map,
framework_routes: FrameworkRoutesMap::default(),
dispatch_tables: DispatchTables::default(),
})
}
}
impl NodeMetadataStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn get_typed(&self, node_id: NodeId) -> Option<&TypedMetadata> {
self.entries
.get(&(node_id.index(), node_id.generation()))?
.typed
.as_ref()
}
pub fn get_typed_mut(&mut self, node_id: NodeId) -> Option<&mut TypedMetadata> {
self.entries
.get_mut(&(node_id.index(), node_id.generation()))?
.typed
.as_mut()
}
#[must_use]
pub fn get_macro(&self, node_id: NodeId) -> Option<&MacroNodeMetadata> {
match self.get_typed(node_id)? {
TypedMetadata::Macro(m) => Some(m),
TypedMetadata::Classpath(_) => None,
}
}
pub fn get_macro_mut(&mut self, node_id: NodeId) -> Option<&mut MacroNodeMetadata> {
match self.get_typed_mut(node_id)? {
TypedMetadata::Macro(m) => Some(m),
TypedMetadata::Classpath(_) => None,
}
}
#[must_use]
pub fn get_flags(&self, node_id: NodeId) -> NodeFlags {
self.entries
.get(&(node_id.index(), node_id.generation()))
.map(|e| e.flags)
.unwrap_or(NodeFlags::EMPTY)
}
#[must_use]
pub fn is_synthetic(&self, node_id: NodeId) -> bool {
self.get_flags(node_id).contains(NodeFlags::SYNTHETIC)
}
#[must_use]
pub fn is_address_taken(&self, node_id: NodeId) -> bool {
self.get_flags(node_id).contains(NodeFlags::ADDRESS_TAKEN)
}
#[must_use]
pub fn is_callsite_promiscuous(&self, node_id: NodeId) -> bool {
self.get_flags(node_id)
.contains(NodeFlags::CALLSITE_PROMISCUOUS)
}
pub fn mark_synthetic(&mut self, node_id: NodeId) {
self.set_flag(node_id, NodeFlags::SYNTHETIC);
}
pub fn mark_address_taken(&mut self, node_id: NodeId) {
self.set_flag(node_id, NodeFlags::ADDRESS_TAKEN);
}
pub fn mark_callsite_promiscuous(&mut self, node_id: NodeId) {
self.set_flag(node_id, NodeFlags::CALLSITE_PROMISCUOUS);
}
fn set_flag(&mut self, node_id: NodeId, flag: NodeFlags) {
self.entries
.entry((node_id.index(), node_id.generation()))
.or_default()
.flags
.insert(flag);
}
pub fn insert(&mut self, node_id: NodeId, metadata: MacroNodeMetadata) {
self.insert_typed(node_id, TypedMetadata::Macro(metadata));
}
pub fn insert_typed(&mut self, node_id: NodeId, typed: TypedMetadata) {
let slot = self
.entries
.entry((node_id.index(), node_id.generation()))
.or_default();
slot.typed = Some(typed);
}
pub fn insert_entry(&mut self, node_id: NodeId, entry: StoredEntry) {
self.entries
.insert((node_id.index(), node_id.generation()), entry);
}
pub fn get_or_insert_default(&mut self, node_id: NodeId) -> &mut MacroNodeMetadata {
let slot = self
.entries
.entry((node_id.index(), node_id.generation()))
.or_insert_with(|| {
StoredEntry::with_typed(TypedMetadata::Macro(MacroNodeMetadata::default()))
});
if slot.typed.is_none() {
slot.typed = Some(TypedMetadata::Macro(MacroNodeMetadata::default()));
}
match slot.typed.as_mut() {
Some(TypedMetadata::Macro(m)) => m,
Some(TypedMetadata::Classpath(_)) => {
panic!("get_or_insert_default called on a Classpath typed metadata entry")
}
None => unreachable!("just populated above"),
}
}
pub fn remove(&mut self, node_id: NodeId) -> Option<MacroNodeMetadata> {
match self
.entries
.remove(&(node_id.index(), node_id.generation()))?
.typed
{
Some(TypedMetadata::Macro(m)) => Some(m),
Some(TypedMetadata::Classpath(_)) | None => None,
}
}
pub fn remove_entry(&mut self, node_id: NodeId) -> Option<StoredEntry> {
self.entries
.remove(&(node_id.index(), node_id.generation()))
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = ((u32, u64), &MacroNodeMetadata)> {
self.entries.iter().filter_map(|(&k, v)| match &v.typed {
Some(TypedMetadata::Macro(m)) => Some((k, m)),
Some(TypedMetadata::Classpath(_)) | None => None,
})
}
pub fn iter_entries(&self) -> impl Iterator<Item = ((u32, u64), &StoredEntry)> {
self.entries.iter().map(|(&k, v)| (k, v))
}
pub fn merge(&mut self, other: &NodeMetadataStore) {
for (&key, value) in &other.entries {
self.entries.insert(key, value.clone());
}
}
#[allow(dead_code)]
pub(crate) fn retain_entries<F>(&mut self, mut keep: F)
where
F: FnMut(u32, u64) -> bool,
{
self.entries
.retain(|&(index, generation), _entry| keep(index, generation));
}
#[cfg(any(test, feature = "test-support"))]
pub fn clear_phase_a_flags_for_test(&mut self) {
let mask = NodeFlags::ADDRESS_TAKEN | NodeFlags::CALLSITE_PROMISCUOUS;
for slot in self.entries.values_mut() {
slot.flags.remove(mask);
}
}
#[must_use]
pub fn framework_routes(&self) -> &FrameworkRoutesMap {
&self.framework_routes
}
pub fn framework_routes_mut(&mut self) -> &mut FrameworkRoutesMap {
&mut self.framework_routes
}
pub fn set_framework_routes(&mut self, routes: FrameworkRoutesMap) {
self.framework_routes = routes;
}
#[must_use]
pub fn framework_route(&self, node_id: NodeId) -> Option<&FrameworkRouteMetadata> {
self.framework_routes.get(&node_id)
}
#[must_use]
pub fn dispatch_tables(&self) -> &DispatchTables {
&self.dispatch_tables
}
pub fn dispatch_tables_mut(&mut self) -> &mut DispatchTables {
&mut self.dispatch_tables
}
pub fn set_dispatch_tables(&mut self, tables: DispatchTables) {
self.dispatch_tables = tables;
}
}
impl PartialEq for NodeMetadataStore {
fn eq(&self, other: &Self) -> bool {
self.entries == other.entries
&& self.framework_routes == other.framework_routes
&& self.dispatch_tables == other.dispatch_tables
}
}
impl Eq for NodeMetadataStore {}
impl crate::graph::unified::memory::GraphMemorySize for NodeMetadataStore {
fn heap_bytes(&self) -> usize {
use crate::graph::unified::memory::HASHMAP_ENTRY_OVERHEAD;
let base = self.entries.capacity()
* (std::mem::size_of::<(u32, u64)>()
+ std::mem::size_of::<StoredEntry>()
+ HASHMAP_ENTRY_OVERHEAD);
let inner: usize = self
.entries
.values()
.map(|entry| match &entry.typed {
None => 0,
Some(TypedMetadata::Macro(m)) => {
m.macro_source.as_ref().map_or(0, String::capacity)
+ m.cfg_condition.as_ref().map_or(0, String::capacity)
+ m.unresolved_attributes
.iter()
.map(String::capacity)
.sum::<usize>()
+ m.unresolved_attributes.capacity() * std::mem::size_of::<String>()
}
Some(TypedMetadata::Classpath(c)) => {
c.coordinates.as_ref().map_or(0, String::capacity)
+ c.jar_path.capacity()
+ c.fqn.capacity()
}
})
.sum();
let framework_routes_bytes = self.framework_routes.len()
* (std::mem::size_of::<NodeId>() + std::mem::size_of::<FrameworkRouteMetadata>());
let dt = &self.dispatch_tables;
let dispatch_tables_bytes = dt.jvm_virtual.len()
* (std::mem::size_of::<NodeId>()
+ std::mem::size_of::<super::dispatch_tables::JvmDispatchEntry>())
+ dt.go_interface.len()
* (std::mem::size_of::<NodeId>()
+ std::mem::size_of::<super::dispatch_tables::GoDispatchEntry>())
+ dt.python_duck.len()
* (std::mem::size_of::<NodeId>()
+ std::mem::size_of::<super::dispatch_tables::PythonDispatchEntry>())
+ dt.ts_structural.len()
* (std::mem::size_of::<NodeId>()
+ std::mem::size_of::<super::dispatch_tables::TsDispatchEntry>())
+ dt.cap_hits.len() * std::mem::size_of::<super::dispatch_tables::CapHit>();
base + inner + framework_routes_bytes + dispatch_tables_bytes
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metadata_store_basic_operations() {
let mut store = NodeMetadataStore::new();
assert!(store.is_empty());
assert_eq!(store.len(), 0);
let node = NodeId::new(5, 1);
let metadata = MacroNodeMetadata {
macro_generated: Some(true),
macro_source: Some("derive_Debug".to_string()),
..Default::default()
};
store.insert(node, metadata.clone());
assert_eq!(store.len(), 1);
assert!(!store.is_empty());
let retrieved = store.get_macro(node).unwrap();
assert_eq!(retrieved.macro_generated, Some(true));
assert_eq!(retrieved.macro_source.as_deref(), Some("derive_Debug"));
}
#[test]
fn test_metadata_full_nodeid_key() {
let mut store = NodeMetadataStore::new();
let node_gen1 = NodeId::new(5, 1);
let node_gen2 = NodeId::new(5, 2);
store.insert(
node_gen1,
MacroNodeMetadata {
macro_generated: Some(true),
..Default::default()
},
);
assert!(store.get_macro(node_gen2).is_none());
assert!(store.get_macro(node_gen1).is_some());
}
#[test]
fn test_metadata_slot_reuse_no_stale_data() {
let mut store = NodeMetadataStore::new();
let old_node = NodeId::new(5, 1);
store.insert(
old_node,
MacroNodeMetadata {
cfg_condition: Some("test".to_string()),
..Default::default()
},
);
let new_node = NodeId::new(5, 2);
assert!(store.get_macro(new_node).is_none());
assert_eq!(
store.get_macro(old_node).unwrap().cfg_condition.as_deref(),
Some("test")
);
}
#[test]
fn test_metadata_store_postcard_roundtrip() {
let mut store = NodeMetadataStore::new();
store.insert(
NodeId::new(1, 0),
MacroNodeMetadata {
macro_generated: Some(true),
macro_source: Some("derive_Debug".to_string()),
cfg_condition: Some("test".to_string()),
cfg_active: Some(true),
proc_macro_kind: Some(ProcMacroFunctionKind::Derive),
expansion_cached: Some(false),
unresolved_attributes: vec!["my_attr".to_string()],
},
);
store.insert(
NodeId::new(42, 3),
MacroNodeMetadata {
cfg_condition: Some("feature = \"serde\"".to_string()),
..Default::default()
},
);
let bytes = postcard::to_allocvec(&store).expect("serialize");
let deserialized: NodeMetadataStore = postcard::from_bytes(&bytes).expect("deserialize");
assert_eq!(store, deserialized);
}
#[test]
fn test_empty_metadata_store_zero_overhead() {
let store = NodeMetadataStore::new();
let bytes = postcard::to_allocvec(&store).expect("serialize");
assert!(
bytes.len() <= 2,
"Empty store should serialize to minimal bytes, got {} bytes",
bytes.len()
);
}
#[test]
fn test_metadata_store_merge() {
let mut store1 = NodeMetadataStore::new();
let mut store2 = NodeMetadataStore::new();
store1.insert(
NodeId::new(1, 0),
MacroNodeMetadata {
macro_generated: Some(true),
..Default::default()
},
);
store2.insert(
NodeId::new(2, 0),
MacroNodeMetadata {
cfg_condition: Some("test".to_string()),
..Default::default()
},
);
store1.merge(&store2);
assert_eq!(store1.len(), 2);
assert!(store1.get_macro(NodeId::new(1, 0)).is_some());
assert!(store1.get_macro(NodeId::new(2, 0)).is_some());
}
#[test]
fn test_proc_macro_function_kind_serde() {
let kinds = [
ProcMacroFunctionKind::Derive,
ProcMacroFunctionKind::Attribute,
ProcMacroFunctionKind::FunctionLike,
];
for kind in kinds {
let bytes = postcard::to_allocvec(&kind).expect("serialize");
let deserialized: ProcMacroFunctionKind =
postcard::from_bytes(&bytes).expect("deserialize");
assert_eq!(kind, deserialized);
}
}
#[test]
fn test_metadata_get_or_insert_default() {
let mut store = NodeMetadataStore::new();
let node = NodeId::new(10, 0);
let meta = store.get_or_insert_default(node);
meta.cfg_condition = Some("test".to_string());
let meta = store.get_macro(node).unwrap();
assert_eq!(meta.cfg_condition.as_deref(), Some("test"));
}
#[test]
fn test_metadata_remove() {
let mut store = NodeMetadataStore::new();
let node = NodeId::new(1, 0);
store.insert(
node,
MacroNodeMetadata {
macro_generated: Some(true),
..Default::default()
},
);
assert!(store.get_macro(node).is_some());
let removed = store.remove(node);
assert!(removed.is_some());
assert!(store.get_macro(node).is_none());
assert!(store.is_empty());
}
#[test]
fn test_metadata_store_large_scale() {
let mut store = NodeMetadataStore::new();
for i in 0..10_000u32 {
store.insert(
NodeId::new(i, 0),
MacroNodeMetadata {
cfg_condition: Some(format!("feature_{i}")),
..Default::default()
},
);
}
assert_eq!(store.len(), 10_000);
assert!(store.get_macro(NodeId::new(0, 0)).is_some());
assert!(store.get_macro(NodeId::new(5_000, 0)).is_some());
assert!(store.get_macro(NodeId::new(9_999, 0)).is_some());
assert!(store.get_macro(NodeId::new(10_000, 0)).is_none());
let bytes = postcard::to_allocvec(&store).expect("serialize");
let deserialized: NodeMetadataStore = postcard::from_bytes(&bytes).expect("deserialize");
assert_eq!(store, deserialized);
}
#[test]
fn test_classpath_metadata_insert_and_get() {
let mut store = NodeMetadataStore::new();
let node = NodeId::new(100, 0);
let cp_meta = ClasspathNodeMetadata {
coordinates: Some("com.google.guava:guava:33.0.0".to_string()),
jar_path: "/home/user/.m2/repository/guava-33.0.0.jar".to_string(),
fqn: "com.google.common.collect.ImmutableList".to_string(),
is_direct_dependency: true,
};
store.insert_typed(node, TypedMetadata::Classpath(cp_meta.clone()));
assert_eq!(store.len(), 1);
assert!(store.get_macro(node).is_none());
let retrieved = store.get_typed(node).unwrap();
match retrieved {
TypedMetadata::Classpath(cp) => {
assert_eq!(cp.fqn, "com.google.common.collect.ImmutableList");
assert_eq!(
cp.coordinates.as_deref(),
Some("com.google.guava:guava:33.0.0")
);
assert!(cp.is_direct_dependency);
}
TypedMetadata::Macro(_) => panic!("expected Classpath variant"),
}
}
#[test]
fn test_classpath_metadata_postcard_roundtrip() {
let mut store = NodeMetadataStore::new();
store.insert(
NodeId::new(1, 0),
MacroNodeMetadata {
macro_generated: Some(true),
..Default::default()
},
);
store.insert_typed(
NodeId::new(2, 0),
TypedMetadata::Classpath(ClasspathNodeMetadata {
coordinates: Some("org.slf4j:slf4j-api:2.0.0".to_string()),
jar_path: "slf4j-api-2.0.0.jar".to_string(),
fqn: "org.slf4j.Logger".to_string(),
is_direct_dependency: false,
}),
);
let bytes = postcard::to_allocvec(&store).expect("serialize");
let deserialized: NodeMetadataStore = postcard::from_bytes(&bytes).expect("deserialize");
assert_eq!(store, deserialized);
assert_eq!(deserialized.len(), 2);
assert!(deserialized.get_macro(NodeId::new(1, 0)).is_some());
let cp = deserialized.get_typed(NodeId::new(2, 0)).unwrap();
assert!(matches!(cp, TypedMetadata::Classpath(_)));
}
#[test]
fn test_node_metadata_store_json_roundtrip() {
let mut store = NodeMetadataStore::new();
store.insert(
NodeId::new(1, 0),
MacroNodeMetadata {
macro_generated: Some(true),
macro_source: Some("serde_derive".to_string()),
..Default::default()
},
);
store.insert_typed(
NodeId::new(2, 0),
TypedMetadata::Classpath(ClasspathNodeMetadata {
coordinates: None,
jar_path: "rt.jar".to_string(),
fqn: "java.lang.String".to_string(),
is_direct_dependency: true,
}),
);
let json = serde_json::to_string(&store).unwrap();
let deserialized: NodeMetadataStore = serde_json::from_str(&json).unwrap();
assert_eq!(store, deserialized);
}
#[test]
fn test_iter_entries_includes_both_types() {
let mut store = NodeMetadataStore::new();
store.insert(
NodeId::new(1, 0),
MacroNodeMetadata {
macro_generated: Some(true),
..Default::default()
},
);
store.insert_typed(
NodeId::new(2, 0),
TypedMetadata::Classpath(ClasspathNodeMetadata {
coordinates: None,
jar_path: "test.jar".to_string(),
fqn: "com.example.Test".to_string(),
is_direct_dependency: true,
}),
);
let macro_entries: Vec<_> = store.iter().collect();
assert_eq!(macro_entries.len(), 1);
let all_entries: Vec<_> = store.iter_entries().collect();
assert_eq!(all_entries.len(), 2);
}
#[test]
fn test_remove_entry_classpath() {
let mut store = NodeMetadataStore::new();
let node = NodeId::new(50, 0);
store.insert_typed(
node,
TypedMetadata::Classpath(ClasspathNodeMetadata {
coordinates: None,
jar_path: "test.jar".to_string(),
fqn: "Test".to_string(),
is_direct_dependency: true,
}),
);
assert_eq!(store.len(), 1);
let removed = store.remove(node);
assert!(removed.is_none());
assert!(store.is_empty());
}
#[test]
fn test_remove_entry_typed() {
let mut store = NodeMetadataStore::new();
let node = NodeId::new(50, 0);
store.insert_typed(
node,
TypedMetadata::Classpath(ClasspathNodeMetadata {
coordinates: None,
jar_path: "test.jar".to_string(),
fqn: "Test".to_string(),
is_direct_dependency: true,
}),
);
let removed = store.remove_entry(node);
assert!(matches!(
removed.as_ref().and_then(|e| e.typed.as_ref()),
Some(TypedMetadata::Classpath(_))
));
assert!(store.is_empty());
}
mod node_flags_tests {
use super::*;
#[test]
fn node_flags_bit_composition() {
let mut f = NodeFlags::EMPTY;
assert!(f.is_empty());
f.insert(NodeFlags::SYNTHETIC);
assert!(f.contains(NodeFlags::SYNTHETIC));
assert!(!f.contains(NodeFlags::ADDRESS_TAKEN));
f.insert(NodeFlags::ADDRESS_TAKEN);
assert!(
f.contains(NodeFlags::SYNTHETIC),
"ADDRESS_TAKEN insert must not clear SYNTHETIC"
);
assert!(f.contains(NodeFlags::ADDRESS_TAKEN));
assert!(f.contains(NodeFlags::EMPTY));
f.remove(NodeFlags::SYNTHETIC);
assert!(!f.contains(NodeFlags::SYNTHETIC));
assert!(f.contains(NodeFlags::ADDRESS_TAKEN));
let combined = NodeFlags::SYNTHETIC | NodeFlags::CALLSITE_PROMISCUOUS;
assert!(combined.contains(NodeFlags::SYNTHETIC));
assert!(combined.contains(NodeFlags::CALLSITE_PROMISCUOUS));
assert!(!combined.contains(NodeFlags::ADDRESS_TAKEN));
let mut acc = NodeFlags::EMPTY;
acc |= NodeFlags::ADDRESS_TAKEN;
acc |= NodeFlags::CALLSITE_PROMISCUOUS;
assert!(acc.contains(NodeFlags::ADDRESS_TAKEN | NodeFlags::CALLSITE_PROMISCUOUS));
}
#[test]
fn stored_entry_flags_only_no_typed_payload() {
let entry = StoredEntry::with_flags(NodeFlags::ADDRESS_TAKEN);
assert!(entry.typed.is_none());
assert!(entry.flags.contains(NodeFlags::ADDRESS_TAKEN));
assert!(!entry.flags.contains(NodeFlags::SYNTHETIC));
assert!(!entry.is_vacant());
assert!(StoredEntry::default().is_vacant());
}
#[test]
fn co_occurrence_macro_and_address_taken() {
let mut store = NodeMetadataStore::new();
let node = NodeId::new(42, 1);
store.insert(
node,
MacroNodeMetadata {
macro_generated: Some(true),
macro_source: Some("DEFINE_HANDLER".to_string()),
..Default::default()
},
);
store.mark_address_taken(node);
let typed = store.get_typed(node).expect("typed entry present");
assert!(matches!(typed, TypedMetadata::Macro(_)));
let macro_meta = store.get_macro(node).expect("macro payload present");
assert_eq!(macro_meta.macro_source.as_deref(), Some("DEFINE_HANDLER"));
assert!(store.is_address_taken(node));
assert!(!store.is_synthetic(node));
assert!(!store.is_callsite_promiscuous(node));
store.mark_synthetic(node);
assert!(store.is_synthetic(node));
assert!(store.is_address_taken(node));
assert!(
store.get_macro(node).is_some(),
"mark_synthetic must not clobber Macro payload"
);
}
#[test]
fn synthetic_via_flag_not_typed() {
let mut store = NodeMetadataStore::new();
let node = NodeId::new(7, 1);
assert!(!store.is_synthetic(node), "missing entry must report false");
store.mark_synthetic(node);
assert!(store.is_synthetic(node));
assert!(
store.get_typed(node).is_none(),
"mark_synthetic must not populate the typed slot"
);
assert!(store.get_macro(node).is_none());
let stale = NodeId::new(7, 2);
assert!(!store.is_synthetic(stale));
}
#[test]
fn mark_address_taken_preserves_typed_payload() {
let mut store = NodeMetadataStore::new();
let node = NodeId::new(101, 3);
let macro_meta = MacroNodeMetadata {
macro_generated: Some(true),
macro_source: Some("foo_macro".to_string()),
cfg_condition: Some("feature = \"x\"".to_string()),
..Default::default()
};
store.insert(node, macro_meta.clone());
store.mark_address_taken(node);
assert!(store.is_address_taken(node));
let retrieved = store.get_macro(node).expect("Macro payload preserved");
assert_eq!(retrieved, ¯o_meta);
}
#[test]
fn mark_callsite_promiscuous_independent_of_typed() {
let mut store = NodeMetadataStore::new();
let node = NodeId::new(202, 0);
store.mark_callsite_promiscuous(node);
assert!(store.is_callsite_promiscuous(node));
assert!(!store.is_synthetic(node));
assert!(!store.is_address_taken(node));
assert!(store.get_typed(node).is_none());
store.mark_synthetic(node);
assert!(store.is_callsite_promiscuous(node));
assert!(store.is_synthetic(node));
}
#[test]
fn get_flags_returns_empty_for_missing_entry() {
let store = NodeMetadataStore::new();
let missing = NodeId::new(999, 0);
assert!(store.get_flags(missing).is_empty());
assert!(!store.is_synthetic(missing));
assert!(!store.is_address_taken(missing));
assert!(!store.is_callsite_promiscuous(missing));
}
#[test]
fn get_macro_roundtrips_typed_macro_storage() {
let mut store = NodeMetadataStore::new();
let node_a = NodeId::new(1, 0);
let node_b = NodeId::new(2, 0);
let payload_a = MacroNodeMetadata {
cfg_condition: Some("a".to_string()),
..Default::default()
};
let payload_b = MacroNodeMetadata {
cfg_condition: Some("b".to_string()),
..Default::default()
};
store.insert(node_a, payload_a.clone());
store.insert_typed(node_b, TypedMetadata::Macro(payload_b.clone()));
assert_eq!(store.get_macro(node_a), Some(&payload_a));
assert_eq!(store.get_macro(node_b), Some(&payload_b));
}
#[test]
fn serialize_deserialize_preserves_typed_and_flags() {
let mut store = NodeMetadataStore::new();
store.insert(
NodeId::new(1, 0),
MacroNodeMetadata {
macro_generated: Some(true),
..Default::default()
},
);
store.mark_address_taken(NodeId::new(1, 0));
store.insert_typed(
NodeId::new(2, 0),
TypedMetadata::Classpath(ClasspathNodeMetadata {
coordinates: None,
jar_path: "x.jar".to_string(),
fqn: "com.example.X".to_string(),
is_direct_dependency: true,
}),
);
store.mark_synthetic(NodeId::new(3, 0));
store.mark_address_taken(NodeId::new(4, 9));
store.mark_callsite_promiscuous(NodeId::new(4, 9));
let bytes = postcard::to_allocvec(&store).expect("serialize");
let decoded: NodeMetadataStore = postcard::from_bytes(&bytes).expect("deserialize");
assert_eq!(store, decoded);
assert!(decoded.get_macro(NodeId::new(1, 0)).is_some());
assert!(decoded.is_address_taken(NodeId::new(1, 0)));
assert!(matches!(
decoded.get_typed(NodeId::new(2, 0)),
Some(TypedMetadata::Classpath(_))
));
assert!(decoded.is_synthetic(NodeId::new(3, 0)));
assert!(decoded.is_address_taken(NodeId::new(4, 9)));
assert!(decoded.is_callsite_promiscuous(NodeId::new(4, 9)));
}
#[test]
fn json_serialize_deserialize_preserves_typed_and_flags() {
let mut store = NodeMetadataStore::new();
store.insert(
NodeId::new(5, 5),
MacroNodeMetadata {
macro_generated: Some(true),
..Default::default()
},
);
store.mark_address_taken(NodeId::new(5, 5));
store.mark_synthetic(NodeId::new(9, 0));
let json = serde_json::to_string(&store).expect("json serialize");
let decoded: NodeMetadataStore = serde_json::from_str(&json).expect("json deserialize");
assert_eq!(store, decoded);
}
#[test]
fn insert_entry_bulk_remap_path() {
let mut original = NodeMetadataStore::new();
original.insert_typed(
NodeId::new(10, 0),
TypedMetadata::Classpath(ClasspathNodeMetadata {
coordinates: Some("g:a:1".to_string()),
jar_path: "j.jar".to_string(),
fqn: "F".to_string(),
is_direct_dependency: true,
}),
);
original.mark_address_taken(NodeId::new(10, 0));
original.mark_synthetic(NodeId::new(11, 0));
let mut remapped = NodeMetadataStore::new();
for (key, entry) in original.iter_entries() {
let nid = NodeId::new(key.0, key.1);
remapped.insert_entry(nid, entry.clone());
}
assert_eq!(original, remapped);
assert!(remapped.is_address_taken(NodeId::new(10, 0)));
assert!(matches!(
remapped.get_typed(NodeId::new(10, 0)),
Some(TypedMetadata::Classpath(_))
));
assert!(remapped.is_synthetic(NodeId::new(11, 0)));
}
}
}