use crate::core::Span;
use crate::semantic::types::TokenType;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ReferenceInfo {
pub source_qname: String,
pub file: PathBuf,
pub span: Span,
pub token_type: Option<TokenType>,
}
#[derive(Debug, Clone, Default)]
struct ReferenceEntry {
references: HashSet<ReferenceInfo>,
}
#[derive(Debug, Clone, Default)]
pub struct ReferenceIndex {
reverse: HashMap<String, ReferenceEntry>,
forward: HashMap<String, HashSet<String>>,
source_to_file: HashMap<String, PathBuf>,
}
impl ReferenceIndex {
pub fn new() -> Self {
Self::default()
}
pub fn add_reference(
&mut self,
source_qname: &str,
target_name: &str,
source_file: Option<&PathBuf>,
span: Option<Span>,
) {
self.add_reference_with_type(source_qname, target_name, source_file, span, None);
}
pub fn add_reference_with_type(
&mut self,
source_qname: &str,
target_name: &str,
source_file: Option<&PathBuf>,
span: Option<Span>,
token_type: Option<TokenType>,
) {
if let (Some(file), Some(span)) = (source_file, span) {
let info = ReferenceInfo {
source_qname: source_qname.to_string(),
file: file.clone(),
span,
token_type,
};
self.reverse
.entry(target_name.to_string())
.or_default()
.references
.insert(info);
self.forward
.entry(source_qname.to_string())
.or_default()
.insert(target_name.to_string());
self.source_to_file
.insert(source_qname.to_string(), file.clone());
}
}
pub fn get_references(&self, target: &str) -> Vec<&ReferenceInfo> {
self.reverse
.get(target)
.map(|entry| entry.references.iter().collect())
.unwrap_or_default()
}
pub fn get_targets(&self, source_qname: &str) -> Vec<&str> {
self.forward
.get(source_qname)
.map(|targets| targets.iter().map(|s| s.as_str()).collect())
.unwrap_or_default()
}
pub fn get_sources(&self, target: &str) -> Vec<&str> {
self.reverse
.get(target)
.map(|entry| {
entry
.references
.iter()
.map(|r| r.source_qname.as_str())
.collect()
})
.unwrap_or_default()
}
pub fn has_references(&self, target: &str) -> bool {
self.reverse
.get(target)
.map(|entry| !entry.references.is_empty())
.unwrap_or(false)
}
pub fn targets(&self) -> Vec<&str> {
self.reverse.keys().map(|s| s.as_str()).collect()
}
pub fn remove_references_from_file(&mut self, file_path: &str) {
let path = PathBuf::from(file_path);
for entry in self.reverse.values_mut() {
entry.references.retain(|r| r.file != path);
}
let sources_to_remove: Vec<String> = self
.source_to_file
.iter()
.filter(|(_, f)| *f == &path)
.map(|(s, _)| s.clone())
.collect();
for source in &sources_to_remove {
self.source_to_file.remove(source);
self.forward.remove(source);
}
self.reverse.retain(|_, entry| !entry.references.is_empty());
}
pub fn remove_source(&mut self, source_qname: &str) {
self.source_to_file.remove(source_qname);
self.forward.remove(source_qname);
for entry in self.reverse.values_mut() {
entry.references.retain(|r| r.source_qname != source_qname);
}
self.reverse.retain(|_, entry| !entry.references.is_empty());
}
pub fn clear(&mut self) {
self.reverse.clear();
self.forward.clear();
self.source_to_file.clear();
}
pub fn target_count(&self) -> usize {
self.reverse.len()
}
pub fn reference_count(&self) -> usize {
self.reverse.values().map(|e| e.references.len()).sum()
}
pub fn get_references_in_file(&self, file_path: &str) -> Vec<&ReferenceInfo> {
let path = PathBuf::from(file_path);
self.reverse
.values()
.flat_map(|entry| entry.references.iter())
.filter(|r| r.file == path)
.collect()
}
pub fn resolve_targets<F>(&mut self, mut resolve_fn: F)
where
F: FnMut(&str, &PathBuf) -> Option<String>,
{
let mut updates: Vec<(String, String, ReferenceInfo)> = Vec::new();
for (target_name, entry) in &self.reverse {
if target_name.contains("::") {
continue;
}
for ref_info in &entry.references {
if let Some(qualified_name) = resolve_fn(target_name, &ref_info.file) {
if &qualified_name != target_name {
updates.push((target_name.clone(), qualified_name, ref_info.clone()));
}
}
}
}
for (old_target, new_target, ref_info) in updates {
if let Some(entry) = self.reverse.get_mut(&old_target) {
entry.references.remove(&ref_info);
}
self.reverse
.entry(new_target.clone())
.or_default()
.references
.insert(ref_info.clone());
if let Some(targets) = self.forward.get_mut(&ref_info.source_qname) {
targets.remove(&old_target);
targets.insert(new_target);
}
}
self.reverse.retain(|_, entry| !entry.references.is_empty());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Position;
fn test_span() -> Span {
Span::new(Position::new(0, 0), Position::new(0, 10))
}
#[test]
fn test_add_and_get_references() {
let mut index = ReferenceIndex::new();
let file = PathBuf::from("test.sysml");
index.add_reference("Car", "Vehicle", Some(&file), Some(test_span()));
index.add_reference("Truck", "Vehicle", Some(&file), Some(test_span()));
let refs = index.get_references("Vehicle");
assert_eq!(refs.len(), 2);
let sources: Vec<&str> = refs.iter().map(|r| r.source_qname.as_str()).collect();
assert!(sources.contains(&"Car"));
assert!(sources.contains(&"Truck"));
}
#[test]
fn test_get_sources() {
let mut index = ReferenceIndex::new();
let file = PathBuf::from("test.sysml");
index.add_reference("Car", "Vehicle", Some(&file), Some(test_span()));
index.add_reference("Truck", "Vehicle", Some(&file), Some(test_span()));
let sources = index.get_sources("Vehicle");
assert_eq!(sources.len(), 2);
assert!(sources.contains(&"Car"));
assert!(sources.contains(&"Truck"));
}
#[test]
fn test_get_sources_empty() {
let index = ReferenceIndex::new();
let sources = index.get_sources("NonExistent");
assert!(sources.is_empty());
}
#[test]
fn test_remove_references_from_file() {
let mut index = ReferenceIndex::new();
let file_a = PathBuf::from("a.sysml");
let file_b = PathBuf::from("b.sysml");
index.add_reference("Car", "Vehicle", Some(&file_a), Some(test_span()));
index.add_reference("Truck", "Vehicle", Some(&file_b), Some(test_span()));
index.remove_references_from_file(file_a.to_str().unwrap());
let sources = index.get_sources("Vehicle");
assert_eq!(sources.len(), 1);
assert!(sources.contains(&"Truck"));
}
#[test]
fn test_remove_source() {
let mut index = ReferenceIndex::new();
let file = PathBuf::from("test.sysml");
index.add_reference("Car", "Vehicle", Some(&file), Some(test_span()));
index.add_reference("Car", "Engine", Some(&file), Some(test_span()));
index.remove_source("Car");
assert!(!index.has_references("Vehicle"));
assert!(!index.has_references("Engine"));
}
#[test]
fn test_reference_count() {
let mut index = ReferenceIndex::new();
let file = PathBuf::from("test.sysml");
index.add_reference("Car", "Vehicle", Some(&file), Some(test_span()));
index.add_reference("Car", "Engine", Some(&file), Some(test_span()));
index.add_reference("Truck", "Vehicle", Some(&file), Some(test_span()));
assert_eq!(index.target_count(), 2); assert_eq!(index.reference_count(), 3); }
}