use perl_semantic_facts::{EntityId, ValueShape};
use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct ValueShapeIndex {
shapes: HashMap<EntityId, ValueShape>,
file_entities: HashMap<String, Vec<EntityId>>,
}
impl ValueShapeIndex {
pub fn new() -> Self {
Self::default()
}
pub fn add_shapes(&mut self, source_uri: &str, shapes: Vec<(EntityId, ValueShape)>) {
let entity_ids: Vec<EntityId> = shapes.iter().map(|(id, _)| *id).collect();
self.file_entities.insert(source_uri.to_string(), entity_ids);
for (entity_id, shape) in shapes {
self.shapes.insert(entity_id, shape);
}
}
pub fn remove_shapes_for_file(&mut self, source_uri: &str) {
let entity_ids = match self.file_entities.remove(source_uri) {
Some(ids) => ids,
None => return,
};
for id in &entity_ids {
self.shapes.remove(id);
}
}
pub fn get(&self, entity_id: EntityId) -> Option<&ValueShape> {
self.shapes.get(&entity_id)
}
pub fn len(&self) -> usize {
self.shapes.len()
}
pub fn is_empty(&self) -> bool {
self.shapes.is_empty()
}
pub fn resolve_receiver_package(shape: &ValueShape) -> Option<&str> {
match shape {
ValueShape::Object { package, .. } => Some(package.as_str()),
ValueShape::PackageName { package } => Some(package.as_str()),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use perl_semantic_facts::Confidence;
#[test]
fn empty_index_has_no_shapes() -> Result<(), Box<dyn std::error::Error>> {
let index = ValueShapeIndex::new();
assert_eq!(index.len(), 0);
assert!(index.is_empty());
Ok(())
}
#[test]
fn add_shapes_populates_index() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ValueShapeIndex::new();
index.add_shapes(
"file:///lib/Foo.pm",
vec![
(
EntityId(1),
ValueShape::Object { package: "Foo".to_string(), confidence: Confidence::High },
),
(EntityId(2), ValueShape::Unknown),
],
);
assert_eq!(index.len(), 2);
assert!(!index.is_empty());
let shape1 = index.get(EntityId(1)).ok_or("expected shape for EntityId(1)")?;
assert_eq!(
*shape1,
ValueShape::Object { package: "Foo".to_string(), confidence: Confidence::High }
);
let shape2 = index.get(EntityId(2)).ok_or("expected shape for EntityId(2)")?;
assert_eq!(*shape2, ValueShape::Unknown);
Ok(())
}
#[test]
fn get_returns_none_for_unknown_entity() -> Result<(), Box<dyn std::error::Error>> {
let index = ValueShapeIndex::new();
assert!(index.get(EntityId(999)).is_none());
Ok(())
}
#[test]
fn remove_shapes_for_file_clears_entries() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ValueShapeIndex::new();
index.add_shapes("file:///lib/Foo.pm", vec![(EntityId(1), ValueShape::Scalar)]);
assert_eq!(index.len(), 1);
index.remove_shapes_for_file("file:///lib/Foo.pm");
assert_eq!(index.len(), 0);
assert!(index.is_empty());
assert!(index.get(EntityId(1)).is_none());
Ok(())
}
#[test]
fn remove_shapes_for_file_is_idempotent() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ValueShapeIndex::new();
index.add_shapes("file:///lib/Foo.pm", vec![(EntityId(1), ValueShape::Scalar)]);
index.remove_shapes_for_file("file:///lib/Foo.pm");
index.remove_shapes_for_file("file:///lib/Foo.pm");
assert_eq!(index.len(), 0);
Ok(())
}
#[test]
fn remove_unknown_file_is_noop() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ValueShapeIndex::new();
index.add_shapes("file:///lib/Foo.pm", vec![(EntityId(1), ValueShape::Scalar)]);
index.remove_shapes_for_file("file:///nonexistent.pm");
assert_eq!(index.len(), 1);
assert!(index.get(EntityId(1)).is_some());
Ok(())
}
#[test]
fn multiple_files_coexist() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ValueShapeIndex::new();
index.add_shapes(
"file:///lib/Foo.pm",
vec![(
EntityId(1),
ValueShape::Object { package: "Foo".to_string(), confidence: Confidence::High },
)],
);
index.add_shapes(
"file:///lib/Bar.pm",
vec![(
EntityId(2),
ValueShape::Object { package: "Bar".to_string(), confidence: Confidence::Medium },
)],
);
assert_eq!(index.len(), 2);
index.remove_shapes_for_file("file:///lib/Foo.pm");
assert_eq!(index.len(), 1);
assert!(index.get(EntityId(1)).is_none());
assert!(index.get(EntityId(2)).is_some());
Ok(())
}
#[test]
fn incremental_reindex_replaces_shapes() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ValueShapeIndex::new();
index.add_shapes("file:///lib/Foo.pm", vec![(EntityId(1), ValueShape::Unknown)]);
let shape = index.get(EntityId(1)).ok_or("expected shape")?;
assert_eq!(*shape, ValueShape::Unknown);
index.remove_shapes_for_file("file:///lib/Foo.pm");
index.add_shapes(
"file:///lib/Foo.pm",
vec![(
EntityId(1),
ValueShape::Object { package: "Foo".to_string(), confidence: Confidence::High },
)],
);
let shape = index.get(EntityId(1)).ok_or("expected updated shape")?;
assert_eq!(
*shape,
ValueShape::Object { package: "Foo".to_string(), confidence: Confidence::High }
);
Ok(())
}
#[test]
fn last_writer_wins_within_file() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ValueShapeIndex::new();
index.add_shapes(
"file:///lib/Foo.pm",
vec![(EntityId(1), ValueShape::Unknown), (EntityId(1), ValueShape::Scalar)],
);
let shape = index.get(EntityId(1)).ok_or("expected shape")?;
assert_eq!(*shape, ValueShape::Scalar);
Ok(())
}
#[test]
fn resolve_receiver_package_for_object() -> Result<(), Box<dyn std::error::Error>> {
let shape = ValueShape::Object { package: "Foo".to_string(), confidence: Confidence::High };
let pkg = ValueShapeIndex::resolve_receiver_package(&shape)
.ok_or("expected package from Object shape")?;
assert_eq!(pkg, "Foo");
Ok(())
}
#[test]
fn resolve_receiver_package_for_package_name() -> Result<(), Box<dyn std::error::Error>> {
let shape = ValueShape::PackageName { package: "Bar".to_string() };
let pkg = ValueShapeIndex::resolve_receiver_package(&shape)
.ok_or("expected package from PackageName shape")?;
assert_eq!(pkg, "Bar");
Ok(())
}
#[test]
fn resolve_receiver_package_returns_none_for_unknown() -> Result<(), Box<dyn std::error::Error>>
{
assert!(ValueShapeIndex::resolve_receiver_package(&ValueShape::Unknown).is_none());
assert!(ValueShapeIndex::resolve_receiver_package(&ValueShape::Scalar).is_none());
assert!(ValueShapeIndex::resolve_receiver_package(&ValueShape::ArrayRef).is_none());
assert!(ValueShapeIndex::resolve_receiver_package(&ValueShape::HashRef).is_none());
assert!(ValueShapeIndex::resolve_receiver_package(&ValueShape::CodeRef).is_none());
Ok(())
}
#[test]
fn all_shape_variants_round_trip() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ValueShapeIndex::new();
let shapes = vec![
(EntityId(1), ValueShape::Unknown),
(EntityId(2), ValueShape::Scalar),
(EntityId(3), ValueShape::ArrayRef),
(EntityId(4), ValueShape::HashRef),
(EntityId(5), ValueShape::CodeRef),
(EntityId(6), ValueShape::PackageName { package: "Foo".to_string() }),
(
EntityId(7),
ValueShape::Object { package: "Bar".to_string(), confidence: Confidence::Low },
),
];
index.add_shapes("file:///lib/Test.pm", shapes.clone());
for (id, expected) in &shapes {
let actual = index.get(*id).ok_or(format!("expected shape for {id:?}"))?;
assert_eq!(actual, expected);
}
Ok(())
}
}