use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use rd_helpdb::{HelpTopicIndex, HelpTopicText, PackageHelpDb};
use smol_str::SmolStr;
use crate::namespace::{
Argument as NamespaceArgument, Directive as NamespaceDirective, DirectiveKind,
};
use crate::rindex::deparse;
use crate::rindex::lazyload::{self, LazyLoadDb};
use crate::rindex::libpaths::LibrarySearch;
use crate::rindex::rd;
use crate::rindex::rds::{Rkind, Robj};
use crate::rindex::schema::{
Formal, HelpDoc, PackageIndex, SCHEMA_VERSION, SymbolEntry, SymbolKind,
};
#[derive(Debug)]
pub enum HarvestError {
NotAPackage(PathBuf),
Io(String),
BadDescription(String),
}
impl std::fmt::Display for HarvestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HarvestError::NotAPackage(p) => {
write!(f, "{} is not an installed R package", p.display())
}
HarvestError::Io(s) => write!(f, "harvest I/O error: {s}"),
HarvestError::BadDescription(s) => write!(f, "malformed DESCRIPTION: {s}"),
}
}
}
impl std::error::Error for HarvestError {}
type Result<T> = std::result::Result<T, HarvestError>;
#[derive(Debug, Clone, Copy)]
pub struct HarvestOptions {
pub help: bool,
}
impl Default for HarvestOptions {
fn default() -> Self {
HarvestOptions { help: true }
}
}
pub fn harvest_package(
pkg_dir: &Path,
opts: HarvestOptions,
harvested_at: u64,
) -> Result<PackageIndex> {
harvest_package_in(
pkg_dir,
opts,
harvested_at,
&LibrarySearch::from_dirs(Vec::new()),
)
}
pub fn harvest_package_in(
pkg_dir: &Path,
opts: HarvestOptions,
harvested_at: u64,
search: &LibrarySearch,
) -> Result<PackageIndex> {
let desc_path = pkg_dir.join("DESCRIPTION");
if !desc_path.is_file() {
return Err(HarvestError::NotAPackage(pkg_dir.to_path_buf()));
}
let desc = read_dcf(&desc_path)?;
let package = desc
.field("Package")
.ok_or_else(|| HarvestError::BadDescription("no Package field".into()))?;
let version = desc
.field("Version")
.ok_or_else(|| HarvestError::BadDescription("no Version field".into()))?;
let r_version = desc
.field("Built")
.as_deref()
.and_then(parse_built_r_version);
let title = desc.field("Title").map(|t| SmolStr::new(flatten_ws(&t)));
let object_names = read_object_names(pkg_dir, &package);
let exports = resolve_package_exports(pkg_dir, &object_names);
let lazydata = read_lazydata_names(pkg_dir);
let help = HelpFiles::when_enabled(opts.help, || HelpFiles::open(pkg_dir, &package));
let db = LazyLoadDb::open(&pkg_dir.join("R").join(format!("{package}.rdx")))
.inspect_err(|error| {
log::debug!("{}: code database unavailable: {error}", pkg_dir.display())
})
.ok();
let export_set: BTreeSet<&str> = exports.iter().map(String::as_str).collect();
let mut symbols: Vec<SymbolEntry> = exports
.iter()
.map(|name| {
let help = build_help(&help, name);
let (kind, formals) = refine_symbol(db.as_ref(), name);
SymbolEntry {
name: SmolStr::new(name),
kind,
exported: true,
formals,
help,
}
})
.collect();
for name in &lazydata {
if export_set.contains(name.as_str()) {
continue;
}
let help = build_help(&help, name);
symbols.push(SymbolEntry {
name: SmolStr::new(name),
kind: SymbolKind::Data,
exported: true,
formals: None,
help,
});
}
symbols.sort_by(|a, b| a.name.cmp(&b.name));
Ok(PackageIndex {
schema_version: SCHEMA_VERSION,
package: SmolStr::new(&package),
version: SmolStr::new(&version),
lib_path: pkg_dir
.parent()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default(),
title,
r_version,
harvested_at,
attaches: detect_attaches(db.as_ref(), &package, search),
symbols,
})
}
const ATTACH_SET_VARS: &[&str] = &["core"];
fn detect_attaches(db: Option<&LazyLoadDb>, package: &str, search: &LibrarySearch) -> Vec<SmolStr> {
let Some(db) = db else {
return Vec::new();
};
if !db.contains(".onAttach") {
return Vec::new();
}
let installed = |member: &str| search.find_package(member).is_some();
for var in ATTACH_SET_VARS {
if let Ok(obj) = db.fetch_shared(var)
&& let Some(members) = validate_attach_set(&obj, package, &installed)
{
return members;
}
}
Vec::new()
}
fn validate_attach_set(
obj: &rd_rds::RObject,
package: &str,
installed: &dyn Fn(&str) -> bool,
) -> Option<Vec<SmolStr>> {
let rd_rds::RValue::Character(strings) = obj.value() else {
return None;
};
let names = strings
.iter()
.map(|s| s.as_str()?.ok())
.collect::<Option<Vec<_>>>()?;
let borrowed: Vec<_> = names.iter().map(|s| s.as_ref()).collect();
validate_attach_names(&borrowed, package, installed)
}
pub(crate) fn validate_attach_names(
names: &[&str],
package: &str,
installed: &dyn Fn(&str) -> bool,
) -> Option<Vec<SmolStr>> {
let mut members: Vec<SmolStr> = Vec::new();
for &name in names {
if name == package {
continue;
}
if !is_valid_package_name(name) {
return None;
}
if !members.iter().any(|m| m == name) {
members.push(SmolStr::new(name));
}
}
if members.is_empty() || !members.iter().all(|m| installed(m)) {
return None;
}
Some(members)
}
pub(crate) fn is_valid_package_name(name: &str) -> bool {
name.len() >= 2
&& name.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '.')
&& !name.ends_with('.')
}
fn resolve_package_exports(pkg_dir: &Path, object_names: &[String]) -> Vec<String> {
let ns_path = pkg_dir.join("NAMESPACE");
if ns_path.is_file() {
let namespace = std::fs::read_to_string(&ns_path).unwrap_or_default();
let borrowed: Vec<&str> = object_names.iter().map(String::as_str).collect();
resolve_exports(&namespace, &borrowed)
} else {
object_names.to_vec()
}
}
fn read_object_names(pkg_dir: &Path, package: &str) -> Vec<String> {
let rdx = pkg_dir.join("R").join(format!("{package}.rdx"));
lazyload::read_index_names(&rdx)
.inspect_err(|error| {
log::debug!("{}: names unavailable: {error}", rdx.display());
})
.unwrap_or_default()
}
fn read_lazydata_names(pkg_dir: &Path) -> Vec<String> {
let rdx = pkg_dir.join("data").join("Rdata.rdx");
lazyload::read_index_names(&rdx)
.inspect_err(|error| {
log::debug!("{}: names unavailable: {error}", rdx.display());
})
.unwrap_or_default()
}
fn refine_symbol(db: Option<&LazyLoadDb>, name: &str) -> (SymbolKind, Option<Vec<Formal>>) {
let Some(db) = db else {
return (SymbolKind::Function, None);
};
let obj = match db.fetch(name) {
Ok(obj) => obj,
Err(error) => {
log::debug!("code binding {name:?} unavailable: {error}");
return (SymbolKind::Function, None);
}
};
match &obj.kind {
Rkind::Closure { formals, .. } => (SymbolKind::Function, Some(extract_formals(formals))),
Rkind::Builtin => (SymbolKind::Function, None),
Rkind::Logical(_) | Rkind::Int(_) | Rkind::Real(_) | Rkind::Str(_) | Rkind::List(_) => {
(SymbolKind::Data, None)
}
_ => (SymbolKind::Other, None),
}
}
fn extract_formals(formals: &Robj) -> Vec<Formal> {
match &formals.kind {
Rkind::Pairlist(items) => items
.iter()
.map(|it| Formal {
name: it.tag.clone().unwrap_or_default(),
default: deparse::deparse(&it.value),
})
.collect(),
_ => Vec::new(),
}
}
struct Dcf {
document: crate::dcf::Document,
}
impl Dcf {
fn field(&self, name: &str) -> Option<String> {
self.document.field(name).map(|field| field.folded_value())
}
}
fn read_dcf(path: &Path) -> Result<Dcf> {
let text = std::fs::read_to_string(path).map_err(|e| HarvestError::Io(e.to_string()))?;
Ok(Dcf {
document: crate::dcf::parse(&text).document(),
})
}
fn flatten_ws(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn parse_built_r_version(built: &str) -> Option<SmolStr> {
let first = built.split(';').next()?.trim();
let ver = first.strip_prefix("R ").unwrap_or(first).trim();
if ver.is_empty() {
None
} else {
Some(SmolStr::new(ver))
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct NamespaceInfo {
pub exports: BTreeSet<String>,
pub imported_names: BTreeSet<String>,
pub imported_packages: BTreeSet<String>,
pub imported_from_packages: BTreeSet<String>,
pub s3_methods: BTreeSet<String>,
pub dynlib_routines: BTreeSet<String>,
pub dynlib_registration: Option<DynLibFixes>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct DynLibFixes {
pub prefix: String,
pub suffix: String,
}
impl DynLibFixes {
pub fn apply(&self, routine: &str) -> String {
format!("{}{routine}{}", self.prefix, self.suffix)
}
}
pub fn parse_namespace(namespace: &str, object_names: &[&str]) -> NamespaceInfo {
let mut info = NamespaceInfo::default();
let mut patterns: Vec<regex::Regex> = Vec::new();
for directive in crate::namespace::parse(namespace).document().directives() {
match directive.kind() {
DirectiveKind::Export
| DirectiveKind::ExportClass
| DirectiveKind::ExportClasses
| DirectiveKind::ExportMethods => {
info.exports.extend(directive_values(&directive));
}
DirectiveKind::S3Method => {
let args: Vec<String> = directive_values(&directive).collect();
let method = match args.as_slice() {
[_, _, method] => Some(method.clone()),
[generic, class] => Some(format!("{generic}.{class}")),
_ => None,
};
if let Some(method) = method {
info.exports.insert(method.clone());
info.s3_methods.insert(method);
}
}
DirectiveKind::ExportPattern | DirectiveKind::ExportClassPattern => {
for arg in directive_values(&directive) {
if let Some(re) = compile_r_pattern(&arg) {
patterns.push(re);
}
}
}
DirectiveKind::ImportFrom
| DirectiveKind::ImportClassFrom
| DirectiveKind::ImportClassesFrom
| DirectiveKind::ImportMethodsFrom => {
let mut args = directive_values(&directive);
if let Some(package) = args.next() {
info.imported_from_packages.insert(package);
info.imported_names.extend(args);
}
}
DirectiveKind::Import => {
info.imported_packages
.extend(directive_positional_values(&directive));
}
DirectiveKind::UseDynLib => parse_use_dyn_lib(&directive, &mut info),
DirectiveKind::Unsupported => {}
}
}
if !patterns.is_empty() {
for name in object_names {
if patterns.iter().any(|re| re.is_match(name)) {
info.exports.insert((*name).to_string());
}
}
}
info
}
fn parse_use_dyn_lib(directive: &NamespaceDirective, info: &mut NamespaceInfo) {
let mut fixes: Option<DynLibFixes> = None;
let mut registration = false;
let mut seen_dll = false;
for arg in directive.arguments() {
let Some(value) = namespace_argument_value(&arg) else {
continue;
};
match arg.name().as_deref() {
Some(".registration") => registration |= is_r_true(&value),
Some(".fixes") => fixes = Some(parse_fixes(&value)),
Some(alias) => {
info.dynlib_routines.insert(alias.to_string());
}
None if !seen_dll => seen_dll = true,
None => {
info.dynlib_routines.insert(value);
}
}
}
if registration {
info.dynlib_registration = Some(fixes.unwrap_or_default());
}
}
fn is_r_true(value: &str) -> bool {
matches!(value.trim(), "TRUE" | "T")
}
fn parse_fixes(value: &str) -> DynLibFixes {
let trimmed = value.trim();
let parts: Vec<String> = match trimmed
.strip_prefix("c(")
.and_then(|rest| rest.strip_suffix(')'))
{
Some(inner) => split_top_level_commas(inner)
.into_iter()
.map(|part| unquote(part).unwrap_or_default())
.collect(),
None => vec![trimmed.to_string()],
};
DynLibFixes {
prefix: parts.first().cloned().unwrap_or_default(),
suffix: parts.get(1).cloned().unwrap_or_default(),
}
}
pub fn resolve_exports(namespace: &str, object_names: &[&str]) -> Vec<String> {
parse_namespace(namespace, object_names)
.exports
.into_iter()
.collect()
}
fn compile_r_pattern(pattern: &str) -> Option<regex::Regex> {
regex::Regex::new(pattern).ok()
}
fn directive_values(directive: &NamespaceDirective) -> impl Iterator<Item = String> + '_ {
directive
.arguments()
.filter_map(|argument| namespace_argument_value(&argument))
}
fn directive_positional_values(
directive: &NamespaceDirective,
) -> impl Iterator<Item = String> + '_ {
directive
.arguments()
.filter(|argument| argument.name().is_none())
.filter_map(|argument| namespace_argument_value(&argument))
}
fn namespace_argument_value(argument: &NamespaceArgument) -> Option<String> {
let value = argument.value()?;
unquote(value.to_string().trim())
}
fn split_top_level_commas(inner: &str) -> Vec<&str> {
let bytes = inner.as_bytes();
let mut parts = Vec::new();
let mut start = 0;
let mut depth = 0i32;
let mut in_str: Option<u8> = None;
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if let Some(q) = in_str {
if c == b'\\' {
i += 2;
continue;
}
if c == q {
in_str = None;
}
} else {
match c {
b'"' | b'\'' | b'`' => in_str = Some(c),
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
b',' if depth == 0 => {
parts.push(&inner[start..i]);
start = i + 1;
}
_ => {}
}
}
i += 1;
}
parts.push(&inner[start..]);
parts
}
fn unquote(value: &str) -> Option<String> {
let value = value.trim();
let bytes = value.as_bytes();
if bytes.len() >= 2
&& (bytes[0] == b'"' || bytes[0] == b'\'' || bytes[0] == b'`')
&& bytes[bytes.len() - 1] == bytes[0]
{
let inner = &value[1..value.len() - 1];
let mut out = String::with_capacity(inner.len());
let mut chars = inner.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => out.push('\n'),
Some('t') => out.push('\t'),
Some(other) => out.push(other),
None => {}
}
} else {
out.push(c);
}
}
Some(out)
} else if value.is_empty() {
None
} else {
Some(value.to_string())
}
}
fn read_help_index(pkg_dir: &Path) -> Option<HelpTopicIndex> {
match HelpTopicIndex::read_installed(pkg_dir) {
Ok(index) => {
if let Some(index) = &index {
for (row, entry) in index.entries().enumerate() {
for (field, value) in [("title", &entry.title), ("file", &entry.file)] {
if let HelpTopicText::Invalid(reason) = value {
log::debug!(
"{}: help metadata row {row} {field}: {reason}",
pkg_dir.display()
);
}
}
}
}
index
}
Err(error) => {
log::debug!("{}: help metadata unavailable: {error}", pkg_dir.display());
None
}
}
}
fn build_help(help: &HelpFiles, name: &str) -> Option<HelpDoc> {
let entry = help.index.as_ref()?.find_alias(name)?;
let sections = entry
.topic_key()
.zip(help.database.as_ref())
.and_then(|(key, database)| match database.document(key) {
Ok(document) => {
let rendered = rd::render_document(&document);
for issue in rendered.issues {
log::debug!(
"{}: topic {key:?}, omitted {} at {:?}",
help.package_dir.display(),
issue.field,
issue.path
);
}
Some(rendered.sections)
}
Err(error) => {
log::debug!(
"{}: topic {key:?} unavailable: {error}",
help.package_dir.display()
);
None
}
})
.unwrap_or_default();
let doc = rd::into_help_doc(entry.title.as_str().map(str::to_owned), sections);
(doc != HelpDoc::default()).then_some(doc)
}
#[derive(Default)]
struct HelpFiles {
index: Option<HelpTopicIndex>,
database: Option<HelpDatabase>,
package_dir: PathBuf,
}
impl HelpFiles {
fn when_enabled(enabled: bool, load: impl FnOnce() -> Self) -> Self {
if enabled { load() } else { Self::default() }
}
fn open(package_dir: &Path, package: &str) -> Self {
let index = read_help_index(package_dir);
let database = HelpDatabase::open(package_dir, package)
.map_err(|error| {
log::debug!(
"{}: compiled help unavailable: {error}",
package_dir.display()
);
})
.ok();
Self {
index,
database,
package_dir: package_dir.to_owned(),
}
}
}
enum HelpDatabase {
Package(PackageHelpDb),
Explicit(rd_rds::lazyload::LazyLoadDb),
}
impl HelpDatabase {
fn open(package_dir: &Path, package: &str) -> std::result::Result<Self, String> {
if package_dir.file_name().and_then(|s| s.to_str()) == Some(package) {
PackageHelpDb::open(package_dir)
.map(Self::Package)
.map_err(|e| e.to_string())
} else {
let help = package_dir.join("help");
rd_rds::lazyload::LazyLoadDb::open(
help.join(format!("{package}.rdx")),
help.join(format!("{package}.rdb")),
)
.map(Self::Explicit)
.map_err(|e| e.to_string())
}
}
fn document(&self, key: &str) -> std::result::Result<rd_ast::RdDocument, String> {
let object = match self {
Self::Package(db) => db.raw_topic(key).map_err(|e| e.to_string())?,
Self::Explicit(db) => {
let record = db.read(key).map_err(|e| e.to_string())?;
rd_rds::parse(record.decompressed_bytes()).map_err(|e| e.to_string())?
}
};
rd_ast::lower_r_object(&object).map_err(|e| e.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disabled_help_does_not_open_metadata_or_records() {
let help = HelpFiles::when_enabled(false, || panic!("help files must not be read"));
assert!(help.index.is_none());
assert!(help.database.is_none());
}
fn str_vec(names: &[Option<&str>]) -> rd_rds::RObject {
let values = names
.iter()
.map(|name| match name {
Some(name) => rd_rds::RStr::new(
name.as_bytes(),
rd_rds::REncoding::Utf8,
rd_rds::NativeEncodingSource::Unknown,
),
None => rd_rds::RStr::Na,
})
.collect();
rd_rds::RObject::from_parts(
rd_rds::RValue::Character(values),
rd_rds::Attributes::default(),
)
}
#[test]
fn attach_set_accepts_installed_package_names() {
let obj = str_vec(&[Some("dplyr"), Some("ggplot2")]);
let members = validate_attach_set(&obj, "tidyverse", &|_| true).unwrap();
assert_eq!(members, [SmolStr::new("dplyr"), SmolStr::new("ggplot2")]);
}
#[test]
fn attach_set_rejects_non_character_objects() {
let obj = rd_rds::RObject::from_parts(rd_rds::RValue::Null, rd_rds::Attributes::default());
assert!(validate_attach_set(&obj, "tidyverse", &|_| true).is_none());
}
#[test]
fn attach_set_rejects_na_and_invalid_names() {
let obj = str_vec(&[Some("dplyr"), None]);
assert!(validate_attach_set(&obj, "tidyverse", &|_| true).is_none());
for bad in ["with space", "path/pkg", "x", "1pkg", "pkg.", ""] {
let obj = str_vec(&[Some(bad)]);
assert!(
validate_attach_set(&obj, "tidyverse", &|_| true).is_none(),
"{bad:?} should be rejected"
);
}
}
#[test]
fn attach_set_rejects_any_uninstalled_member() {
let obj = str_vec(&[Some("dplyr"), Some("notinstalled")]);
let installed = |m: &str| m == "dplyr";
assert!(validate_attach_set(&obj, "tidyverse", &installed).is_none());
}
#[test]
fn attach_set_strips_self_and_duplicates() {
let obj = str_vec(&[Some("tidyverse"), Some("dplyr"), Some("dplyr")]);
let members = validate_attach_set(&obj, "tidyverse", &|_| true).unwrap();
assert_eq!(members, [SmolStr::new("dplyr")]);
}
#[test]
fn attach_set_rejects_empty_and_self_only_sets() {
let obj = str_vec(&[]);
assert!(validate_attach_set(&obj, "tidyverse", &|_| true).is_none());
let obj = str_vec(&[Some("tidyverse")]);
assert!(validate_attach_set(&obj, "tidyverse", &|_| true).is_none());
}
#[test]
fn dcf_folds_continuation_lines() {
let text = "Package: testpkg\nCollate:\n a.R\n b.R\nVersion: 1.0\n";
let desc = Dcf {
document: crate::dcf::parse(text).document(),
};
assert_eq!(desc.field("Package").as_deref(), Some("testpkg"));
assert_eq!(desc.field("Collate").as_deref(), Some("a.R\nb.R"));
assert_eq!(desc.field("Version").as_deref(), Some("1.0"));
}
#[test]
fn resolves_explicit_exports() {
let ns = r#"
export(foo)
export("bar")
S3method(print, baz)
exportMethods(show)
"#;
let exports = resolve_exports(ns, &[]);
assert!(exports.contains(&"foo".to_string()));
assert!(exports.contains(&"bar".to_string()));
assert!(exports.contains(&"show".to_string()));
assert!(exports.contains(&"print.baz".to_string()));
assert!(!exports.contains(&"print".to_string()));
assert!(!exports.contains(&"baz".to_string()));
}
#[test]
fn s3method_registers_dotted_method_name() {
let ns = r#"
S3method(coef, SLOPE)
S3method(predict, "GaussianSLOPE")
"#;
let exports = resolve_exports(ns, &[]);
assert!(exports.contains(&"coef.SLOPE".to_string()));
assert!(exports.contains(&"predict.GaussianSLOPE".to_string()));
}
#[test]
fn s3method_three_arg_form_uses_explicit_method() {
let ns = "S3method(print, foo, print_foo_impl)\n";
let exports = resolve_exports(ns, &[]);
assert!(exports.contains(&"print_foo_impl".to_string()));
assert!(!exports.contains(&"print.foo".to_string()));
}
#[test]
fn expands_export_pattern_excluding_dotted() {
let ns = r#"exportPattern("^[^\\.]")"#;
let objs = ["alpha", "beta", ".hidden", ".__NAMESPACE__."];
let exports = resolve_exports(ns, &objs);
assert_eq!(exports, vec!["alpha".to_string(), "beta".to_string()]);
}
#[test]
fn unquotes_operator_exports() {
let ns = r#"export("%>%")
export("n'est pas")"#;
let exports = resolve_exports(ns, &[]);
assert!(exports.contains(&"%>%".to_string()));
assert!(exports.contains(&"n'est pas".to_string()));
}
#[test]
fn ignores_export_inside_export_pattern_keyword() {
let ns = r#"exportPattern("^x")"#;
let exports = resolve_exports(ns, &["xa", "yb"]);
assert_eq!(exports, vec!["xa".to_string()]);
}
#[test]
fn ignores_directive_spellings_outside_namespace_positions() {
let ns = r#"
# export(in_comment)
custom("export(in_string)", c(export(in_argument)))
if (requireNamespace("pkg") && export(in_condition)) export(real)
pkg::export(in_qualified_call)
"#;
let info = parse_namespace(ns, &[]);
assert_eq!(info.exports, ["real".to_string()].into());
}
#[test]
fn reads_directives_from_both_conditional_branches() {
let ns = r#"
if (getRversion() >= "4.0.0") {
export(new_api)
} else {
export(old_api)
}
"#;
let info = parse_namespace(ns, &[]);
assert_eq!(
info.exports,
["new_api".to_string(), "old_api".to_string()].into()
);
}
#[test]
fn parses_import_directives() {
let ns = "import(rlang)\nimportFrom(dplyr, filter, select)\nexport(foo)\n";
let info = parse_namespace(ns, &[]);
assert!(info.exports.contains("foo"));
assert!(info.imported_names.contains("filter"));
assert!(info.imported_names.contains("select"));
assert!(!info.imported_names.contains("dplyr"));
assert!(info.imported_packages.contains("rlang"));
}
#[test]
fn import_keeps_only_the_packages_it_names() {
let info = parse_namespace("import(dplyr, except = c(filter, lag))\n", &[]);
assert_eq!(info.imported_packages, ["dplyr".to_string()].into());
let info = parse_namespace("import(stats, utils)\n", &[]);
assert_eq!(
info.imported_packages,
["stats".to_string(), "utils".to_string()].into()
);
}
#[test]
fn import_from_records_the_package_it_names() {
let info = parse_namespace("importFrom(dplyr, filter, select)\n", &[]);
assert_eq!(info.imported_from_packages, ["dplyr".to_string()].into());
assert!(info.imported_packages.is_empty());
assert_eq!(
info.imported_names,
["filter".to_string(), "select".to_string()].into()
);
}
#[test]
fn use_dyn_lib_enumerates_explicit_routines() {
let info = parse_namespace("useDynLib(backports, dotsElt, dotsLength)\n", &[]);
assert_eq!(
info.dynlib_routines,
["dotsElt".to_string(), "dotsLength".to_string()].into()
);
assert!(info.dynlib_registration.is_none());
}
#[test]
fn use_dyn_lib_binds_the_alias_not_the_routine() {
let info = parse_namespace("useDynLib(pkg, my_alias = c_routine)\n", &[]);
assert_eq!(info.dynlib_routines, ["my_alias".to_string()].into());
}
#[test]
fn use_dyn_lib_registration_records_its_fixes() {
let info = parse_namespace(
"useDynLib(bit, .registration = TRUE, .fixes = \"C_\")\n",
&[],
);
assert!(info.dynlib_routines.is_empty());
let fixes = info.dynlib_registration.expect("registration declared");
assert_eq!(fixes.apply("foo"), "C_foo");
let info = parse_namespace(
"useDynLib(pkg,.registration=TRUE,.fixes=c(\"C_\",\"_\"))",
&[],
);
let fixes = info.dynlib_registration.expect("registration declared");
assert_eq!(fixes.apply("foo"), "C_foo_");
let info = parse_namespace("useDynLib(rlang, .registration = TRUE)\n", &[]);
assert_eq!(
info.dynlib_registration.expect("registration").apply("ffi"),
"ffi"
);
}
#[test]
fn use_dyn_lib_without_registration_declares_none() {
assert!(
parse_namespace("useDynLib(caret)\n", &[])
.dynlib_registration
.is_none()
);
let info = parse_namespace("useDynLib(pkg, .registration = FALSE)\n", &[]);
assert!(info.dynlib_registration.is_none());
assert!(info.dynlib_routines.is_empty());
}
#[test]
fn s4_import_forms_record_the_package_they_name() {
let info = parse_namespace(
"importClassesFrom(Matrix, dgCMatrix)\nimportMethodsFrom(Matrix, crossprod)\n",
&[],
);
assert_eq!(info.imported_from_packages, ["Matrix".to_string()].into());
assert_eq!(
info.imported_names,
["dgCMatrix".to_string(), "crossprod".to_string()].into()
);
}
#[test]
fn singular_s4_aliases_match_their_plural_forms() {
let info = parse_namespace(
"exportClass(OldClass)\nimportClassFrom(methods, oldClass)\n",
&[],
);
assert_eq!(info.exports, ["OldClass".to_string()].into());
assert_eq!(info.imported_from_packages, ["methods".to_string()].into());
assert_eq!(info.imported_names, ["oldClass".to_string()].into());
}
#[test]
fn no_namespace_exports_every_object() {
let tmp = tempfile::tempdir().unwrap();
let objs = vec!["as.matrix".to_string(), "cbind".to_string()];
let exports = resolve_package_exports(tmp.path(), &objs);
assert_eq!(exports, objs);
}
#[test]
fn namespace_present_restricts_to_declared_exports() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("NAMESPACE"), "export(foo)\n").unwrap();
let exports = resolve_package_exports(tmp.path(), &["foo".to_string(), "bar".to_string()]);
assert_eq!(exports, vec!["foo".to_string()]);
}
#[test]
fn parses_built_r_version() {
assert_eq!(
parse_built_r_version("R 4.5.3; ; 2025-01-01 00:00:00 UTC; unix").as_deref(),
Some("4.5.3")
);
}
}