use std::collections::{HashMap, HashSet};
use crate::ir::ExtractResult;
use crate::model::{Language, NodeLabel};
use super::includes_graph::IncludesGraph;
#[derive(Debug, Clone)]
pub struct SymbolEntry {
pub name: String,
pub qn: String,
pub label: NodeLabel,
pub file_path: String,
pub project: String,
pub signature: Option<String>,
pub language: Option<Language>,
pub is_exported: bool,
}
impl SymbolEntry {
#[must_use]
pub fn new(
name: impl Into<String>,
qn: impl Into<String>,
label: NodeLabel,
file_path: impl Into<String>,
project: impl Into<String>,
) -> Self {
Self {
name: name.into(),
qn: qn.into(),
label,
file_path: file_path.into(),
project: project.into(),
signature: None,
language: None,
is_exported: false,
}
}
#[must_use]
pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
self.signature = Some(signature.into());
self
}
#[must_use]
pub fn with_signature_opt(mut self, signature: Option<String>) -> Self {
self.signature = signature;
self
}
#[must_use]
pub fn with_language(mut self, language: Language) -> Self {
self.language = Some(language);
self
}
#[must_use]
pub fn with_exported(mut self, is_exported: bool) -> Self {
self.is_exported = is_exported;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct FileSymbolTable {
symbols: HashMap<String, Vec<SymbolEntry>>,
}
impl FileSymbolTable {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, entry: SymbolEntry) {
self.symbols
.entry(entry.name.clone())
.or_default()
.push(entry);
}
#[must_use]
pub fn lookup(&self, name: &str) -> Vec<&SymbolEntry> {
self.symbols
.get(name)
.map(|v| v.iter().collect())
.unwrap_or_default()
}
#[must_use]
pub fn lookup_exact(&self, name: &str) -> Option<&SymbolEntry> {
self.symbols.get(name).and_then(|v| v.first())
}
pub fn all_symbols(&self) -> impl Iterator<Item = &SymbolEntry> {
self.symbols.values().flat_map(|v| v.iter())
}
#[must_use]
pub fn len(&self) -> usize {
self.symbols.values().map(Vec::len).sum()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone, Default)]
pub struct ProjectSymbolTable {
files: HashMap<String, FileSymbolTable>,
global_symbols: HashMap<String, Vec<SymbolEntry>>,
reexport_target_qns: HashSet<String>,
}
impl ProjectSymbolTable {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add_file_table(&mut self, file_path: &str, table: FileSymbolTable) {
let mut entries: Vec<SymbolEntry> = table.all_symbols().cloned().collect();
entries.sort_by(|a, b| a.qn.cmp(&b.qn));
for entry in entries {
self.global_symbols
.entry(entry.name.clone())
.or_default()
.push(entry);
}
self.files.insert(file_path.to_string(), table);
}
pub fn add_symbol(&mut self, entry: SymbolEntry) {
let file_path = entry.file_path.clone();
self.global_symbols
.entry(entry.name.clone())
.or_default()
.push(entry.clone());
self.files.entry(file_path).or_default().add(entry);
}
#[must_use]
pub fn lookup(&self, name: &str) -> Vec<&SymbolEntry> {
let mut v: Vec<&SymbolEntry> = self
.global_symbols
.get(name)
.map(|v| v.iter().collect())
.unwrap_or_default();
if v.len() > 1 {
v.sort_by(|a, b| a.qn.cmp(&b.qn));
}
v
}
#[must_use]
pub fn lookup_exact(&self, name: &str) -> Option<&SymbolEntry> {
self.global_symbols.get(name).and_then(|v| v.first())
}
#[must_use]
pub fn lookup_in_file(&self, file_path: &str, name: &str) -> Vec<&SymbolEntry> {
self.files
.get(file_path)
.map(|t| t.lookup(name))
.unwrap_or_default()
}
#[must_use]
pub fn lookup_exported(&self, name: &str) -> Vec<&SymbolEntry> {
self.lookup(name)
.into_iter()
.filter(|e| e.is_exported)
.collect()
}
#[must_use]
pub fn lookup_exported_in_scope(
&self,
name: &str,
calling_file: &str,
includes_graph: &IncludesGraph,
) -> Vec<&SymbolEntry> {
let mut reachable = HashSet::new();
includes_graph.fill_reachable_from(calling_file, &mut reachable);
self.lookup(name)
.into_iter()
.filter(|e| e.is_exported && reachable.contains(e.file_path.as_str()))
.collect()
}
#[must_use]
pub fn all_symbols(&self) -> Vec<&SymbolEntry> {
self.global_symbols
.values()
.flat_map(|v| v.iter())
.collect()
}
#[must_use]
pub fn file_count(&self) -> usize {
self.files.len()
}
#[must_use]
pub fn symbol_count(&self) -> usize {
self.global_symbols.values().map(Vec::len).sum()
}
pub fn mark_reexport_target(&mut self, qn: String) {
self.reexport_target_qns.insert(qn);
}
#[must_use]
pub fn is_reexport_target(&self, qn: &str) -> bool {
self.reexport_target_qns.contains(qn)
}
pub(crate) fn populate_reexport_targets(&mut self, results: &[ExtractResult]) {
for result in results {
for import in &result.imports {
if !import.is_reexport || import.imported_names.is_empty() {
continue;
}
let candidates = resolve_reexport_target_file_candidates(&import.source_file);
if candidates.is_empty() {
continue;
}
let matched_file = self
.files
.keys()
.filter_map(|fp| {
let fp_norm = fp.replace('\\', "/");
candidates
.iter()
.rev()
.find(|c| file_path_matches_candidate(&fp_norm, c))
.map(|c| (c.len(), fp.clone()))
})
.max_by_key(|(len, _)| *len)
.map(|(_, fp)| fp);
let Some(target_file) = matched_file else {
continue;
};
let Some(file_table) = self.files.get(&target_file) else {
continue;
};
for name in &import.imported_names {
if let Some(entry) = file_table.lookup_exact(name) {
self.reexport_target_qns.insert(entry.qn.clone());
}
}
}
}
}
}
fn resolve_reexport_target_file_candidates(source_file: &str) -> Vec<String> {
let path = if let Some(p) = source_file.strip_prefix("crate::") {
p
} else if let Some(p) = source_file.strip_prefix("self::") {
p
} else if let Some(p) = source_file.strip_prefix("super::") {
p
} else if source_file.contains("::") && !source_file.contains('/') && !source_file.contains('.')
{
source_file
} else {
return Vec::new();
};
if path.contains("..") {
return Vec::new();
}
let path = path.replace("::", "/");
let mut candidates = Vec::new();
candidates.push(format!("src/{path}.rs"));
candidates.push(format!("src/{path}/mod.rs"));
if let Some((parent, _)) = path.rsplit_once('/') {
candidates.push(format!("src/{parent}.rs"));
candidates.push(format!("src/{parent}/mod.rs"));
}
candidates
}
fn file_path_matches_candidate(file_path_norm: &str, candidate: &str) -> bool {
if file_path_norm == candidate {
return true;
}
if let Some(prefix) = file_path_norm.strip_suffix(candidate) {
return prefix.ends_with('/');
}
false
}
#[cfg(test)]
mod tests {
use super::*;
fn make_entry(name: &str, qn: &str, file: &str) -> SymbolEntry {
SymbolEntry::new(name, qn, NodeLabel::Function, file, "proj")
}
fn make_exported_entry(name: &str, qn: &str, file: &str) -> SymbolEntry {
make_entry(name, qn, file).with_exported(true)
}
#[test]
fn symbol_entry_new_sets_required_fields() {
let entry = SymbolEntry::new(
"foo",
"proj.src.foo",
NodeLabel::Function,
"src/main.rs",
"proj",
);
assert_eq!(entry.name, "foo");
assert_eq!(entry.qn, "proj.src.foo");
assert_eq!(entry.label, NodeLabel::Function);
assert_eq!(entry.file_path, "src/main.rs");
assert_eq!(entry.project, "proj");
assert!(entry.signature.is_none());
assert!(entry.language.is_none());
assert!(!entry.is_exported);
}
#[test]
fn symbol_entry_with_builders() {
let entry = SymbolEntry::new("foo", "qn", NodeLabel::Function, "f", "p")
.with_signature("fn foo()")
.with_language(Language::Rust)
.with_exported(true);
assert_eq!(entry.signature.as_deref(), Some("fn foo()"));
assert_eq!(entry.language, Some(Language::Rust));
assert!(entry.is_exported);
}
#[test]
fn symbol_entry_with_signature_opt_some() {
let entry = SymbolEntry::new("foo", "qn", NodeLabel::Function, "f", "p")
.with_signature_opt(Some("fn foo()".to_string()));
assert_eq!(entry.signature.as_deref(), Some("fn foo()"));
}
#[test]
fn symbol_entry_with_signature_opt_none() {
let entry =
SymbolEntry::new("foo", "qn", NodeLabel::Function, "f", "p").with_signature_opt(None);
assert!(entry.signature.is_none());
}
#[test]
fn symbol_entry_clone_is_equal() {
let entry = make_entry("foo", "proj.foo", "f.rs");
let cloned = entry.clone();
assert_eq!(entry.name, cloned.name);
assert_eq!(entry.qn, cloned.qn);
assert_eq!(entry.label, cloned.label);
}
#[test]
fn symbol_entry_accepts_string_and_str() {
let entry = SymbolEntry::new(
String::from("foo"),
String::from("qn"),
NodeLabel::Function,
String::from("f"),
String::from("p"),
);
assert_eq!(entry.name, "foo");
assert_eq!(entry.file_path, "f");
}
#[test]
fn symbol_entry_debug_contains_name() {
let entry = make_entry("foo", "proj.foo", "f.rs");
let debug = format!("{entry:?}");
assert!(debug.contains("foo"));
}
#[test]
fn file_table_new_is_empty() {
let table = FileSymbolTable::new();
assert!(table.is_empty());
assert_eq!(table.len(), 0);
}
#[test]
fn file_table_add_and_lookup() {
let mut table = FileSymbolTable::new();
table.add(make_entry("foo", "proj.foo", "f.rs"));
let results = table.lookup("foo");
assert_eq!(results.len(), 1);
assert_eq!(results[0].qn, "proj.foo");
}
#[test]
fn file_table_multiple_entries_same_name() {
let mut table = FileSymbolTable::new();
table.add(make_entry("foo", "proj.foo1", "f.rs"));
table.add(make_entry("foo", "proj.foo2", "f.rs"));
let results = table.lookup("foo");
assert_eq!(results.len(), 2);
}
#[test]
fn file_table_lookup_exact_returns_first() {
let mut table = FileSymbolTable::new();
table.add(make_entry("foo", "proj.foo1", "f.rs"));
table.add(make_entry("foo", "proj.foo2", "f.rs"));
let result = table.lookup_exact("foo");
assert!(result.is_some());
assert_eq!(result.unwrap().qn, "proj.foo1");
}
#[test]
fn file_table_lookup_exact_returns_none_if_not_found() {
let table = FileSymbolTable::new();
assert!(table.lookup_exact("missing").is_none());
}
#[test]
fn file_table_lookup_returns_empty_if_not_found() {
let mut table = FileSymbolTable::new();
table.add(make_entry("foo", "proj.foo", "f.rs"));
assert!(table.lookup("missing").is_empty());
}
#[test]
fn file_table_all_symbols_iterates_all() {
let mut table = FileSymbolTable::new();
table.add(make_entry("a", "proj.a", "f.rs"));
table.add(make_entry("b", "proj.b", "f.rs"));
table.add(make_entry("a", "proj.a2", "f.rs"));
let count = table.all_symbols().count();
assert_eq!(count, 3);
}
#[test]
fn file_table_len_counts_all_entries() {
let mut table = FileSymbolTable::new();
table.add(make_entry("a", "proj.a", "f.rs"));
table.add(make_entry("a", "proj.a2", "f.rs"));
table.add(make_entry("b", "proj.b", "f.rs"));
assert_eq!(table.len(), 3);
}
#[test]
fn file_table_default_is_empty() {
let table = FileSymbolTable::default();
assert!(table.is_empty());
}
#[test]
fn file_table_clone_preserves_entries() {
let mut table = FileSymbolTable::new();
table.add(make_entry("foo", "proj.foo", "f.rs"));
let cloned = table.clone();
assert_eq!(cloned.len(), 1);
assert_eq!(cloned.lookup("foo").len(), 1);
}
#[test]
fn file_table_all_symbols_returns_correct_entries() {
let mut table = FileSymbolTable::new();
table.add(make_entry("a", "proj.a", "f.rs"));
table.add(make_entry("b", "proj.b", "f.rs"));
let qns: Vec<&str> = table.all_symbols().map(|e| e.qn.as_str()).collect();
assert!(qns.contains(&"proj.a"));
assert!(qns.contains(&"proj.b"));
}
#[test]
fn project_table_new_is_empty() {
let table = ProjectSymbolTable::new();
assert_eq!(table.file_count(), 0);
assert_eq!(table.symbol_count(), 0);
}
#[test]
fn project_table_add_file_table() {
let mut project = ProjectSymbolTable::new();
let mut file_table = FileSymbolTable::new();
file_table.add(make_entry("foo", "proj.foo", "a.rs"));
project.add_file_table("a.rs", file_table);
assert_eq!(project.file_count(), 1);
assert_eq!(project.symbol_count(), 1);
}
#[test]
fn project_table_add_symbol() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_entry("foo", "proj.foo", "a.rs"));
assert_eq!(project.symbol_count(), 1);
assert_eq!(project.file_count(), 1);
}
#[test]
fn project_table_lookup_global() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_entry("foo", "proj.foo", "a.rs"));
let results = project.lookup("foo");
assert_eq!(results.len(), 1);
assert_eq!(results[0].qn, "proj.foo");
}
#[test]
fn project_table_lookup_returns_empty_if_not_found() {
let project = ProjectSymbolTable::new();
assert!(project.lookup("missing").is_empty());
}
#[test]
fn project_table_lookup_in_file() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_entry("foo", "proj.foo", "a.rs"));
project.add_symbol(make_entry("bar", "proj.bar", "b.rs"));
let results = project.lookup_in_file("a.rs", "foo");
assert_eq!(results.len(), 1);
let results = project.lookup_in_file("a.rs", "bar");
assert!(results.is_empty());
}
#[test]
fn project_table_lookup_in_file_nonexistent_returns_empty() {
let project = ProjectSymbolTable::new();
assert!(project.lookup_in_file("missing.rs", "foo").is_empty());
}
#[test]
fn project_table_lookup_exported() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_exported_entry("foo", "proj.foo1", "a.rs"));
project.add_symbol(make_entry("foo", "proj.foo2", "b.rs"));
let exported = project.lookup_exported("foo");
assert_eq!(exported.len(), 1);
assert_eq!(exported[0].qn, "proj.foo1");
}
#[test]
fn project_table_lookup_exported_returns_empty_if_none_exported() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_entry("foo", "proj.foo", "a.rs"));
assert!(project.lookup_exported("foo").is_empty());
}
#[test]
fn project_table_lookup_exported_returns_empty_if_not_found() {
let project = ProjectSymbolTable::new();
assert!(project.lookup_exported("missing").is_empty());
}
use crate::resolve::includes_graph::IncludesGraph;
#[test]
fn lookup_exported_in_scope_filters_by_include() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_exported_entry("foo", "proj.B.foo", "/abs/B.h"));
project.add_symbol(make_exported_entry("foo", "proj.C.foo", "/abs/C.h"));
let mut graph = IncludesGraph::new();
graph.add_include("/abs/A.cpp", "/abs/B.h");
let results = project.lookup_exported_in_scope("foo", "/abs/A.cpp", &graph);
assert_eq!(results.len(), 1, "only B's foo should be in scope");
assert_eq!(results[0].qn, "proj.B.foo");
}
#[test]
fn lookup_exported_in_scope_includes_same_file() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_exported_entry("foo", "proj.A.foo", "/abs/A.cpp"));
let graph = IncludesGraph::new(); let results = project.lookup_exported_in_scope("foo", "/abs/A.cpp", &graph);
assert_eq!(results.len(), 1);
assert_eq!(results[0].qn, "proj.A.foo");
}
#[test]
fn lookup_exported_in_scope_transitive_include() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_exported_entry("foo", "proj.C.foo", "/abs/C.h"));
let mut graph = IncludesGraph::new();
graph.add_include("/abs/A.cpp", "/abs/B.h");
graph.add_include("/abs/B.h", "/abs/C.h");
let results = project.lookup_exported_in_scope("foo", "/abs/A.cpp", &graph);
assert_eq!(results.len(), 1, "transitive include should reach C");
assert_eq!(results[0].qn, "proj.C.foo");
}
#[test]
fn lookup_exported_in_scope_excludes_unreachable() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_exported_entry("foo", "proj.B.foo", "/abs/B.h"));
let graph = IncludesGraph::new(); let results = project.lookup_exported_in_scope("foo", "/abs/A.cpp", &graph);
assert!(results.is_empty(), "unreachable file should be excluded");
}
#[test]
fn lookup_exported_in_scope_filters_non_exported() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_entry("foo", "proj.B.foo", "/abs/B.h"));
let mut graph = IncludesGraph::new();
graph.add_include("/abs/A.cpp", "/abs/B.h");
let results = project.lookup_exported_in_scope("foo", "/abs/A.cpp", &graph);
assert!(results.is_empty(), "non-exported symbol should be excluded");
}
#[test]
fn lookup_exported_in_scope_returns_empty_if_name_not_found() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_exported_entry("foo", "proj.B.foo", "/abs/B.h"));
let mut graph = IncludesGraph::new();
graph.add_include("/abs/A.cpp", "/abs/B.h");
let results = project.lookup_exported_in_scope("missing", "/abs/A.cpp", &graph);
assert!(results.is_empty());
}
#[test]
fn lookup_exported_in_scope_multiple_in_scope_entries() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_exported_entry("foo", "proj.B.foo", "/abs/B.h"));
project.add_symbol(make_exported_entry("foo", "proj.C.foo", "/abs/C.h"));
project.add_symbol(make_exported_entry("foo", "proj.D.foo", "/abs/D.h"));
let mut graph = IncludesGraph::new();
graph.add_include("/abs/A.cpp", "/abs/B.h");
graph.add_include("/abs/A.cpp", "/abs/C.h");
let results = project.lookup_exported_in_scope("foo", "/abs/A.cpp", &graph);
assert_eq!(results.len(), 2, "B and C in scope, D excluded");
let qns: Vec<&str> = results.iter().map(|e| e.qn.as_str()).collect();
assert!(qns.contains(&"proj.B.foo"));
assert!(qns.contains(&"proj.C.foo"));
assert!(!qns.contains(&"proj.D.foo"));
}
#[test]
fn project_table_all_symbols() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_entry("a", "proj.a", "f1.rs"));
project.add_symbol(make_entry("b", "proj.b", "f2.rs"));
project.add_symbol(make_entry("a", "proj.a2", "f3.rs"));
let all = project.all_symbols();
assert_eq!(all.len(), 3);
}
#[test]
fn project_table_file_count() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_entry("a", "proj.a", "f1.rs"));
project.add_symbol(make_entry("b", "proj.b", "f2.rs"));
project.add_symbol(make_entry("c", "proj.c", "f1.rs"));
assert_eq!(project.file_count(), 2);
}
#[test]
fn project_table_symbol_count() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_entry("a", "proj.a", "f1.rs"));
project.add_symbol(make_entry("a", "proj.a2", "f1.rs"));
project.add_symbol(make_entry("b", "proj.b", "f2.rs"));
assert_eq!(project.symbol_count(), 3);
}
#[test]
fn project_table_cross_file_lookup() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_entry("foo", "proj.a.foo", "a.rs"));
project.add_symbol(make_entry("foo", "proj.b.foo", "b.rs"));
let results = project.lookup("foo");
assert_eq!(results.len(), 2);
}
#[test]
fn project_table_add_file_table_populates_global() {
let mut project = ProjectSymbolTable::new();
let mut file_table = FileSymbolTable::new();
file_table.add(make_entry("foo", "proj.foo", "a.rs"));
file_table.add(make_entry("bar", "proj.bar", "a.rs"));
project.add_file_table("a.rs", file_table);
assert_eq!(project.lookup("foo").len(), 1);
assert_eq!(project.lookup("bar").len(), 1);
assert_eq!(project.symbol_count(), 2);
}
#[test]
fn project_table_add_file_table_populates_file_lookup() {
let mut project = ProjectSymbolTable::new();
let mut file_table = FileSymbolTable::new();
file_table.add(make_entry("foo", "proj.foo", "a.rs"));
project.add_file_table("a.rs", file_table);
assert_eq!(project.lookup_in_file("a.rs", "foo").len(), 1);
}
#[test]
fn project_table_default_is_empty() {
let table = ProjectSymbolTable::default();
assert_eq!(table.file_count(), 0);
assert_eq!(table.symbol_count(), 0);
}
#[test]
fn project_table_clone_preserves_data() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_entry("foo", "proj.foo", "a.rs"));
let cloned = project.clone();
assert_eq!(cloned.symbol_count(), 1);
assert_eq!(cloned.lookup("foo").len(), 1);
}
#[test]
fn project_table_add_multiple_file_tables() {
let mut project = ProjectSymbolTable::new();
let mut ft1 = FileSymbolTable::new();
ft1.add(make_entry("foo", "proj.a.foo", "a.rs"));
let mut ft2 = FileSymbolTable::new();
ft2.add(make_entry("bar", "proj.b.bar", "b.rs"));
project.add_file_table("a.rs", ft1);
project.add_file_table("b.rs", ft2);
assert_eq!(project.file_count(), 2);
assert_eq!(project.symbol_count(), 2);
assert_eq!(project.lookup("foo").len(), 1);
assert_eq!(project.lookup("bar").len(), 1);
}
#[test]
fn project_table_add_symbol_creates_file_table_if_needed() {
let mut project = ProjectSymbolTable::new();
project.add_symbol(make_entry("foo", "proj.foo", "a.rs"));
project.add_symbol(make_entry("bar", "proj.bar", "a.rs"));
assert_eq!(project.file_count(), 1);
assert_eq!(project.lookup_in_file("a.rs", "foo").len(), 1);
assert_eq!(project.lookup_in_file("a.rs", "bar").len(), 1);
}
use crate::ir::ImportInfo;
fn make_import_result(file_path: &str, imports: Vec<ImportInfo>) -> ExtractResult {
let mut result = ExtractResult::new(file_path, Language::Rust);
result.imports = imports;
result
}
#[test]
fn populate_reexport_targets_marks_reexported_function_via_pub_use() {
let mut project = ProjectSymbolTable::new();
let mut cli_table = FileSymbolTable::new();
cli_table.add(make_exported_entry(
"run",
"proj.src.cli.rs.run",
"src/cli.rs",
));
project.add_file_table("src/cli.rs", cli_table);
let lib_result = make_import_result(
"src/lib.rs",
vec![ImportInfo {
source_file: "cli::run".to_string(),
imported_names: vec!["run".to_string()],
line: 38,
is_reexport: true,
}],
);
project.populate_reexport_targets(&[lib_result]);
assert!(project.is_reexport_target("proj.src.cli.rs.run"));
}
#[test]
fn populate_reexport_targets_skips_non_reexport_imports() {
let mut project = ProjectSymbolTable::new();
let mut cli_table = FileSymbolTable::new();
cli_table.add(make_exported_entry(
"run",
"proj.src.cli.rs.run",
"src/cli.rs",
));
project.add_file_table("src/cli.rs", cli_table);
let main_result = make_import_result(
"src/main.rs",
vec![ImportInfo {
source_file: "cli::run".to_string(),
imported_names: vec!["run".to_string()],
line: 1,
is_reexport: false,
}],
);
project.populate_reexport_targets(&[main_result]);
assert!(!project.is_reexport_target("proj.src.cli.rs.run"));
}
#[test]
fn populate_reexport_targets_skips_wildcard_reexports() {
let mut project = ProjectSymbolTable::new();
let mut cli_table = FileSymbolTable::new();
cli_table.add(make_exported_entry(
"run",
"proj.src.cli.rs.run",
"src/cli.rs",
));
project.add_file_table("src/cli.rs", cli_table);
let lib_result = make_import_result(
"src/lib.rs",
vec![ImportInfo {
source_file: "cli::*".to_string(),
imported_names: vec![],
line: 38,
is_reexport: true,
}],
);
project.populate_reexport_targets(&[lib_result]);
assert!(!project.is_reexport_target("proj.src.cli.rs.run"));
}
#[test]
fn populate_reexport_targets_handles_crate_prefix() {
let mut project = ProjectSymbolTable::new();
let mut cli_table = FileSymbolTable::new();
cli_table.add(make_exported_entry(
"run",
"proj.src.cli.rs.run",
"src/cli.rs",
));
project.add_file_table("src/cli.rs", cli_table);
let lib_result = make_import_result(
"src/lib.rs",
vec![ImportInfo {
source_file: "crate::cli::run".to_string(),
imported_names: vec!["run".to_string()],
line: 1,
is_reexport: true,
}],
);
project.populate_reexport_targets(&[lib_result]);
assert!(project.is_reexport_target("proj.src.cli.rs.run"));
}
#[test]
fn populate_reexport_targets_handles_absolute_file_paths() {
let mut project = ProjectSymbolTable::new();
let mut cli_table = FileSymbolTable::new();
cli_table.add(make_exported_entry(
"run",
"proj.src.cli.rs.run",
"src/cli.rs",
));
project.add_file_table("/home/user/projects/CalNexus/src/cli.rs", cli_table);
let lib_result = make_import_result(
"/home/user/projects/CalNexus/src/lib.rs",
vec![ImportInfo {
source_file: "cli::run".to_string(),
imported_names: vec!["run".to_string()],
line: 38,
is_reexport: true,
}],
);
project.populate_reexport_targets(&[lib_result]);
assert!(project.is_reexport_target("proj.src.cli.rs.run"));
}
#[test]
fn populate_reexport_targets_no_match_does_not_mark() {
let mut project = ProjectSymbolTable::new();
let mut cli_table = FileSymbolTable::new();
cli_table.add(make_exported_entry(
"run",
"proj.src.cli.rs.run",
"src/cli.rs",
));
project.add_file_table("src/cli.rs", cli_table);
let lib_result = make_import_result(
"src/lib.rs",
vec![ImportInfo {
source_file: "serde::Serialize".to_string(),
imported_names: vec!["Serialize".to_string()],
line: 1,
is_reexport: true,
}],
);
project.populate_reexport_targets(&[lib_result]);
assert!(!project.is_reexport_target("proj.src.cli.rs.run"));
}
#[test]
fn populate_reexport_targets_prefers_most_specific_match() {
let mut project = ProjectSymbolTable::new();
let mut cli_table = FileSymbolTable::new();
cli_table.add(make_exported_entry(
"run",
"proj.src.cli.rs.run",
"src/cli.rs",
));
let mut deep_cli_table = FileSymbolTable::new();
deep_cli_table.add(make_exported_entry(
"run",
"proj.src.deep.cli.rs.run",
"src/deep/cli.rs",
));
project.add_file_table("src/cli.rs", cli_table);
project.add_file_table("src/deep/cli.rs", deep_cli_table);
let lib_result = make_import_result(
"src/lib.rs",
vec![ImportInfo {
source_file: "cli::run".to_string(),
imported_names: vec!["run".to_string()],
line: 1,
is_reexport: true,
}],
);
project.populate_reexport_targets(&[lib_result]);
assert!(project.is_reexport_target("proj.src.cli.rs.run"));
assert!(!project.is_reexport_target("proj.src.deep.cli.rs.run"));
}
}