use super::{IncludeIdentity, IncludeResolution};
use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticLabel, Severity};
use crate::model::Located;
use crate::source::SourceSpan;
use std::fmt;
use std::path::{Path, PathBuf};
pub const INCLUDE_PROJECT_DIRECTORY_UNRESOLVED: DiagnosticCode =
DiagnosticCode::new("compose.include.project-directory-unresolved");
#[derive(Clone, PartialEq, Eq)]
pub enum IncludeProjectDirectoryResolution {
Resolved(PathBuf),
Deferred,
}
impl fmt::Debug for IncludeProjectDirectoryResolution {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Resolved(_) => formatter.write_str("Resolved(<authorized-directory>)"),
Self::Deferred => formatter.write_str("Deferred"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum IncludeProjectDirectoryResolveError {
Unresolved,
}
pub struct IncludeProjectDirectoryRequest<'a> {
edge_index: usize,
request_index: usize,
parent_node_index: usize,
child_node_index: usize,
parent_identity: &'a IncludeIdentity,
child_identity: &'a IncludeIdentity,
parent_effective_directory: Option<&'a Path>,
declaration: &'a Located<String>,
child_first_document_directory: Option<&'a Path>,
}
impl IncludeProjectDirectoryRequest<'_> {
#[must_use]
pub const fn edge_index(&self) -> usize {
self.edge_index
}
#[must_use]
pub const fn request_index(&self) -> usize {
self.request_index
}
#[must_use]
pub const fn parent_node_index(&self) -> usize {
self.parent_node_index
}
#[must_use]
pub const fn child_node_index(&self) -> usize {
self.child_node_index
}
#[must_use]
pub const fn parent_identity(&self) -> &IncludeIdentity {
self.parent_identity
}
#[must_use]
pub const fn child_identity(&self) -> &IncludeIdentity {
self.child_identity
}
#[must_use]
pub const fn parent_effective_directory(&self) -> Option<&Path> {
self.parent_effective_directory
}
#[must_use]
pub const fn declaration(&self) -> &Located<String> {
self.declaration
}
#[must_use]
pub const fn declaration_span(&self) -> SourceSpan {
self.declaration.span()
}
#[must_use]
pub const fn child_first_document_directory(&self) -> Option<&Path> {
self.child_first_document_directory
}
}
pub trait IncludeProjectDirectoryResolver {
fn resolve_project_directory(
&self,
request: &IncludeProjectDirectoryRequest<'_>,
) -> Result<IncludeProjectDirectoryResolution, IncludeProjectDirectoryResolveError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum IncludeProjectDirectoryStatus {
Root,
Defaulted,
Resolved,
Deferred,
Unresolved,
}
#[derive(Clone, PartialEq, Eq)]
pub struct IncludeProjectDirectoryEntry {
node_index: usize,
identity: IncludeIdentity,
status: IncludeProjectDirectoryStatus,
effective_directory: Option<PathBuf>,
edge_index: Option<usize>,
request_index: Option<usize>,
declaration: Option<Located<String>>,
}
impl fmt::Debug for IncludeProjectDirectoryEntry {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IncludeProjectDirectoryEntry")
.field("node_index", &self.node_index)
.field("identity", &"<redacted-identity>")
.field("status", &self.status)
.field(
"effective_directory",
&self.effective_directory.as_ref().map(|_| "<authorized-directory>"),
)
.field("edge_index", &self.edge_index)
.field("request_index", &self.request_index)
.field(
"declaration",
&self.declaration.as_ref().map(|_| "<redacted-declaration>"),
)
.finish()
}
}
impl IncludeProjectDirectoryEntry {
#[must_use]
pub const fn node_index(&self) -> usize {
self.node_index
}
#[must_use]
pub const fn identity(&self) -> &IncludeIdentity {
&self.identity
}
#[must_use]
pub const fn status(&self) -> IncludeProjectDirectoryStatus {
self.status
}
#[must_use]
pub fn effective_directory(&self) -> Option<&Path> {
self.effective_directory.as_deref()
}
#[must_use]
pub const fn edge_index(&self) -> Option<usize> {
self.edge_index
}
#[must_use]
pub const fn request_index(&self) -> Option<usize> {
self.request_index
}
#[must_use]
pub const fn declaration(&self) -> Option<&Located<String>> {
self.declaration.as_ref()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct IncludeProjectDirectoryPlan {
entries: Vec<IncludeProjectDirectoryEntry>,
diagnostics: Vec<Diagnostic>,
}
impl fmt::Debug for IncludeProjectDirectoryPlan {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IncludeProjectDirectoryPlan")
.field("entries", &self.entries)
.field("diagnostics", &self.diagnostics)
.finish()
}
}
impl IncludeProjectDirectoryPlan {
#[must_use]
pub fn entries(&self) -> &[IncludeProjectDirectoryEntry] {
&self.entries
}
#[must_use]
pub fn entry(&self, node_index: usize) -> Option<&IncludeProjectDirectoryEntry> {
self.entries.get(node_index)
}
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity() != Severity::Error)
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.is_valid() && self.entries.iter().all(|entry| entry.effective_directory.is_some())
}
}
pub(super) fn plan_project_directories(
resolution: &IncludeResolution,
resolver: &dyn IncludeProjectDirectoryResolver,
) -> IncludeProjectDirectoryPlan {
let mut entries = vec![None; resolution.nodes.len()];
let mut diagnostics = resolution.diagnostics.clone();
if !resolution.nodes.is_empty() {
let root = &resolution.nodes[0];
entries[0] = Some(IncludeProjectDirectoryEntry {
node_index: 0,
identity: root.identity().clone(),
status: IncludeProjectDirectoryStatus::Root,
effective_directory: first_document_directory(root),
edge_index: None,
request_index: None,
declaration: None,
});
plan_children(0, resolution, resolver, &mut entries, &mut diagnostics);
}
IncludeProjectDirectoryPlan {
entries: entries.into_iter().flatten().collect(),
diagnostics,
}
}
fn plan_children(
parent_node_index: usize,
resolution: &IncludeResolution,
resolver: &dyn IncludeProjectDirectoryResolver,
entries: &mut [Option<IncludeProjectDirectoryEntry>],
diagnostics: &mut Vec<Diagnostic>,
) {
let parent_effective_directory = entries[parent_node_index]
.as_ref()
.and_then(IncludeProjectDirectoryEntry::effective_directory)
.map(Path::to_path_buf);
for (edge_index, edge) in resolution.edges.iter().enumerate() {
if edge.parent_node_index != parent_node_index || edge.cycle {
continue;
}
let child_node_index = edge.child_node_index;
let request = &resolution.requests[edge.request_index];
let declaration = request.project_directory();
let child = &resolution.nodes[child_node_index];
let child_first_document_directory = first_document_directory(child);
let entry = match declaration {
None => IncludeProjectDirectoryEntry {
node_index: child_node_index,
identity: child.identity().clone(),
status: IncludeProjectDirectoryStatus::Defaulted,
effective_directory: child_first_document_directory,
edge_index: Some(edge_index),
request_index: Some(edge.request_index),
declaration: None,
},
Some(declaration) => {
let context = IncludeProjectDirectoryRequest {
edge_index,
request_index: edge.request_index,
parent_node_index,
child_node_index,
parent_identity: &edge.parent,
child_identity: &edge.child,
parent_effective_directory: parent_effective_directory.as_deref(),
declaration,
child_first_document_directory: child_first_document_directory.as_deref(),
};
match resolver.resolve_project_directory(&context) {
Ok(IncludeProjectDirectoryResolution::Resolved(directory)) => IncludeProjectDirectoryEntry {
node_index: child_node_index,
identity: child.identity().clone(),
status: IncludeProjectDirectoryStatus::Resolved,
effective_directory: Some(directory),
edge_index: Some(edge_index),
request_index: Some(edge.request_index),
declaration: Some(declaration.clone()),
},
Ok(IncludeProjectDirectoryResolution::Deferred) => IncludeProjectDirectoryEntry {
node_index: child_node_index,
identity: child.identity().clone(),
status: IncludeProjectDirectoryStatus::Deferred,
effective_directory: None,
edge_index: Some(edge_index),
request_index: Some(edge.request_index),
declaration: Some(declaration.clone()),
},
Err(IncludeProjectDirectoryResolveError::Unresolved) => {
diagnostics.push(project_directory_unresolved_diagnostic(declaration.span()));
IncludeProjectDirectoryEntry {
node_index: child_node_index,
identity: child.identity().clone(),
status: IncludeProjectDirectoryStatus::Unresolved,
effective_directory: None,
edge_index: Some(edge_index),
request_index: Some(edge.request_index),
declaration: Some(declaration.clone()),
}
}
}
}
};
entries[child_node_index] = Some(entry);
plan_children(child_node_index, resolution, resolver, entries, diagnostics);
}
}
fn first_document_directory(node: &super::IncludeNode) -> Option<PathBuf> {
node.inputs()
.documents()
.first()
.map(|document| document.origin().directory().to_path_buf())
}
fn project_directory_unresolved_diagnostic(span: SourceSpan) -> Diagnostic {
Diagnostic::new(
INCLUDE_PROJECT_DIRECTORY_UNRESOLVED,
Severity::Error,
"included project directory could not be resolved",
)
.with_label(DiagnosticLabel::primary(span, "project directory declaration"))
}