use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
use smol_str::SmolStr;
use crate::rindex::cache::Cache;
use crate::rindex::schema::{PackageIndex, SymbolEntry};
use crate::semantic::symbols::{
BundledPackages, LoadedPackage, PackageOrigin, StaticBaseR, SymbolProvider,
};
static BASE_R: LazyLock<StaticBaseR> = LazyLock::new(StaticBaseR::new);
static BUNDLED: LazyLock<BundledPackages> = LazyLock::new(BundledPackages::new);
pub fn resolve_origin(
indexed: &IndexedProvider,
name: &str,
loaded: &[LoadedPackage],
) -> PackageOrigin {
let mut candidates: Vec<SmolStr> = match BASE_R.origin(name, &[]) {
PackageOrigin::Resolved(p) => vec![p],
PackageOrigin::Ambiguous(v) => v,
PackageOrigin::Unknown => Vec::new(),
};
for pkg in loaded {
let exports_it = if indexed.has_package(&pkg.name) {
indexed.exports(&pkg.name, name)
} else {
BUNDLED.exports(&pkg.name, name)
};
if exports_it && !candidates.contains(&pkg.name) {
candidates.push(pkg.name.clone());
}
}
match candidates.len() {
0 => PackageOrigin::Unknown,
1 => PackageOrigin::Resolved(candidates.into_iter().next().unwrap()),
_ => PackageOrigin::Ambiguous(candidates),
}
}
pub fn package_indexed(indexed: &IndexedProvider, pkg: &str) -> bool {
BASE_R.package_indexed(pkg) || indexed.has_package(pkg) || BUNDLED.has_package(pkg)
}
pub fn is_base(name: &str) -> bool {
BASE_R.is_base(name)
}
#[derive(Debug, Default)]
pub struct IndexedProvider {
pkg_exports: HashMap<SmolStr, HashSet<SmolStr>>,
indices: HashMap<SmolStr, PackageIndex>,
}
impl IndexedProvider {
pub fn empty() -> Self {
Self::default()
}
pub fn from_indices(indices: impl IntoIterator<Item = PackageIndex>) -> Self {
let mut pkg_exports: HashMap<SmolStr, HashSet<SmolStr>> = HashMap::new();
let mut map: HashMap<SmolStr, PackageIndex> = HashMap::new();
for idx in indices {
let names: HashSet<SmolStr> = idx
.symbols
.iter()
.filter(|s| s.exported)
.map(|s| s.name.clone())
.collect();
pkg_exports.insert(idx.package.clone(), names);
map.insert(idx.package.clone(), idx);
}
IndexedProvider {
pkg_exports,
indices: map,
}
}
pub fn from_cache(cache: &Cache) -> Self {
Self::from_indices(cache.load_all())
}
pub fn has_package(&self, package: &str) -> bool {
self.pkg_exports.contains_key(package)
}
pub fn lookup(&self, package: &str, name: &str) -> Option<&SymbolEntry> {
self.indices
.get(package)?
.symbols
.iter()
.find(|s| s.name == name)
}
pub fn package(&self, package: &str) -> Option<&PackageIndex> {
self.indices.get(package)
}
fn exports(&self, package: &str, name: &str) -> bool {
self.pkg_exports
.get(package)
.is_some_and(|set| set.contains(name))
}
}
#[derive(Debug)]
pub struct CompositeProvider {
indexed: IndexedProvider,
}
impl CompositeProvider {
pub fn base_only() -> Self {
CompositeProvider {
indexed: IndexedProvider::empty(),
}
}
pub fn with_index(indexed: IndexedProvider) -> Self {
CompositeProvider { indexed }
}
pub fn indexed(&self) -> &IndexedProvider {
&self.indexed
}
}
impl SymbolProvider for CompositeProvider {
fn origin(&self, name: &str, loaded: &[LoadedPackage]) -> PackageOrigin {
resolve_origin(&self.indexed, name, loaded)
}
fn is_base(&self, name: &str) -> bool {
is_base(name)
}
fn package_indexed(&self, pkg: &str) -> bool {
package_indexed(&self.indexed, pkg)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rindex::schema::{SCHEMA_VERSION, SymbolKind};
use rowan::{TextRange, TextSize};
fn pkg(name: &str, exports: &[&str]) -> PackageIndex {
PackageIndex {
schema_version: SCHEMA_VERSION,
package: SmolStr::new(name),
version: SmolStr::new("1.0"),
lib_path: "/lib".into(),
r_version: None,
harvested_at: 0,
symbols: exports
.iter()
.map(|n| SymbolEntry {
name: SmolStr::new(*n),
kind: SymbolKind::Function,
exported: true,
formals: None,
help: None,
})
.collect(),
}
}
fn loaded(name: &str) -> LoadedPackage {
LoadedPackage {
name: SmolStr::new(name),
range: TextRange::new(TextSize::new(0), TextSize::new(0)),
}
}
#[test]
fn is_base_delegates_to_base_only() {
let p = CompositeProvider::with_index(IndexedProvider::from_indices([pkg(
"dplyr",
&["across"],
)]));
assert!(p.is_base("c"));
assert!(!p.is_base("across"));
}
#[test]
fn loaded_package_masks_base_name() {
let p = CompositeProvider::with_index(IndexedProvider::from_indices([pkg(
"dplyr",
&["filter"],
)]));
match p.origin("filter", &[loaded("dplyr")]) {
PackageOrigin::Ambiguous(v) => {
assert_eq!(v.last().map(|s| s.as_str()), Some("dplyr"));
assert!(v.iter().any(|s| s == "stats"));
}
other => panic!("expected Ambiguous, got {other:?}"),
}
}
#[test]
fn resolves_indexed_only_name() {
let p = CompositeProvider::with_index(IndexedProvider::from_indices([pkg(
"dplyr",
&["across"],
)]));
assert_eq!(
p.origin("across", &[loaded("dplyr")]),
PackageOrigin::Resolved(SmolStr::new("dplyr"))
);
}
#[test]
fn unindexed_unbundled_loaded_package_leaves_name_unknown() {
let p = CompositeProvider::base_only();
assert!(!p.package_indexed("not_a_real_package_xyz"));
assert_eq!(
p.origin("some_export_xyz", &[loaded("not_a_real_package_xyz")]),
PackageOrigin::Unknown
);
}
#[test]
fn bundled_package_is_indexed_and_resolves() {
let p = CompositeProvider::base_only();
assert!(p.package_indexed("data.table"));
assert_eq!(
p.origin("fread", &[loaded("data.table")]),
PackageOrigin::Resolved(SmolStr::new("data.table"))
);
assert_eq!(
p.origin("not_a_real_export_xyz", &[loaded("data.table")]),
PackageOrigin::Unknown
);
}
#[test]
fn installed_index_wins_over_bundled() {
let p = CompositeProvider::with_index(IndexedProvider::from_indices([pkg(
"data.table",
&["custom_installed_sym"],
)]));
assert_eq!(
p.origin("custom_installed_sym", &[loaded("data.table")]),
PackageOrigin::Resolved(SmolStr::new("data.table"))
);
assert_eq!(
p.origin("fread", &[loaded("data.table")]),
PackageOrigin::Unknown
);
}
#[test]
fn lookup_exposes_rich_data() {
let provider = IndexedProvider::from_indices([pkg("dplyr", &["filter"])]);
assert!(provider.lookup("dplyr", "filter").is_some());
assert!(provider.lookup("dplyr", "nope").is_none());
assert!(provider.has_package("dplyr"));
}
}