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_UNPARSED_INPUT: DiagnosticCode = DiagnosticCode::new("compose.yaml.unparsed-input");
18
19pub const YAML_UNCLOSED_FLOW_SEQUENCE: DiagnosticCode = DiagnosticCode::new("compose.yaml.unclosed-flow-sequence");
21
22pub const YAML_UNCLOSED_FLOW_MAPPING: DiagnosticCode = DiagnosticCode::new("compose.yaml.unclosed-flow-mapping");
24
25pub const YAML_UNTERMINATED_STRING: DiagnosticCode = DiagnosticCode::new("compose.yaml.unterminated-string");
27
28#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct SyntaxDocument {
35 source_id: SourceId,
36 source: Arc<str>,
37 parse: Parse<YamlFile>,
38}
39
40impl SyntaxDocument {
41 pub fn parse(source_id: SourceId, source: impl Into<Arc<str>>) -> Result<SyntaxParse, SyntaxParseError> {
52 let source = source.into();
53 if u32::try_from(source.len()).is_err() {
54 return Err(SyntaxParseError {
55 source_id,
56 byte_len: source.len(),
57 });
58 }
59
60 let parser_source = parser_compatible_source(&source);
61 let parse = YamlFile::parse(parser_source.as_deref().unwrap_or(&source));
62 let mut diagnostics: Vec<_> = parse
63 .positioned_errors()
64 .iter()
65 .map(|error| syntax_diagnostic(source_id, source.len(), error))
66 .collect();
67 if diagnostics.is_empty() {
68 if let Some(document_end) = unparsed_input_offset(&parse, &source) {
69 diagnostics.push(unparsed_input_diagnostic(source_id, source.len(), document_end));
70 }
71 }
72
73 Ok(SyntaxParse {
74 document: Self {
75 source_id,
76 source,
77 parse,
78 },
79 diagnostics,
80 })
81 }
82
83 #[must_use]
85 pub const fn source_id(&self) -> SourceId {
86 self.source_id
87 }
88
89 #[must_use]
91 pub fn source_text(&self) -> &str {
92 &self.source
93 }
94
95 #[must_use]
97 pub fn source_span(&self) -> SourceSpan {
98 SourceSpan::from_valid_offsets(self.source_id, 0, self.source.len())
99 }
100
101 #[must_use]
103 pub fn text(&self, span: SourceSpan) -> Option<&str> {
104 if span.source_id() != self.source_id || span.end() > self.source.len() {
105 return None;
106 }
107
108 self.source.get(span.range())
109 }
110
111 #[must_use]
113 pub fn line_column(&self, byte_offset: usize) -> Option<LineColumn> {
114 line_column(&self.source, byte_offset)
115 }
116
117 #[must_use]
119 pub fn document_count(&self) -> usize {
120 self.parse.tree().documents().count()
121 }
122
123 #[must_use]
125 pub fn comment_count(&self) -> usize {
126 self.parse.tree().comments().count()
127 }
128
129 #[must_use]
131 pub fn render_preserved(&self) -> String {
132 self.source.to_string()
133 }
134
135 pub(crate) fn yaml_file(&self) -> YamlFile {
136 self.parse.tree()
137 }
138
139 pub(crate) fn interpolatable_value_scalars(&self) -> Vec<ValueScalar> {
140 let mut values = Vec::new();
141 if let Some(document) = self.parse.tree().document() {
142 if let Some(mapping) = document.as_mapping() {
143 collect_value_scalars(self.source_id, &self.source, YamlNode::Mapping(mapping), &mut values);
144 } else if let Some(sequence) = document.as_sequence() {
145 collect_value_scalars(self.source_id, &self.source, YamlNode::Sequence(sequence), &mut values);
146 } else if let Some(scalar) = document.as_scalar() {
147 collect_value_scalars(self.source_id, &self.source, YamlNode::Scalar(scalar), &mut values);
148 }
149 }
150 values
151 }
152
153 pub(crate) fn editable_value_scalars(&self) -> Vec<EditableValueScalar> {
154 let mut values = Vec::new();
155 if let Some(document) = self.parse.tree().document() {
156 if let Some(mapping) = document.as_mapping() {
157 collect_editable_value_scalars(self.source_id, &self.source, YamlNode::Mapping(mapping), &mut values);
158 } else if let Some(sequence) = document.as_sequence() {
159 collect_editable_value_scalars(self.source_id, &self.source, YamlNode::Sequence(sequence), &mut values);
160 } else if let Some(scalar) = document.as_scalar() {
161 collect_editable_value_scalars(self.source_id, &self.source, YamlNode::Scalar(scalar), &mut values);
162 }
163 }
164 values
165 }
166
167 pub(crate) fn merge_root(&self) -> Option<MergeSyntaxValue> {
168 let document = self.parse.tree().document()?;
169 let root = if let Some(mapping) = document.as_mapping() {
170 YamlNode::Mapping(mapping)
171 } else if let Some(sequence) = document.as_sequence() {
172 YamlNode::Sequence(sequence)
173 } else {
174 YamlNode::Scalar(document.as_scalar()?)
175 };
176 let registry = AnchorRegistry::from_document(&document);
177 Some(extract_merge_value(
178 self.source_id,
179 &self.source,
180 root,
181 ®istry,
182 &mut Vec::new(),
183 ))
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub(crate) struct ValueScalar {
189 pub(crate) value: String,
190 pub(crate) span: SourceSpan,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub(crate) struct EditableValueScalar {
195 pub(crate) raw: String,
196 pub(crate) span: SourceSpan,
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub(crate) enum MergeScalarKind {
201 String,
202 Boolean,
203 Number,
204 Null,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub(crate) struct MergeSyntaxScalar {
209 pub(crate) raw: String,
210 pub(crate) value: String,
211 pub(crate) kind: MergeScalarKind,
212 pub(crate) span: SourceSpan,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub(crate) struct MergeSyntaxEntry {
217 pub(crate) key: MergeSyntaxScalar,
218 pub(crate) value: MergeSyntaxValue,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub(crate) enum MergeSyntaxValue {
223 Empty(SourceSpan),
224 Scalar(MergeSyntaxScalar),
225 Mapping {
226 entries: Vec<MergeSyntaxEntry>,
227 span: SourceSpan,
228 },
229 Sequence {
230 values: Vec<MergeSyntaxValue>,
231 span: SourceSpan,
232 },
233 Alias {
234 name: String,
235 span: SourceSpan,
236 },
237 Tagged {
238 tag: String,
239 value: Box<MergeSyntaxValue>,
240 span: SourceSpan,
241 },
242}
243
244#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct SyntaxParse {
247 document: SyntaxDocument,
248 diagnostics: Vec<Diagnostic>,
249}
250
251impl SyntaxParse {
252 #[must_use]
254 pub const fn document(&self) -> &SyntaxDocument {
255 &self.document
256 }
257
258 #[must_use]
260 pub fn diagnostics(&self) -> &[Diagnostic] {
261 &self.diagnostics
262 }
263
264 #[must_use]
266 pub fn is_valid(&self) -> bool {
267 !self
268 .diagnostics
269 .iter()
270 .any(|diagnostic| diagnostic.severity() == Severity::Error)
271 }
272
273 #[must_use]
275 pub fn into_parts(self) -> (SyntaxDocument, Vec<Diagnostic>) {
276 (self.document, self.diagnostics)
277 }
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282pub struct SyntaxParseError {
283 source_id: SourceId,
284 byte_len: usize,
285}
286
287impl SyntaxParseError {
288 #[must_use]
290 pub const fn source_id(self) -> SourceId {
291 self.source_id
292 }
293
294 #[must_use]
296 pub const fn byte_len(self) -> usize {
297 self.byte_len
298 }
299}
300
301impl fmt::Display for SyntaxParseError {
302 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
303 write!(
304 formatter,
305 "{} contains {} bytes, exceeding the YAML syntax tree limit",
306 self.source_id, self.byte_len
307 )
308 }
309}
310
311impl Error for SyntaxParseError {}
312
313fn syntax_diagnostic(source_id: SourceId, source_len: usize, error: &yaml_edit::PositionedParseError) -> Diagnostic {
314 let (code, message) = match error.kind {
315 ParseErrorKind::UnclosedFlowSequence => (YAML_UNCLOSED_FLOW_SEQUENCE, "flow sequence is missing a closing `]`"),
316 ParseErrorKind::UnclosedFlowMapping => (YAML_UNCLOSED_FLOW_MAPPING, "flow mapping is missing a closing `}`"),
317 ParseErrorKind::UnterminatedString => (YAML_UNTERMINATED_STRING, "quoted scalar is missing its closing quote"),
318 ParseErrorKind::Other => (YAML_SYNTAX_ERROR, "invalid YAML syntax"),
319 };
320 let start = (error.range.start as usize).min(source_len);
321 let end = (error.range.end as usize).clamp(start, source_len);
322 let span = SourceSpan::from_valid_offsets(source_id, start, end);
323
324 Diagnostic::new(code, Severity::Error, message).with_label(DiagnosticLabel::primary(span, "syntax error"))
325}
326
327fn unparsed_input_diagnostic(source_id: SourceId, source_len: usize, document_end: usize) -> Diagnostic {
328 let span = SourceSpan::from_valid_offsets(source_id, document_end.min(source_len), source_len);
329
330 Diagnostic::new(
331 YAML_UNPARSED_INPUT,
332 Severity::Error,
333 "YAML parser did not include the complete input in the document root",
334 )
335 .with_label(DiagnosticLabel::primary(span, "input omitted from YAML document"))
336 .with_note("the original source remains available, but typed processing must not continue silently")
337}
338
339fn unparsed_input_offset(parse: &Parse<YamlFile>, source: &str) -> Option<usize> {
340 let document_end = parse
341 .tree()
342 .document()
343 .and_then(|document| {
344 document
345 .as_node()
346 .map(|node| u32::from(node.text_range().end()) as usize)
347 })
348 .unwrap_or_default();
349 source
350 .as_bytes()
351 .get(document_end..)
352 .is_some_and(|suffix| suffix.iter().any(|byte| !byte.is_ascii_whitespace()))
353 .then_some(document_end)
354}
355
356fn parser_compatible_source(source: &str) -> Option<String> {
360 let mut compatible = source.as_bytes().to_vec();
361 let mut changed = false;
362 let mut offset = 0;
363 let mut block_scalar_indent = None;
364 let mut state = ParserCompatibilityState::default();
365
366 for line in source.split_inclusive('\n') {
367 let content = line.trim_end_matches(['\r', '\n']);
368 let indent = content.bytes().take_while(|byte| *byte == b' ').count();
369 let blank = content[indent..].trim().is_empty();
370
371 if let Some(header_indent) = block_scalar_indent {
372 if blank || indent > header_indent {
373 offset += line.len();
374 continue;
375 }
376 block_scalar_indent = None;
377 }
378
379 let (line_changed, starts_block_scalar) = mask_block_plain_commas(content, offset, &mut compatible, &mut state);
380 changed |= line_changed;
381 if starts_block_scalar {
382 block_scalar_indent = Some(indent);
383 }
384 offset += line.len();
385 }
386
387 if changed {
388 String::from_utf8(compatible).ok()
389 } else {
390 None
391 }
392}
393
394#[derive(Debug, Default)]
395struct ParserCompatibilityState {
396 flow_depth: u32,
397 quote: Option<u8>,
398 escaped: bool,
399}
400
401fn mask_block_plain_commas(
402 content: &str,
403 offset: usize,
404 compatible: &mut [u8],
405 state: &mut ParserCompatibilityState,
406) -> (bool, bool) {
407 let bytes = content.as_bytes();
408 let mut index = 0;
409 let mut first_token = true;
410 let mut plain_started = false;
411 let mut eligible_plain_value = false;
412 let mut changed = false;
413
414 while index < bytes.len() {
415 let byte = bytes[index];
416
417 if let Some(delimiter) = state.quote {
418 if delimiter == b'"' && state.escaped {
419 state.escaped = false;
420 } else if delimiter == b'"' && byte == b'\\' {
421 state.escaped = true;
422 } else if byte == delimiter {
423 if delimiter == b'\'' && bytes.get(index + 1) == Some(&b'\'') {
424 index += 1;
425 } else {
426 state.quote = None;
427 }
428 }
429 index += 1;
430 continue;
431 }
432
433 if byte == b'#' && (index == 0 || bytes[index - 1].is_ascii_whitespace()) {
434 break;
435 }
436
437 if eligible_plain_value && !plain_started && matches!(byte, b'!' | b'&') {
438 index += 1;
439 while bytes.get(index).is_some_and(|byte| !byte.is_ascii_whitespace()) {
440 index += 1;
441 }
442 first_token = false;
443 continue;
444 }
445
446 if matches!(byte, b'\'' | b'"') && !plain_started {
447 state.quote = Some(byte);
448 first_token = false;
449 index += 1;
450 continue;
451 }
452
453 if state.flow_depth > 0 {
454 match byte {
455 b'[' | b'{' => state.flow_depth += 1,
456 b']' | b'}' => state.flow_depth -= 1,
457 _ => {}
458 }
459 index += 1;
460 continue;
461 }
462
463 let token_boundary = index == 0 || bytes[index - 1].is_ascii_whitespace();
464 if matches!(byte, b'[' | b'{') && (!plain_started || (eligible_plain_value && token_boundary)) {
465 state.flow_depth = 1;
466 first_token = false;
467 index += 1;
468 continue;
469 }
470
471 if byte.is_ascii_whitespace() {
472 index += 1;
473 continue;
474 }
475
476 if first_token && byte == b'-' && bytes.get(index + 1).is_none_or(u8::is_ascii_whitespace) {
477 eligible_plain_value = true;
478 plain_started = false;
479 first_token = false;
480 index += 1;
481 continue;
482 }
483
484 if byte == b':' && bytes.get(index + 1).is_none_or(u8::is_ascii_whitespace) {
485 eligible_plain_value = true;
486 plain_started = false;
487 first_token = false;
488 index += 1;
489 continue;
490 }
491
492 if eligible_plain_value && !plain_started && matches!(byte, b'|' | b'>') {
493 return (changed, true);
494 }
495
496 if eligible_plain_value && plain_started && byte == b',' {
497 compatible[offset + index] = b'_';
498 changed = true;
499 }
500
501 plain_started = true;
502 first_token = false;
503 index += 1;
504 }
505
506 state.escaped = false;
507 (changed, false)
508}
509
510pub(crate) fn scalar_raw_from_source(source: &str, scalar: &Scalar) -> String {
511 let range = scalar.byte_range();
512 source
513 .get(range.start as usize..range.end as usize)
514 .map_or_else(|| scalar.value(), str::to_owned)
515}
516
517pub(crate) fn scalar_string_from_source(source: &str, scalar: &Scalar) -> String {
518 let authored = scalar_raw_from_source(source, scalar);
519 if authored == scalar.value() {
520 scalar.as_string()
521 } else {
522 authored
524 }
525}
526
527fn collect_value_scalars(source_id: SourceId, source: &str, node: YamlNode, values: &mut Vec<ValueScalar>) {
528 match node {
529 YamlNode::Scalar(scalar) => collect_scalar(source_id, source, &scalar, values),
530 YamlNode::Mapping(mapping) => {
531 for value in mapping.entries().filter_map(|entry| entry.value_node()) {
532 collect_value_scalars(source_id, source, value, values);
533 }
534 }
535 YamlNode::Sequence(sequence) => {
536 for value in sequence.values() {
537 collect_value_scalars(source_id, source, value, values);
538 }
539 }
540 YamlNode::TaggedNode(tagged) => {
541 if let Some(node) = tagged
542 .as_node()
543 .and_then(|node| node.children().find_map(YamlNode::from_syntax))
544 {
545 collect_value_scalars(source_id, source, node, values);
546 }
547 }
548 YamlNode::Alias(_) => {}
549 }
550}
551
552fn collect_editable_value_scalars(
553 source_id: SourceId,
554 source: &str,
555 node: YamlNode,
556 values: &mut Vec<EditableValueScalar>,
557) {
558 match node {
559 YamlNode::Scalar(scalar) => {
560 values.push(EditableValueScalar {
561 raw: scalar_raw_from_source(source, &scalar),
562 span: position_span(source_id, scalar.byte_range()),
563 });
564 }
565 YamlNode::Mapping(mapping) => {
566 for value in mapping.entries().filter_map(|entry| entry.value_node()) {
567 collect_editable_value_scalars(source_id, source, value, values);
568 }
569 }
570 YamlNode::Sequence(sequence) => {
571 for value in sequence.values() {
572 collect_editable_value_scalars(source_id, source, value, values);
573 }
574 }
575 YamlNode::TaggedNode(tagged) => {
576 if let Some(node) = tagged
577 .as_node()
578 .and_then(|node| node.children().find_map(YamlNode::from_syntax))
579 {
580 collect_editable_value_scalars(source_id, source, node, values);
581 }
582 }
583 YamlNode::Alias(_) => {}
584 }
585}
586
587fn extract_merge_value(
588 source_id: SourceId,
589 source: &str,
590 node: YamlNode,
591 registry: &AnchorRegistry,
592 aliases: &mut Vec<String>,
593) -> MergeSyntaxValue {
594 match node {
595 YamlNode::Scalar(scalar) => MergeSyntaxValue::Scalar(extract_merge_scalar(source_id, source, &scalar)),
596 YamlNode::Mapping(mapping) => extract_merge_mapping(source_id, source, &mapping, registry, aliases),
597 YamlNode::Sequence(sequence) => {
598 let span = position_span(source_id, sequence.byte_range());
599 let values = sequence
600 .values()
601 .map(|value| extract_merge_value(source_id, source, value, registry, aliases))
602 .collect();
603 MergeSyntaxValue::Sequence { values, span }
604 }
605 YamlNode::Alias(alias) => {
606 let name = alias.name();
607 let span = yaml_node_span(source_id, &YamlNode::Alias(alias.clone()));
608 if aliases.contains(&name) || aliases.len() >= 64 {
609 return MergeSyntaxValue::Alias { name, span };
610 }
611 if let Some(target) = registry.resolve(&name).and_then(|node| {
612 YamlNode::from_syntax(node.clone()).or_else(|| node.children().find_map(YamlNode::from_syntax))
613 }) {
614 aliases.push(name);
615 let value = extract_merge_value(source_id, source, target, registry, aliases);
616 let _ = aliases.pop();
617 value
618 } else {
619 MergeSyntaxValue::Alias { name, span }
620 }
621 }
622 YamlNode::TaggedNode(tagged) => {
623 let span = yaml_node_span(source_id, &YamlNode::TaggedNode(tagged.clone()));
624 let value = tagged
625 .as_node()
626 .and_then(|node| node.children().find_map(YamlNode::from_syntax))
627 .map_or_else(
628 || MergeSyntaxValue::Empty(span),
629 |value| extract_merge_value(source_id, source, value, registry, aliases),
630 );
631 MergeSyntaxValue::Tagged {
632 tag: tagged.tag().unwrap_or_default(),
633 value: Box::new(value),
634 span,
635 }
636 }
637 }
638}
639
640fn extract_merge_mapping(
641 source_id: SourceId,
642 source: &str,
643 mapping: &Mapping,
644 registry: &AnchorRegistry,
645 aliases: &mut Vec<String>,
646) -> MergeSyntaxValue {
647 let span = position_span(source_id, mapping.byte_range());
648 let direct = flatten_merge_fields(source_id, source, raw_merge_fields(source_id, source, mapping));
649 let mut entries = Vec::new();
650 let mut direct_keys = Vec::new();
651
652 for field in direct {
653 if field.key.value == "<<" {
654 continue;
655 }
656 direct_keys.push(field.key.value.clone());
657 let value = field.value.map_or_else(
658 || {
659 MergeSyntaxValue::Empty(SourceSpan::from_valid_offsets(
660 source_id,
661 field.key.span.end(),
662 field.key.span.end(),
663 ))
664 },
665 |value| extract_merge_value(source_id, source, resolve_alias(value, registry), registry, aliases),
666 );
667 entries.push(MergeSyntaxEntry { key: field.key, value });
668 }
669
670 for (key, value) in mapping.merged(registry).iter() {
671 let Some(key) = key
672 .as_scalar()
673 .map(|scalar| extract_merge_scalar(source_id, source, scalar))
674 else {
675 continue;
676 };
677 if direct_keys.contains(&key.value) {
678 continue;
679 }
680 entries.push(MergeSyntaxEntry {
681 key,
682 value: extract_merge_value(source_id, source, value, registry, aliases),
683 });
684 }
685
686 MergeSyntaxValue::Mapping { entries, span }
687}
688
689#[derive(Debug)]
690struct RawMergeField {
691 key: MergeSyntaxScalar,
692 value: Option<YamlNode>,
693}
694
695fn raw_merge_fields(source_id: SourceId, source: &str, mapping: &Mapping) -> Vec<RawMergeField> {
696 mapping
697 .entries()
698 .filter_map(|entry| {
699 let key = entry.key_node()?.as_scalar().cloned()?;
700 Some(RawMergeField {
701 key: extract_merge_scalar(source_id, source, &key),
702 value: entry.value_node(),
703 })
704 })
705 .collect()
706}
707
708fn flatten_merge_fields(source_id: SourceId, source: &str, fields: Vec<RawMergeField>) -> Vec<RawMergeField> {
709 let Some(target_column) = fields
710 .first()
711 .map(|field| source_column(source, field.key.span.start()))
712 else {
713 return fields;
714 };
715 recover_merge_fields(source_id, source, fields, target_column)
716}
717
718fn recover_merge_fields(
719 source_id: SourceId,
720 source: &str,
721 fields: Vec<RawMergeField>,
722 target_column: usize,
723) -> Vec<RawMergeField> {
724 let mut flattened = Vec::new();
725 for mut field in fields {
726 let field_column = source_column(source, field.key.span.start());
727 let nested_mapping = field.value.as_ref().and_then(YamlNode::as_mapping).cloned();
728 let continuation = nested_mapping.as_ref().is_some_and(|mapping| {
729 !is_flow_mapping(source, mapping)
730 && mapping
731 .entries()
732 .find_map(|entry| entry.key_node()?.as_scalar().map(Scalar::byte_range))
733 .is_some_and(|position| source_column(source, position.start as usize) <= field_column)
734 });
735 if continuation {
736 field.value = None;
737 }
738 if field_column == target_column {
739 flattened.push(field);
740 }
741 if let Some(mapping) = nested_mapping.filter(|mapping| !is_flow_mapping(source, mapping)) {
742 let nested = raw_merge_fields(source_id, source, &mapping);
743 flattened.extend(recover_merge_fields(source_id, source, nested, target_column));
744 }
745 }
746 flattened
747}
748
749fn is_flow_mapping(source: &str, mapping: &Mapping) -> bool {
750 let position = mapping.byte_range();
751 source
752 .get(position.start as usize..position.end as usize)
753 .is_some_and(|text| text.trim_start().starts_with('{'))
754}
755
756fn source_column(source: &str, offset: usize) -> usize {
757 let prefix = &source[..offset.min(source.len())];
758 let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
759 source[line_start..offset.min(source.len())].chars().count()
760}
761
762fn resolve_alias(node: YamlNode, registry: &AnchorRegistry) -> YamlNode {
763 let YamlNode::Alias(alias) = &node else {
764 return node;
765 };
766 registry
767 .resolve(&alias.name())
768 .and_then(|target| {
769 YamlNode::from_syntax(target.clone()).or_else(|| target.children().find_map(YamlNode::from_syntax))
770 })
771 .unwrap_or(node)
772}
773
774fn extract_merge_scalar(source_id: SourceId, source: &str, scalar: &Scalar) -> MergeSyntaxScalar {
775 let kind = match ScalarValue::from_scalar(scalar).scalar_type() {
776 ScalarType::Boolean => MergeScalarKind::Boolean,
777 ScalarType::Integer | ScalarType::Float => MergeScalarKind::Number,
778 ScalarType::Null => MergeScalarKind::Null,
779 ScalarType::String | ScalarType::Timestamp | ScalarType::Regex => MergeScalarKind::String,
780 };
781 MergeSyntaxScalar {
782 raw: scalar_raw_from_source(source, scalar),
783 value: scalar_string_from_source(source, scalar),
784 kind,
785 span: position_span(source_id, scalar.byte_range()),
786 }
787}
788
789fn yaml_node_span(source_id: SourceId, node: &YamlNode) -> SourceSpan {
790 let Some(syntax) = node.as_node() else {
791 return SourceSpan::from_valid_offsets(source_id, 0, 0);
792 };
793 let range = syntax.text_range();
794 SourceSpan::from_valid_offsets(
795 source_id,
796 u32::from(range.start()) as usize,
797 u32::from(range.end()) as usize,
798 )
799}
800
801fn position_span(source_id: SourceId, position: yaml_edit::TextPosition) -> SourceSpan {
802 SourceSpan::from_valid_offsets(source_id, position.start as usize, position.end as usize)
803}
804
805fn collect_scalar(source_id: SourceId, source: &str, scalar: &Scalar, values: &mut Vec<ValueScalar>) {
806 let raw = scalar_raw_from_source(source, scalar);
807 let eligible_style = !raw.starts_with('\'') && !raw.starts_with('|') && !raw.starts_with('>');
808 if !eligible_style || !raw.contains('$') {
809 return;
810 }
811 let position = scalar.byte_range();
812 values.push(ValueScalar {
813 value: scalar_string_from_source(source, scalar),
814 span: SourceSpan::from_valid_offsets(source_id, position.start as usize, position.end as usize),
815 });
816}
817
818#[cfg(test)]
819mod tests {
820 use super::{SyntaxDocument, unparsed_input_offset};
821 use crate::source::SourceId;
822 use yaml_edit::YamlFile;
823
824 fn assert_send_and_sync<T: Send + Sync>() {}
825
826 #[test]
827 fn syntax_documents_are_send_and_sync() {
828 assert_send_and_sync::<SyntaxDocument>();
829 }
830
831 #[test]
832 fn parsing_never_reads_the_process_environment() -> Result<(), Box<dyn std::error::Error>> {
833 let source = "services:\n app:\n image: ${COMPOSE_LENS_SECRET}\n";
834 let parsed = SyntaxDocument::parse(SourceId::new(1), source)?;
835
836 assert_eq!(parsed.document().render_preserved(), source);
837 assert!(parsed.is_valid());
838 Ok(())
839 }
840
841 #[test]
842 fn complete_root_guard_detects_the_private_backends_raw_comma_omission() {
843 let source = "services:\n app:\n volumes:\n - ./data:/data:Z,ro\n later:\n image: later\n";
844 let backend = YamlFile::parse(source);
845
846 assert!(backend.positioned_errors().is_empty());
847 assert!(unparsed_input_offset(&backend, source).is_some());
848 }
849}