use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
use rowan::TextRange;
use smol_str::SmolStr;
const PACKAGE_BASE: &str = "base";
const PACKAGE_UTILS: &str = "utils";
const PACKAGE_STATS: &str = "stats";
const PACKAGE_METHODS: &str = "methods";
const PACKAGE_DATASETS: &str = "datasets";
const PACKAGE_GRDEVICES: &str = "grDevices";
const PACKAGE_GRAPHICS: &str = "graphics";
const DEFAULT_PACKAGES: &[&str] = &[
PACKAGE_BASE,
PACKAGE_UTILS,
PACKAGE_STATS,
PACKAGE_METHODS,
PACKAGE_DATASETS,
PACKAGE_GRDEVICES,
PACKAGE_GRAPHICS,
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadedPackage {
pub name: SmolStr,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PackageOrigin {
Resolved(SmolStr),
Ambiguous(Vec<SmolStr>),
Unknown,
}
pub trait SymbolProvider: Send + Sync {
fn origin(&self, name: &str, loaded: &[LoadedPackage]) -> PackageOrigin;
fn is_base(&self, name: &str) -> bool;
fn package_indexed(&self, pkg: &str) -> bool {
let _ = pkg;
false
}
}
#[derive(Debug)]
pub struct StaticBaseR {
name_to_packages: HashMap<SmolStr, Vec<SmolStr>>,
base_names: HashSet<SmolStr>,
}
impl Default for StaticBaseR {
fn default() -> Self {
Self::new()
}
}
impl StaticBaseR {
pub fn new() -> Self {
let mut name_to_packages: HashMap<SmolStr, Vec<SmolStr>> = HashMap::new();
let mut base_names = HashSet::new();
for &(pkg, list) in PACKAGE_LISTS {
let pkg_str = SmolStr::new(pkg);
for name in list.lines() {
let name = name.trim();
if name.is_empty() {
continue;
}
let name_str = SmolStr::new(name);
name_to_packages
.entry(name_str.clone())
.or_default()
.push(pkg_str.clone());
base_names.insert(name_str);
}
}
Self {
name_to_packages,
base_names,
}
}
}
impl SymbolProvider for StaticBaseR {
fn origin(&self, name: &str, loaded: &[LoadedPackage]) -> PackageOrigin {
let mut candidates: Vec<SmolStr> = Vec::new();
if let Some(pkgs) = self.name_to_packages.get(name) {
candidates.extend(pkgs.iter().cloned());
}
let _ = loaded;
match candidates.len() {
0 => PackageOrigin::Unknown,
1 => PackageOrigin::Resolved(candidates.into_iter().next().unwrap()),
_ => PackageOrigin::Ambiguous(candidates),
}
}
fn is_base(&self, name: &str) -> bool {
self.base_names.contains(name)
}
fn package_indexed(&self, pkg: &str) -> bool {
DEFAULT_PACKAGES.contains(&pkg)
}
}
pub fn default_packages() -> &'static [&'static str] {
DEFAULT_PACKAGES
}
#[derive(Debug)]
pub struct BundledPackages {
exports: &'static HashMap<SmolStr, HashSet<SmolStr>>,
}
static BUNDLED_EXPORTS: LazyLock<HashMap<SmolStr, HashSet<SmolStr>>> =
LazyLock::new(|| parse_bundled(include_str!("cran/exports.txt")));
fn parse_bundled(text: &str) -> HashMap<SmolStr, HashSet<SmolStr>> {
let mut map: HashMap<SmolStr, HashSet<SmolStr>> = HashMap::new();
let mut current: Option<SmolStr> = None;
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(pkg) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
current = Some(SmolStr::new(pkg));
map.entry(current.clone().unwrap()).or_default();
} else if let Some(pkg) = ¤t {
map.get_mut(pkg).unwrap().insert(SmolStr::new(line));
}
}
map
}
impl Default for BundledPackages {
fn default() -> Self {
Self::new()
}
}
impl BundledPackages {
pub fn new() -> Self {
Self {
exports: &BUNDLED_EXPORTS,
}
}
pub fn has_package(&self, package: &str) -> bool {
self.exports.contains_key(package)
}
pub fn exports(&self, package: &str, name: &str) -> bool {
self.exports
.get(package)
.is_some_and(|set| set.contains(name))
}
}
const PACKAGE_LISTS: &[(&str, &str)] = &[
(PACKAGE_BASE, include_str!("base_r/base.txt")),
(PACKAGE_UTILS, include_str!("base_r/utils.txt")),
(PACKAGE_STATS, include_str!("base_r/stats.txt")),
(PACKAGE_METHODS, include_str!("base_r/methods.txt")),
(PACKAGE_DATASETS, include_str!("base_r/datasets.txt")),
(PACKAGE_GRDEVICES, include_str!("base_r/grDevices.txt")),
(PACKAGE_GRAPHICS, include_str!("base_r/graphics.txt")),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn knows_common_base_names() {
let p = StaticBaseR::new();
assert!(p.is_base("c"));
assert!(p.is_base("length"));
assert!(p.is_base("print"));
}
#[test]
fn resolves_base_function() {
let p = StaticBaseR::new();
match p.origin("length", &[]) {
PackageOrigin::Resolved(pkg) => assert_eq!(pkg.as_str(), "base"),
other => panic!("expected Resolved(base), got {other:?}"),
}
}
#[test]
fn returns_unknown_for_unknown_name() {
let p = StaticBaseR::new();
assert_eq!(
p.origin("not_a_real_symbol_xyz", &[]),
PackageOrigin::Unknown
);
}
#[test]
fn knows_stats_function() {
let p = StaticBaseR::new();
assert!(p.is_base("lm"));
}
#[test]
fn knows_datasets_lazydata() {
let p = StaticBaseR::new();
assert!(p.is_base("iris"));
}
#[test]
fn bundled_knows_curated_package() {
let b = BundledPackages::new();
assert!(b.has_package("data.table"));
assert!(b.exports("data.table", "fread"));
assert!(!b.exports("data.table", "definitely_not_a_real_export"));
}
#[test]
fn bundled_unknown_package_is_absent() {
let b = BundledPackages::new();
assert!(!b.has_package("not_a_real_package_xyz"));
assert!(!b.exports("not_a_real_package_xyz", "anything"));
}
#[test]
fn bundled_names_are_not_base() {
let base = StaticBaseR::new();
let bundled = BundledPackages::new();
assert!(bundled.exports("rlang", "abort"));
assert!(!base.is_base("abort"));
}
}