use perl_semantic_facts::{
Confidence, EntityKind, FileId, ImportKind, ImportSymbols, ScopeId, VisibleSymbol,
VisibleSymbolContext, VisibleSymbolSource,
};
use super::imports::ImportExportIndex;
use crate::workspace::workspace_index::FileFactShard;
pub fn visible_symbols_at(
file_id: FileId,
byte_offset: u32,
scope_id: Option<ScopeId>,
shard: &FileFactShard,
import_export_index: &ImportExportIndex,
) -> Vec<VisibleSymbol> {
let mut result: Vec<VisibleSymbol> = Vec::new();
collect_local_lexical(&mut result, shard, byte_offset, scope_id);
collect_local_package(&mut result, shard);
collect_import_visibility(&mut result, file_id, import_export_index);
collect_constants(&mut result, shard);
collect_generated_members(&mut result, shard);
result
}
fn collect_local_lexical(
result: &mut Vec<VisibleSymbol>,
shard: &FileFactShard,
byte_offset: u32,
scope_id: Option<ScopeId>,
) {
for entity in &shard.entities {
if entity.kind != EntityKind::Variable {
continue;
}
if let Some(query_scope) = scope_id {
match entity.scope_id {
Some(entity_scope) if entity_scope == query_scope => {}
Some(_) => continue,
None => {}
}
}
let declared_before_offset = entity
.anchor_id
.and_then(|aid| shard.anchors.iter().find(|a| a.id == aid))
.map(|anchor| anchor.span_start_byte <= byte_offset)
.unwrap_or(false);
if !declared_before_offset {
continue;
}
result.push(VisibleSymbol {
name: bare_name(&entity.canonical_name),
entity_id: Some(entity.id),
source: VisibleSymbolSource::LocalLexical,
confidence: entity.confidence,
context: None,
});
}
}
fn collect_local_package(result: &mut Vec<VisibleSymbol>, shard: &FileFactShard) {
for entity in &shard.entities {
match entity.kind {
EntityKind::Subroutine | EntityKind::Method | EntityKind::Package => {}
_ => continue,
}
result.push(VisibleSymbol {
name: bare_name(&entity.canonical_name),
entity_id: Some(entity.id),
source: VisibleSymbolSource::LocalPackage,
confidence: entity.confidence,
context: None,
});
}
}
fn collect_import_visibility(
result: &mut Vec<VisibleSymbol>,
file_id: FileId,
import_export_index: &ImportExportIndex,
) {
let imports = import_export_index.get_imports_for_file(file_id);
for import in imports {
let context = VisibleSymbolContext::new(
Some(import.module.clone()),
import.anchor_id,
import_export_index.get_exports_for_module(&import.module).and_then(|es| es.anchor_id),
);
match import.kind {
ImportKind::UseExplicitList => {
if let ImportSymbols::Explicit(ref names) = import.symbols {
for name in names {
result.push(VisibleSymbol {
name: name.clone(),
entity_id: None,
source: VisibleSymbolSource::ExplicitImport,
confidence: Confidence::High,
context: Some(context.clone()),
});
}
}
}
ImportKind::UseEmpty => {
}
ImportKind::Use => {
if let Some(export_set) = import_export_index.get_exports_for_module(&import.module)
{
for name in &export_set.default_exports {
result.push(VisibleSymbol {
name: name.clone(),
entity_id: None,
source: VisibleSymbolSource::DefaultExport,
confidence: export_set.confidence,
context: Some(context.clone()),
});
}
}
}
ImportKind::UseTag => {
if let ImportSymbols::Tags(ref tag_names) = import.symbols {
if let Some(export_set) =
import_export_index.get_exports_for_module(&import.module)
{
for tag_name in tag_names {
for tag in &export_set.tags {
if tag.name == *tag_name {
for member in &tag.members {
result.push(VisibleSymbol {
name: member.clone(),
entity_id: None,
source: VisibleSymbolSource::ExportTag,
confidence: export_set.confidence,
context: Some(context.clone()),
});
}
}
}
}
}
}
}
ImportKind::RequireThenImport => {
if let ImportSymbols::Explicit(ref names) = import.symbols {
for name in names {
result.push(VisibleSymbol {
name: name.clone(),
entity_id: None,
source: VisibleSymbolSource::ExplicitImport,
confidence: import.confidence,
context: Some(context.clone()),
});
}
}
}
ImportKind::UseConstant | ImportKind::Require | ImportKind::DynamicRequire => {
}
}
}
}
fn collect_constants(result: &mut Vec<VisibleSymbol>, shard: &FileFactShard) {
for entity in &shard.entities {
if entity.kind != EntityKind::Constant {
continue;
}
result.push(VisibleSymbol {
name: bare_name(&entity.canonical_name),
entity_id: Some(entity.id),
source: VisibleSymbolSource::Constant,
confidence: entity.confidence,
context: None,
});
}
}
fn collect_generated_members(result: &mut Vec<VisibleSymbol>, shard: &FileFactShard) {
for entity in &shard.entities {
if entity.kind != EntityKind::GeneratedMember {
continue;
}
result.push(VisibleSymbol {
name: bare_name(&entity.canonical_name),
entity_id: Some(entity.id),
source: VisibleSymbolSource::Generated,
confidence: entity.confidence,
context: None,
});
}
}
fn bare_name(qualified: &str) -> String {
match qualified.rsplit_once("::") {
Some((_, bare)) => bare.to_string(),
None => qualified.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use perl_semantic_facts::{
AnchorFact, AnchorId, Confidence, EntityFact, EntityId, EntityKind, ExportSet, ExportTag,
ImportKind, ImportSpec, ImportSymbols, Provenance, ScopeId,
};
fn shard_with_sub(file_id: FileId) -> FileFactShard {
FileFactShard {
source_uri: "file:///lib/Main.pm".to_string(),
file_id,
content_hash: 1,
anchors_hash: None,
entities_hash: None,
occurrences_hash: None,
edges_hash: None,
anchors: vec![AnchorFact {
id: AnchorId(10),
file_id,
span_start_byte: 0,
span_end_byte: 20,
scope_id: None,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
}],
entities: vec![EntityFact {
id: EntityId(100),
kind: EntityKind::Subroutine,
canonical_name: "Main::do_stuff".to_string(),
anchor_id: Some(AnchorId(10)),
scope_id: None,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
}],
occurrences: vec![],
edges: vec![],
}
}
fn shard_with_variable(file_id: FileId) -> FileFactShard {
FileFactShard {
source_uri: "file:///lib/Main.pm".to_string(),
file_id,
content_hash: 2,
anchors_hash: None,
entities_hash: None,
occurrences_hash: None,
edges_hash: None,
anchors: vec![AnchorFact {
id: AnchorId(20),
file_id,
span_start_byte: 5,
span_end_byte: 10,
scope_id: Some(ScopeId(1)),
provenance: Provenance::ExactAst,
confidence: Confidence::High,
}],
entities: vec![EntityFact {
id: EntityId(200),
kind: EntityKind::Variable,
canonical_name: "$count".to_string(),
anchor_id: Some(AnchorId(20)),
scope_id: Some(ScopeId(1)),
provenance: Provenance::ExactAst,
confidence: Confidence::High,
}],
occurrences: vec![],
edges: vec![],
}
}
fn shard_with_constant(file_id: FileId) -> FileFactShard {
FileFactShard {
source_uri: "file:///lib/Main.pm".to_string(),
file_id,
content_hash: 3,
anchors_hash: None,
entities_hash: None,
occurrences_hash: None,
edges_hash: None,
anchors: vec![AnchorFact {
id: AnchorId(30),
file_id,
span_start_byte: 0,
span_end_byte: 15,
scope_id: None,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
}],
entities: vec![EntityFact {
id: EntityId(300),
kind: EntityKind::Constant,
canonical_name: "MAX_SIZE".to_string(),
anchor_id: Some(AnchorId(30)),
scope_id: None,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
}],
occurrences: vec![],
edges: vec![],
}
}
fn shard_with_generated(file_id: FileId) -> FileFactShard {
FileFactShard {
source_uri: "file:///lib/Main.pm".to_string(),
file_id,
content_hash: 4,
anchors_hash: None,
entities_hash: None,
occurrences_hash: None,
edges_hash: None,
anchors: vec![AnchorFact {
id: AnchorId(40),
file_id,
span_start_byte: 0,
span_end_byte: 20,
scope_id: None,
provenance: Provenance::FrameworkSynthesis,
confidence: Confidence::Medium,
}],
entities: vec![EntityFact {
id: EntityId(400),
kind: EntityKind::GeneratedMember,
canonical_name: "MyClass::name".to_string(),
anchor_id: Some(AnchorId(40)),
scope_id: None,
provenance: Provenance::FrameworkSynthesis,
confidence: Confidence::Medium,
}],
occurrences: vec![],
edges: vec![],
}
}
fn empty_shard(file_id: FileId) -> FileFactShard {
FileFactShard {
source_uri: "file:///lib/Main.pm".to_string(),
file_id,
content_hash: 0,
anchors_hash: None,
entities_hash: None,
occurrences_hash: None,
edges_hash: None,
anchors: vec![],
entities: vec![],
occurrences: vec![],
edges: vec![],
}
}
fn sample_export_set() -> ExportSet {
ExportSet {
default_exports: vec!["foo".to_string(), "bar".to_string()],
optional_exports: vec!["baz".to_string()],
tags: vec![ExportTag {
name: "all".to_string(),
members: vec!["foo".to_string(), "bar".to_string(), "baz".to_string()],
}],
provenance: Provenance::ExactAst,
confidence: Confidence::High,
module_name: Some("Foo".to_string()),
anchor_id: Some(AnchorId(500)),
}
}
#[test]
fn local_subroutine_is_visible() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = shard_with_sub(file_id);
let index = ImportExportIndex::new();
let symbols = visible_symbols_at(file_id, 50, None, &shard, &index);
let sub_sym = symbols.iter().find(|s| s.name == "do_stuff");
assert!(sub_sym.is_some(), "subroutine should be visible");
let sym = sub_sym.ok_or("expected visible symbol")?;
assert_eq!(sym.source, VisibleSymbolSource::LocalPackage);
assert!(sym.context.is_none());
Ok(())
}
#[test]
fn lexical_variable_visible_in_same_scope() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = shard_with_variable(file_id);
let index = ImportExportIndex::new();
let symbols = visible_symbols_at(file_id, 15, Some(ScopeId(1)), &shard, &index);
let var_sym = symbols.iter().find(|s| s.name == "$count");
assert!(var_sym.is_some(), "variable should be visible in same scope");
let sym = var_sym.ok_or("expected visible symbol")?;
assert_eq!(sym.source, VisibleSymbolSource::LocalLexical);
Ok(())
}
#[test]
fn lexical_variable_not_visible_in_different_scope() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = shard_with_variable(file_id);
let index = ImportExportIndex::new();
let symbols = visible_symbols_at(file_id, 15, Some(ScopeId(2)), &shard, &index);
let var_sym = symbols.iter().find(|s| s.name == "$count");
assert!(var_sym.is_none(), "variable should not be visible in different scope");
Ok(())
}
#[test]
fn lexical_variable_not_visible_before_declaration() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = shard_with_variable(file_id);
let index = ImportExportIndex::new();
let symbols = visible_symbols_at(file_id, 2, Some(ScopeId(1)), &shard, &index);
let var_sym = symbols.iter().find(|s| s.name == "$count");
assert!(var_sym.is_none(), "variable should not be visible before declaration");
Ok(())
}
#[test]
fn use_explicit_list_makes_names_visible() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = empty_shard(file_id);
let mut index = ImportExportIndex::new();
let import = ImportSpec {
module: "Foo".to_string(),
kind: ImportKind::UseExplicitList,
symbols: ImportSymbols::Explicit(vec!["alpha".to_string(), "beta".to_string()]),
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(AnchorId(100)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![import]);
let symbols = visible_symbols_at(file_id, 0, None, &shard, &index);
let alpha = symbols.iter().find(|s| s.name == "alpha");
let beta = symbols.iter().find(|s| s.name == "beta");
assert!(alpha.is_some(), "alpha should be visible");
assert!(beta.is_some(), "beta should be visible");
let a = alpha.ok_or("expected alpha")?;
assert_eq!(a.source, VisibleSymbolSource::ExplicitImport);
let ctx = a.context.as_ref().ok_or("expected context")?;
assert_eq!(ctx.source_module.as_deref(), Some("Foo"));
Ok(())
}
#[test]
fn use_empty_suppresses_defaults() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = empty_shard(file_id);
let mut index = ImportExportIndex::new();
index.add_module_exports("file:///lib/Foo.pm", "Foo", sample_export_set());
let import = ImportSpec {
module: "Foo".to_string(),
kind: ImportKind::UseEmpty,
symbols: ImportSymbols::None,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(AnchorId(101)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![import]);
let symbols = visible_symbols_at(file_id, 0, None, &shard, &index);
let from_foo: Vec<_> = symbols
.iter()
.filter(|s| {
s.context
.as_ref()
.and_then(|c| c.source_module.as_deref())
.map(|m| m == "Foo")
.unwrap_or(false)
})
.collect();
assert!(from_foo.is_empty(), "use Foo () should suppress all defaults");
Ok(())
}
#[test]
fn bare_use_makes_default_exports_visible() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = empty_shard(file_id);
let mut index = ImportExportIndex::new();
index.add_module_exports("file:///lib/Foo.pm", "Foo", sample_export_set());
let import = ImportSpec {
module: "Foo".to_string(),
kind: ImportKind::Use,
symbols: ImportSymbols::Default,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(AnchorId(102)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![import]);
let symbols = visible_symbols_at(file_id, 0, None, &shard, &index);
let foo_sym = symbols.iter().find(|s| s.name == "foo");
let bar_sym = symbols.iter().find(|s| s.name == "bar");
assert!(foo_sym.is_some(), "foo should be visible via default export");
assert!(bar_sym.is_some(), "bar should be visible via default export");
let f = foo_sym.ok_or("expected foo")?;
assert_eq!(f.source, VisibleSymbolSource::DefaultExport);
let ctx = f.context.as_ref().ok_or("expected context")?;
assert_eq!(ctx.source_module.as_deref(), Some("Foo"));
assert_eq!(ctx.source_export_anchor_id, Some(AnchorId(500)));
let baz_sym = symbols.iter().find(|s| s.name == "baz");
assert!(baz_sym.is_none(), "baz should not be visible via bare use");
Ok(())
}
#[test]
fn use_tag_makes_tag_members_visible() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = empty_shard(file_id);
let mut index = ImportExportIndex::new();
index.add_module_exports("file:///lib/Foo.pm", "Foo", sample_export_set());
let import = ImportSpec {
module: "Foo".to_string(),
kind: ImportKind::UseTag,
symbols: ImportSymbols::Tags(vec!["all".to_string()]),
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(AnchorId(103)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![import]);
let symbols = visible_symbols_at(file_id, 0, None, &shard, &index);
let foo_sym = symbols.iter().find(|s| s.name == "foo");
let bar_sym = symbols.iter().find(|s| s.name == "bar");
let baz_sym = symbols.iter().find(|s| s.name == "baz");
assert!(foo_sym.is_some(), "foo should be visible via :all tag");
assert!(bar_sym.is_some(), "bar should be visible via :all tag");
assert!(baz_sym.is_some(), "baz should be visible via :all tag");
let f = foo_sym.ok_or("expected foo")?;
assert_eq!(f.source, VisibleSymbolSource::ExportTag);
let ctx = f.context.as_ref().ok_or("expected context")?;
assert_eq!(ctx.source_module.as_deref(), Some("Foo"));
Ok(())
}
#[test]
fn constants_are_visible() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = shard_with_constant(file_id);
let index = ImportExportIndex::new();
let symbols = visible_symbols_at(file_id, 50, None, &shard, &index);
let const_sym = symbols.iter().find(|s| s.name == "MAX_SIZE");
assert!(const_sym.is_some(), "constant should be visible");
let sym = const_sym.ok_or("expected constant")?;
assert_eq!(sym.source, VisibleSymbolSource::Constant);
Ok(())
}
#[test]
fn generated_members_are_visible() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = shard_with_generated(file_id);
let index = ImportExportIndex::new();
let symbols = visible_symbols_at(file_id, 50, None, &shard, &index);
let gen_sym = symbols.iter().find(|s| s.name == "name");
assert!(gen_sym.is_some(), "generated member should be visible");
let sym = gen_sym.ok_or("expected generated member")?;
assert_eq!(sym.source, VisibleSymbolSource::Generated);
assert_eq!(sym.confidence, Confidence::Medium);
Ok(())
}
#[test]
fn require_then_import_makes_explicit_names_visible() -> Result<(), Box<dyn std::error::Error>>
{
let file_id = FileId(1);
let shard = empty_shard(file_id);
let mut index = ImportExportIndex::new();
let import = ImportSpec {
module: "Bar".to_string(),
kind: ImportKind::RequireThenImport,
symbols: ImportSymbols::Explicit(vec!["x".to_string(), "y".to_string()]),
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(AnchorId(104)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![import]);
let symbols = visible_symbols_at(file_id, 0, None, &shard, &index);
let x_sym = symbols.iter().find(|s| s.name == "x");
let y_sym = symbols.iter().find(|s| s.name == "y");
assert!(x_sym.is_some(), "x should be visible via require+import");
assert!(y_sym.is_some(), "y should be visible via require+import");
let x = x_sym.ok_or("expected x")?;
assert_eq!(x.source, VisibleSymbolSource::ExplicitImport);
Ok(())
}
#[test]
fn require_then_import_dynamic_symbols_do_not_invent_visibility()
-> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = empty_shard(file_id);
let mut index = ImportExportIndex::new();
let import = ImportSpec {
module: "Bar".to_string(),
kind: ImportKind::RequireThenImport,
symbols: ImportSymbols::Dynamic,
provenance: Provenance::ExactAst,
confidence: Confidence::Low,
file_id: Some(file_id),
anchor_id: Some(AnchorId(104)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![import]);
let symbols = visible_symbols_at(file_id, 0, None, &shard, &index);
assert!(symbols.is_empty(), "dynamic import list should not invent exact symbols");
Ok(())
}
#[test]
fn bare_use_with_no_exports_produces_nothing() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = empty_shard(file_id);
let mut index = ImportExportIndex::new();
let import = ImportSpec {
module: "Unknown".to_string(),
kind: ImportKind::Use,
symbols: ImportSymbols::Default,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(AnchorId(105)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![import]);
let symbols = visible_symbols_at(file_id, 0, None, &shard, &index);
assert!(symbols.is_empty(), "no symbols should be visible from unknown module");
Ok(())
}
#[test]
fn use_tag_with_nonexistent_tag_produces_nothing() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = empty_shard(file_id);
let mut index = ImportExportIndex::new();
index.add_module_exports("file:///lib/Foo.pm", "Foo", sample_export_set());
let import = ImportSpec {
module: "Foo".to_string(),
kind: ImportKind::UseTag,
symbols: ImportSymbols::Tags(vec!["nonexistent".to_string()]),
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(AnchorId(106)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![import]);
let symbols = visible_symbols_at(file_id, 0, None, &shard, &index);
assert!(symbols.is_empty(), "nonexistent tag should produce no symbols");
Ok(())
}
#[test]
fn bare_name_extracts_last_component() -> Result<(), Box<dyn std::error::Error>> {
assert_eq!(bare_name("Foo::Bar::baz"), "baz");
assert_eq!(bare_name("baz"), "baz");
assert_eq!(bare_name("A::B"), "B");
Ok(())
}
#[test]
fn combined_local_and_import_visibility() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = shard_with_sub(file_id);
let mut index = ImportExportIndex::new();
index.add_module_exports("file:///lib/Foo.pm", "Foo", sample_export_set());
let import = ImportSpec {
module: "Foo".to_string(),
kind: ImportKind::UseExplicitList,
symbols: ImportSymbols::Explicit(vec!["alpha".to_string()]),
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(AnchorId(107)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![import]);
let symbols = visible_symbols_at(file_id, 50, None, &shard, &index);
assert!(symbols.iter().any(|s| s.name == "do_stuff"));
assert!(symbols.iter().any(|s| s.name == "alpha"));
Ok(())
}
mod prop_tests {
use super::*;
use proptest::prelude::*;
use proptest::test_runner::Config as ProptestConfig;
fn arb_symbol_name() -> impl Strategy<Value = String> {
"[a-z_][a-z_0-9]{0,19}".prop_filter("non-empty", |s| !s.is_empty())
}
fn arb_module_name() -> impl Strategy<Value = String> {
"[A-Z][a-z]{1,9}".prop_filter("non-empty", |s| !s.is_empty())
}
fn arb_symbol_set(min: usize, max: usize) -> impl Strategy<Value = Vec<String>> {
prop::collection::hash_set(arb_symbol_name(), min..=max)
.prop_map(|s| s.into_iter().collect::<Vec<_>>())
}
fn arb_tag_name() -> impl Strategy<Value = String> {
"[a-z]{2,8}"
}
proptest! {
#![proptest_config(ProptestConfig {
failure_persistence: None,
..ProptestConfig::default()
})]
#[test]
fn prop_explicit_import_source_attribution(
module_name in arb_module_name(),
symbols in arb_symbol_set(1, 8),
) {
let file_id = FileId(1);
let shard = empty_shard(file_id);
let mut index = ImportExportIndex::new();
let import = ImportSpec {
module: module_name.clone(),
kind: ImportKind::UseExplicitList,
symbols: ImportSymbols::Explicit(symbols.clone()),
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(AnchorId(100)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![import]);
let visible = visible_symbols_at(file_id, 0, None, &shard, &index);
for sym_name in &symbols {
let found = visible.iter().find(|v| v.name == *sym_name);
prop_assert!(
found.is_some(),
"explicit import '{}' should be visible",
sym_name
);
let vs = found.expect("checked above");
prop_assert_eq!(
&vs.source,
&VisibleSymbolSource::ExplicitImport,
"explicit import '{}' should have ExplicitImport source",
sym_name
);
let ctx = vs.context.as_ref();
prop_assert!(ctx.is_some(), "explicit import should have context");
prop_assert_eq!(
ctx.expect("checked above").source_module.as_deref(),
Some(module_name.as_str()),
"context source_module should match import module"
);
}
}
}
proptest! {
#![proptest_config(ProptestConfig {
failure_persistence: None,
..ProptestConfig::default()
})]
#[test]
fn prop_default_export_source_attribution(
module_name in arb_module_name(),
default_exports in arb_symbol_set(1, 8),
optional_exports in arb_symbol_set(0, 4),
) {
let file_id = FileId(1);
let shard = empty_shard(file_id);
let mut index = ImportExportIndex::new();
let export_set = ExportSet {
default_exports: default_exports.clone(),
optional_exports: optional_exports.clone(),
tags: vec![],
provenance: Provenance::ExactAst,
confidence: Confidence::High,
module_name: Some(module_name.clone()),
anchor_id: Some(AnchorId(500)),
};
index.add_module_exports(
&format!("file:///lib/{module_name}.pm"),
&module_name,
export_set,
);
let import = ImportSpec {
module: module_name.clone(),
kind: ImportKind::Use,
symbols: ImportSymbols::Default,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(AnchorId(100)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![import]);
let visible = visible_symbols_at(file_id, 0, None, &shard, &index);
for sym_name in &default_exports {
let found = visible.iter().find(|v| v.name == *sym_name);
prop_assert!(
found.is_some(),
"default export '{}' should be visible",
sym_name
);
let vs = found.expect("checked above");
prop_assert_eq!(
&vs.source,
&VisibleSymbolSource::DefaultExport,
"default export '{}' should have DefaultExport source",
sym_name
);
let ctx = vs.context.as_ref();
prop_assert!(ctx.is_some(), "default export should have context");
prop_assert_eq!(
ctx.expect("checked above").source_module.as_deref(),
Some(module_name.as_str()),
"context source_module should match export module"
);
}
for sym_name in &optional_exports {
if !default_exports.contains(sym_name) {
let found = visible.iter().find(|v| v.name == *sym_name);
prop_assert!(
found.is_none(),
"optional-only export '{}' should NOT be visible via bare use",
sym_name
);
}
}
}
}
proptest! {
#![proptest_config(ProptestConfig {
failure_persistence: None,
..ProptestConfig::default()
})]
#[test]
fn prop_tag_export_source_attribution(
module_name in arb_module_name(),
tag_name in arb_tag_name(),
tag_members in arb_symbol_set(1, 8),
) {
let file_id = FileId(1);
let shard = empty_shard(file_id);
let mut index = ImportExportIndex::new();
let export_set = ExportSet {
default_exports: vec![],
optional_exports: vec![],
tags: vec![ExportTag {
name: tag_name.clone(),
members: tag_members.clone(),
}],
provenance: Provenance::ExactAst,
confidence: Confidence::High,
module_name: Some(module_name.clone()),
anchor_id: Some(AnchorId(500)),
};
index.add_module_exports(
&format!("file:///lib/{module_name}.pm"),
&module_name,
export_set,
);
let import = ImportSpec {
module: module_name.clone(),
kind: ImportKind::UseTag,
symbols: ImportSymbols::Tags(vec![tag_name.clone()]),
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(AnchorId(100)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![import]);
let visible = visible_symbols_at(file_id, 0, None, &shard, &index);
for sym_name in &tag_members {
let found = visible.iter().find(|v| v.name == *sym_name);
prop_assert!(
found.is_some(),
"tag member '{}' should be visible via :{} tag",
sym_name,
tag_name
);
let vs = found.expect("checked above");
prop_assert_eq!(
&vs.source,
&VisibleSymbolSource::ExportTag,
"tag member '{}' should have ExportTag source",
sym_name
);
let ctx = vs.context.as_ref();
prop_assert!(ctx.is_some(), "tag export should have context");
prop_assert_eq!(
ctx.expect("checked above").source_module.as_deref(),
Some(module_name.as_str()),
"context source_module should match export module"
);
}
}
}
}
#[test]
fn visible_symbol_context_carries_export_anchor() -> Result<(), Box<dyn std::error::Error>> {
let file_id = FileId(1);
let shard = empty_shard(file_id);
let mut index = ImportExportIndex::new();
index.add_module_exports("file:///lib/Foo.pm", "Foo", sample_export_set());
let import = ImportSpec {
module: "Foo".to_string(),
kind: ImportKind::Use,
symbols: ImportSymbols::Default,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(AnchorId(108)),
scope_id: None,
};
index.add_file_imports("file:///lib/Main.pm", file_id, vec![import]);
let symbols = visible_symbols_at(file_id, 0, None, &shard, &index);
let foo_sym = symbols.iter().find(|s| s.name == "foo").ok_or("expected foo")?;
let ctx = foo_sym.context.as_ref().ok_or("expected context")?;
assert_eq!(ctx.source_module.as_deref(), Some("Foo"));
assert_eq!(ctx.source_import_anchor_id, Some(AnchorId(108)));
assert_eq!(ctx.source_export_anchor_id, Some(AnchorId(500)));
Ok(())
}
}