#![allow(dead_code)]
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
pub use crate::grammar::{GrammarCapabilities, GrammarId};
use crate::layout::project_packages_root;
use crate::models::{CliError, InstalledPackage, PackageManifestFile, PackagesState, ReleaseResponse};
use crate::state::{
load_project_resolver_cache,
ProjectGrammarDescriptorRecord, ProjectGrammarModuleEntryRecord, ProjectGrammarSegmentRecord,
ProjectGrammarSegmentState, ProjectResolverCache,
};
#[derive(Debug, Clone)]
pub struct GrammarDescriptor {
pub id: GrammarId,
pub capabilities: GrammarCapabilities,
}
#[derive(Debug, Clone)]
pub struct ModuleQuery {
pub package_scope: PackageScope,
pub name: String,
}
#[derive(Debug, Clone)]
pub struct IncludeQuery {
pub package_scope: PackageScope,
pub name: String,
}
#[derive(Debug, Clone)]
pub struct ImportSpecifierQuery {
pub package_scope: PackageScope,
pub value: String,
}
#[derive(Debug, Clone)]
pub struct PackageReferenceQuery {
pub package_scope: PackageScope,
pub name: String,
}
#[derive(Debug, Clone)]
pub enum ReferenceQuery {
Modules(Vec<ModuleQuery>),
Includes(Vec<IncludeQuery>),
ImportSpecifiers(Vec<ImportSpecifierQuery>),
Packages(Vec<PackageReferenceQuery>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QuerySelection {
Files(FileSelection),
Unsupported,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FileSelection {
pub file_paths: Vec<String>,
pub authoritative: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct PackageIdentity {
pub name: String,
pub version: String,
pub dialect: String,
pub target: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParticipatingFileRecord {
pub package: PackageIdentity,
pub direct: bool,
pub package_relative_path: String,
pub source_root_path: String,
pub content_fingerprint: Option<String>,
pub path: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ParticipatingFileSet {
pub files: Vec<ParticipatingFileRecord>,
pub authoritative: bool,
}
#[derive(Debug, Clone)]
pub enum PackageScope {
AnyDependency,
DirectDependenciesOnly,
Package(String),
}
pub type ModulePackageScope = PackageScope;
#[derive(Debug, Clone)]
pub struct ParticipatingFileQuery {
pub package_scope: PackageScope,
}
#[derive(Debug, Clone)]
pub struct ModulePrefix {
pub package_scope: PackageScope,
pub segments: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum ModuleResolution {
AuthoritativeHit(ModuleMatchSet),
AuthoritativeMiss,
NonAuthoritativeMiss,
UnsupportedGrammar,
StaleIndex,
}
#[derive(Debug, Clone, Default)]
pub struct ModuleMatchSet {
pub package_names: Vec<String>,
pub file_paths: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PrefixPresence {
Present,
Absent,
Unknown,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ModuleChildren {
pub segments: Vec<String>,
pub authoritative: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnapshotState {
Fresh,
Stale,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompiledGrammarSegmentState {
Unavailable,
Loaded,
Stale,
}
pub trait CompiledGrammarSegment {
fn descriptor(&self) -> &GrammarDescriptor;
fn state(&self) -> CompiledGrammarSegmentState;
}
pub trait GrammarIndex {
fn descriptor(&self) -> &GrammarDescriptor;
fn files_for_query(&self, query: &ReferenceQuery) -> Result<QuerySelection, CliError>;
fn files_for_modules(&self, queries: &[ModuleQuery]) -> Result<FileSelection, CliError>;
fn resolve_module(&self, query: &ModuleQuery) -> Result<ModuleResolution, CliError>;
fn maybe_contains_prefix(&self, prefix: &ModulePrefix) -> Result<PrefixPresence, CliError>;
fn enumerate_children(&self, prefix: &ModulePrefix) -> Result<ModuleChildren, CliError>;
}
#[derive(Debug, Clone)]
struct CompiledGrammarSegmentRecord {
descriptor: GrammarDescriptor,
state: CompiledGrammarSegmentState,
module_entries: Vec<ProjectGrammarModuleEntryRecord>,
separator: String,
case_sensitive: bool,
negative_lookup_authoritative: bool,
prefix_authoritative: bool,
child_enumeration_authoritative: bool,
exact_index: BTreeMap<String, Vec<ProjectGrammarModuleEntryRecord>>,
prefix_presence_index: BTreeMap<String, Vec<ProjectGrammarModuleEntryRecord>>,
child_index: BTreeMap<String, Vec<CompiledChildEntry>>,
}
#[derive(Debug, Clone)]
struct CompiledChildEntry {
child_segment: String,
package_name: String,
direct: bool,
}
impl CompiledGrammarSegmentRecord {
fn from_cache_segment(segment: &ProjectGrammarSegmentRecord) -> Self {
let separator = segment.namespace_separator.clone();
let case_sensitive = segment.case_sensitive;
let (exact_index, prefix_presence_index, child_index) = build_compiled_indexes(
&segment.module_entries,
&separator,
case_sensitive,
);
Self {
descriptor: grammar_descriptor_from_segment(segment),
state: match segment.state {
ProjectGrammarSegmentState::Unavailable => CompiledGrammarSegmentState::Unavailable,
ProjectGrammarSegmentState::Loaded => CompiledGrammarSegmentState::Loaded,
ProjectGrammarSegmentState::Stale => CompiledGrammarSegmentState::Stale,
},
module_entries: segment.module_entries.clone(),
separator,
case_sensitive,
negative_lookup_authoritative: segment.completeness.negative_lookup_authoritative,
prefix_authoritative: segment.completeness.prefix_authoritative,
child_enumeration_authoritative: segment.completeness.child_enumeration_authoritative,
exact_index,
prefix_presence_index,
child_index,
}
}
}
impl CompiledGrammarSegment for CompiledGrammarSegmentRecord {
fn descriptor(&self) -> &GrammarDescriptor {
&self.descriptor
}
fn state(&self) -> CompiledGrammarSegmentState {
self.state.clone()
}
}
#[derive(Debug, Clone)]
struct CompiledGrammarIndex {
segment: CompiledGrammarSegmentRecord,
}
impl CompiledGrammarIndex {
fn from_cache_segment(segment: &ProjectGrammarSegmentRecord) -> Self {
Self {
segment: CompiledGrammarSegmentRecord::from_cache_segment(segment),
}
}
}
impl GrammarIndex for CompiledGrammarIndex {
fn descriptor(&self) -> &GrammarDescriptor {
self.segment.descriptor()
}
fn files_for_query(&self, query: &ReferenceQuery) -> Result<QuerySelection, CliError> {
match query {
ReferenceQuery::Modules(queries) => {
Ok(QuerySelection::Files(self.files_for_modules(queries.as_slice())?))
}
ReferenceQuery::Includes(queries) => Ok(QuerySelection::Files(
self.files_for_exact_names(
queries
.iter()
.map(|query| (&query.package_scope, query.name.as_str())),
)?,
)),
ReferenceQuery::ImportSpecifiers(queries) => Ok(QuerySelection::Files(
self.files_for_exact_names(
queries
.iter()
.map(|query| (&query.package_scope, query.value.as_str())),
)?,
)),
ReferenceQuery::Packages(queries) => Ok(QuerySelection::Files(
self.files_for_exact_names(
queries
.iter()
.map(|query| (&query.package_scope, query.name.as_str())),
)?,
)),
}
}
fn files_for_modules(&self, queries: &[ModuleQuery]) -> Result<FileSelection, CliError> {
let mut file_paths = Vec::new();
let mut authoritative = self.segment.state() == CompiledGrammarSegmentState::Loaded;
for query in queries {
match self.resolve_module(query)? {
ModuleResolution::AuthoritativeHit(matches) => {
for file_path in matches.file_paths {
if !file_paths.contains(&file_path) {
file_paths.push(file_path);
}
}
}
ModuleResolution::AuthoritativeMiss => {}
ModuleResolution::NonAuthoritativeMiss
| ModuleResolution::UnsupportedGrammar
| ModuleResolution::StaleIndex => authoritative = false,
}
}
Ok(FileSelection {
file_paths,
authoritative,
})
}
fn resolve_module(&self, query: &ModuleQuery) -> Result<ModuleResolution, CliError> {
if self.segment.state() == CompiledGrammarSegmentState::Stale {
return Ok(ModuleResolution::StaleIndex);
}
if self.segment.state() == CompiledGrammarSegmentState::Unavailable {
return Ok(ModuleResolution::NonAuthoritativeMiss);
}
let normalized_query = normalize_module_name(query.name.as_str(), self.segment.case_sensitive);
let mut package_names = Vec::new();
let mut file_paths = Vec::new();
for entry in self
.segment
.exact_index
.get(&normalized_query)
.into_iter()
.flatten()
.filter(|entry| module_query_matches_scope(query, entry))
{
if !package_names.contains(&entry.package_name) {
package_names.push(entry.package_name.clone());
}
if !file_paths.contains(&entry.file_path) {
file_paths.push(entry.file_path.clone());
}
}
if !file_paths.is_empty() {
return Ok(ModuleResolution::AuthoritativeHit(ModuleMatchSet {
package_names,
file_paths,
}));
}
Ok(match self.segment.state() {
CompiledGrammarSegmentState::Loaded => {
if self.segment.negative_lookup_authoritative {
ModuleResolution::AuthoritativeMiss
} else {
ModuleResolution::NonAuthoritativeMiss
}
}
CompiledGrammarSegmentState::Unavailable => ModuleResolution::NonAuthoritativeMiss,
CompiledGrammarSegmentState::Stale => ModuleResolution::StaleIndex,
})
}
fn maybe_contains_prefix(&self, prefix: &ModulePrefix) -> Result<PrefixPresence, CliError> {
if self.segment.state() != CompiledGrammarSegmentState::Loaded {
return Ok(PrefixPresence::Unknown);
}
if !self.segment.descriptor.capabilities.supports_prefix_queries {
return Ok(PrefixPresence::Unknown);
}
let prefix_name = normalize_segments(
&prefix.segments,
self.segment.case_sensitive,
&self.segment.separator,
);
let any_match = self
.segment
.prefix_presence_index
.get(&prefix_name)
.into_iter()
.flatten()
.any(|entry| module_prefix_matches_scope(prefix, entry));
if any_match {
Ok(PrefixPresence::Present)
} else if self.segment.prefix_authoritative {
Ok(PrefixPresence::Absent)
} else {
Ok(PrefixPresence::Unknown)
}
}
fn enumerate_children(&self, prefix: &ModulePrefix) -> Result<ModuleChildren, CliError> {
if self.segment.state() != CompiledGrammarSegmentState::Loaded
|| !self.segment.descriptor.capabilities.supports_child_enumeration
{
return Ok(ModuleChildren {
segments: Vec::new(),
authoritative: false,
});
}
let prefix_name = normalize_segments(
&prefix.segments,
self.segment.case_sensitive,
&self.segment.separator,
);
let mut segments = Vec::new();
for entry in self
.segment
.child_index
.get(&prefix_name)
.into_iter()
.flatten()
.filter(|entry| child_entry_matches_scope(prefix, entry))
{
if !segments.contains(&entry.child_segment) {
segments.push(entry.child_segment.clone());
}
}
Ok(ModuleChildren {
segments,
authoritative: self.segment.child_enumeration_authoritative,
})
}
}
impl CompiledGrammarIndex {
fn files_for_exact_names<'a>(
&self,
queries: impl IntoIterator<Item = (&'a PackageScope, &'a str)>,
) -> Result<FileSelection, CliError> {
if self.segment.state() == CompiledGrammarSegmentState::Stale {
return Ok(FileSelection {
file_paths: Vec::new(),
authoritative: false,
});
}
if self.segment.state() == CompiledGrammarSegmentState::Unavailable {
return Ok(FileSelection {
file_paths: Vec::new(),
authoritative: false,
});
}
let mut file_paths = Vec::new();
let mut authoritative = self.segment.state() == CompiledGrammarSegmentState::Loaded;
for (package_scope, name) in queries {
let normalized_query = normalize_module_name(name, self.segment.case_sensitive);
let mut found = false;
for entry in self
.segment
.exact_index
.get(&normalized_query)
.into_iter()
.flatten()
.filter(|entry| package_scope_matches_query_scope(package_scope, entry))
{
found = true;
if !file_paths.contains(&entry.file_path) {
file_paths.push(entry.file_path.clone());
}
}
if !found && !self.segment.negative_lookup_authoritative {
authoritative = false;
}
}
Ok(FileSelection {
file_paths,
authoritative,
})
}
}
fn grammar_descriptor_from_segment(segment: &ProjectGrammarSegmentRecord) -> GrammarDescriptor {
GrammarDescriptor {
id: grammar_id_from_record(&segment.descriptor),
capabilities: GrammarCapabilities {
authoritative_negative_lookup: segment
.descriptor
.capabilities
.authoritative_negative_lookup,
supports_prefix_queries: segment.descriptor.capabilities.supports_prefix_queries,
supports_child_enumeration: segment
.descriptor
.capabilities
.supports_child_enumeration,
supports_multi_file_modules: segment
.descriptor
.capabilities
.supports_multi_file_modules,
},
}
}
fn grammar_id_from_record(record: &ProjectGrammarDescriptorRecord) -> GrammarId {
GrammarId {
dialect: record.dialect.clone(),
name: record.name.clone(),
version: record.version,
}
}
pub struct LoadedProjectResolver {
cache: ProjectResolverCache,
grammar_indexes: Vec<CompiledGrammarIndex>,
}
impl LoadedProjectResolver {
pub async fn open(project_root: &Path) -> Result<Self, CliError> {
let install_root = project_packages_root(project_root);
Ok(Self::from_cache(load_project_resolver_cache(&install_root).await?))
}
pub(crate) fn from_cache(cache: ProjectResolverCache) -> Self {
let grammar_indexes = cache
.grammar_segments()
.iter()
.map(CompiledGrammarIndex::from_cache_segment)
.collect();
Self {
cache,
grammar_indexes,
}
}
pub(crate) fn packages_state(&self) -> &PackagesState {
self.cache.packages_state()
}
pub fn snapshot_fingerprint(&self) -> Option<&str> {
self.cache.snapshot_fingerprint()
}
pub fn snapshot_state(&self) -> SnapshotState {
if self.cache.is_runtime_stale() {
SnapshotState::Stale
} else {
SnapshotState::Fresh
}
}
pub fn local_command_dirs(&self) -> Vec<PathBuf> {
self.cache.local_path_dirs()
}
pub fn local_command(&self, name: &str) -> Result<Option<PathBuf>, CliError> {
self.cache.resolve_local_command(name)
}
pub fn participating_files(&self, query: &ParticipatingFileQuery) -> ParticipatingFileSet {
let mut files = self
.cache
.participating_files()
.iter()
.filter(|record| package_scope_matches(&query.package_scope, record))
.map(participating_file_record)
.collect::<Vec<_>>();
files.sort_by(|left, right| {
left.package
.cmp(&right.package)
.then(left.package_relative_path.cmp(&right.package_relative_path))
});
ParticipatingFileSet {
files,
authoritative: !self.cache.is_runtime_stale(),
}
}
pub(crate) fn find_matching_installed_package(
&self,
selected: &ReleaseResponse,
fallback_dialect: &str,
constraint: &str,
require_existing_path: bool,
) -> Option<InstalledPackage> {
self.cache.find_matching_installed_package(
selected,
fallback_dialect,
constraint,
require_existing_path,
)
}
pub(crate) fn preserved_package_for_manifest(
&self,
package_root: &Path,
manifest: &PackageManifestFile,
) -> Option<InstalledPackage> {
self.cache.preserved_package_for_manifest(package_root, manifest)
}
pub fn grammar_index(&self, grammar: &GrammarId) -> Option<&dyn GrammarIndex> {
self.grammar_indexes
.iter()
.find(|index| index.descriptor().id == *grammar)
.map(|index| index as &dyn GrammarIndex)
}
pub fn files_for_modules(
&self,
grammar: &GrammarId,
queries: &[ModuleQuery],
) -> Result<Option<FileSelection>, CliError> {
self.files_for_query(grammar, &ReferenceQuery::Modules(queries.to_vec()))
.map(|selection| selection.map(module_selection_from_query_selection))
}
pub fn files_for_query(
&self,
grammar: &GrammarId,
query: &ReferenceQuery,
) -> Result<Option<QuerySelection>, CliError> {
self.grammar_index(grammar)
.map(|index| index.files_for_query(query))
.transpose()
}
}
fn package_scope_matches(
package_scope: &PackageScope,
record: &crate::state::ProjectParticipatingFileRecord,
) -> bool {
match package_scope {
PackageScope::AnyDependency => true,
PackageScope::DirectDependenciesOnly => record.direct,
PackageScope::Package(name) => record.package_name == *name,
}
}
fn participating_file_record(
record: &crate::state::ProjectParticipatingFileRecord,
) -> ParticipatingFileRecord {
ParticipatingFileRecord {
package: PackageIdentity {
name: record.package_name.clone(),
version: record.package_version.clone(),
dialect: record.package_dialect.clone(),
target: record.package_target.clone(),
},
direct: record.direct,
package_relative_path: record.package_relative_path.clone(),
source_root_path: record.source_root_path.clone(),
content_fingerprint: record.content_fingerprint.clone(),
path: record.path.clone(),
}
}
pub(crate) fn module_selection_from_query_selection(selection: QuerySelection) -> FileSelection {
match selection {
QuerySelection::Files(files) => files,
QuerySelection::Unsupported => FileSelection {
file_paths: Vec::new(),
authoritative: false,
},
}
}
fn normalize_module_name(module_name: &str, case_sensitive: bool) -> String {
if case_sensitive {
module_name.to_string()
} else {
module_name.to_lowercase()
}
}
fn normalize_segments(segments: &[String], case_sensitive: bool, separator: &str) -> String {
let mut normalized = segments.to_vec();
if !case_sensitive {
for segment in &mut normalized {
*segment = segment.to_lowercase();
}
}
normalized.join(separator)
}
fn child_segment_after_prefix(module_name: &str, prefix_name: &str, separator: &str) -> Option<String> {
let suffix = if prefix_name.is_empty() {
module_name
} else {
module_name.strip_prefix(prefix_name)?.strip_prefix(separator)?
};
suffix
.split(separator)
.next()
.map(|segment| segment.to_string())
.filter(|segment| !segment.is_empty())
}
fn build_compiled_indexes(
module_entries: &[ProjectGrammarModuleEntryRecord],
separator: &str,
case_sensitive: bool,
) -> (
BTreeMap<String, Vec<ProjectGrammarModuleEntryRecord>>,
BTreeMap<String, Vec<ProjectGrammarModuleEntryRecord>>,
BTreeMap<String, Vec<CompiledChildEntry>>,
) {
let mut exact_index = BTreeMap::<String, Vec<ProjectGrammarModuleEntryRecord>>::new();
let mut prefix_presence_index = BTreeMap::<String, Vec<ProjectGrammarModuleEntryRecord>>::new();
let mut child_index = BTreeMap::<String, Vec<CompiledChildEntry>>::new();
for entry in module_entries {
let normalized = normalize_module_name(&entry.module_name, case_sensitive);
exact_index
.entry(normalized.clone())
.or_default()
.push(entry.clone());
let segments = split_module_segments(&normalized, separator);
for prefix_len in 1..=segments.len() {
let prefix = segments[..prefix_len].join(separator);
prefix_presence_index
.entry(prefix)
.or_default()
.push(entry.clone());
}
for prefix_len in 0..segments.len() {
let prefix = if prefix_len == 0 {
String::new()
} else {
segments[..prefix_len].join(separator)
};
let child_segment = segments[prefix_len].clone();
let child_entries = child_index.entry(prefix).or_default();
if !child_entries.iter().any(|existing| {
existing.child_segment == child_segment
&& existing.package_name == entry.package_name
&& existing.direct == entry.direct
}) {
child_entries.push(CompiledChildEntry {
child_segment,
package_name: entry.package_name.clone(),
direct: entry.direct,
});
}
}
}
(exact_index, prefix_presence_index, child_index)
}
fn split_module_segments(module_name: &str, separator: &str) -> Vec<String> {
if module_name.is_empty() {
return Vec::new();
}
module_name
.split(separator)
.filter(|segment| !segment.is_empty())
.map(|segment| segment.to_string())
.collect()
}
fn module_query_matches_scope(
query: &ModuleQuery,
entry: &ProjectGrammarModuleEntryRecord,
) -> bool {
match &query.package_scope {
PackageScope::AnyDependency => true,
PackageScope::DirectDependenciesOnly => entry.direct,
PackageScope::Package(name) => entry.package_name == *name,
}
}
fn module_prefix_matches_scope(
prefix: &ModulePrefix,
entry: &ProjectGrammarModuleEntryRecord,
) -> bool {
match &prefix.package_scope {
PackageScope::AnyDependency => true,
PackageScope::DirectDependenciesOnly => entry.direct,
PackageScope::Package(name) => entry.package_name == *name,
}
}
fn package_scope_matches_query_scope(
package_scope: &PackageScope,
entry: &ProjectGrammarModuleEntryRecord,
) -> bool {
match package_scope {
PackageScope::AnyDependency => true,
PackageScope::DirectDependenciesOnly => entry.direct,
PackageScope::Package(name) => entry.package_name == *name,
}
}
fn child_entry_matches_scope(prefix: &ModulePrefix, entry: &CompiledChildEntry) -> bool {
match &prefix.package_scope {
PackageScope::AnyDependency => true,
PackageScope::DirectDependenciesOnly => entry.direct,
PackageScope::Package(name) => entry.package_name == *name,
}
}