use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
use smol_str::SmolStr;
use crate::rindex::cache::Cache;
use crate::rindex::remote::RemoteExports;
use crate::rindex::schema::{PackageIndex, SymbolEntry};
use crate::semantic::symbols::{
BundledPackages, LoadedPackage, PackageOrigin, StaticBaseR, SymbolProvider,
meta_package_members,
};
static BASE_R: LazyLock<StaticBaseR> = LazyLock::new(StaticBaseR::new);
static BUNDLED: LazyLock<BundledPackages> = LazyLock::new(BundledPackages::new);
pub fn resolve_origin(
indexed: &IndexedProvider,
remote: &RemoteExports,
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(),
};
let mut consider = |pkg: &str| {
let exports_it = if indexed.has_package(pkg) {
indexed.exports(pkg, name)
} else if remote.has_package(pkg) {
remote.exports(pkg, name)
} else {
BUNDLED.exports(pkg, name)
};
if exports_it && !candidates.iter().any(|c| c == pkg) {
candidates.push(SmolStr::new(pkg));
}
};
for pkg in loaded {
consider(&pkg.name);
for member in attach_members(indexed, &pkg.name) {
consider(member);
}
}
match candidates.len() {
0 => PackageOrigin::Unknown,
1 => PackageOrigin::Resolved(candidates.into_iter().next().unwrap()),
_ => PackageOrigin::Ambiguous(candidates),
}
}
pub fn attach_members<'a>(
indexed: &'a IndexedProvider,
pkg: &str,
) -> impl Iterator<Item = &'a str> {
match indexed.attaches(pkg) {
Some(harvested) => AttachMembers::Harvested(harvested.iter()),
None => AttachMembers::Static(meta_package_members(pkg).iter()),
}
}
enum AttachMembers<'a> {
Harvested(std::slice::Iter<'a, SmolStr>),
Static(std::slice::Iter<'static, &'static str>),
}
impl<'a> Iterator for AttachMembers<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
match self {
AttachMembers::Harvested(it) => it.next().map(SmolStr::as_str),
AttachMembers::Static(it) => it.next().copied(),
}
}
}
pub fn package_indexed(indexed: &IndexedProvider, remote: &RemoteExports, pkg: &str) -> bool {
BASE_R.package_indexed(pkg)
|| indexed.has_package(pkg)
|| remote.has_package(pkg)
|| BUNDLED.has_package(pkg)
}
pub fn is_base(name: &str) -> bool {
BASE_R.is_base(name)
}
pub fn base_names() -> impl Iterator<Item = &'static SmolStr> {
let base: &'static StaticBaseR = &BASE_R;
base.base_names()
}
pub fn base_package_of(name: &str) -> Option<&'static SmolStr> {
let base: &'static StaticBaseR = &BASE_R;
base.package_of(name)
}
pub fn bundled_exports(package: &str) -> Option<impl Iterator<Item = &'static SmolStr>> {
let bundled: &'static BundledPackages = &BUNDLED;
bundled.package_exports(package)
}
#[derive(Debug, Default)]
pub struct IndexedProvider {
pkg_exports: HashMap<SmolStr, HashSet<SmolStr>>,
indices: HashMap<SmolStr, PackageIndex>,
attaches: HashMap<SmolStr, Vec<SmolStr>>,
}
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();
let mut attaches: HashMap<SmolStr, Vec<SmolStr>> = 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);
if !idx.attaches.is_empty() {
attaches.insert(idx.package.clone(), idx.attaches.clone());
}
map.insert(idx.package.clone(), idx);
}
IndexedProvider {
pkg_exports,
indices: map,
attaches,
}
}
pub fn from_cache(cache: &Cache) -> Self {
Self::from_indices(cache.load_all())
}
pub fn from_cache_exports(cache: &Cache) -> Self {
let mut pkg_exports: HashMap<SmolStr, HashSet<SmolStr>> = HashMap::new();
let mut attaches: HashMap<SmolStr, Vec<SmolStr>> = HashMap::new();
for exp in cache.load_all_exports() {
let names: HashSet<SmolStr> = exp
.symbols
.into_iter()
.filter(|s| s.exported)
.map(|s| s.name)
.collect();
if !exp.attaches.is_empty() {
attaches.insert(exp.package.clone(), exp.attaches);
}
pkg_exports.insert(exp.package, names);
}
IndexedProvider {
pkg_exports,
indices: HashMap::new(),
attaches,
}
}
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)
}
pub fn attaches(&self, package: &str) -> Option<&[SmolStr]> {
self.attaches.get(package).map(Vec::as_slice)
}
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,
remote: RemoteExports,
}
impl CompositeProvider {
pub fn base_only() -> Self {
CompositeProvider {
indexed: IndexedProvider::empty(),
remote: RemoteExports::new(),
}
}
pub fn with_index(indexed: IndexedProvider) -> Self {
CompositeProvider {
indexed,
remote: RemoteExports::new(),
}
}
pub fn with_remote(mut self, remote: RemoteExports) -> Self {
self.remote = remote;
self
}
pub fn indexed(&self) -> &IndexedProvider {
&self.indexed
}
}
impl SymbolProvider for CompositeProvider {
fn origin(&self, name: &str, loaded: &[LoadedPackage]) -> PackageOrigin {
resolve_origin(&self.indexed, &self.remote, name, loaded)
}
fn is_base(&self, name: &str) -> bool {
is_base(name)
}
fn package_indexed(&self, pkg: &str) -> bool {
package_indexed(&self.indexed, &self.remote, pkg)
}
fn attached_packages(&self, pkg: &str) -> Vec<SmolStr> {
attach_members(&self.indexed, pkg)
.map(SmolStr::new)
.collect()
}
}
#[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,
attaches: Vec::new(),
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
);
}
fn remote(pkgs: &[(&str, &[&str])]) -> RemoteExports {
let mut r = RemoteExports::new();
for (pkg, names) in pkgs {
r.insert_package(*pkg, names.iter().map(|n| SmolStr::new(*n)));
}
r
}
#[test]
fn remote_resolves_uninstalled_unbundled_package() {
let p = CompositeProvider::base_only().with_remote(remote(&[("tinytable", &["tt"])]));
assert!(p.package_indexed("tinytable"));
assert_eq!(
p.origin("tt", &[loaded("tinytable")]),
PackageOrigin::Resolved(SmolStr::new("tinytable"))
);
assert_eq!(
p.origin("not_a_real_export", &[loaded("tinytable")]),
PackageOrigin::Unknown
);
}
#[test]
fn installed_index_wins_over_remote() {
let p = CompositeProvider::with_index(IndexedProvider::from_indices([pkg(
"tinytable",
&["installed_sym"],
)]))
.with_remote(remote(&[("tinytable", &["remote_only_sym"])]));
assert_eq!(
p.origin("installed_sym", &[loaded("tinytable")]),
PackageOrigin::Resolved(SmolStr::new("tinytable"))
);
assert_eq!(
p.origin("remote_only_sym", &[loaded("tinytable")]),
PackageOrigin::Unknown
);
}
#[test]
fn remote_wins_over_bundled() {
let p = CompositeProvider::base_only().with_remote(remote(&[("data.table", &["new_sym"])]));
assert_eq!(
p.origin("new_sym", &[loaded("data.table")]),
PackageOrigin::Resolved(SmolStr::new("data.table"))
);
assert_eq!(
p.origin("fread", &[loaded("data.table")]),
PackageOrigin::Unknown
);
}
#[test]
fn meta_package_attaches_resolve_member_exports() {
let p = CompositeProvider::base_only();
assert!(matches!(
p.origin("tibble", &[loaded("tidyverse")]),
PackageOrigin::Resolved(_) | PackageOrigin::Ambiguous(_)
));
assert_eq!(
p.origin("across", &[loaded("tidyverse")]),
PackageOrigin::Resolved(SmolStr::new("dplyr"))
);
assert_eq!(
p.origin("not_a_real_export_xyz", &[loaded("tidyverse")]),
PackageOrigin::Unknown
);
}
fn meta_pkg(name: &str, exports: &[&str], attaches: &[&str]) -> PackageIndex {
let mut idx = pkg(name, exports);
idx.attaches = attaches.iter().map(|m| SmolStr::new(*m)).collect();
idx
}
#[test]
fn harvested_attach_set_resolves_member_exports() {
let p = CompositeProvider::with_index(IndexedProvider::from_indices([
meta_pkg("metaverse", &[], &["dplyr"]),
pkg("dplyr", &["across"]),
]));
assert_eq!(
p.origin("across", &[loaded("metaverse")]),
PackageOrigin::Resolved(SmolStr::new("dplyr"))
);
}
#[test]
fn harvested_attach_set_overrides_static_table() {
let p = CompositeProvider::with_index(IndexedProvider::from_indices([meta_pkg(
"tidyverse",
&[],
&["stringr"],
)]));
assert_eq!(
p.origin("across", &[loaded("tidyverse")]),
PackageOrigin::Unknown
);
}
#[test]
fn empty_harvested_attach_set_falls_back_to_static_table() {
let p = CompositeProvider::with_index(IndexedProvider::from_indices([meta_pkg(
"tidyverse",
&[],
&[],
)]));
assert_eq!(
p.origin("across", &[loaded("tidyverse")]),
PackageOrigin::Resolved(SmolStr::new("dplyr"))
);
}
#[test]
fn attached_packages_prefers_harvested_over_static() {
let p = CompositeProvider::with_index(IndexedProvider::from_indices([meta_pkg(
"tidyverse",
&[],
&["stringr"],
)]));
assert_eq!(
p.attached_packages("tidyverse"),
vec![SmolStr::new("stringr")]
);
let p = CompositeProvider::base_only();
assert_eq!(p.attached_packages("tidyverse").len(), 9);
assert!(p.attached_packages("dplyr").is_empty());
}
#[test]
fn exports_only_load_matches_full_load_membership() {
use crate::rindex::cache::Cache;
use crate::rindex::schema::{Formal, HelpDoc};
let tmp = tempfile::tempdir().unwrap();
let cache = Cache::new(tmp.path().to_path_buf());
let mut idx = pkg("dplyr", &["filter", "across"]);
idx.symbols[0].formals = Some(vec![Formal {
name: SmolStr::new(".data"),
default: None,
}]);
idx.symbols[0].help = Some(HelpDoc {
title: Some("Keep rows".to_string()),
..Default::default()
});
idx.symbols.push(SymbolEntry {
name: SmolStr::new("internal_helper"),
kind: SymbolKind::Function,
exported: false,
formals: None,
help: None,
});
cache.write_package(&idx).unwrap();
cache
.write_package(&meta_pkg("tidyverse", &[], &["dplyr"]))
.unwrap();
let full = IndexedProvider::from_cache(&cache);
let lean = IndexedProvider::from_cache_exports(&cache);
assert!(lean.has_package("dplyr"));
for name in ["filter", "across", "internal_helper", "nope"] {
assert_eq!(
lean.exports("dplyr", name),
full.exports("dplyr", name),
"membership diverged for {name}"
);
}
for pkg in ["tidyverse", "dplyr"] {
assert_eq!(
lean.attaches(pkg),
full.attaches(pkg),
"attaches diverged for {pkg}"
);
}
assert_eq!(
full.attaches("tidyverse"),
Some(&[SmolStr::new("dplyr")][..])
);
assert!(lean.lookup("dplyr", "filter").is_none());
assert!(lean.package("dplyr").is_none());
}
#[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"));
}
}