1use super::{IncludeIdentity, IncludeResolution};
4use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticLabel, Severity};
5use crate::model::Located;
6use crate::source::SourceSpan;
7use std::fmt;
8use std::path::{Path, PathBuf};
9
10pub const INCLUDE_PROJECT_DIRECTORY_UNRESOLVED: DiagnosticCode =
12 DiagnosticCode::new("compose.include.project-directory-unresolved");
13
14#[derive(Clone, PartialEq, Eq)]
16pub enum IncludeProjectDirectoryResolution {
17 Resolved(PathBuf),
19 Deferred,
21}
22
23impl fmt::Debug for IncludeProjectDirectoryResolution {
24 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 Self::Resolved(_) => formatter.write_str("Resolved(<authorized-directory>)"),
27 Self::Deferred => formatter.write_str("Deferred"),
28 }
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[non_exhaustive]
35pub enum IncludeProjectDirectoryResolveError {
36 Unresolved,
38}
39
40pub struct IncludeProjectDirectoryRequest<'a> {
42 edge_index: usize,
43 request_index: usize,
44 parent_node_index: usize,
45 child_node_index: usize,
46 parent_identity: &'a IncludeIdentity,
47 child_identity: &'a IncludeIdentity,
48 parent_effective_directory: Option<&'a Path>,
49 declaration: &'a Located<String>,
50 child_first_document_directory: Option<&'a Path>,
51}
52
53impl IncludeProjectDirectoryRequest<'_> {
54 #[must_use]
56 pub const fn edge_index(&self) -> usize {
57 self.edge_index
58 }
59
60 #[must_use]
62 pub const fn request_index(&self) -> usize {
63 self.request_index
64 }
65
66 #[must_use]
68 pub const fn parent_node_index(&self) -> usize {
69 self.parent_node_index
70 }
71
72 #[must_use]
74 pub const fn child_node_index(&self) -> usize {
75 self.child_node_index
76 }
77
78 #[must_use]
80 pub const fn parent_identity(&self) -> &IncludeIdentity {
81 self.parent_identity
82 }
83
84 #[must_use]
86 pub const fn child_identity(&self) -> &IncludeIdentity {
87 self.child_identity
88 }
89
90 #[must_use]
92 pub const fn parent_effective_directory(&self) -> Option<&Path> {
93 self.parent_effective_directory
94 }
95
96 #[must_use]
98 pub const fn declaration(&self) -> &Located<String> {
99 self.declaration
100 }
101
102 #[must_use]
104 pub const fn declaration_span(&self) -> SourceSpan {
105 self.declaration.span()
106 }
107
108 #[must_use]
110 pub const fn child_first_document_directory(&self) -> Option<&Path> {
111 self.child_first_document_directory
112 }
113}
114
115pub trait IncludeProjectDirectoryResolver {
117 fn resolve_project_directory(
128 &self,
129 request: &IncludeProjectDirectoryRequest<'_>,
130 ) -> Result<IncludeProjectDirectoryResolution, IncludeProjectDirectoryResolveError>;
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135#[non_exhaustive]
136pub enum IncludeProjectDirectoryStatus {
137 Root,
139 Defaulted,
141 Resolved,
143 Deferred,
145 Unresolved,
147}
148
149#[derive(Clone, PartialEq, Eq)]
151pub struct IncludeProjectDirectoryEntry {
152 node_index: usize,
153 identity: IncludeIdentity,
154 status: IncludeProjectDirectoryStatus,
155 effective_directory: Option<PathBuf>,
156 edge_index: Option<usize>,
157 request_index: Option<usize>,
158 declaration: Option<Located<String>>,
159}
160
161impl fmt::Debug for IncludeProjectDirectoryEntry {
162 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163 formatter
164 .debug_struct("IncludeProjectDirectoryEntry")
165 .field("node_index", &self.node_index)
166 .field("identity", &"<redacted-identity>")
167 .field("status", &self.status)
168 .field(
169 "effective_directory",
170 &self.effective_directory.as_ref().map(|_| "<authorized-directory>"),
171 )
172 .field("edge_index", &self.edge_index)
173 .field("request_index", &self.request_index)
174 .field(
175 "declaration",
176 &self.declaration.as_ref().map(|_| "<redacted-declaration>"),
177 )
178 .finish()
179 }
180}
181
182impl IncludeProjectDirectoryEntry {
183 #[must_use]
185 pub const fn node_index(&self) -> usize {
186 self.node_index
187 }
188
189 #[must_use]
191 pub const fn identity(&self) -> &IncludeIdentity {
192 &self.identity
193 }
194
195 #[must_use]
197 pub const fn status(&self) -> IncludeProjectDirectoryStatus {
198 self.status
199 }
200
201 #[must_use]
203 pub fn effective_directory(&self) -> Option<&Path> {
204 self.effective_directory.as_deref()
205 }
206
207 #[must_use]
209 pub const fn edge_index(&self) -> Option<usize> {
210 self.edge_index
211 }
212
213 #[must_use]
215 pub const fn request_index(&self) -> Option<usize> {
216 self.request_index
217 }
218
219 #[must_use]
221 pub const fn declaration(&self) -> Option<&Located<String>> {
222 self.declaration.as_ref()
223 }
224}
225
226#[derive(Clone, PartialEq, Eq)]
228pub struct IncludeProjectDirectoryPlan {
229 entries: Vec<IncludeProjectDirectoryEntry>,
230 diagnostics: Vec<Diagnostic>,
231}
232
233impl fmt::Debug for IncludeProjectDirectoryPlan {
234 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
235 formatter
236 .debug_struct("IncludeProjectDirectoryPlan")
237 .field("entries", &self.entries)
238 .field("diagnostics", &self.diagnostics)
239 .finish()
240 }
241}
242
243impl IncludeProjectDirectoryPlan {
244 #[must_use]
246 pub fn entries(&self) -> &[IncludeProjectDirectoryEntry] {
247 &self.entries
248 }
249
250 #[must_use]
252 pub fn entry(&self, node_index: usize) -> Option<&IncludeProjectDirectoryEntry> {
253 self.entries.get(node_index)
254 }
255
256 #[must_use]
258 pub fn diagnostics(&self) -> &[Diagnostic] {
259 &self.diagnostics
260 }
261
262 #[must_use]
264 pub fn is_valid(&self) -> bool {
265 self.diagnostics
266 .iter()
267 .all(|diagnostic| diagnostic.severity() != Severity::Error)
268 }
269
270 #[must_use]
272 pub fn is_complete(&self) -> bool {
273 self.is_valid() && self.entries.iter().all(|entry| entry.effective_directory.is_some())
274 }
275}
276
277pub(super) fn plan_project_directories(
278 resolution: &IncludeResolution,
279 resolver: &dyn IncludeProjectDirectoryResolver,
280) -> IncludeProjectDirectoryPlan {
281 let mut entries = vec![None; resolution.nodes.len()];
282 let mut diagnostics = resolution.diagnostics.clone();
283 if !resolution.nodes.is_empty() {
284 let root = &resolution.nodes[0];
285 entries[0] = Some(IncludeProjectDirectoryEntry {
286 node_index: 0,
287 identity: root.identity().clone(),
288 status: IncludeProjectDirectoryStatus::Root,
289 effective_directory: first_document_directory(root),
290 edge_index: None,
291 request_index: None,
292 declaration: None,
293 });
294 plan_children(0, resolution, resolver, &mut entries, &mut diagnostics);
295 }
296 IncludeProjectDirectoryPlan {
297 entries: entries.into_iter().flatten().collect(),
298 diagnostics,
299 }
300}
301
302fn plan_children(
303 parent_node_index: usize,
304 resolution: &IncludeResolution,
305 resolver: &dyn IncludeProjectDirectoryResolver,
306 entries: &mut [Option<IncludeProjectDirectoryEntry>],
307 diagnostics: &mut Vec<Diagnostic>,
308) {
309 let parent_effective_directory = entries[parent_node_index]
310 .as_ref()
311 .and_then(IncludeProjectDirectoryEntry::effective_directory)
312 .map(Path::to_path_buf);
313 for (edge_index, edge) in resolution.edges.iter().enumerate() {
314 if edge.parent_node_index != parent_node_index || edge.cycle {
315 continue;
316 }
317 let child_node_index = edge.child_node_index;
318 let request = &resolution.requests[edge.request_index];
319 let declaration = request.project_directory();
320 let child = &resolution.nodes[child_node_index];
321 let child_first_document_directory = first_document_directory(child);
322 let entry = match declaration {
323 None => IncludeProjectDirectoryEntry {
324 node_index: child_node_index,
325 identity: child.identity().clone(),
326 status: IncludeProjectDirectoryStatus::Defaulted,
327 effective_directory: child_first_document_directory,
328 edge_index: Some(edge_index),
329 request_index: Some(edge.request_index),
330 declaration: None,
331 },
332 Some(declaration) => {
333 let context = IncludeProjectDirectoryRequest {
334 edge_index,
335 request_index: edge.request_index,
336 parent_node_index,
337 child_node_index,
338 parent_identity: &edge.parent,
339 child_identity: &edge.child,
340 parent_effective_directory: parent_effective_directory.as_deref(),
341 declaration,
342 child_first_document_directory: child_first_document_directory.as_deref(),
343 };
344 match resolver.resolve_project_directory(&context) {
345 Ok(IncludeProjectDirectoryResolution::Resolved(directory)) => IncludeProjectDirectoryEntry {
346 node_index: child_node_index,
347 identity: child.identity().clone(),
348 status: IncludeProjectDirectoryStatus::Resolved,
349 effective_directory: Some(directory),
350 edge_index: Some(edge_index),
351 request_index: Some(edge.request_index),
352 declaration: Some(declaration.clone()),
353 },
354 Ok(IncludeProjectDirectoryResolution::Deferred) => IncludeProjectDirectoryEntry {
355 node_index: child_node_index,
356 identity: child.identity().clone(),
357 status: IncludeProjectDirectoryStatus::Deferred,
358 effective_directory: None,
359 edge_index: Some(edge_index),
360 request_index: Some(edge.request_index),
361 declaration: Some(declaration.clone()),
362 },
363 Err(IncludeProjectDirectoryResolveError::Unresolved) => {
364 diagnostics.push(project_directory_unresolved_diagnostic(declaration.span()));
365 IncludeProjectDirectoryEntry {
366 node_index: child_node_index,
367 identity: child.identity().clone(),
368 status: IncludeProjectDirectoryStatus::Unresolved,
369 effective_directory: None,
370 edge_index: Some(edge_index),
371 request_index: Some(edge.request_index),
372 declaration: Some(declaration.clone()),
373 }
374 }
375 }
376 }
377 };
378 entries[child_node_index] = Some(entry);
379 plan_children(child_node_index, resolution, resolver, entries, diagnostics);
380 }
381}
382
383fn first_document_directory(node: &super::IncludeNode) -> Option<PathBuf> {
384 node.inputs()
385 .documents()
386 .first()
387 .map(|document| document.origin().directory().to_path_buf())
388}
389
390fn project_directory_unresolved_diagnostic(span: SourceSpan) -> Diagnostic {
391 Diagnostic::new(
392 INCLUDE_PROJECT_DIRECTORY_UNRESOLVED,
393 Severity::Error,
394 "included project directory could not be resolved",
395 )
396 .with_label(DiagnosticLabel::primary(span, "project directory declaration"))
397}