use perl_semantic_facts::{ExportSet, FileId, ImportSpec};
use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct ImportExportIndex {
imports_by_file: HashMap<FileId, Vec<ImportSpec>>,
exports_by_module: HashMap<String, ExportSet>,
file_uri_to_id: HashMap<String, FileId>,
module_to_source_uri: HashMap<String, String>,
}
impl ImportExportIndex {
pub fn new() -> Self {
Self::default()
}
pub fn add_file_imports(
&mut self,
source_uri: &str,
file_id: FileId,
imports: Vec<ImportSpec>,
) {
self.file_uri_to_id.insert(source_uri.to_string(), file_id);
self.imports_by_file.insert(file_id, imports);
}
pub fn remove_file_imports(&mut self, source_uri: &str) {
let file_id = match self.file_uri_to_id.remove(source_uri) {
Some(id) => id,
None => return,
};
self.imports_by_file.remove(&file_id);
}
pub fn get_imports_for_file(&self, file_id: FileId) -> &[ImportSpec] {
self.imports_by_file.get(&file_id).map(Vec::as_slice).unwrap_or_default()
}
pub fn add_module_exports(&mut self, source_uri: &str, module_name: &str, exports: ExportSet) {
self.module_to_source_uri.insert(module_name.to_string(), source_uri.to_string());
self.exports_by_module.insert(module_name.to_string(), exports);
}
pub fn remove_module_exports(&mut self, source_uri: &str) {
let modules_to_remove: Vec<String> = self
.module_to_source_uri
.iter()
.filter(|(_, uri)| uri.as_str() == source_uri)
.map(|(module, _)| module.clone())
.collect();
for module in &modules_to_remove {
self.exports_by_module.remove(module);
self.module_to_source_uri.remove(module);
}
}
pub fn get_exports_for_module(&self, module_name: &str) -> Option<&ExportSet> {
self.exports_by_module.get(module_name)
}
pub fn import_file_count(&self) -> usize {
self.imports_by_file.len()
}
pub fn export_module_count(&self) -> usize {
self.exports_by_module.len()
}
pub fn find_exporting_module(&self, symbol_name: &str) -> Option<&str> {
for (module, exports) in &self.exports_by_module {
if exports.default_exports.iter().any(|s| s == symbol_name)
|| exports.optional_exports.iter().any(|s| s == symbol_name)
{
return Some(module.as_str());
}
}
None
}
pub fn is_imported_by_other_file(&self, symbol_name: &str, exclude_file_id: FileId) -> bool {
for (&file_id, imports) in &self.imports_by_file {
if file_id == exclude_file_id {
continue;
}
for spec in imports {
if import_spec_names_symbol(spec, symbol_name) {
return true;
}
}
}
false
}
pub fn all_imports(&self) -> impl Iterator<Item = (FileId, &[ImportSpec])> {
self.imports_by_file.iter().map(|(&fid, specs)| (fid, specs.as_slice()))
}
}
fn import_spec_names_symbol(spec: &ImportSpec, symbol_name: &str) -> bool {
match &spec.symbols {
perl_semantic_facts::ImportSymbols::Explicit(names) => {
names.iter().any(|n| n == symbol_name)
}
perl_semantic_facts::ImportSymbols::Mixed { names, .. } => {
names.iter().any(|n| n == symbol_name)
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use perl_semantic_facts::{
AnchorId, Confidence, ExportTag, ImportKind, ImportSymbols, Provenance, ScopeId,
};
fn sample_import_explicit() -> ImportSpec {
ImportSpec {
module: "Foo".to_string(),
kind: ImportKind::UseExplicitList,
symbols: ImportSymbols::Explicit(vec!["bar".to_string(), "baz".to_string()]),
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(FileId(1)),
anchor_id: Some(AnchorId(10)),
scope_id: Some(ScopeId(1)),
}
}
fn sample_import_empty() -> ImportSpec {
ImportSpec {
module: "Bar".to_string(),
kind: ImportKind::UseEmpty,
symbols: ImportSymbols::None,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(FileId(1)),
anchor_id: Some(AnchorId(11)),
scope_id: None,
}
}
fn sample_export_set() -> ExportSet {
ExportSet {
default_exports: vec!["bar".to_string()],
optional_exports: vec!["baz".to_string(), "qux".to_string()],
tags: vec![ExportTag {
name: "all".to_string(),
members: vec!["bar".to_string(), "baz".to_string(), "qux".to_string()],
}],
provenance: Provenance::ExactAst,
confidence: Confidence::High,
module_name: Some("Foo".to_string()),
anchor_id: Some(AnchorId(20)),
}
}
#[test]
fn add_and_get_file_imports() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ImportExportIndex::new();
let file_id = FileId(1);
let imports = vec![sample_import_explicit(), sample_import_empty()];
index.add_file_imports("file:///lib/Main.pm", file_id, imports);
let result = index.get_imports_for_file(file_id);
assert_eq!(result.len(), 2);
assert_eq!(result[0].module, "Foo");
assert_eq!(result[1].module, "Bar");
Ok(())
}
#[test]
fn get_imports_for_unknown_file_returns_empty() -> Result<(), Box<dyn std::error::Error>> {
let index = ImportExportIndex::new();
let result = index.get_imports_for_file(FileId(999));
assert!(result.is_empty());
Ok(())
}
#[test]
fn remove_file_imports_clears_entries() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ImportExportIndex::new();
let file_id = FileId(1);
index.add_file_imports("file:///lib/Main.pm", file_id, vec![sample_import_explicit()]);
assert_eq!(index.import_file_count(), 1);
index.remove_file_imports("file:///lib/Main.pm");
assert_eq!(index.import_file_count(), 0);
assert!(index.get_imports_for_file(file_id).is_empty());
Ok(())
}
#[test]
fn remove_file_imports_is_idempotent() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ImportExportIndex::new();
let file_id = FileId(1);
index.add_file_imports("file:///lib/Main.pm", file_id, vec![sample_import_explicit()]);
index.remove_file_imports("file:///lib/Main.pm");
index.remove_file_imports("file:///lib/Main.pm");
assert_eq!(index.import_file_count(), 0);
Ok(())
}
#[test]
fn remove_unknown_file_imports_is_noop() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ImportExportIndex::new();
index.add_file_imports("file:///lib/Main.pm", FileId(1), vec![sample_import_explicit()]);
index.remove_file_imports("file:///nonexistent.pm");
assert_eq!(index.import_file_count(), 1);
Ok(())
}
#[test]
fn add_and_get_module_exports() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ImportExportIndex::new();
index.add_module_exports("file:///lib/Foo.pm", "Foo", sample_export_set());
let result = index.get_exports_for_module("Foo");
assert!(result.is_some());
let exports = result.ok_or("expected export set")?;
assert_eq!(exports.default_exports, vec!["bar"]);
assert_eq!(exports.optional_exports, vec!["baz", "qux"]);
assert_eq!(exports.tags.len(), 1);
assert_eq!(exports.tags[0].name, "all");
Ok(())
}
#[test]
fn get_exports_for_unknown_module_returns_none() -> Result<(), Box<dyn std::error::Error>> {
let index = ImportExportIndex::new();
assert!(index.get_exports_for_module("Unknown").is_none());
Ok(())
}
#[test]
fn remove_module_exports_clears_entries() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ImportExportIndex::new();
index.add_module_exports("file:///lib/Foo.pm", "Foo", sample_export_set());
assert_eq!(index.export_module_count(), 1);
index.remove_module_exports("file:///lib/Foo.pm");
assert_eq!(index.export_module_count(), 0);
assert!(index.get_exports_for_module("Foo").is_none());
Ok(())
}
#[test]
fn remove_module_exports_is_idempotent() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ImportExportIndex::new();
index.add_module_exports("file:///lib/Foo.pm", "Foo", sample_export_set());
index.remove_module_exports("file:///lib/Foo.pm");
index.remove_module_exports("file:///lib/Foo.pm");
assert_eq!(index.export_module_count(), 0);
Ok(())
}
#[test]
fn remove_unknown_module_exports_is_noop() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ImportExportIndex::new();
index.add_module_exports("file:///lib/Foo.pm", "Foo", sample_export_set());
index.remove_module_exports("file:///nonexistent.pm");
assert_eq!(index.export_module_count(), 1);
Ok(())
}
#[test]
fn multiple_files_coexist_in_import_index() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ImportExportIndex::new();
let file_a = FileId(1);
let file_b = FileId(2);
index.add_file_imports("file:///lib/A.pm", file_a, vec![sample_import_explicit()]);
index.add_file_imports("file:///lib/B.pm", file_b, vec![sample_import_empty()]);
assert_eq!(index.import_file_count(), 2);
assert_eq!(index.get_imports_for_file(file_a).len(), 1);
assert_eq!(index.get_imports_for_file(file_b).len(), 1);
index.remove_file_imports("file:///lib/A.pm");
assert_eq!(index.import_file_count(), 1);
assert!(index.get_imports_for_file(file_a).is_empty());
assert_eq!(index.get_imports_for_file(file_b).len(), 1);
Ok(())
}
#[test]
fn multiple_modules_coexist_in_export_index() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ImportExportIndex::new();
let export_foo = sample_export_set();
let export_bar = ExportSet {
default_exports: vec!["init".to_string()],
optional_exports: vec![],
tags: vec![],
provenance: Provenance::ExactAst,
confidence: Confidence::High,
module_name: Some("Bar".to_string()),
anchor_id: None,
};
index.add_module_exports("file:///lib/Foo.pm", "Foo", export_foo);
index.add_module_exports("file:///lib/Bar.pm", "Bar", export_bar);
assert_eq!(index.export_module_count(), 2);
assert!(index.get_exports_for_module("Foo").is_some());
assert!(index.get_exports_for_module("Bar").is_some());
index.remove_module_exports("file:///lib/Foo.pm");
assert_eq!(index.export_module_count(), 1);
assert!(index.get_exports_for_module("Foo").is_none());
assert!(index.get_exports_for_module("Bar").is_some());
Ok(())
}
#[test]
fn incremental_reindex_replaces_imports() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ImportExportIndex::new();
let file_id = FileId(1);
index.add_file_imports("file:///lib/Main.pm", file_id, vec![sample_import_explicit()]);
assert_eq!(index.get_imports_for_file(file_id).len(), 1);
assert_eq!(index.get_imports_for_file(file_id)[0].module, "Foo");
index.remove_file_imports("file:///lib/Main.pm");
let updated_import = ImportSpec {
module: "Baz".to_string(),
kind: ImportKind::Use,
symbols: ImportSymbols::Default,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(AnchorId(30)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![updated_import]);
let result = index.get_imports_for_file(file_id);
assert_eq!(result.len(), 1);
assert_eq!(result[0].module, "Baz");
Ok(())
}
#[test]
fn incremental_reindex_replaces_exports() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ImportExportIndex::new();
index.add_module_exports("file:///lib/Foo.pm", "Foo", sample_export_set());
let original = index.get_exports_for_module("Foo");
assert!(original.is_some());
let orig = original.ok_or("expected export set")?;
assert_eq!(orig.default_exports, vec!["bar"]);
index.remove_module_exports("file:///lib/Foo.pm");
let updated_exports = ExportSet {
default_exports: vec!["new_func".to_string()],
optional_exports: vec![],
tags: vec![],
provenance: Provenance::ExactAst,
confidence: Confidence::High,
module_name: Some("Foo".to_string()),
anchor_id: Some(AnchorId(40)),
};
index.add_module_exports("file:///lib/Foo.pm", "Foo", updated_exports);
let result = index.get_exports_for_module("Foo");
assert!(result.is_some());
let exports = result.ok_or("expected export set")?;
assert_eq!(exports.default_exports, vec!["new_func"]);
Ok(())
}
#[test]
fn import_spec_fields_are_preserved() -> Result<(), Box<dyn std::error::Error>> {
let mut index = ImportExportIndex::new();
let file_id = FileId(42);
let import = ImportSpec {
module: "My::Module".to_string(),
kind: ImportKind::UseTag,
symbols: ImportSymbols::Tags(vec!["all".to_string()]),
provenance: Provenance::ImportExportInference,
confidence: Confidence::Medium,
file_id: Some(file_id),
anchor_id: Some(AnchorId(99)),
scope_id: Some(ScopeId(5)),
};
index.add_file_imports("file:///lib/My/Module.pm", file_id, vec![import]);
let result = index.get_imports_for_file(file_id);
assert_eq!(result.len(), 1);
assert_eq!(result[0].module, "My::Module");
assert_eq!(result[0].kind, ImportKind::UseTag);
assert_eq!(result[0].symbols, ImportSymbols::Tags(vec!["all".to_string()]));
assert_eq!(result[0].provenance, Provenance::ImportExportInference);
assert_eq!(result[0].confidence, Confidence::Medium);
assert_eq!(result[0].file_id, Some(file_id));
assert_eq!(result[0].anchor_id, Some(AnchorId(99)));
assert_eq!(result[0].scope_id, Some(ScopeId(5)));
Ok(())
}
}