use std::collections::{BTreeSet, HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock};
use rowan::TextRange;
use crate::project::source::{SourceEdgeKey, SourceTarget, TopLevelEvent};
use crate::rindex::harvest::parse_namespace;
use crate::semantic::symbols::unbacktick;
static EMPTY_PATHS: LazyLock<HashSet<PathBuf>> = LazyLock::new(HashSet::new);
static EMPTY_LAYER: LazyLock<LayeredSet> = LazyLock::new(LayeredSet::default);
static EMPTY_NAMES: LazyLock<Arc<BTreeSet<String>>> = LazyLock::new(Arc::default);
#[derive(Debug, Default, Clone, PartialEq, Eq, salsa::SalsaValue)]
pub struct LayeredSet {
shared: Arc<BTreeSet<String>>,
added: BTreeSet<String>,
removed: BTreeSet<String>,
}
impl LayeredSet {
pub fn new(
shared: Arc<BTreeSet<String>>,
added: BTreeSet<String>,
removed: BTreeSet<String>,
) -> Self {
Self {
shared,
added,
removed,
}
}
#[inline]
pub fn contains(&self, name: &str) -> bool {
(self.shared.contains(name) || self.added.contains(name)) && !self.removed.contains(name)
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.shared
.iter()
.chain(self.added.iter().filter(|n| !self.shared.contains(*n)))
.map(String::as_str)
.filter(|n| !self.removed.contains(*n))
}
pub fn is_empty(&self) -> bool {
self.iter().next().is_none()
}
}
#[derive(Debug, Clone, Copy)]
pub struct PathSetView<'a> {
members: Option<&'a HashSet<PathBuf>>,
extra: &'a HashSet<PathBuf>,
exclude: &'a Path,
}
impl<'a> PathSetView<'a> {
#[inline]
pub fn contains(&self, path: &Path) -> bool {
path != self.exclude
&& (self.members.is_some_and(|m| m.contains(path)) || self.extra.contains(path))
}
pub fn len(&self) -> usize {
self.members
.map_or(0, |m| m.len() - usize::from(m.contains(self.exclude)))
+ self.extra.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn iter(&self) -> impl Iterator<Item = &'a Path> {
let exclude = self.exclude;
self.members
.into_iter()
.flatten()
.chain(self.extra.iter())
.map(PathBuf::as_path)
.filter(move |p| *p != exclude)
}
}
#[derive(Debug, Clone)]
pub struct FileFacts {
pub path: PathBuf,
pub exports: Arc<BTreeSet<String>>,
pub free_reads: Arc<BTreeSet<String>>,
pub qualified_reads: Arc<BTreeSet<String>>,
pub source_edges: Arc<Vec<SourceEdgeKey>>,
pub top_level_events: Arc<Vec<TopLevelEvent>>,
pub package_root: Option<PathBuf>,
}
#[derive(Debug, Default)]
pub struct ProjectScope {
visible: HashMap<PathBuf, LayeredSet>,
read_by_others: HashMap<PathBuf, LayeredSet>,
namespace_exports: HashMap<PathBuf, Arc<BTreeSet<String>>>,
s3_methods: HashMap<PathBuf, Arc<BTreeSet<String>>>,
wildcard_imports: HashMap<PathBuf, Arc<BTreeSet<String>>>,
dynamic: HashSet<PathBuf>,
root_members: HashMap<PathBuf, Arc<HashSet<PathBuf>>>,
file_root: HashMap<PathBuf, PathBuf>,
sees_extra: HashMap<PathBuf, HashSet<PathBuf>>,
seen_by_extra: HashMap<PathBuf, HashSet<PathBuf>>,
package_complete: HashMap<PathBuf, bool>,
retained: HashMap<PathBuf, Retained>,
}
#[derive(Debug)]
struct Retained {
top_level_events: Arc<Vec<TopLevelEvent>>,
exports: Arc<BTreeSet<String>>,
source_edges: Arc<Vec<SourceEdgeKey>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReadBinding {
Resolved(PathBuf),
Unresolved,
NoTopLevelRead,
OrderUnknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReadSite {
Bound(PathBuf),
Unbound,
Unknown,
}
pub struct FileScope<'a> {
visible: &'a LayeredSet,
read_by_others: &'a LayeredSet,
namespace_exports: &'a Arc<BTreeSet<String>>,
s3_methods: &'a Arc<BTreeSet<String>>,
wildcard_imports: &'a Arc<BTreeSet<String>>,
pub resolution_incomplete: bool,
}
impl<'a> FileScope<'a> {
pub fn new(
visible: &'a LayeredSet,
read_by_others: &'a LayeredSet,
namespace_exports: &'a Arc<BTreeSet<String>>,
s3_methods: &'a Arc<BTreeSet<String>>,
wildcard_imports: &'a Arc<BTreeSet<String>>,
resolution_incomplete: bool,
) -> Self {
Self {
visible,
read_by_others,
namespace_exports,
s3_methods,
wildcard_imports,
resolution_incomplete,
}
}
pub fn wildcard_import_packages(&self) -> &BTreeSet<String> {
self.wildcard_imports
}
pub fn visible_layer(&self) -> &LayeredSet {
self.visible
}
pub fn read_layer(&self) -> &LayeredSet {
self.read_by_others
}
pub fn namespace_export_names(&self) -> &BTreeSet<String> {
self.namespace_exports
}
pub fn s3_method_names(&self) -> &BTreeSet<String> {
self.s3_methods
}
pub fn namespace_exports_handle(&self) -> &Arc<BTreeSet<String>> {
self.namespace_exports
}
pub fn s3_methods_handle(&self) -> &Arc<BTreeSet<String>> {
self.s3_methods
}
pub fn wildcard_imports_handle(&self) -> &Arc<BTreeSet<String>> {
self.wildcard_imports
}
pub fn resolves(&self, name: &str) -> bool {
self.visible.contains(name)
}
pub fn read_elsewhere(&self, name: &str) -> bool {
self.read_by_others.contains(unbacktick(name))
}
pub fn exported_by_namespace(&self, name: &str) -> bool {
self.namespace_exports.contains(unbacktick(name))
}
pub fn is_s3_method(&self, name: &str) -> bool {
self.s3_methods.contains(unbacktick(name))
}
pub fn used_elsewhere(&self, name: &str) -> bool {
self.read_elsewhere(name) || self.exported_by_namespace(name)
}
}
impl ProjectScope {
pub fn build(
files: &[FileFacts],
namespaces: &HashMap<PathBuf, String>,
package_complete: &HashMap<PathBuf, bool>,
native_routines: &HashMap<PathBuf, BTreeSet<String>>,
) -> Self {
let by_path: HashMap<&Path, &FileFacts> =
files.iter().map(|f| (f.path.as_path(), f)).collect();
let mut package_members: HashMap<&Path, Vec<&Path>> = HashMap::new();
for f in files {
if let Some(root) = &f.package_root {
package_members
.entry(root.as_path())
.or_default()
.push(f.path.as_path());
}
}
let root_members: HashMap<PathBuf, Arc<HashSet<PathBuf>>> = package_members
.iter()
.map(|(&root, members)| {
(
root.to_path_buf(),
Arc::new(members.iter().map(|&p| p.to_path_buf()).collect()),
)
})
.collect();
let file_root: HashMap<PathBuf, PathBuf> = files
.iter()
.filter_map(|f| Some((f.path.clone(), f.package_root.clone()?)))
.collect();
let package_complete: HashMap<PathBuf, bool> = files
.iter()
.filter_map(|f| {
let root = f.package_root.as_ref()?;
Some((
f.path.clone(),
package_complete.get(root).copied().unwrap_or(true),
))
})
.collect();
let retained: HashMap<PathBuf, Retained> = files
.iter()
.map(|f| {
(
f.path.clone(),
Retained {
top_level_events: Arc::clone(&f.top_level_events),
exports: Arc::clone(&f.exports),
source_edges: Arc::clone(&f.source_edges),
},
)
})
.collect();
let mut sees_extra: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new();
let mut seen_by_extra: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new();
let mut sourced: Vec<Vec<&Path>> = Vec::with_capacity(files.len());
let mut dynamic: HashSet<PathBuf> = HashSet::new();
let mut wildcard_imports: HashMap<PathBuf, Arc<BTreeSet<String>>> = HashMap::new();
for f in files {
let mut via_source: Vec<&Path> = Vec::new();
let mut unresolved = false;
let mut visited: HashSet<&Path> = HashSet::from([f.path.as_path()]);
let mut queue: Vec<&FileFacts> = vec![f];
while let Some(cur) = queue.pop() {
for edge in cur.source_edges.iter() {
match source_dependency(edge) {
Dependency::Skip => {}
Dependency::Unresolved => unresolved = true,
Dependency::Path(p) => match by_path.get(p) {
Some(target) if visited.insert(target.path.as_path()) => {
via_source.push(target.path.as_path());
queue.push(target);
}
Some(_) => {}
None => unresolved = true,
},
}
}
}
if unresolved {
dynamic.insert(f.path.clone());
}
let extra: HashSet<PathBuf> = via_source
.iter()
.filter(|t| f.package_root.is_none() || by_path[*t].package_root != f.package_root)
.map(|t| t.to_path_buf())
.collect();
for target in &extra {
seen_by_extra
.entry(target.clone())
.or_default()
.insert(f.path.clone());
}
sees_extra.insert(f.path.clone(), extra);
sourced.push(via_source);
}
let reads: Vec<BTreeSet<String>> = files
.iter()
.map(|f| {
f.free_reads
.iter()
.chain(f.qualified_reads.iter())
.map(|n| unbacktick(n).to_string())
.collect()
})
.collect();
let mut package_exports: HashMap<&Path, BTreeSet<String>> = HashMap::new();
let mut package_read_counts: HashMap<&Path, HashMap<&str, usize>> = HashMap::new();
for (f, reads) in files.iter().zip(&reads) {
let Some(root) = f.package_root.as_deref() else {
continue;
};
package_exports
.entry(root)
.or_default()
.extend(f.exports.iter().cloned());
let counts = package_read_counts.entry(root).or_default();
for name in reads {
*counts.entry(name.as_str()).or_default() += 1;
}
}
let mut ns_exported: HashMap<&Path, Arc<BTreeSet<String>>> = HashMap::new();
let mut ns_s3: HashMap<&Path, Arc<BTreeSet<String>>> = HashMap::new();
let mut ns_imported: HashMap<&Path, BTreeSet<String>> = HashMap::new();
let mut ns_wildcards: HashMap<&Path, Arc<BTreeSet<String>>> = HashMap::new();
for (root, text) in namespaces {
let Some(members) = package_members.get(root.as_path()) else {
continue;
};
let object_names: Vec<&str> = members
.iter()
.filter_map(|m| by_path.get(m))
.flat_map(|f| f.exports.iter().map(String::as_str))
.collect();
let info = parse_namespace(text, &object_names);
let root = root.as_path();
ns_exported.insert(root, Arc::new(info.exports.iter().cloned().collect()));
ns_s3.insert(root, Arc::new(info.s3_methods.iter().cloned().collect()));
ns_imported.insert(root, info.imported_names.iter().cloned().collect());
let wildcards: BTreeSet<String> = info.imported_packages.iter().cloned().collect();
if !wildcards.is_empty() {
ns_wildcards.insert(root, Arc::new(wildcards));
}
}
let mut visible_shared: HashMap<&Path, Arc<BTreeSet<String>>> = HashMap::new();
let mut visible_exempt: HashMap<&Path, BTreeSet<String>> = HashMap::new();
for (&root, exports) in &package_exports {
let mut exempt = BTreeSet::new();
if let Some(imported) = ns_imported.get(root) {
exempt.extend(imported.iter().cloned());
}
if let Some(routines) = native_routines.get(root) {
exempt.extend(routines.iter().cloned());
}
let mut shared = exports.clone();
shared.extend(exempt.iter().cloned());
visible_shared.insert(root, Arc::new(shared));
visible_exempt.insert(root, exempt);
}
let empty_shared: Arc<BTreeSet<String>> = Arc::new(BTreeSet::new());
let mut visible: HashMap<PathBuf, LayeredSet> = HashMap::with_capacity(files.len());
for (i, f) in files.iter().enumerate() {
let root = f.package_root.as_deref();
let shared = root
.and_then(|r| visible_shared.get(r))
.unwrap_or(&empty_shared)
.clone();
let mut added = BTreeSet::new();
for target in &sourced[i] {
if let Some(target) = by_path.get(target) {
added.extend(target.exports.iter().cloned());
}
}
let exempt = root.and_then(|r| visible_exempt.get(r));
let removed: BTreeSet<String> = f
.exports
.iter()
.filter(|n| exempt.is_none_or(|e| !e.contains(*n)))
.cloned()
.collect();
visible.insert(f.path.clone(), LayeredSet::new(shared, added, removed));
}
let mut read_added: HashMap<&Path, BTreeSet<String>> = HashMap::new();
for i in 0..files.len() {
for target in &sourced[i] {
read_added
.entry(*target)
.or_default()
.extend(reads[i].iter().cloned());
}
}
let read_shared: HashMap<&Path, Arc<BTreeSet<String>>> = package_read_counts
.iter()
.map(|(&root, counts)| {
(
root,
Arc::new(counts.keys().map(|n| (*n).to_string()).collect()),
)
})
.collect();
let mut read_by_others: HashMap<PathBuf, LayeredSet> = HashMap::with_capacity(files.len());
for (i, f) in files.iter().enumerate() {
let root = f.package_root.as_deref();
let shared = root
.and_then(|r| read_shared.get(r))
.unwrap_or(&empty_shared)
.clone();
let added = read_added.remove(f.path.as_path()).unwrap_or_default();
let removed: BTreeSet<String> = match root.and_then(|r| package_read_counts.get(r)) {
Some(counts) => reads[i]
.iter()
.filter(|n| counts.get(n.as_str()) == Some(&1) && !added.contains(*n))
.cloned()
.collect(),
None => BTreeSet::new(),
};
read_by_others.insert(f.path.clone(), LayeredSet::new(shared, added, removed));
}
let mut namespace_exports: HashMap<PathBuf, Arc<BTreeSet<String>>> = HashMap::new();
let mut s3_methods: HashMap<PathBuf, Arc<BTreeSet<String>>> = HashMap::new();
for (&root, members) in &package_members {
for member in members {
let path = member.to_path_buf();
if let Some(exported) = ns_exported.get(root) {
namespace_exports.insert(path.clone(), Arc::clone(exported));
}
if let Some(s3) = ns_s3.get(root) {
s3_methods.insert(path.clone(), Arc::clone(s3));
}
if let Some(wildcards) = ns_wildcards.get(root) {
wildcard_imports.insert(path, Arc::clone(wildcards));
}
}
}
Self {
visible,
read_by_others,
namespace_exports,
s3_methods,
wildcard_imports,
dynamic,
root_members,
file_root,
sees_extra,
seen_by_extra,
package_complete,
retained,
}
}
pub fn for_file(&self, path: &Path) -> FileScope<'_> {
FileScope {
visible: self.visible.get(path).unwrap_or(&EMPTY_LAYER),
read_by_others: self.read_by_others.get(path).unwrap_or(&EMPTY_LAYER),
namespace_exports: self.namespace_exports.get(path).unwrap_or(&EMPTY_NAMES),
s3_methods: self.s3_methods.get(path).unwrap_or(&EMPTY_NAMES),
wildcard_imports: self.wildcard_imports.get(path).unwrap_or(&EMPTY_NAMES),
resolution_incomplete: self.dynamic.contains(path),
}
}
fn members_of(&self, path: &Path) -> Option<&HashSet<PathBuf>> {
self.file_root
.get(path)
.and_then(|root| self.root_members.get(root))
.map(|members| &**members)
}
pub fn sees<'a>(&'a self, path: &'a Path) -> PathSetView<'a> {
PathSetView {
members: self.members_of(path),
extra: self.sees_extra.get(path).unwrap_or(&EMPTY_PATHS),
exclude: path,
}
}
pub fn seen_by<'a>(&'a self, path: &'a Path) -> PathSetView<'a> {
PathSetView {
members: self.members_of(path),
extra: self.seen_by_extra.get(path).unwrap_or(&EMPTY_PATHS),
exclude: path,
}
}
pub fn package_siblings<'a>(&'a self, path: &'a Path) -> PathSetView<'a> {
PathSetView {
members: self.members_of(path),
extra: &EMPTY_PATHS,
exclude: path,
}
}
pub fn package_complete(&self, path: &Path) -> bool {
self.package_complete.get(path).copied().unwrap_or(true)
}
pub fn top_level_read_binding(&self, from_file: &Path, name: &str) -> ReadBinding {
let Some(events) = self.retained.get(from_file).map(|r| &r.top_level_events) else {
return ReadBinding::NoTopLevelRead;
};
let mut live: Option<PathBuf> = None;
let mut name_ambiguous = false;
let mut poisoned = false;
let mut saw_read = false;
let mut resolved: BTreeSet<PathBuf> = BTreeSet::new();
let mut saw_unresolved = false;
let mut saw_unknown = false;
for event in events.iter() {
match event {
TopLevelEvent::Define(n) if n == name => {
live = Some(from_file.to_path_buf());
name_ambiguous = false;
}
TopLevelEvent::SourceEdge(key) => match source_dependency(key) {
Dependency::Skip => {}
Dependency::Unresolved => poisoned = true,
Dependency::Path(p) => {
let mut definers = self.closure_definers(p, name);
match definers.len() {
0 => {}
1 => {
live = definers.pop();
name_ambiguous = false;
}
_ => name_ambiguous = true,
}
}
},
TopLevelEvent::Read(n) if n == name => {
saw_read = true;
if poisoned || name_ambiguous {
saw_unknown = true;
} else if let Some(p) = &live {
resolved.insert(p.clone());
} else {
saw_unresolved = true;
}
}
_ => {}
}
}
if !saw_read {
return ReadBinding::NoTopLevelRead;
}
if saw_unknown {
return ReadBinding::OrderUnknown;
}
match (resolved.len(), saw_unresolved) {
(0, _) => ReadBinding::Unresolved,
(1, false) => ReadBinding::Resolved(resolved.into_iter().next().expect("len == 1")),
_ => ReadBinding::OrderUnknown,
}
}
pub fn top_level_read_provenance(
&self,
from_file: &Path,
name: &str,
spanned: &[(TopLevelEvent, Option<TextRange>)],
) -> Vec<(TextRange, ReadSite)> {
let mut live: Option<PathBuf> = None;
let mut name_ambiguous = false;
let mut poisoned = false;
let mut sites: Vec<(TextRange, ReadSite)> = Vec::new();
for (event, span) in spanned {
match event {
TopLevelEvent::Define(n) if n == name => {
live = Some(from_file.to_path_buf());
name_ambiguous = false;
}
TopLevelEvent::SourceEdge(key) => match source_dependency(key) {
Dependency::Skip => {}
Dependency::Unresolved => poisoned = true,
Dependency::Path(p) => {
let mut definers = self.closure_definers(p, name);
match definers.len() {
0 => {}
1 => {
live = definers.pop();
name_ambiguous = false;
}
_ => name_ambiguous = true,
}
}
},
TopLevelEvent::Read(n) if n == name => {
let range = span.expect("a Read event always carries its span");
let site = if poisoned || name_ambiguous {
ReadSite::Unknown
} else if let Some(p) = &live {
ReadSite::Bound(p.clone())
} else {
ReadSite::Unbound
};
sites.push((range, site));
}
_ => {}
}
}
sites
}
pub fn final_scope_binding(&self, from_file: &Path, name: &str) -> ReadSite {
let Some(events) = self.retained.get(from_file).map(|r| &r.top_level_events) else {
return ReadSite::Unbound;
};
let mut live: Option<PathBuf> = None;
let mut name_ambiguous = false;
let mut poisoned = false;
for event in events.iter() {
match event {
TopLevelEvent::Define(n) if n == name => {
live = Some(from_file.to_path_buf());
name_ambiguous = false;
}
TopLevelEvent::SourceEdge(key) => match source_dependency(key) {
Dependency::Skip => {}
Dependency::Unresolved => poisoned = true,
Dependency::Path(p) => {
let mut definers = self.closure_definers(p, name);
match definers.len() {
0 => {}
1 => {
live = definers.pop();
name_ambiguous = false;
}
_ => name_ambiguous = true,
}
}
},
_ => {}
}
}
if poisoned || name_ambiguous {
ReadSite::Unknown
} else if let Some(p) = live {
ReadSite::Bound(p)
} else {
ReadSite::Unbound
}
}
fn closure_definers(&self, start: &Path, name: &str) -> Vec<PathBuf> {
let mut definers: Vec<PathBuf> = Vec::new();
let mut visited: HashSet<PathBuf> = HashSet::new();
let mut stack: Vec<PathBuf> = vec![start.to_path_buf()];
while let Some(cur) = stack.pop() {
if !visited.insert(cur.clone()) {
continue;
}
let Some(retained) = self.retained.get(&cur) else {
continue;
};
if retained.exports.contains(name) {
definers.push(cur.clone());
}
{
for edge in retained.source_edges.iter() {
if let SourceTarget::Path(target) = &edge.target
&& !edge.local
{
stack.push(target.clone());
}
}
}
}
definers
}
}
enum Dependency<'a> {
Path(&'a Path),
Unresolved,
Skip,
}
fn source_dependency(edge: &SourceEdgeKey) -> Dependency<'_> {
match &edge.target {
SourceTarget::Dynamic => Dependency::Unresolved,
SourceTarget::Path(_) if edge.local => Dependency::Skip,
SourceTarget::Path(p) => Dependency::Path(p.as_path()),
}
}
pub fn is_package_root(dir: &Path) -> bool {
dir.join("DESCRIPTION").is_file() && dir.join("R").is_dir()
}
pub fn package_root(path: &Path) -> Option<PathBuf> {
path.parent().and_then(package_root_of_dir)
}
pub fn package_root_of_dir(dir: &Path) -> Option<PathBuf> {
let mut dir = Some(dir);
while let Some(d) = dir {
if is_package_root(d) {
return Some(d.to_path_buf());
}
dir = d.parent();
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn set(names: &[&str]) -> BTreeSet<String> {
names.iter().map(|n| n.to_string()).collect()
}
fn source_path(target: &str, local: bool) -> SourceEdgeKey {
SourceEdgeKey {
target: SourceTarget::Path(PathBuf::from(target)),
local,
}
}
fn dynamic_edge() -> SourceEdgeKey {
SourceEdgeKey {
target: SourceTarget::Dynamic,
local: false,
}
}
fn facts(
path: &str,
exp: &[&str],
reads: &[&str],
edges: Vec<SourceEdgeKey>,
root: Option<&str>,
) -> FileFacts {
FileFacts {
path: PathBuf::from(path),
exports: Arc::new(set(exp)),
free_reads: Arc::new(set(reads)),
qualified_reads: Arc::new(BTreeSet::new()),
source_edges: Arc::new(edges),
top_level_events: Arc::new(Vec::new()),
package_root: root.map(PathBuf::from),
}
}
fn layer_names(layer: &LayeredSet) -> Vec<String> {
let mut v: Vec<String> = layer.iter().map(|s| s.to_string()).collect();
v.sort();
v
}
fn read_ev(name: &str) -> TopLevelEvent {
TopLevelEvent::Read(name.to_string())
}
fn def_ev(name: &str) -> TopLevelEvent {
TopLevelEvent::Define(name.to_string())
}
fn src_ev(target: &str) -> TopLevelEvent {
TopLevelEvent::SourceEdge(source_path(target, false))
}
fn dyn_src_ev() -> TopLevelEvent {
TopLevelEvent::SourceEdge(dynamic_edge())
}
fn facts_seq(
path: &str,
exp: &[&str],
edges: Vec<SourceEdgeKey>,
events: Vec<TopLevelEvent>,
) -> FileFacts {
FileFacts {
path: PathBuf::from(path),
exports: Arc::new(set(exp)),
free_reads: Arc::new(BTreeSet::new()),
qualified_reads: Arc::new(BTreeSet::new()),
source_edges: Arc::new(edges),
top_level_events: Arc::new(events),
package_root: None,
}
}
fn build_scope(files: &[FileFacts]) -> ProjectScope {
ProjectScope::build(files, &HashMap::new(), &HashMap::new(), &HashMap::new())
}
#[test]
fn package_files_share_one_namespace() {
let files = [
facts("/pkg/R/a.R", &["foo"], &[], vec![], Some("/pkg")),
facts("/pkg/R/b.R", &["bar"], &["foo"], vec![], Some("/pkg")),
];
let scope = build_scope(&files);
assert!(scope.for_file(Path::new("/pkg/R/b.R")).resolves("foo"));
assert!(
scope
.for_file(Path::new("/pkg/R/a.R"))
.used_elsewhere("foo")
);
assert!(
!scope
.for_file(Path::new("/pkg/R/b.R"))
.used_elsewhere("bar")
);
}
#[test]
fn a_members_own_qualified_read_is_not_a_use_by_others() {
let mut a = facts("/pkg/R/a.R", &["foo"], &[], vec![], Some("/pkg"));
a.qualified_reads = Arc::new(set(&["foo"]));
let b = facts("/pkg/R/b.R", &["bar"], &[], vec![], Some("/pkg"));
let scope = build_scope(&[a.clone(), b.clone()]);
assert!(
!scope
.for_file(Path::new("/pkg/R/a.R"))
.used_elsewhere("foo")
);
assert!(
scope
.for_file(Path::new("/pkg/R/b.R"))
.read_elsewhere("foo")
);
let mut c = facts("/pkg/R/c.R", &["baz"], &[], vec![], Some("/pkg"));
c.qualified_reads = Arc::new(set(&["foo"]));
let scope = build_scope(&[a, b, c]);
assert!(
scope
.for_file(Path::new("/pkg/R/a.R"))
.used_elsewhere("foo")
);
}
#[test]
fn source_closure_is_directional() {
let files = [
facts(
"/s/a.R",
&["foo"],
&["bar"],
vec![source_path("/s/b.R", false)],
None,
),
facts("/s/b.R", &["bar"], &[], vec![], None),
];
let scope = build_scope(&files);
assert!(scope.for_file(Path::new("/s/a.R")).resolves("bar"));
assert!(!scope.for_file(Path::new("/s/b.R")).resolves("foo"));
assert!(scope.for_file(Path::new("/s/b.R")).used_elsewhere("bar"));
assert!(!scope.for_file(Path::new("/s/a.R")).resolution_incomplete);
}
#[test]
fn source_closure_is_transitive_and_cycle_safe() {
let files = [
facts(
"/s/a.R",
&["foo"],
&[],
vec![source_path("/s/b.R", false)],
None,
),
facts(
"/s/b.R",
&["bar"],
&[],
vec![source_path("/s/c.R", false)],
None,
),
facts(
"/s/c.R",
&["baz"],
&[],
vec![source_path("/s/a.R", false)],
None,
),
];
let scope = build_scope(&files);
assert_eq!(
layer_names(scope.for_file(Path::new("/s/a.R")).visible_layer()),
vec!["bar", "baz"]
);
}
#[test]
fn seen_by_is_inverse_of_sees() {
let files = [
facts(
"/s/a.R",
&["foo"],
&[],
vec![source_path("/s/b.R", false)],
None,
),
facts("/s/b.R", &["bar"], &[], vec![], None),
];
let scope = build_scope(&files);
assert!(
scope
.sees(Path::new("/s/a.R"))
.contains(Path::new("/s/b.R"))
);
assert!(scope.sees(Path::new("/s/b.R")).is_empty());
assert!(
scope
.seen_by(Path::new("/s/b.R"))
.contains(Path::new("/s/a.R"))
);
assert!(scope.seen_by(Path::new("/s/a.R")).is_empty());
}
#[test]
fn seen_by_includes_package_siblings_symmetrically() {
let files = [
facts("/pkg/R/a.R", &["foo"], &[], vec![], Some("/pkg")),
facts("/pkg/R/b.R", &["bar"], &[], vec![], Some("/pkg")),
];
let scope = build_scope(&files);
assert!(
scope
.sees(Path::new("/pkg/R/a.R"))
.contains(Path::new("/pkg/R/b.R"))
);
assert!(
scope
.sees(Path::new("/pkg/R/b.R"))
.contains(Path::new("/pkg/R/a.R"))
);
assert!(
scope
.seen_by(Path::new("/pkg/R/a.R"))
.contains(Path::new("/pkg/R/b.R"))
);
}
#[test]
fn seen_by_excludes_unconnected_file() {
let files = [
facts("/s/a.R", &["foo"], &[], vec![], None),
facts("/s/b.R", &["foo"], &[], vec![], None),
];
let scope = build_scope(&files);
assert!(scope.sees(Path::new("/s/a.R")).is_empty());
assert!(scope.seen_by(Path::new("/s/a.R")).is_empty());
}
#[test]
fn sees_does_not_double_count_a_sourced_sibling() {
let files = [
facts(
"/pkg/R/a.R",
&[],
&[],
vec![source_path("/pkg/R/b.R", false)],
Some("/pkg"),
),
facts("/pkg/R/b.R", &["bar"], &[], vec![], Some("/pkg")),
];
let scope = build_scope(&files);
let a = scope.sees(Path::new("/pkg/R/a.R"));
assert_eq!(a.len(), 1);
assert!(a.contains(Path::new("/pkg/R/b.R")));
}
#[test]
fn seen_by_is_siblings_plus_outside_sourcers() {
let files = [
facts("/pkg/R/a.R", &["foo"], &[], vec![], Some("/pkg")),
facts("/pkg/R/b.R", &["bar"], &[], vec![], Some("/pkg")),
facts(
"/s/x.R",
&[],
&["foo"],
vec![source_path("/pkg/R/a.R", false)],
None,
),
];
let scope = build_scope(&files);
let seen_by = scope.seen_by(Path::new("/pkg/R/a.R"));
assert_eq!(seen_by.len(), 2);
assert!(seen_by.contains(Path::new("/pkg/R/b.R")));
assert!(seen_by.contains(Path::new("/s/x.R")));
assert!(!seen_by.contains(Path::new("/pkg/R/a.R")));
}
#[test]
fn package_siblings_excludes_self_and_source_targets() {
let files = [
facts(
"/pkg/R/a.R",
&[],
&[],
vec![source_path("/s/c.R", false)],
Some("/pkg"),
),
facts("/pkg/R/b.R", &["bar"], &[], vec![], Some("/pkg")),
facts("/s/c.R", &["baz"], &[], vec![], None),
];
let scope = build_scope(&files);
let siblings = scope.package_siblings(Path::new("/pkg/R/a.R"));
assert_eq!(siblings.len(), 1);
assert!(siblings.contains(Path::new("/pkg/R/b.R")));
assert!(!siblings.contains(Path::new("/s/c.R")));
assert!(!siblings.contains(Path::new("/pkg/R/a.R")));
assert!(scope.package_siblings(Path::new("/s/c.R")).is_empty());
}
#[test]
fn dynamic_source_marks_scope_incomplete() {
let files = [facts("/s/a.R", &[], &[], vec![dynamic_edge()], None)];
let scope = build_scope(&files);
assert!(scope.for_file(Path::new("/s/a.R")).resolution_incomplete);
}
#[test]
fn source_to_unanalyzed_file_marks_scope_incomplete() {
let files = [facts(
"/s/a.R",
&[],
&[],
vec![source_path("/s/missing.R", false)],
None,
)];
let scope = build_scope(&files);
assert!(scope.for_file(Path::new("/s/a.R")).resolution_incomplete);
}
#[test]
fn local_source_neither_contributes_nor_marks_dynamic() {
let files = [
facts(
"/s/a.R",
&[],
&["bar"],
vec![source_path("/s/b.R", true)],
None,
),
facts("/s/b.R", &["bar"], &[], vec![], None),
];
let scope = build_scope(&files);
let a = scope.for_file(Path::new("/s/a.R"));
assert!(!a.resolves("bar"));
assert!(!a.resolution_incomplete);
assert!(!scope.for_file(Path::new("/s/b.R")).used_elsewhere("bar"));
}
fn namespaces(entries: &[(&str, &str)]) -> HashMap<PathBuf, String> {
entries
.iter()
.map(|(root, text)| (PathBuf::from(*root), text.to_string()))
.collect()
}
#[test]
fn namespace_export_marks_binding_used() {
let files = [facts("/pkg/R/a.R", &["foo"], &[], vec![], Some("/pkg"))];
let ns = namespaces(&[("/pkg", "export(foo)\n")]);
let scope = ProjectScope::build(&files, &ns, &HashMap::new(), &HashMap::new());
assert!(
scope
.for_file(Path::new("/pkg/R/a.R"))
.used_elsewhere("foo")
);
}
#[test]
fn namespace_import_from_resolves_name() {
let files = [facts("/pkg/R/a.R", &[], &["filter"], vec![], Some("/pkg"))];
let ns = namespaces(&[("/pkg", "importFrom(dplyr, filter)\n")]);
let scope = ProjectScope::build(&files, &ns, &HashMap::new(), &HashMap::new());
let a = scope.for_file(Path::new("/pkg/R/a.R"));
assert!(a.resolves("filter"));
assert!(!a.resolution_incomplete);
}
#[test]
fn namespace_wholesale_import_is_recorded_not_poisoned() {
let files = [facts("/pkg/R/a.R", &[], &["abort"], vec![], Some("/pkg"))];
let ns = namespaces(&[("/pkg", "import(rlang)\n")]);
let scope = ProjectScope::build(&files, &ns, &HashMap::new(), &HashMap::new());
let a = scope.for_file(Path::new("/pkg/R/a.R"));
assert_eq!(
a.wildcard_import_packages(),
&["rlang".to_string()].into_iter().collect()
);
assert!(
!a.resolution_incomplete,
"a wildcard import is a question for the index, not an unresolvable"
);
}
fn routines(entries: &[(&str, &[&str])]) -> HashMap<PathBuf, BTreeSet<String>> {
entries
.iter()
.map(|(root, names)| (PathBuf::from(*root), set(names)))
.collect()
}
#[test]
fn own_export_that_is_also_importfrom_stays_visible() {
let files = [facts("/pkg/R/a.R", &["filter"], &[], vec![], Some("/pkg"))];
let ns = namespaces(&[("/pkg", "importFrom(dplyr, filter)\n")]);
let scope = ProjectScope::build(&files, &ns, &HashMap::new(), &HashMap::new());
assert!(scope.for_file(Path::new("/pkg/R/a.R")).resolves("filter"));
}
#[test]
fn own_export_that_is_also_a_native_routine_stays_visible() {
let files = [facts("/pkg/R/a.R", &["c_foo"], &[], vec![], Some("/pkg"))];
let native = routines(&[("/pkg", &["c_foo"])]);
let scope = ProjectScope::build(&files, &HashMap::new(), &HashMap::new(), &native);
assert!(scope.for_file(Path::new("/pkg/R/a.R")).resolves("c_foo"));
}
#[test]
fn native_routine_is_visible_to_every_member() {
let files = [
facts("/pkg/R/a.R", &["foo"], &[], vec![], Some("/pkg")),
facts("/pkg/R/b.R", &["bar"], &[], vec![], Some("/pkg")),
];
let native = routines(&[("/pkg", &["c_foo"])]);
let scope = ProjectScope::build(&files, &HashMap::new(), &HashMap::new(), &native);
assert!(scope.for_file(Path::new("/pkg/R/a.R")).resolves("c_foo"));
assert!(scope.for_file(Path::new("/pkg/R/b.R")).resolves("c_foo"));
}
#[test]
fn own_export_shadowing_a_sourced_export_is_not_visible() {
let files = [
facts("/s/a.R", &["foo"], &[], vec![], None),
facts(
"/s/b.R",
&["foo"],
&[],
vec![source_path("/s/a.R", false)],
None,
),
];
let scope = build_scope(&files);
assert!(!scope.for_file(Path::new("/s/b.R")).resolves("foo"));
}
#[test]
fn own_export_is_never_self_visible() {
let files = [facts("/pkg/R/a.R", &["foo"], &[], vec![], Some("/pkg"))];
let scope = build_scope(&files);
assert!(!scope.for_file(Path::new("/pkg/R/a.R")).resolves("foo"));
}
#[test]
fn two_roots_do_not_leak() {
let files = [
facts("/p1/R/a.R", &["one"], &["shared"], vec![], Some("/p1")),
facts("/p2/R/a.R", &["two"], &[], vec![], Some("/p2")),
];
let ns = namespaces(&[
(
"/p1",
"export(one)\nimportFrom(dplyr, filter)\nimport(rlang)\n",
),
("/p2", "S3method(print, two)\n"),
]);
let native = routines(&[("/p1", &["c_one"])]);
let scope = ProjectScope::build(&files, &ns, &HashMap::new(), &native);
let p2 = scope.for_file(Path::new("/p2/R/a.R"));
assert!(!p2.resolves("one"));
assert!(!p2.resolves("filter"));
assert!(!p2.resolves("c_one"));
assert!(!p2.exported_by_namespace("one"));
assert!(!p2.read_elsewhere("shared"));
assert!(p2.wildcard_import_packages().is_empty());
let p1 = scope.for_file(Path::new("/p1/R/a.R"));
assert!(!p1.resolves("two"));
assert!(!p1.is_s3_method("print.two"));
}
#[test]
fn facts_for_a_root_with_no_members_are_ignored() {
let files = [facts("/s/a.R", &[], &["filter"], vec![], None)];
let ns = namespaces(&[("/pkg", "importFrom(dplyr, filter)\nexport(foo)\n")]);
let native = routines(&[("/pkg", &["c_foo"])]);
let scope = ProjectScope::build(&files, &ns, &HashMap::new(), &native);
let a = scope.for_file(Path::new("/s/a.R"));
assert!(!a.resolves("filter"));
assert!(!a.resolves("c_foo"));
assert!(!a.exported_by_namespace("foo"));
}
#[test]
fn sourcer_read_beats_the_solo_reader_exclusion() {
let mut a = facts("/pkg/R/a.R", &["foo"], &[], vec![], Some("/pkg"));
a.qualified_reads = Arc::new(set(&["foo"]));
let files = [
a,
facts("/pkg/R/b.R", &["bar"], &[], vec![], Some("/pkg")),
facts(
"/s/x.R",
&[],
&["foo"],
vec![source_path("/pkg/R/a.R", false)],
None,
),
];
let scope = build_scope(&files);
assert!(
scope
.for_file(Path::new("/pkg/R/a.R"))
.read_elsewhere("foo")
);
}
#[test]
fn sourcer_that_does_not_read_the_name_leaves_it_excluded() {
let mut a = facts("/pkg/R/a.R", &["foo"], &[], vec![], Some("/pkg"));
a.qualified_reads = Arc::new(set(&["foo"]));
let files = [
a,
facts("/pkg/R/b.R", &["bar"], &[], vec![], Some("/pkg")),
facts(
"/s/x.R",
&[],
&["other"],
vec![source_path("/pkg/R/a.R", false)],
None,
),
];
let scope = build_scope(&files);
assert!(
!scope
.for_file(Path::new("/pkg/R/a.R"))
.read_elsewhere("foo")
);
}
#[test]
fn nonpackage_target_gets_reads_only_from_its_sourcers() {
let files = [
facts("/s/a.R", &["foo"], &["qux"], vec![], None),
facts(
"/s/x.R",
&[],
&["foo"],
vec![source_path("/s/a.R", false)],
None,
),
];
let scope = build_scope(&files);
let a = scope.for_file(Path::new("/s/a.R"));
assert!(a.read_elsewhere("foo"));
assert!(!a.read_elsewhere("qux"));
}
#[test]
fn a_cycle_between_two_package_members_does_not_self_source() {
let mut a = facts(
"/pkg/R/a.R",
&["foo"],
&[],
vec![source_path("/pkg/R/b.R", false)],
Some("/pkg"),
);
a.qualified_reads = Arc::new(set(&["foo"]));
let files = [
a,
facts(
"/pkg/R/b.R",
&["bar"],
&[],
vec![source_path("/pkg/R/a.R", false)],
Some("/pkg"),
),
];
let scope = build_scope(&files);
assert!(
!scope
.for_file(Path::new("/pkg/R/a.R"))
.read_elsewhere("foo")
);
assert!(
scope
.for_file(Path::new("/pkg/R/b.R"))
.read_elsewhere("foo")
);
}
#[test]
fn read_before_source_is_unresolved() {
let files = [
facts("/s/a.R", &["foo"], &[], vec![], None),
facts_seq(
"/s/b.R",
&[],
vec![source_path("/s/a.R", false)],
vec![read_ev("foo"), src_ev("/s/a.R")],
),
];
let scope = build_scope(&files);
assert_eq!(
scope.top_level_read_binding(Path::new("/s/b.R"), "foo"),
ReadBinding::Unresolved
);
}
#[test]
fn read_after_source_resolves_to_the_sourced_def() {
let files = [
facts("/s/a.R", &["foo"], &[], vec![], None),
facts_seq(
"/s/b.R",
&[],
vec![source_path("/s/a.R", false)],
vec![src_ev("/s/a.R"), read_ev("foo")],
),
];
let scope = build_scope(&files);
assert_eq!(
scope.top_level_read_binding(Path::new("/s/b.R"), "foo"),
ReadBinding::Resolved(PathBuf::from("/s/a.R"))
);
}
#[test]
fn local_def_after_source_shadows_the_sourced_def() {
let files = [
facts("/s/a.R", &["foo"], &[], vec![], None),
facts_seq(
"/s/b.R",
&["foo"],
vec![source_path("/s/a.R", false)],
vec![src_ev("/s/a.R"), def_ev("foo"), read_ev("foo")],
),
];
let scope = build_scope(&files);
assert_eq!(
scope.top_level_read_binding(Path::new("/s/b.R"), "foo"),
ReadBinding::Resolved(PathBuf::from("/s/b.R"))
);
}
#[test]
fn dynamic_source_before_read_is_order_unknown() {
let files = [facts_seq(
"/s/b.R",
&[],
vec![dynamic_edge()],
vec![dyn_src_ev(), read_ev("foo")],
)];
let scope = build_scope(&files);
assert_eq!(
scope.top_level_read_binding(Path::new("/s/b.R"), "foo"),
ReadBinding::OrderUnknown
);
}
#[test]
fn body_only_read_has_no_top_level_event() {
let files = [
facts("/s/a.R", &["foo"], &[], vec![], None),
facts_seq(
"/s/b.R",
&[],
vec![source_path("/s/a.R", false)],
vec![src_ev("/s/a.R")],
),
];
let scope = build_scope(&files);
assert_eq!(
scope.top_level_read_binding(Path::new("/s/b.R"), "foo"),
ReadBinding::NoTopLevelRead
);
}
#[test]
fn same_name_in_one_sourced_closure_is_order_unknown() {
let files = [
facts("/s/a.R", &["foo"], &[], vec![], None),
facts("/s/c.R", &["foo"], &[], vec![], None),
facts(
"/s/d.R",
&[],
&[],
vec![source_path("/s/a.R", false), source_path("/s/c.R", false)],
None,
),
facts_seq(
"/s/b.R",
&[],
vec![source_path("/s/d.R", false)],
vec![src_ev("/s/d.R"), read_ev("foo")],
),
];
let scope = build_scope(&files);
assert_eq!(
scope.top_level_read_binding(Path::new("/s/b.R"), "foo"),
ReadBinding::OrderUnknown
);
}
fn span(n: u32) -> TextRange {
TextRange::new(n.into(), (n + 1).into())
}
fn s_read(name: &str, at: u32) -> (TopLevelEvent, Option<TextRange>) {
(read_ev(name), Some(span(at)))
}
fn s_def(name: &str) -> (TopLevelEvent, Option<TextRange>) {
(def_ev(name), None)
}
fn s_src(target: &str) -> (TopLevelEvent, Option<TextRange>) {
(src_ev(target), None)
}
fn s_dyn() -> (TopLevelEvent, Option<TextRange>) {
(dyn_src_ev(), None)
}
#[test]
fn provenance_read_before_source_is_unbound() {
let files = [facts("/s/a.R", &["foo"], &[], vec![], None)];
let scope = build_scope(&files);
let events = [s_read("foo", 1), s_src("/s/a.R")];
assert_eq!(
scope.top_level_read_provenance(Path::new("/s/b.R"), "foo", &events),
vec![(span(1), ReadSite::Unbound)]
);
}
#[test]
fn provenance_read_after_source_binds_to_the_def() {
let files = [facts("/s/a.R", &["foo"], &[], vec![], None)];
let scope = build_scope(&files);
let events = [s_src("/s/a.R"), s_read("foo", 1)];
assert_eq!(
scope.top_level_read_provenance(Path::new("/s/b.R"), "foo", &events),
vec![(span(1), ReadSite::Bound(PathBuf::from("/s/a.R")))]
);
}
#[test]
fn provenance_local_shadow_binds_to_self() {
let files = [facts("/s/a.R", &["foo"], &[], vec![], None)];
let scope = build_scope(&files);
let events = [s_src("/s/a.R"), s_def("foo"), s_read("foo", 1)];
assert_eq!(
scope.top_level_read_provenance(Path::new("/s/b.R"), "foo", &events),
vec![(span(1), ReadSite::Bound(PathBuf::from("/s/b.R")))]
);
}
#[test]
fn provenance_dynamic_source_poisons_the_read() {
let scope = build_scope(&[]);
let events = [s_dyn(), s_read("foo", 1)];
assert_eq!(
scope.top_level_read_provenance(Path::new("/s/b.R"), "foo", &events),
vec![(span(1), ReadSite::Unknown)]
);
}
#[test]
fn provenance_two_closure_definers_is_unknown() {
let files = [
facts("/s/a.R", &["foo"], &[], vec![], None),
facts("/s/c.R", &["foo"], &[], vec![], None),
facts(
"/s/d.R",
&[],
&[],
vec![source_path("/s/a.R", false), source_path("/s/c.R", false)],
None,
),
];
let scope = build_scope(&files);
let events = [s_src("/s/d.R"), s_read("foo", 1)];
assert_eq!(
scope.top_level_read_provenance(Path::new("/s/b.R"), "foo", &events),
vec![(span(1), ReadSite::Unknown)]
);
}
#[test]
fn provenance_distinguishes_pre_and_post_source_reads() {
let files = [facts("/s/a.R", &["foo"], &[], vec![], None)];
let scope = build_scope(&files);
let events = [s_read("foo", 1), s_src("/s/a.R"), s_read("foo", 2)];
assert_eq!(
scope.top_level_read_provenance(Path::new("/s/b.R"), "foo", &events),
vec![
(span(1), ReadSite::Unbound),
(span(2), ReadSite::Bound(PathBuf::from("/s/a.R"))),
]
);
}
#[test]
fn provenance_ignores_reads_of_other_names() {
let files = [facts("/s/a.R", &["foo"], &[], vec![], None)];
let scope = build_scope(&files);
let events = [s_src("/s/a.R"), s_read("other", 1), s_read("foo", 2)];
assert_eq!(
scope.top_level_read_provenance(Path::new("/s/b.R"), "foo", &events),
vec![(span(2), ReadSite::Bound(PathBuf::from("/s/a.R")))]
);
}
#[test]
fn final_scope_binds_to_the_last_sourced_definer() {
let files = [
facts("/s/a.R", &["foo"], &[], vec![], None),
facts("/s/z.R", &["foo"], &[], vec![], None),
facts_seq(
"/s/b.R",
&[],
vec![source_path("/s/a.R", false), source_path("/s/z.R", false)],
vec![src_ev("/s/a.R"), src_ev("/s/z.R")],
),
];
let scope = build_scope(&files);
assert_eq!(
scope.final_scope_binding(Path::new("/s/b.R"), "foo"),
ReadSite::Bound(PathBuf::from("/s/z.R"))
);
}
#[test]
fn final_scope_binds_to_the_sole_sourced_definer() {
let files = [
facts("/s/a.R", &["foo"], &[], vec![], None),
facts_seq(
"/s/b.R",
&[],
vec![source_path("/s/a.R", false)],
vec![src_ev("/s/a.R")],
),
];
let scope = build_scope(&files);
assert_eq!(
scope.final_scope_binding(Path::new("/s/b.R"), "foo"),
ReadSite::Bound(PathBuf::from("/s/a.R"))
);
}
#[test]
fn final_scope_ignores_a_pre_source_read() {
let files = [
facts("/s/a.R", &["foo"], &[], vec![], None),
facts_seq(
"/s/b.R",
&[],
vec![source_path("/s/a.R", false)],
vec![read_ev("foo"), src_ev("/s/a.R")],
),
];
let scope = build_scope(&files);
assert_eq!(
scope.final_scope_binding(Path::new("/s/b.R"), "foo"),
ReadSite::Bound(PathBuf::from("/s/a.R"))
);
}
#[test]
fn final_scope_is_unbound_without_a_definer() {
let files = [facts_seq("/s/b.R", &[], vec![], vec![read_ev("foo")])];
let scope = build_scope(&files);
assert_eq!(
scope.final_scope_binding(Path::new("/s/b.R"), "foo"),
ReadSite::Unbound
);
}
#[test]
fn final_scope_is_unbound_for_a_file_without_events() {
let scope = build_scope(&[]);
assert_eq!(
scope.final_scope_binding(Path::new("/s/b.R"), "foo"),
ReadSite::Unbound
);
}
#[test]
fn final_scope_is_unknown_under_a_dynamic_source() {
let files = [facts_seq(
"/s/b.R",
&[],
vec![dynamic_edge()],
vec![dyn_src_ev()],
)];
let scope = build_scope(&files);
assert_eq!(
scope.final_scope_binding(Path::new("/s/b.R"), "foo"),
ReadSite::Unknown
);
}
#[test]
fn final_scope_is_unknown_with_two_closure_definers() {
let files = [
facts("/s/a.R", &["foo"], &[], vec![], None),
facts("/s/c.R", &["foo"], &[], vec![], None),
facts(
"/s/d.R",
&[],
&[],
vec![source_path("/s/a.R", false), source_path("/s/c.R", false)],
None,
),
facts_seq(
"/s/b.R",
&[],
vec![source_path("/s/d.R", false)],
vec![src_ev("/s/d.R")],
),
];
let scope = build_scope(&files);
assert_eq!(
scope.final_scope_binding(Path::new("/s/b.R"), "foo"),
ReadSite::Unknown
);
}
#[test]
fn final_scope_local_def_shadows_a_sourced_def() {
let files = [
facts("/s/a.R", &["foo"], &[], vec![], None),
facts_seq(
"/s/b.R",
&["foo"],
vec![source_path("/s/a.R", false)],
vec![src_ev("/s/a.R"), def_ev("foo")],
),
];
let scope = build_scope(&files);
assert_eq!(
scope.final_scope_binding(Path::new("/s/b.R"), "foo"),
ReadSite::Bound(PathBuf::from("/s/b.R"))
);
}
}