1use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticLabel, Severity};
4use crate::source::{LineColumn, SourceId, SourceSpan, line_column};
5use std::error::Error;
6use std::fmt;
7use std::sync::Arc;
8use yaml_edit::{
9 AnchorRegistry, AsYaml, Mapping, MappingMergedExt, Parse, ParseErrorKind, Scalar, ScalarType, ScalarValue,
10 YamlFile, YamlNode,
11};
12
13pub const YAML_SYNTAX_ERROR: DiagnosticCode = DiagnosticCode::new("compose.yaml.syntax");
15
16pub const YAML_UNCLOSED_FLOW_SEQUENCE: DiagnosticCode = DiagnosticCode::new("compose.yaml.unclosed-flow-sequence");
18
19pub const YAML_UNCLOSED_FLOW_MAPPING: DiagnosticCode = DiagnosticCode::new("compose.yaml.unclosed-flow-mapping");
21
22pub const YAML_UNTERMINATED_STRING: DiagnosticCode = DiagnosticCode::new("compose.yaml.unterminated-string");
24
25#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct SyntaxDocument {
32 source_id: SourceId,
33 source: Arc<str>,
34 parse: Parse<YamlFile>,
35}
36
37impl SyntaxDocument {
38 pub fn parse(source_id: SourceId, source: impl Into<Arc<str>>) -> Result<SyntaxParse, SyntaxParseError> {
49 let source = source.into();
50 if u32::try_from(source.len()).is_err() {
51 return Err(SyntaxParseError {
52 source_id,
53 byte_len: source.len(),
54 });
55 }
56
57 let parse = YamlFile::parse(&source);
58 let diagnostics = parse
59 .positioned_errors()
60 .iter()
61 .map(|error| syntax_diagnostic(source_id, source.len(), error))
62 .collect();
63
64 Ok(SyntaxParse {
65 document: Self {
66 source_id,
67 source,
68 parse,
69 },
70 diagnostics,
71 })
72 }
73
74 #[must_use]
76 pub const fn source_id(&self) -> SourceId {
77 self.source_id
78 }
79
80 #[must_use]
82 pub fn source_text(&self) -> &str {
83 &self.source
84 }
85
86 #[must_use]
88 pub fn source_span(&self) -> SourceSpan {
89 SourceSpan::from_valid_offsets(self.source_id, 0, self.source.len())
90 }
91
92 #[must_use]
94 pub fn text(&self, span: SourceSpan) -> Option<&str> {
95 if span.source_id() != self.source_id || span.end() > self.source.len() {
96 return None;
97 }
98
99 self.source.get(span.range())
100 }
101
102 #[must_use]
104 pub fn line_column(&self, byte_offset: usize) -> Option<LineColumn> {
105 line_column(&self.source, byte_offset)
106 }
107
108 #[must_use]
110 pub fn document_count(&self) -> usize {
111 self.parse.tree().documents().count()
112 }
113
114 #[must_use]
116 pub fn comment_count(&self) -> usize {
117 self.parse.tree().comments().count()
118 }
119
120 #[must_use]
122 pub fn render_preserved(&self) -> String {
123 self.parse.tree().to_string()
124 }
125
126 pub(crate) fn yaml_file(&self) -> YamlFile {
127 self.parse.tree()
128 }
129
130 pub(crate) fn interpolatable_value_scalars(&self) -> Vec<ValueScalar> {
131 let mut values = Vec::new();
132 if let Some(document) = self.parse.tree().document() {
133 if let Some(mapping) = document.as_mapping() {
134 collect_value_scalars(self.source_id, YamlNode::Mapping(mapping), &mut values);
135 } else if let Some(sequence) = document.as_sequence() {
136 collect_value_scalars(self.source_id, YamlNode::Sequence(sequence), &mut values);
137 } else if let Some(scalar) = document.as_scalar() {
138 collect_value_scalars(self.source_id, YamlNode::Scalar(scalar), &mut values);
139 }
140 }
141 values
142 }
143
144 pub(crate) fn editable_value_scalars(&self) -> Vec<EditableValueScalar> {
145 let mut values = Vec::new();
146 if let Some(document) = self.parse.tree().document() {
147 if let Some(mapping) = document.as_mapping() {
148 collect_editable_value_scalars(self.source_id, YamlNode::Mapping(mapping), &mut values);
149 } else if let Some(sequence) = document.as_sequence() {
150 collect_editable_value_scalars(self.source_id, YamlNode::Sequence(sequence), &mut values);
151 } else if let Some(scalar) = document.as_scalar() {
152 collect_editable_value_scalars(self.source_id, YamlNode::Scalar(scalar), &mut values);
153 }
154 }
155 values
156 }
157
158 pub(crate) fn merge_root(&self) -> Option<MergeSyntaxValue> {
159 let document = self.parse.tree().document()?;
160 let root = if let Some(mapping) = document.as_mapping() {
161 YamlNode::Mapping(mapping)
162 } else if let Some(sequence) = document.as_sequence() {
163 YamlNode::Sequence(sequence)
164 } else {
165 YamlNode::Scalar(document.as_scalar()?)
166 };
167 let registry = AnchorRegistry::from_document(&document);
168 Some(extract_merge_value(
169 self.source_id,
170 &self.source,
171 root,
172 ®istry,
173 &mut Vec::new(),
174 ))
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub(crate) struct ValueScalar {
180 pub(crate) value: String,
181 pub(crate) span: SourceSpan,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub(crate) struct EditableValueScalar {
186 pub(crate) raw: String,
187 pub(crate) span: SourceSpan,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub(crate) enum MergeScalarKind {
192 String,
193 Boolean,
194 Number,
195 Null,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub(crate) struct MergeSyntaxScalar {
200 pub(crate) raw: String,
201 pub(crate) value: String,
202 pub(crate) kind: MergeScalarKind,
203 pub(crate) span: SourceSpan,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub(crate) struct MergeSyntaxEntry {
208 pub(crate) key: MergeSyntaxScalar,
209 pub(crate) value: MergeSyntaxValue,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub(crate) enum MergeSyntaxValue {
214 Empty(SourceSpan),
215 Scalar(MergeSyntaxScalar),
216 Mapping {
217 entries: Vec<MergeSyntaxEntry>,
218 span: SourceSpan,
219 },
220 Sequence {
221 values: Vec<MergeSyntaxValue>,
222 span: SourceSpan,
223 },
224 Alias {
225 name: String,
226 span: SourceSpan,
227 },
228 Tagged {
229 tag: String,
230 value: Box<MergeSyntaxValue>,
231 span: SourceSpan,
232 },
233}
234
235#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct SyntaxParse {
238 document: SyntaxDocument,
239 diagnostics: Vec<Diagnostic>,
240}
241
242impl SyntaxParse {
243 #[must_use]
245 pub const fn document(&self) -> &SyntaxDocument {
246 &self.document
247 }
248
249 #[must_use]
251 pub fn diagnostics(&self) -> &[Diagnostic] {
252 &self.diagnostics
253 }
254
255 #[must_use]
257 pub fn is_valid(&self) -> bool {
258 !self
259 .diagnostics
260 .iter()
261 .any(|diagnostic| diagnostic.severity() == Severity::Error)
262 }
263
264 #[must_use]
266 pub fn into_parts(self) -> (SyntaxDocument, Vec<Diagnostic>) {
267 (self.document, self.diagnostics)
268 }
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub struct SyntaxParseError {
274 source_id: SourceId,
275 byte_len: usize,
276}
277
278impl SyntaxParseError {
279 #[must_use]
281 pub const fn source_id(self) -> SourceId {
282 self.source_id
283 }
284
285 #[must_use]
287 pub const fn byte_len(self) -> usize {
288 self.byte_len
289 }
290}
291
292impl fmt::Display for SyntaxParseError {
293 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
294 write!(
295 formatter,
296 "{} contains {} bytes, exceeding the YAML syntax tree limit",
297 self.source_id, self.byte_len
298 )
299 }
300}
301
302impl Error for SyntaxParseError {}
303
304fn syntax_diagnostic(source_id: SourceId, source_len: usize, error: &yaml_edit::PositionedParseError) -> Diagnostic {
305 let (code, message) = match error.kind {
306 ParseErrorKind::UnclosedFlowSequence => (YAML_UNCLOSED_FLOW_SEQUENCE, "flow sequence is missing a closing `]`"),
307 ParseErrorKind::UnclosedFlowMapping => (YAML_UNCLOSED_FLOW_MAPPING, "flow mapping is missing a closing `}`"),
308 ParseErrorKind::UnterminatedString => (YAML_UNTERMINATED_STRING, "quoted scalar is missing its closing quote"),
309 ParseErrorKind::Other => (YAML_SYNTAX_ERROR, "invalid YAML syntax"),
310 };
311 let start = (error.range.start as usize).min(source_len);
312 let end = (error.range.end as usize).clamp(start, source_len);
313 let span = SourceSpan::from_valid_offsets(source_id, start, end);
314
315 Diagnostic::new(code, Severity::Error, message).with_label(DiagnosticLabel::primary(span, "syntax error"))
316}
317
318fn collect_value_scalars(source_id: SourceId, node: YamlNode, values: &mut Vec<ValueScalar>) {
319 match node {
320 YamlNode::Scalar(scalar) => collect_scalar(source_id, &scalar, values),
321 YamlNode::Mapping(mapping) => {
322 for value in mapping.entries().filter_map(|entry| entry.value_node()) {
323 collect_value_scalars(source_id, value, values);
324 }
325 }
326 YamlNode::Sequence(sequence) => {
327 for value in sequence.values() {
328 collect_value_scalars(source_id, value, values);
329 }
330 }
331 YamlNode::TaggedNode(tagged) => {
332 if let Some(node) = tagged
333 .as_node()
334 .and_then(|node| node.children().find_map(YamlNode::from_syntax))
335 {
336 collect_value_scalars(source_id, node, values);
337 }
338 }
339 YamlNode::Alias(_) => {}
340 }
341}
342
343fn collect_editable_value_scalars(source_id: SourceId, node: YamlNode, values: &mut Vec<EditableValueScalar>) {
344 match node {
345 YamlNode::Scalar(scalar) => {
346 values.push(EditableValueScalar {
347 raw: scalar.value(),
348 span: position_span(source_id, scalar.byte_range()),
349 });
350 }
351 YamlNode::Mapping(mapping) => {
352 for value in mapping.entries().filter_map(|entry| entry.value_node()) {
353 collect_editable_value_scalars(source_id, value, values);
354 }
355 }
356 YamlNode::Sequence(sequence) => {
357 for value in sequence.values() {
358 collect_editable_value_scalars(source_id, value, values);
359 }
360 }
361 YamlNode::TaggedNode(tagged) => {
362 if let Some(node) = tagged
363 .as_node()
364 .and_then(|node| node.children().find_map(YamlNode::from_syntax))
365 {
366 collect_editable_value_scalars(source_id, node, values);
367 }
368 }
369 YamlNode::Alias(_) => {}
370 }
371}
372
373fn extract_merge_value(
374 source_id: SourceId,
375 source: &str,
376 node: YamlNode,
377 registry: &AnchorRegistry,
378 aliases: &mut Vec<String>,
379) -> MergeSyntaxValue {
380 match node {
381 YamlNode::Scalar(scalar) => MergeSyntaxValue::Scalar(extract_merge_scalar(source_id, &scalar)),
382 YamlNode::Mapping(mapping) => extract_merge_mapping(source_id, source, &mapping, registry, aliases),
383 YamlNode::Sequence(sequence) => {
384 let span = position_span(source_id, sequence.byte_range());
385 let values = sequence
386 .values()
387 .map(|value| extract_merge_value(source_id, source, value, registry, aliases))
388 .collect();
389 MergeSyntaxValue::Sequence { values, span }
390 }
391 YamlNode::Alias(alias) => {
392 let name = alias.name();
393 let span = yaml_node_span(source_id, &YamlNode::Alias(alias.clone()));
394 if aliases.contains(&name) || aliases.len() >= 64 {
395 return MergeSyntaxValue::Alias { name, span };
396 }
397 if let Some(target) = registry.resolve(&name).and_then(|node| {
398 YamlNode::from_syntax(node.clone()).or_else(|| node.children().find_map(YamlNode::from_syntax))
399 }) {
400 aliases.push(name);
401 let value = extract_merge_value(source_id, source, target, registry, aliases);
402 let _ = aliases.pop();
403 value
404 } else {
405 MergeSyntaxValue::Alias { name, span }
406 }
407 }
408 YamlNode::TaggedNode(tagged) => {
409 let span = yaml_node_span(source_id, &YamlNode::TaggedNode(tagged.clone()));
410 let value = tagged
411 .as_node()
412 .and_then(|node| node.children().find_map(YamlNode::from_syntax))
413 .map_or_else(
414 || MergeSyntaxValue::Empty(span),
415 |value| extract_merge_value(source_id, source, value, registry, aliases),
416 );
417 MergeSyntaxValue::Tagged {
418 tag: tagged.tag().unwrap_or_default(),
419 value: Box::new(value),
420 span,
421 }
422 }
423 }
424}
425
426fn extract_merge_mapping(
427 source_id: SourceId,
428 source: &str,
429 mapping: &Mapping,
430 registry: &AnchorRegistry,
431 aliases: &mut Vec<String>,
432) -> MergeSyntaxValue {
433 let span = position_span(source_id, mapping.byte_range());
434 let direct = flatten_merge_fields(source_id, source, raw_merge_fields(source_id, mapping));
435 let mut entries = Vec::new();
436 let mut direct_keys = Vec::new();
437
438 for field in direct {
439 if field.key.value == "<<" {
440 continue;
441 }
442 direct_keys.push(field.key.value.clone());
443 let value = field.value.map_or_else(
444 || {
445 MergeSyntaxValue::Empty(SourceSpan::from_valid_offsets(
446 source_id,
447 field.key.span.end(),
448 field.key.span.end(),
449 ))
450 },
451 |value| extract_merge_value(source_id, source, resolve_alias(value, registry), registry, aliases),
452 );
453 entries.push(MergeSyntaxEntry { key: field.key, value });
454 }
455
456 for (key, value) in mapping.merged(registry).iter() {
457 let Some(key) = key.as_scalar().map(|scalar| extract_merge_scalar(source_id, scalar)) else {
458 continue;
459 };
460 if direct_keys.contains(&key.value) {
461 continue;
462 }
463 entries.push(MergeSyntaxEntry {
464 key,
465 value: extract_merge_value(source_id, source, value, registry, aliases),
466 });
467 }
468
469 MergeSyntaxValue::Mapping { entries, span }
470}
471
472#[derive(Debug)]
473struct RawMergeField {
474 key: MergeSyntaxScalar,
475 value: Option<YamlNode>,
476}
477
478fn raw_merge_fields(source_id: SourceId, mapping: &Mapping) -> Vec<RawMergeField> {
479 mapping
480 .entries()
481 .filter_map(|entry| {
482 let key = entry.key_node()?.as_scalar().cloned()?;
483 Some(RawMergeField {
484 key: extract_merge_scalar(source_id, &key),
485 value: entry.value_node(),
486 })
487 })
488 .collect()
489}
490
491fn flatten_merge_fields(source_id: SourceId, source: &str, fields: Vec<RawMergeField>) -> Vec<RawMergeField> {
492 let Some(target_column) = fields
493 .first()
494 .map(|field| source_column(source, field.key.span.start()))
495 else {
496 return fields;
497 };
498 recover_merge_fields(source_id, source, fields, target_column)
499}
500
501fn recover_merge_fields(
502 source_id: SourceId,
503 source: &str,
504 fields: Vec<RawMergeField>,
505 target_column: usize,
506) -> Vec<RawMergeField> {
507 let mut flattened = Vec::new();
508 for mut field in fields {
509 let field_column = source_column(source, field.key.span.start());
510 let nested_mapping = field.value.as_ref().and_then(YamlNode::as_mapping).cloned();
511 let continuation = nested_mapping.as_ref().is_some_and(|mapping| {
512 !is_flow_mapping(source, mapping)
513 && mapping
514 .entries()
515 .find_map(|entry| entry.key_node()?.as_scalar().map(Scalar::byte_range))
516 .is_some_and(|position| source_column(source, position.start as usize) <= field_column)
517 });
518 if continuation {
519 field.value = None;
520 }
521 if field_column == target_column {
522 flattened.push(field);
523 }
524 if let Some(mapping) = nested_mapping.filter(|mapping| !is_flow_mapping(source, mapping)) {
525 let nested = raw_merge_fields(source_id, &mapping);
526 flattened.extend(recover_merge_fields(source_id, source, nested, target_column));
527 }
528 }
529 flattened
530}
531
532fn is_flow_mapping(source: &str, mapping: &Mapping) -> bool {
533 let position = mapping.byte_range();
534 source
535 .get(position.start as usize..position.end as usize)
536 .is_some_and(|text| text.trim_start().starts_with('{'))
537}
538
539fn source_column(source: &str, offset: usize) -> usize {
540 let prefix = &source[..offset.min(source.len())];
541 let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
542 source[line_start..offset.min(source.len())].chars().count()
543}
544
545fn resolve_alias(node: YamlNode, registry: &AnchorRegistry) -> YamlNode {
546 let YamlNode::Alias(alias) = &node else {
547 return node;
548 };
549 registry
550 .resolve(&alias.name())
551 .and_then(|target| {
552 YamlNode::from_syntax(target.clone()).or_else(|| target.children().find_map(YamlNode::from_syntax))
553 })
554 .unwrap_or(node)
555}
556
557fn extract_merge_scalar(source_id: SourceId, scalar: &Scalar) -> MergeSyntaxScalar {
558 let kind = match ScalarValue::from_scalar(scalar).scalar_type() {
559 ScalarType::Boolean => MergeScalarKind::Boolean,
560 ScalarType::Integer | ScalarType::Float => MergeScalarKind::Number,
561 ScalarType::Null => MergeScalarKind::Null,
562 ScalarType::String | ScalarType::Timestamp | ScalarType::Regex => MergeScalarKind::String,
563 };
564 MergeSyntaxScalar {
565 raw: scalar.value(),
566 value: scalar.as_string(),
567 kind,
568 span: position_span(source_id, scalar.byte_range()),
569 }
570}
571
572fn yaml_node_span(source_id: SourceId, node: &YamlNode) -> SourceSpan {
573 let Some(syntax) = node.as_node() else {
574 return SourceSpan::from_valid_offsets(source_id, 0, 0);
575 };
576 let range = syntax.text_range();
577 SourceSpan::from_valid_offsets(
578 source_id,
579 u32::from(range.start()) as usize,
580 u32::from(range.end()) as usize,
581 )
582}
583
584fn position_span(source_id: SourceId, position: yaml_edit::TextPosition) -> SourceSpan {
585 SourceSpan::from_valid_offsets(source_id, position.start as usize, position.end as usize)
586}
587
588fn collect_scalar(source_id: SourceId, scalar: &Scalar, values: &mut Vec<ValueScalar>) {
589 let raw = scalar.value();
590 let eligible_style = !raw.starts_with('\'') && !raw.starts_with('|') && !raw.starts_with('>');
591 if !eligible_style || !raw.contains('$') {
592 return;
593 }
594 let position = scalar.byte_range();
595 values.push(ValueScalar {
596 value: scalar.as_string(),
597 span: SourceSpan::from_valid_offsets(source_id, position.start as usize, position.end as usize),
598 });
599}
600
601#[cfg(test)]
602mod tests {
603 use super::SyntaxDocument;
604 use crate::source::SourceId;
605
606 fn assert_send_and_sync<T: Send + Sync>() {}
607
608 #[test]
609 fn syntax_documents_are_send_and_sync() {
610 assert_send_and_sync::<SyntaxDocument>();
611 }
612
613 #[test]
614 fn parsing_never_reads_the_process_environment() -> Result<(), Box<dyn std::error::Error>> {
615 let source = "services:\n app:\n image: ${COMPOSE_LENS_SECRET}\n";
616 let parsed = SyntaxDocument::parse(SourceId::new(1), source)?;
617
618 assert_eq!(parsed.document().render_preserved(), source);
619 assert!(parsed.is_valid());
620 Ok(())
621 }
622}