use std::collections::{BTreeSet, HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use rowan::TextRange;
use crate::project::source::{SourceEdgeKey, SourceTarget, TopLevelEvent};
use crate::rindex::harvest::parse_namespace;
static EMPTY: BTreeSet<String> = BTreeSet::new();
static EMPTY_PATHS: LazyLock<HashSet<PathBuf>> = LazyLock::new(HashSet::new);
#[derive(Debug, Clone)]
pub struct FileFacts {
pub path: PathBuf,
pub exports: BTreeSet<String>,
pub free_reads: BTreeSet<String>,
pub source_edges: Vec<SourceEdgeKey>,
pub top_level_events: Vec<TopLevelEvent>,
pub package_root: Option<PathBuf>,
}
#[derive(Debug, Default)]
pub struct ProjectScope {
visible: HashMap<PathBuf, BTreeSet<String>>,
used_by_others: HashMap<PathBuf, BTreeSet<String>>,
dynamic: HashSet<PathBuf>,
sees: HashMap<PathBuf, HashSet<PathBuf>>,
package_siblings: HashMap<PathBuf, HashSet<PathBuf>>,
package_complete: HashMap<PathBuf, bool>,
top_level_events: HashMap<PathBuf, Vec<TopLevelEvent>>,
exports: HashMap<PathBuf, BTreeSet<String>>,
source_edges: HashMap<PathBuf, 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 BTreeSet<String>,
used_by_others: &'a BTreeSet<String>,
pub resolution_incomplete: bool,
}
impl<'a> FileScope<'a> {
pub fn new(
visible: &'a BTreeSet<String>,
used_by_others: &'a BTreeSet<String>,
resolution_incomplete: bool,
) -> Self {
Self {
visible,
used_by_others,
resolution_incomplete,
}
}
pub fn visible_names(&self) -> &BTreeSet<String> {
self.visible
}
pub fn used_names(&self) -> &BTreeSet<String> {
self.used_by_others
}
pub fn resolves(&self, name: &str) -> bool {
self.visible.contains(name)
}
pub fn used_elsewhere(&self, name: &str) -> bool {
self.used_by_others.contains(name)
}
}
impl ProjectScope {
pub fn build(
files: &[FileFacts],
namespaces: &HashMap<PathBuf, String>,
package_complete: &HashMap<PathBuf, bool>,
) -> 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 mut package_siblings: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new();
for members in package_members.values() {
for &member in members {
let siblings = members
.iter()
.filter(|&&other| other != member)
.map(|&other| other.to_path_buf())
.collect();
package_siblings.insert(member.to_path_buf(), siblings);
}
}
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 top_level_events: HashMap<PathBuf, Vec<TopLevelEvent>> = files
.iter()
.map(|f| (f.path.clone(), f.top_level_events.clone()))
.collect();
let exports_by_path: HashMap<PathBuf, BTreeSet<String>> = files
.iter()
.map(|f| (f.path.clone(), f.exports.clone()))
.collect();
let source_edges_by_path: HashMap<PathBuf, Vec<SourceEdgeKey>> = files
.iter()
.map(|f| (f.path.clone(), f.source_edges.clone()))
.collect();
let mut sees: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new();
let mut dynamic: HashSet<PathBuf> = HashSet::new();
for f in files {
let mut seen: HashSet<PathBuf> = HashSet::new();
if let Some(root) = &f.package_root {
for member in &package_members[root.as_path()] {
if *member != f.path {
seen.insert(member.to_path_buf());
}
}
}
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 {
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()) => {
seen.insert(target.path.clone());
queue.push(target);
}
Some(_) => {}
None => unresolved = true,
},
}
}
}
if unresolved {
dynamic.insert(f.path.clone());
}
sees.insert(f.path.clone(), seen);
}
let mut visible: HashMap<PathBuf, BTreeSet<String>> = HashMap::new();
let mut used_by_others: HashMap<PathBuf, BTreeSet<String>> = files
.iter()
.map(|f| (f.path.clone(), BTreeSet::new()))
.collect();
for f in files {
let mut defs = BTreeSet::new();
for seen in &sees[&f.path] {
if let Some(target) = by_path.get(seen.as_path()) {
defs.extend(target.exports.iter().cloned());
}
}
for name in &f.exports {
defs.remove(name);
}
visible.insert(f.path.clone(), defs);
for seen in &sees[&f.path] {
if let Some(used) = used_by_others.get_mut(seen) {
used.extend(f.free_reads.iter().cloned());
}
}
}
for (root, text) in namespaces {
let Some(members) = package_members.get(root.as_path()) else {
continue;
};
let object_names: Vec<String> = members
.iter()
.filter_map(|m| by_path.get(m))
.flat_map(|f| f.exports.iter().map(|n| n.to_string()))
.collect();
let info = parse_namespace(text, &object_names);
let exported: BTreeSet<String> = info.exports.iter().cloned().collect();
let imported: BTreeSet<String> = info.imported_names.iter().cloned().collect();
let incomplete = !info.imported_packages.is_empty();
for member in members {
let path = member.to_path_buf();
if let Some(used) = used_by_others.get_mut(&path) {
used.extend(exported.iter().cloned());
}
if let Some(vis) = visible.get_mut(&path) {
vis.extend(imported.iter().cloned());
}
if incomplete {
dynamic.insert(path);
}
}
}
Self {
visible,
used_by_others,
dynamic,
sees,
package_siblings,
package_complete,
top_level_events,
exports: exports_by_path,
source_edges: source_edges_by_path,
}
}
pub fn for_file(&self, path: &Path) -> FileScope<'_> {
FileScope {
visible: self.visible.get(path).unwrap_or(&EMPTY),
used_by_others: self.used_by_others.get(path).unwrap_or(&EMPTY),
resolution_incomplete: self.dynamic.contains(path),
}
}
pub fn sees(&self, path: &Path) -> &HashSet<PathBuf> {
match self.sees.get(path) {
Some(seen) => seen,
None => &EMPTY_PATHS,
}
}
pub fn seen_by(&self, path: &Path) -> HashSet<PathBuf> {
self.sees
.iter()
.filter(|(_, seen)| seen.contains(path))
.map(|(p, _)| p.clone())
.collect()
}
pub fn package_siblings(&self, path: &Path) -> &HashSet<PathBuf> {
match self.package_siblings.get(path) {
Some(siblings) => siblings,
None => &EMPTY_PATHS,
}
}
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.top_level_events.get(from_file) 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 {
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.top_level_events.get(from_file) else {
return ReadSite::Unbound;
};
let mut live: Option<PathBuf> = None;
let mut name_ambiguous = false;
let mut poisoned = false;
for event in events {
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;
}
if self.exports.get(&cur).is_some_and(|e| e.contains(name)) {
definers.push(cur.clone());
}
if let Some(edges) = self.source_edges.get(&cur) {
for edge in edges {
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 package_root(path: &Path) -> Option<PathBuf> {
let mut dir = path.parent();
while let Some(d) = dir {
if d.join("DESCRIPTION").is_file() && d.join("R").is_dir() {
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: set(exp),
free_reads: set(reads),
source_edges: edges,
top_level_events: Vec::new(),
package_root: root.map(PathBuf::from),
}
}
fn names(set: &BTreeSet<String>) -> Vec<String> {
let mut v: Vec<String> = set.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: set(exp),
free_reads: BTreeSet::new(),
source_edges: edges,
top_level_events: events,
package_root: None,
}
}
fn build_scope(files: &[FileFacts]) -> ProjectScope {
ProjectScope::build(files, &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 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!(
names(scope.for_file(Path::new("/s/a.R")).visible),
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 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());
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());
let a = scope.for_file(Path::new("/pkg/R/a.R"));
assert!(a.resolves("filter"));
assert!(!a.resolution_incomplete);
}
#[test]
fn namespace_wholesale_import_marks_resolution_incomplete() {
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());
assert!(
scope
.for_file(Path::new("/pkg/R/a.R"))
.resolution_incomplete
);
}
#[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"))
);
}
}