use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::{Path, PathBuf};
use rowan::TextRange;
use smol_str::SmolStr;
use crate::incremental::{
IncrementalDb, LibraryIndex, QueryKind, QueryLogEntry, SourceFile, Workspace, file_def_sites,
file_exports, file_free_reads, loaded_names, parse_diagnostics, source_edges,
};
use crate::project::exports::DefKind;
use crate::project::scope::{FileFacts, FileScope, ProjectScope, package_root};
use crate::project::source::{SourceEdgeKey, SourceTarget};
use crate::rindex::provider::{package_indexed, resolve_origin};
use crate::semantic::symbols::{LoadedPackage, PackageOrigin};
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct ProjectMember {
pub file: SourceFile,
pub path: PathBuf,
pub package_root: Option<PathBuf>,
}
#[salsa::interned]
pub struct Project<'db> {
#[returns(ref)]
pub members: Vec<ProjectMember>,
#[returns(ref)]
pub namespaces: Vec<(PathBuf, String)>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, salsa::Update)]
pub struct Visibility {
pub visible: BTreeSet<String>,
pub used_by_others: BTreeSet<String>,
pub incomplete: bool,
}
impl Visibility {
pub fn scope(&self) -> FileScope<'_> {
FileScope::new(&self.visible, &self.used_by_others, self.incomplete)
}
}
pub(crate) fn read_namespaces(members: &[ProjectMember]) -> Vec<(PathBuf, String)> {
let mut namespaces: HashMap<PathBuf, String> = HashMap::new();
for member in members {
if let Some(root) = &member.package_root
&& !namespaces.contains_key(root)
&& let Ok(text) = std::fs::read_to_string(root.join("NAMESPACE"))
{
namespaces.insert(root.clone(), text);
}
}
let mut namespaces: Vec<(PathBuf, String)> = namespaces.into_iter().collect();
namespaces.sort_by(|a, b| a.0.cmp(&b.0));
namespaces
}
#[salsa::tracked]
pub fn workspace_project<'db>(db: &'db dyn IncrementalDb) -> Project<'db> {
db.record_query(QueryLogEntry {
kind: QueryKind::WorkspaceProject,
file: None,
});
let mut members: Vec<ProjectMember> = match Workspace::try_get(db) {
Some(ws) => ws
.members(db)
.iter()
.filter_map(|&file| {
let path = file.path(db).as_deref()?.to_path_buf();
if !parse_diagnostics(db, file).is_empty() {
return None;
}
let package_root = package_root(&path);
Some(ProjectMember {
file,
path,
package_root,
})
})
.collect(),
None => Vec::new(),
};
members.sort_by(|a, b| a.path.cmp(&b.path));
let namespaces = read_namespaces(&members);
Project::new(db, members, namespaces)
}
#[salsa::tracked(returns(ref), no_eq, unsafe(non_update_types))]
pub fn project_graph<'db>(db: &'db dyn IncrementalDb, project: Project<'db>) -> ProjectScope {
db.record_query(QueryLogEntry {
kind: QueryKind::ProjectGraph,
file: None,
});
let facts: Vec<FileFacts> = project
.members(db)
.iter()
.map(|m| FileFacts {
path: m.path.clone(),
exports: file_exports(db, m.file).clone(),
free_reads: file_free_reads(db, m.file).clone(),
source_edges: source_edges(db, m.file).clone(),
package_root: m.package_root.clone(),
})
.collect();
let namespaces: HashMap<PathBuf, String> = project.namespaces(db).iter().cloned().collect();
ProjectScope::build(&facts, &namespaces)
}
#[salsa::tracked(returns(ref))]
pub fn visible_symbols<'db>(
db: &'db dyn IncrementalDb,
project: Project<'db>,
file: SourceFile,
) -> Visibility {
db.record_query(QueryLogEntry {
kind: QueryKind::VisibleSymbols,
file: Some(file),
});
let graph = project_graph(db, project);
let Some(path) = file.path(db).as_deref() else {
return Visibility::default();
};
let scope = graph.for_file(path);
Visibility {
visible: scope.visible_names().clone(),
used_by_others: scope.used_names().clone(),
incomplete: scope.resolution_incomplete,
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, salsa::Update)]
pub struct ReverseSources {
pub sourced_by: BTreeMap<PathBuf, BTreeSet<PathBuf>>,
pub dynamic_sources: BTreeSet<PathBuf>,
}
fn invert_source_edges<'a>(
members: impl IntoIterator<Item = (&'a Path, &'a [SourceEdgeKey])>,
) -> ReverseSources {
let mut rev = ReverseSources::default();
for (path, edges) in members {
for edge in edges {
match &edge.target {
SourceTarget::Dynamic => {
rev.dynamic_sources.insert(path.to_path_buf());
}
SourceTarget::Path(target) => {
rev.sourced_by
.entry(target.clone())
.or_default()
.insert(path.to_path_buf());
}
}
}
}
rev
}
#[salsa::tracked(returns(ref))]
pub fn reverse_source_edges<'db>(
db: &'db dyn IncrementalDb,
project: Project<'db>,
) -> ReverseSources {
db.record_query(QueryLogEntry {
kind: QueryKind::ReverseSourceEdges,
file: None,
});
invert_source_edges(
project
.members(db)
.iter()
.map(|m| (m.path.as_path(), source_edges(db, m.file).as_slice())),
)
}
#[derive(Debug, Default, Clone, PartialEq, Eq, salsa::Update)]
pub struct DefIndex {
pub by_name: BTreeMap<String, BTreeSet<(PathBuf, DefKind)>>,
}
#[salsa::tracked(returns(ref))]
pub fn project_defs<'db>(db: &'db dyn IncrementalDb, project: Project<'db>) -> DefIndex {
db.record_query(QueryLogEntry {
kind: QueryKind::ProjectDefs,
file: None,
});
let mut index = DefIndex::default();
for member in project.members(db) {
for (name, kind) in file_def_sites(db, member.file) {
index
.by_name
.entry(name.clone())
.or_default()
.insert((member.path.clone(), *kind));
}
}
index
}
#[derive(Debug, Default, Clone, PartialEq, Eq, salsa::Update)]
pub struct ReadIndex {
pub by_name: BTreeMap<String, BTreeSet<PathBuf>>,
}
#[salsa::tracked(returns(ref))]
pub fn project_reads<'db>(db: &'db dyn IncrementalDb, project: Project<'db>) -> ReadIndex {
db.record_query(QueryLogEntry {
kind: QueryKind::ProjectReads,
file: None,
});
let mut index = ReadIndex::default();
for member in project.members(db) {
for name in file_free_reads(db, member.file) {
index
.by_name
.entry(name.clone())
.or_default()
.insert(member.path.clone());
}
}
index
}
#[derive(Debug, Default, Clone, PartialEq, Eq, salsa::Update)]
pub struct ExternalResolution {
pub unresolved: BTreeSet<String>,
}
#[salsa::tracked(returns(ref))]
pub fn external_resolution<'db>(
db: &'db dyn IncrementalDb,
manifest: LibraryIndex,
project: Project<'db>,
file: SourceFile,
) -> ExternalResolution {
db.record_query(QueryLogEntry {
kind: QueryKind::ExternalResolution,
file: Some(file),
});
let index: &crate::rindex::provider::IndexedProvider = manifest.data(db);
let loaded = loaded_names(db, file);
if loaded.iter().any(|pkg| !package_indexed(index, pkg)) {
return ExternalResolution::default();
}
let visibility = visible_symbols(db, project, file);
if visibility.incomplete {
return ExternalResolution::default();
}
let loaded_pkgs: Vec<LoadedPackage> = loaded
.iter()
.map(|name| LoadedPackage {
name: SmolStr::new(name),
range: TextRange::default(),
})
.collect();
let unresolved = file_free_reads(db, file)
.iter()
.filter(|name| !visibility.visible.contains(name.as_str()))
.filter(|name| {
matches!(
resolve_origin(index, name, &loaded_pkgs),
PackageOrigin::Unknown
)
})
.cloned()
.collect();
ExternalResolution { unresolved }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::project::source::{SourceEdgeKey, SourceTarget};
fn path_edge(target: &str, local: bool) -> SourceEdgeKey {
SourceEdgeKey {
target: SourceTarget::Path(PathBuf::from(target)),
local,
}
}
fn dynamic_edge() -> SourceEdgeKey {
SourceEdgeKey {
target: SourceTarget::Dynamic,
local: false,
}
}
fn invert(members: &[(&str, Vec<SourceEdgeKey>)]) -> ReverseSources {
invert_source_edges(
members
.iter()
.map(|(p, edges)| (Path::new(*p), edges.as_slice())),
)
}
fn sourcers<'a>(rev: &'a ReverseSources, target: &str) -> Vec<&'a str> {
rev.sourced_by
.get(Path::new(target))
.into_iter()
.flat_map(|set| set.iter().map(|p| p.to_str().unwrap()))
.collect()
}
#[test]
fn single_edge_inverts() {
let rev = invert(&[
("/s/a.R", vec![path_edge("/s/b.R", false)]),
("/s/b.R", vec![]),
]);
assert_eq!(sourcers(&rev, "/s/b.R"), vec!["/s/a.R"]);
assert!(!rev.sourced_by.contains_key(Path::new("/s/a.R")));
assert!(rev.dynamic_sources.is_empty());
}
#[test]
fn multiple_sourcers_aggregate() {
let rev = invert(&[
("/s/a.R", vec![path_edge("/s/c.R", false)]),
("/s/b.R", vec![path_edge("/s/c.R", false)]),
]);
assert_eq!(sourcers(&rev, "/s/c.R"), vec!["/s/a.R", "/s/b.R"]);
}
#[test]
fn local_edge_is_retained() {
let rev = invert(&[("/s/a.R", vec![path_edge("/s/b.R", true)])]);
assert_eq!(sourcers(&rev, "/s/b.R"), vec!["/s/a.R"]);
}
#[test]
fn dynamic_edge_recorded_separately() {
let rev = invert(&[("/s/a.R", vec![dynamic_edge()])]);
assert!(rev.sourced_by.is_empty());
assert!(rev.dynamic_sources.contains(Path::new("/s/a.R")));
}
#[test]
fn target_outside_member_set_is_retained() {
let rev = invert(&[("/s/a.R", vec![path_edge("/s/gen.R", false)])]);
assert_eq!(sourcers(&rev, "/s/gen.R"), vec!["/s/a.R"]);
}
}