1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
42use std::rc::Rc;
43
44use helm_schema_ast::{
45 ResourceSpan, TemplateExpr, TemplateHeader, parse_expr_text,
46 range_has_destructured_variable_definition, range_header_from_source,
47};
48use helm_schema_syntax as syntax;
49use helm_schema_syntax::{
50 Node, OpaqueKind, ScalarPart, ScalarParts, Span, TemplatedDocument, parse_go_template,
51};
52
53use crate::abstract_value::AbstractValue;
54use crate::analysis_db::IrAnalysisDb;
55use crate::eval_effect::{CaptureKind, FailCapture};
56use crate::fragment_expr_eval::FragmentEvalContext;
57use crate::helper_meta::{HelperOutputMeta, merge_provenance_sites};
58use crate::node_eval::control_header;
59use crate::symbolic_local_state::SymbolicLocalState;
60use crate::value_path_context::ValuePathContext;
61use crate::{ContractProvenance, Guard, ResourceRef, SourceSpan};
62use helm_schema_core::{GuardDnf, Predicate};
63
64use super::domain::{
65 AbstractFragment, AbstractString, EntryKey, Guarded, Mapping, MappingEntry, Opaque,
66 PathCondition, Sequence, SiteFacts, StringPart, and_conditions,
67};
68
69#[derive(Debug, Default)]
72pub struct EvaluatedDocument {
73 pub root: Guarded<AbstractFragment>,
76 pub reads: Vec<ValueRead>,
79 pub(crate) type_hints: BTreeMap<String, BTreeSet<String>>,
81 pub(crate) guarded_type_hints: BTreeMap<String, BTreeSet<String>>,
84 pub(crate) fallback_type_hints: BTreeMap<String, BTreeSet<String>>,
87 pub(crate) guarded_fallback_type_hints: BTreeMap<String, BTreeSet<String>>,
91 pub(crate) shape_erased_paths: BTreeSet<String>,
95 pub(crate) string_contract_paths: BTreeSet<String>,
98 pub(crate) range_modes: crate::range_modes::RangeModes,
101 pub(crate) values_default_sources: BTreeSet<crate::ValuesDefaultSource>,
103 pub(crate) values_root_overlay_prefixes: BTreeSet<String>,
106 pub(crate) values_root_helper_includes: BTreeSet<String>,
108 pub(crate) pre_rewrite_strict_paths: BTreeSet<String>,
112 pub(crate) fail_conditions: Vec<FailCapture>,
115}
116
117#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
119pub struct ValueRead {
120 pub values_path: String,
122 pub kind: crate::ValueKind,
125 pub condition: GuardDnf,
127 pub resource: Option<ResourceRef>,
131 pub provenance: Vec<ContractProvenance>,
133 pub dependency: bool,
136}
137
138pub(crate) fn eval_document(
140 source: &str,
141 source_path: Option<&str>,
142 db: &IrAnalysisDb,
143) -> EvaluatedDocument {
144 let Some(tree) = parse_go_template(source) else {
145 return EvaluatedDocument::default();
146 };
147 let document = TemplatedDocument::parse_with_root(source, tree.root_node());
148 let mut interpreter = Interpreter::for_source(source, source_path, db, &tree, &document);
149 let roots: Vec<NodeView<'_>> = document.roots().iter().map(NodeView::plain).collect();
150 let contributions = interpreter.eval_node_list(&roots);
151 EvaluatedDocument {
152 root: contributions.assemble(),
153 reads: interpreter.reads,
154 type_hints: interpreter.type_hints,
155 guarded_type_hints: interpreter.guarded_type_hints,
156 fallback_type_hints: interpreter.fallback_type_hints,
157 guarded_fallback_type_hints: interpreter.guarded_fallback_type_hints,
158 shape_erased_paths: interpreter.shape_erased_paths,
159 string_contract_paths: interpreter.string_contract_paths,
160 range_modes: interpreter.range_modes,
161 values_default_sources: interpreter.values_default_sources_observed,
162 values_root_overlay_prefixes: interpreter.values_root_overlay_prefixes_observed,
163 values_root_helper_includes: interpreter.values_root_helper_includes_observed,
164 pre_rewrite_strict_paths: interpreter.pre_rewrite_strict_paths,
165 fail_conditions: interpreter.fail_conditions,
166 }
167}
168
169pub(crate) struct ControlFacts {
173 pub(super) header: Option<TemplateHeader>,
174 pub(super) is_range: bool,
175 pub(super) range_destructured: bool,
176 pub(super) range_value_variable: Option<String>,
179 pub(super) range_key_variable: Option<String>,
182 pub(super) region_end: usize,
185}
186
187pub(crate) struct BodyEvalFacts {
191 pub(super) control_facts: HashMap<usize, ControlFacts>,
192 pub(super) resource_spans: Vec<ResourceSpan>,
193}
194
195impl BodyEvalFacts {
196 pub(crate) fn collect(
197 source: &str,
198 db: &IrAnalysisDb,
199 tree: &tree_sitter::Tree,
200 document: &TemplatedDocument<'_>,
201 ) -> Self {
202 let mut control_facts = HashMap::new();
203 collect_control_facts(tree.root_node(), source, &mut control_facts);
204 Self {
205 control_facts,
206 resource_spans: crate::resource_identity::collect_resource_spans(document, db),
207 }
208 }
209}
210
211fn collect_control_facts(
212 node: tree_sitter::Node<'_>,
213 source: &str,
214 out: &mut HashMap<usize, ControlFacts>,
215) {
216 match node.kind() {
217 "if_action" | "with_action" => {
218 out.insert(
219 node.start_byte(),
220 ControlFacts {
221 header: control_header(source, node),
222 is_range: false,
223 range_destructured: false,
224 range_value_variable: None,
225 range_key_variable: None,
226 region_end: node.end_byte(),
227 },
228 );
229 }
230 "range_action" => {
231 out.insert(
232 node.start_byte(),
233 ControlFacts {
234 header: range_header_from_source(node, source),
235 is_range: true,
236 range_destructured: range_has_destructured_variable_definition(node),
237 range_value_variable: helm_schema_ast::range_destructured_value_variable(
238 node, source,
239 ),
240 range_key_variable: helm_schema_ast::range_destructured_key_variable(
241 node, source,
242 ),
243 region_end: node.end_byte(),
244 },
245 );
246 }
247 _ => {}
248 }
249 let mut cursor = node.walk();
250 for child in node.named_children(&mut cursor) {
251 collect_control_facts(child, source, out);
252 }
253}
254
255fn content_child_mark(children: &[Node], container_indent: usize) -> Option<usize> {
259 children
260 .iter()
261 .filter_map(|child| match child {
262 Node::Mapping(entry) if entry.indent > container_indent => Some(entry.span.start),
263 Node::Sequence(item) if item.indent > container_indent => Some(item.span.start),
264 Node::Scalar(line) if line.indent > container_indent => Some(line.span.start),
265 _ => None,
266 })
267 .min()
268}
269
270fn collect_inline_regions(nodes: &[Node], out: &mut Vec<Span>) {
271 for node in nodes {
272 match node {
273 Node::Opaque(opaque) if opaque.kind == OpaqueKind::InlineRegion => {
274 out.push(opaque.span);
275 }
276 Node::Mapping(entry) => collect_inline_regions(&entry.children, out),
277 Node::Sequence(item) => collect_inline_regions(&item.children, out),
278 Node::Control(region) => {
279 for branch in ®ion.branches {
280 collect_inline_regions(&branch.body, out);
281 }
282 }
283 _ => {}
284 }
285 }
286}
287
288#[derive(Default)]
290pub(super) struct Contributions {
291 pub(super) entries: Vec<MappingEntry>,
292 pub(super) items: Vec<Guarded<AbstractFragment>>,
293 pub(super) values: Guarded<AbstractFragment>,
294 pub(super) floating: Vec<FloatingOutput>,
299 pub(super) loop_control: LoopControl,
300}
301
302#[derive(Default)]
303pub(super) struct LoopControl {
304 pub(super) breaks: Vec<PathCondition>,
305 pub(super) continues: Vec<PathCondition>,
306}
307
308impl LoopControl {
309 pub(super) fn exit_condition(&self) -> PathCondition {
310 any_conditions(self.breaks.iter().chain(&self.continues).cloned().collect())
311 }
312
313 pub(super) fn break_condition(&self) -> PathCondition {
314 any_conditions(self.breaks.clone())
315 }
316
317 fn guard_all(&mut self, condition: &PathCondition) {
318 for exit in self.breaks.iter_mut().chain(&mut self.continues) {
319 *exit = and_conditions(condition.clone(), exit.clone());
320 }
321 }
322
323 fn extend(&mut self, other: Self) {
324 self.breaks.extend(other.breaks);
325 self.continues.extend(other.continues);
326 }
327}
328
329fn any_conditions(mut conditions: Vec<PathCondition>) -> PathCondition {
330 conditions.retain(|condition| *condition != Predicate::False);
331 if conditions.contains(&Predicate::True) {
332 return Predicate::True;
333 }
334 conditions.sort();
335 conditions.dedup();
336 match conditions.as_slice() {
337 [] => Predicate::False,
338 [condition] => condition.clone(),
339 _ => Predicate::Or(conditions),
340 }
341}
342
343pub(super) struct FloatingOutput {
345 pub(super) width: usize,
347 pub(super) origin: usize,
350 pub(super) value: Guarded<AbstractFragment>,
351}
352
353impl Contributions {
354 pub(super) fn merge_entry(&mut self, key: EntryKey, value: Guarded<AbstractFragment>) {
355 if let EntryKey::Literal(name) = &key
356 && let Some(existing) = self.entries.iter_mut().find(
357 |entry| matches!(&entry.key, EntryKey::Literal(existing_name) if existing_name == name),
358 )
359 {
360 existing.value.extend(value);
361 return;
362 }
363 self.entries.push(MappingEntry { key, value });
364 }
365
366 pub(super) fn push_value_arm(&mut self, arm: (PathCondition, AbstractFragment)) {
367 self.values.arms.push(arm);
368 }
369
370 pub(super) fn guard_all(&mut self, condition: &PathCondition) {
371 for entry in &mut self.entries {
372 entry.value.guard_all(condition);
373 }
374 for item in &mut self.items {
375 item.guard_all(condition);
376 }
377 self.values.guard_all(condition);
378 for floating in &mut self.floating {
379 floating.value.guard_all(condition);
380 }
381 self.loop_control.guard_all(condition);
382 }
383
384 pub(super) fn extend(&mut self, other: Self) {
385 for entry in other.entries {
386 self.merge_entry(entry.key, entry.value);
387 }
388 self.items.extend(other.items);
389 self.values.extend(other.values);
390 self.floating.extend(other.floating);
391 self.loop_control.extend(other.loop_control);
392 }
393
394 pub(super) fn take_loop_control(&mut self) -> LoopControl {
395 std::mem::take(&mut self.loop_control)
396 }
397
398 pub(super) fn take_floating_below(
405 &mut self,
406 container_indent: usize,
407 accepts_same_indent: bool,
408 marked_at: Option<usize>,
409 ) -> Guarded<AbstractFragment> {
410 let mut attached = Guarded::empty();
411 let mut keep = Vec::new();
412 for floating in std::mem::take(&mut self.floating) {
413 let same_indent_ok = floating.width == container_indent
414 && accepts_same_indent
415 && marked_at.is_none_or(|marked| marked >= floating.origin);
416 if floating.width > container_indent || same_indent_ok {
417 attached.extend(floating.value);
418 } else {
419 keep.push(floating);
420 }
421 }
422 self.floating = keep;
423 attached
424 }
425
426 pub(super) fn assemble(self) -> Guarded<AbstractFragment> {
427 let mut out = Guarded::empty();
428 if !self.entries.is_empty() {
429 out.arms.push((
430 Predicate::True,
431 AbstractFragment::Mapping(Mapping {
432 entries: self.entries,
433 }),
434 ));
435 }
436 if !self.items.is_empty() {
437 out.arms.push((
438 Predicate::True,
439 AbstractFragment::Sequence(Sequence { items: self.items }),
440 ));
441 }
442 out.extend(self.values);
443 for floating in self.floating {
446 out.extend(floating.value);
447 }
448 out
449 }
450}
451
452#[derive(Clone, Copy)]
457pub(super) struct NodeView<'n> {
458 pub(super) node: &'n Node,
459 pub(super) child_limit: Option<usize>,
460}
461
462impl<'n> NodeView<'n> {
463 pub(super) fn plain(node: &'n Node) -> Self {
464 Self {
465 node,
466 child_limit: None,
467 }
468 }
469
470 pub(super) fn in_scope_children(&self) -> Vec<NodeView<'n>> {
474 let children = match self.node {
475 Node::Mapping(entry) => &entry.children,
476 Node::Sequence(item) => &item.children,
477 _ => return Vec::new(),
478 };
479 children
480 .iter()
481 .filter(|child| {
482 self.child_limit
483 .is_none_or(|limit| child.span_start() < limit)
484 })
485 .map(|child| NodeView {
486 node: child,
487 child_limit: self.child_limit,
488 })
489 .collect()
490 }
491}
492
493pub(super) struct Adopted<'n> {
496 pub(super) view: NodeView<'n>,
497 pub(super) defer_upper: Option<usize>,
498}
499
500pub(super) enum ArmSpec {
502 If(Option<TemplateHeader>),
503 With(Option<TemplateHeader>),
504 Range {
505 header: Option<TemplateHeader>,
506 destructured: bool,
507 value_variable: Option<String>,
508 key_variable: Option<String>,
509 },
510 Else,
511}
512
513pub(super) struct Interpreter<'a> {
514 pub(super) source: &'a str,
515 pub(super) source_path: Option<&'a str>,
516 pub(super) source_offset: usize,
519 pub(super) db: &'a IrAnalysisDb,
520 pub(super) body_facts: Rc<BodyEvalFacts>,
523 pub(super) inline_regions: Vec<Span>,
524 pub(super) inline_files: Vec<String>,
527 pub(super) helper_scope: bool,
529 pub(super) helper_seen: HashSet<String>,
532 pub(super) locals: SymbolicLocalState,
533 pub(super) dot_stack: Vec<Option<AbstractValue>>,
534 pub(super) root_value_dot: Option<AbstractValue>,
538 pub(super) root_bindings: HashMap<String, AbstractValue>,
539 pub(super) root_truthy_predicates: HashMap<String, Predicate>,
540 pub(super) root_value_dispatches: HashMap<String, crate::eval_effect::RootValueDispatch>,
544 pub(super) root_set_mutations_observed: BTreeMap<String, AbstractValue>,
546 pub(super) root_set_predicates_observed: BTreeMap<String, Predicate>,
547 pub(super) root_value_dispatches_observed:
548 BTreeMap<String, crate::eval_effect::RootValueDispatch>,
549 pub(super) values_default_sources_observed: BTreeSet<crate::ValuesDefaultSource>,
550 pub(super) values_root_overlay_prefixes_observed: BTreeSet<String>,
551 pub(super) values_root_helper_includes_observed: BTreeSet<String>,
552 pub(super) pre_rewrite_strict_paths: BTreeSet<String>,
560 pub(super) active_predicates: Vec<Predicate>,
561 pub(super) loop_depth: usize,
564 pub(super) reads: Vec<ValueRead>,
565 reads_seen: HashSet<ValueRead>,
567 pub(super) type_hints: BTreeMap<String, BTreeSet<String>>,
568 pub(super) parsed_yaml_input_paths: BTreeSet<String>,
572 pub(super) yaml_serialized_paths: BTreeSet<String>,
575 pub(super) guarded_type_hints: BTreeMap<String, BTreeSet<String>>,
579 pub(super) fallback_type_hints: BTreeMap<String, BTreeSet<String>>,
582 pub(super) guarded_fallback_type_hints: BTreeMap<String, BTreeSet<String>>,
584 pub(super) shape_erased_paths: BTreeSet<String>,
588 pub(super) string_contract_paths: BTreeSet<String>,
591 pub(super) range_modes: crate::range_modes::RangeModes,
594 pub(super) fail_conditions: Vec<FailCapture>,
597 pub(super) member_host_conversions: BTreeSet<crate::eval_effect::MemberHostConversion>,
599 pub(super) active_direct_ranged_paths: Vec<String>,
603 pub(super) alternative_capture_approximates: Vec<Predicate>,
609 pub(super) suppress_predicate_paths: BTreeSet<String>,
614 pub(super) chart_defaults_observed: BTreeSet<String>,
620 pub(super) current_site: Option<Rc<SiteFacts>>,
623}
624
625impl<'a> Interpreter<'a> {
626 pub(super) fn for_source(
630 source: &'a str,
631 source_path: Option<&'a str>,
632 db: &'a IrAnalysisDb,
633 tree: &tree_sitter::Tree,
634 document: &TemplatedDocument<'_>,
635 ) -> Self {
636 let body_facts = Rc::new(BodyEvalFacts::collect(source, db, tree, document));
637 Self::with_body_facts(source, source_path, db, document, body_facts)
638 }
639
640 pub(super) fn with_body_facts(
643 source: &'a str,
644 source_path: Option<&'a str>,
645 db: &'a IrAnalysisDb,
646 document: &TemplatedDocument<'_>,
647 body_facts: Rc<BodyEvalFacts>,
648 ) -> Self {
649 let mut inline_regions = Vec::new();
650 collect_inline_regions(document.roots(), &mut inline_regions);
651 Self {
652 source,
653 source_path,
654 source_offset: 0,
655 db,
656 body_facts,
657 inline_regions,
658 inline_files: Vec::new(),
659 helper_scope: false,
660 helper_seen: HashSet::new(),
661 locals: SymbolicLocalState::default(),
662 dot_stack: Vec::new(),
663 root_value_dot: None,
664 root_bindings: HashMap::new(),
665 root_truthy_predicates: HashMap::new(),
666 root_value_dispatches: HashMap::new(),
667 root_set_mutations_observed: BTreeMap::new(),
668 root_set_predicates_observed: BTreeMap::new(),
669 root_value_dispatches_observed: BTreeMap::new(),
670 values_default_sources_observed: BTreeSet::new(),
671 values_root_overlay_prefixes_observed: BTreeSet::new(),
672 values_root_helper_includes_observed: BTreeSet::new(),
673 pre_rewrite_strict_paths: BTreeSet::new(),
674 active_predicates: Vec::new(),
675 loop_depth: 0,
676 reads: Vec::new(),
677 reads_seen: HashSet::new(),
678 type_hints: BTreeMap::new(),
679 parsed_yaml_input_paths: BTreeSet::new(),
680 yaml_serialized_paths: BTreeSet::new(),
681 guarded_type_hints: BTreeMap::new(),
682 fallback_type_hints: BTreeMap::new(),
683 guarded_fallback_type_hints: BTreeMap::new(),
684 shape_erased_paths: BTreeSet::new(),
685 string_contract_paths: BTreeSet::new(),
686 range_modes: crate::range_modes::RangeModes::default(),
687 fail_conditions: Vec::new(),
688 member_host_conversions: BTreeSet::new(),
689 active_direct_ranged_paths: Vec::new(),
690 alternative_capture_approximates: Vec::new(),
691 suppress_predicate_paths: BTreeSet::new(),
692 chart_defaults_observed: BTreeSet::new(),
693 current_site: None,
694 }
695 }
696
697 pub(super) fn text(&self, span: Span) -> &'a str {
698 self.source.get(span.start..span.end).unwrap_or("")
699 }
700
701 pub(super) fn hole_site(&self, span: Span) -> Option<Rc<SiteFacts>> {
704 let resource_span = self
705 .body_facts
706 .resource_spans
707 .iter()
708 .filter(|resource| resource.start <= span.start && span.start < resource.end)
709 .min_by(|left, right| {
710 let left_len = left.end.saturating_sub(left.start);
711 let right_len = right.end.saturating_sub(right.start);
712 left_len
713 .cmp(&right_len)
714 .then_with(|| right.start.cmp(&left.start))
715 });
716 self.site_facts(resource_span.map(|span| self.span_resource(span)), span)
717 }
718
719 pub(super) fn region_site(&self, span: Span) -> Option<Rc<SiteFacts>> {
723 let mut unique: Option<&ResourceSpan> = None;
724 for resource in &self.body_facts.resource_spans {
725 if resource.start >= span.end || span.start >= resource.end {
726 continue;
727 }
728 match unique {
729 Some(existing) if existing.resource != resource.resource => {
730 unique = None;
731 break;
732 }
733 Some(_) => {}
734 None => unique = Some(resource),
735 }
736 }
737 self.site_facts(unique.map(|span| self.span_resource(span)), span)
738 }
739
740 fn span_resource(&self, resource_span: &ResourceSpan) -> (ResourceRef, Vec<String>) {
746 let mut resource = resource_span.resource.clone();
747 resource.kind_branches = self.resolved_kind_branches(&resource_span.kind_branch_sources);
748 (resource, resource_span.path_prefix.clone())
749 }
750
751 fn resolved_kind_branches(
756 &self,
757 sources: &[helm_schema_ast::KindBranchSource],
758 ) -> Vec<helm_schema_core::KindBranch> {
759 if sources.is_empty() {
760 return Vec::new();
761 }
762 let context = self.value_path_context();
763 let mut prior_negations: Vec<Predicate> = Vec::new();
764 let mut branches = Vec::new();
765 for source in sources {
766 let mut conjuncts = prior_negations.clone();
767 if let Some(text) = &source.condition {
768 let wrapped = format!("{{{{ {} }}}}", text.trim());
769 let exprs = helm_schema_ast::parse_action_expressions(&wrapped);
770 let [expr] = exprs.as_slice() else {
771 return Vec::new();
772 };
773 if !context.condition_lowering_is_faithful(expr) {
774 return Vec::new();
775 }
776 let predicate = context.condition_predicate_expr(expr);
777 prior_negations.push(predicate.negated());
778 conjuncts.push(predicate);
779 }
780 branches.push(helm_schema_core::KindBranch {
781 predicate: Predicate::all(conjuncts),
782 kind: source.kind.clone(),
783 });
784 }
785 branches
786 }
787
788 fn site_facts(
789 &self,
790 resource: Option<(ResourceRef, Vec<String>)>,
791 span: Span,
792 ) -> Option<Rc<SiteFacts>> {
793 let provenance = self.source_path.map(|source_path| {
794 let helper_chain = self
795 .inline_files
796 .iter()
797 .filter_map(|entry| entry.strip_prefix("define:"))
798 .map(std::string::ToString::to_string)
799 .collect();
800 ContractProvenance::new(
801 source_path,
802 SourceSpan::new(
803 self.source_offset + span.start,
804 self.source_offset + span.end,
805 ),
806 helper_chain,
807 )
808 });
809 let (resource, path_prefix) = match resource {
810 Some((resource, path_prefix)) => (Some(resource), path_prefix),
811 None => (None, Vec::new()),
812 };
813 if resource.is_none() && provenance.is_none() {
814 return None;
815 }
816 Some(Rc::new(SiteFacts {
817 resource,
818 path_prefix,
819 provenance,
820 }))
821 }
822
823 pub(super) fn enter_hole_site(&mut self, span: Span) -> Option<Rc<SiteFacts>> {
826 let site = self.hole_site(span);
827 std::mem::replace(&mut self.current_site, site)
828 }
829
830 pub(super) fn restore_site(&mut self, previous: Option<Rc<SiteFacts>>) {
831 self.current_site = previous;
832 }
833
834 pub(super) fn current_dot_fragment(&self) -> Option<AbstractValue> {
835 self.dot_stack.last().cloned().flatten()
836 }
837
838 pub(super) fn current_dot_binding(&self) -> Option<AbstractValue> {
839 if self.dot_stack.len() <= 1
840 && let Some(root) = &self.root_value_dot
841 {
842 return Some(root.clone());
843 }
844 self.dot_stack
845 .last()
846 .and_then(|binding| binding.as_ref())
847 .and_then(AbstractValue::to_current_dot_context_value)
848 }
849
850 pub(super) fn current_value_dot(&self) -> Option<AbstractValue> {
854 if self.dot_stack.len() <= 1
855 && let Some(root) = &self.root_value_dot
856 {
857 return Some(root.clone());
858 }
859 self.current_dot_fragment()
860 .map(|value| value.to_context_value())
861 .or_else(|| self.current_dot_binding())
862 }
863
864 pub(super) fn value_path_context(&self) -> ValuePathContext<'_> {
865 let mut template_bindings = self.locals.range_member_values.clone();
868 template_bindings.extend(
869 self.locals
870 .fragment_values
871 .iter()
872 .map(|(name, value)| (name.clone(), value.clone())),
873 );
874 ValuePathContext {
875 root_bindings: &self.root_bindings,
876 root_truthy_predicates: &self.root_truthy_predicates,
877 root_value_dispatches: &self.root_value_dispatches,
878 template_bindings,
879 range_domains: &self.locals.range_domains,
880 get_bindings: &self.locals.get_bindings,
881 template_default_paths: &self.locals.default_paths,
882 template_output_meta: &self.locals.output_meta,
883 template_truthy_reductions: &self.locals.truthy_reductions,
884 typeof_bindings: &self.locals.typeof_sources,
885 int_cast_bindings: &self.locals.int_cast_sources,
886 kube_version_bindings: &self.locals.kube_version_sources,
887 fragment_context: FragmentEvalContext::new(self.db),
888 current_dot_fragment: self.current_dot_fragment(),
889 current_dot_binding: self.current_value_dot(),
890 }
891 }
892
893 pub(super) fn record_fail_condition(&mut self) {
899 let capture = FailCapture {
900 conjunction: self.fail_capture_conjunction(Vec::new()),
901 ranged: self.capture_ranged_modes(),
902 kind: CaptureKind::Fail,
903 };
904 if capture
905 .conjunction
906 .iter()
907 .any(|p| matches!(p, Predicate::False))
908 {
909 return;
910 }
911 if !self.fail_conditions.contains(&capture) {
912 self.fail_conditions.push(capture);
913 }
914 }
915
916 pub(super) fn record_required_condition(&mut self, subject_path: &str) {
920 let empty = Predicate::Or(vec![
921 Predicate::from(Guard::Absent {
922 path: subject_path.to_string(),
923 }),
924 Predicate::from(Guard::Eq {
925 path: subject_path.to_string(),
926 value: helm_schema_core::GuardValue::Null,
927 }),
928 Predicate::from(Guard::Eq {
929 path: subject_path.to_string(),
930 value: helm_schema_core::GuardValue::string(""),
931 }),
932 ]);
933 let capture = FailCapture {
934 conjunction: self.fail_capture_conjunction(vec![empty]),
935 ranged: self.capture_ranged_modes(),
936 kind: CaptureKind::Fail,
937 };
938 if capture
939 .conjunction
940 .iter()
941 .any(|p| matches!(p, Predicate::False))
942 {
943 return;
944 }
945 if !self.fail_conditions.contains(&capture) {
946 self.fail_conditions.push(capture);
947 }
948 }
949
950 pub(super) fn fail_capture_conjunction(&self, tail: Vec<Predicate>) -> Vec<Predicate> {
953 let mut conjunction = self.active_predicates.clone();
954 for path in &self.active_direct_ranged_paths {
955 let range = Predicate::from(Guard::Range { path: path.clone() });
956 if !conjunction.contains(&range) {
957 conjunction.push(range);
958 }
959 }
960 for approximate in &self.alternative_capture_approximates {
961 if !conjunction.contains(approximate) {
962 conjunction.push(approximate.clone());
963 }
964 }
965 conjunction.extend(tail);
966 conjunction
967 }
968
969 pub(super) fn capture_ranged_modes(&self) -> crate::range_modes::RangeModes {
974 let mut ranged = crate::range_modes::RangeModes::default();
975 for path in &self.active_direct_ranged_paths {
976 ranged.mark_direct(path);
977 }
978 for (path, mode) in self.range_modes.iter() {
979 if mode.json_decoded {
980 ranged.mark_json_decoded(path);
981 }
982 if mode.destructured {
983 ranged.mark_destructured(path);
984 }
985 }
986 ranged
987 }
988
989 pub(super) fn ambient_condition(&self) -> GuardDnf {
990 GuardDnf::from_conjunction(self.active_predicates.iter().cloned())
991 }
992
993 pub(super) fn push_predicate(&mut self, predicate: Predicate) {
994 if !matches!(predicate, Predicate::True) && !self.active_predicates.contains(&predicate) {
998 self.active_predicates.push(predicate);
999 }
1000 }
1001
1002 pub(super) fn under_approximate_condition(&self) -> bool {
1010 self.active_predicates
1011 .iter()
1012 .any(Predicate::contains_approximation)
1013 }
1014
1015 pub(super) fn push_read(&mut self, values_path: &str, extra_guards: &[Guard]) {
1020 let (resource, provenance) = match &self.current_site {
1021 Some(site) => (
1022 site.resource.clone(),
1023 site.provenance.iter().cloned().collect(),
1024 ),
1025 None => (None, Vec::new()),
1026 };
1027 self.push_read_row(
1028 values_path,
1029 crate::ValueKind::Scalar,
1030 extra_guards,
1031 resource,
1032 provenance,
1033 false,
1034 );
1035 }
1036
1037 pub(super) fn push_read_row(
1038 &mut self,
1039 values_path: &str,
1040 kind: crate::ValueKind,
1041 extra_guards: &[Guard],
1042 resource: Option<ResourceRef>,
1043 provenance: Vec<ContractProvenance>,
1044 dependency: bool,
1045 ) {
1046 let condition = self
1047 .ambient_condition()
1048 .conjoined_with_guards(extra_guards.iter().cloned());
1049 self.push_read_row_with_condition(
1050 values_path,
1051 kind,
1052 condition,
1053 resource,
1054 provenance,
1055 dependency,
1056 );
1057 }
1058
1059 fn push_read_row_with_condition(
1060 &mut self,
1061 values_path: &str,
1062 kind: crate::ValueKind,
1063 condition: GuardDnf,
1064 resource: Option<ResourceRef>,
1065 provenance: Vec<ContractProvenance>,
1066 dependency: bool,
1067 ) {
1068 if values_path.trim().is_empty() {
1069 return;
1070 }
1071 let read = ValueRead {
1072 values_path: values_path.to_string(),
1073 kind,
1074 condition,
1075 resource,
1076 provenance,
1077 dependency,
1078 };
1079 if self.reads_seen.insert(read.clone()) {
1080 self.reads.push(read);
1081 }
1082 }
1083
1084 pub(super) fn push_nested_read(&mut self, read: ValueRead) {
1087 if self.reads_seen.insert(read.clone()) {
1088 self.reads.push(read);
1089 }
1090 }
1091
1092 pub(super) fn push_key_reads(&mut self, key: &EntryKey) {
1097 let EntryKey::Dynamic(string) = key else {
1098 return;
1099 };
1100 for part in &string.parts {
1101 match part {
1102 StringPart::Text(_) => {}
1103 StringPart::Splice(splice) if splice.meta.range_key => {}
1106 StringPart::Splice(splice) => {
1107 let mut extra = Vec::new();
1108 if splice.meta.defaulted {
1109 extra.push(Guard::Default {
1110 path: splice.values_path.clone(),
1111 });
1112 }
1113 let (resource, provenance) = match &self.current_site {
1119 Some(site) => (
1120 site.resource.clone(),
1121 site.provenance.iter().cloned().collect(),
1122 ),
1123 None => (None, Vec::new()),
1124 };
1125 self.push_read_row(
1126 &splice.values_path,
1127 crate::ValueKind::PartialScalar,
1128 &extra,
1129 resource,
1130 provenance,
1131 false,
1132 );
1133 }
1134 StringPart::Taint(taint) => {
1135 for path in &taint.paths {
1136 self.push_read(path, &[]);
1137 }
1138 }
1139 }
1140 }
1141 }
1142
1143 pub(super) fn push_meta_reads(
1147 &mut self,
1148 values_path: &str,
1149 kind: crate::ValueKind,
1150 meta: &HelperOutputMeta,
1151 sibling_claims: &BTreeSet<String>,
1152 dependency: bool,
1153 ) {
1154 let helper_condition = if meta.predicates.is_empty() {
1155 GuardDnf::unconditional()
1156 } else {
1157 GuardDnf::from_disjunction(meta.predicates.iter().map(|branch| branch.iter().cloned()))
1158 };
1159 let mut provenance: Vec<ContractProvenance> = self
1160 .current_site
1161 .as_ref()
1162 .and_then(|site| site.provenance.clone())
1163 .into_iter()
1164 .collect();
1165 merge_provenance_sites(&mut provenance, &meta.provenance);
1166 let mut condition = self
1167 .claim_scoped_ambient_condition(values_path, sibling_claims)
1168 .conjoined(&helper_condition);
1169 if meta.defaulted {
1170 condition = condition.conjoined_with_guards([Guard::Default {
1171 path: values_path.to_string(),
1172 }]);
1173 }
1174 self.push_read_row_with_condition(
1175 values_path,
1176 kind,
1177 condition,
1178 None,
1179 provenance,
1180 dependency,
1181 );
1182 }
1183
1184 fn claim_scoped_ambient_condition(
1189 &self,
1190 claim_path: &str,
1191 sibling_claims: &BTreeSet<String>,
1192 ) -> GuardDnf {
1193 if !self.helper_scope {
1194 return GuardDnf::from_conjunction(self.active_predicates.iter().cloned());
1195 }
1196 GuardDnf::from_conjunction(
1197 self.active_predicates
1198 .iter()
1199 .filter(|predicate| {
1200 let path = match predicate {
1201 Predicate::Guard(Guard::Truthy { path }) => path,
1202 Predicate::Not(inner) => match inner.as_ref() {
1203 Predicate::Guard(Guard::Truthy { path }) => path,
1204 _ => return true,
1205 },
1206 _ => return true,
1207 };
1208 path == claim_path
1209 || !sibling_claims.contains(path)
1210 || crate::helper_meta::values_paths_are_related(path, claim_path)
1211 })
1212 .cloned(),
1213 )
1214 }
1215
1216 pub(super) fn strict_string_capture_paths(&self) -> BTreeSet<String> {
1232 let mut paths = self.string_contract_paths.clone();
1233 for capture in &self.fail_conditions {
1234 match &capture.kind {
1235 crate::eval_effect::CaptureKind::ValueType { path, schema_type }
1236 if schema_type == "string" =>
1237 {
1238 paths.insert(path.clone());
1239 }
1240 crate::eval_effect::CaptureKind::ValuePattern { path, .. } => {
1241 paths.insert(path.clone());
1242 }
1243 _ => {}
1244 }
1245 }
1246 paths.retain(|path| !path.trim().is_empty() && !path.contains('*'));
1247 paths
1248 }
1249
1250 pub(super) fn absorb_helper_fails(&mut self, fails: &[FailCapture]) {
1251 for body_capture in fails {
1252 let mut ranged = self.capture_ranged_modes();
1253 ranged.merge(&body_capture.ranged);
1254 let conjunction = self.fail_capture_conjunction(body_capture.conjunction.clone());
1255 let mut kind = body_capture.kind.clone();
1256 if let CaptureKind::MemberAccess { handled_kinds } = &mut kind {
1257 let target = conjunction.iter().find_map(|predicate| match predicate {
1258 Predicate::Not(inner) => match inner.as_ref() {
1259 Predicate::Guard(Guard::TypeIs { path, schema_type })
1260 if schema_type == "object" =>
1261 {
1262 Some(path)
1263 }
1264 _ => None,
1265 },
1266 _ => None,
1267 });
1268 if let Some(target) = target {
1269 handled_kinds.extend(
1270 self.member_host_conversions
1271 .iter()
1272 .filter(|conversion| {
1273 &conversion.path == target
1274 && conversion
1275 .outer_predicates
1276 .iter()
1277 .all(|predicate| conjunction.contains(predicate))
1278 })
1279 .map(|conversion| conversion.input_kind.clone()),
1280 );
1281 }
1282 }
1283 let capture = FailCapture {
1284 conjunction,
1285 ranged,
1286 kind,
1287 };
1288 if capture
1289 .conjunction
1290 .iter()
1291 .any(|p| matches!(p, Predicate::False))
1292 {
1293 continue;
1294 }
1295 if !self.fail_conditions.contains(&capture) {
1296 self.fail_conditions.push(capture);
1297 }
1298 }
1299 }
1300
1301 pub(super) fn absorb_helper_reads_with_suppression(
1302 &mut self,
1303 reads: &[ValueRead],
1304 suppressed: &BTreeSet<&String>,
1305 sibling_claims: &BTreeSet<String>,
1306 ) {
1307 let site_provenance: Vec<ContractProvenance> = self
1308 .current_site
1309 .as_ref()
1310 .and_then(|site| site.provenance.clone())
1311 .into_iter()
1312 .collect();
1313 for read in reads {
1314 if !read.dependency
1318 && !suppressed.contains(&read.values_path)
1319 && suppressed.iter().any(|narrowed| {
1320 helm_schema_core::values_path_is_descendant(narrowed, &read.values_path)
1321 })
1322 {
1323 continue;
1324 }
1325 let mut provenance = site_provenance.clone();
1326 merge_provenance_sites(&mut provenance, &read.provenance);
1327 let condition = self
1328 .claim_scoped_ambient_condition(&read.values_path, sibling_claims)
1329 .conjoined(&read.condition);
1330 self.push_read_row_with_condition(
1331 &read.values_path,
1332 read.kind,
1333 condition,
1334 read.resource.clone(),
1335 provenance,
1336 read.dependency,
1337 );
1338 }
1339 }
1340
1341 pub(super) fn eval_node_list(&mut self, nodes: &[NodeView<'_>]) -> Contributions {
1342 let mut ordered: Vec<NodeView<'_>> = nodes.to_vec();
1346 ordered.sort_by_key(|view| view.node.span_start());
1347 let nodes = &ordered;
1348 let mut out = Contributions::default();
1349 let mut index = 0;
1350 let mut remaining = Predicate::True;
1351 while let Some(view) = nodes.get(index) {
1352 if remaining == Predicate::False {
1353 break;
1354 }
1355 let entry_predicates = self.active_predicates.len();
1356 self.push_predicate(remaining.clone());
1357 let mut next = Contributions::default();
1358 match view.node {
1359 Node::Control(region) => {
1360 let region_index = index;
1366 let mut adopted = Vec::new();
1367 while let Some(next) = nodes.get(index + 1) {
1368 if next.node.span_start() < region.span.end {
1369 let in_scope = next
1374 .child_limit
1375 .map_or(region.span.end, |limit| limit.min(region.span.end));
1376 adopted.push(Adopted {
1377 view: NodeView {
1378 node: next.node,
1379 child_limit: Some(in_scope),
1380 },
1381 defer_upper: next.child_limit,
1382 });
1383 index += 1;
1384 } else {
1385 break;
1386 }
1387 }
1388 let mut escaped = Vec::new();
1393 for prior in nodes.get(..region_index).unwrap_or_default() {
1394 if matches!(prior.node, Node::Control(_)) {
1395 continue;
1396 }
1397 let mut chain = Vec::new();
1398 super::control::collect_deferred(
1399 prior.node,
1400 region.span.start,
1401 prior.child_limit,
1402 &mut chain,
1403 &mut escaped,
1404 );
1405 }
1406 next.extend(self.eval_control(region, &adopted, escaped));
1407 }
1408 Node::Output(action) => {
1409 let consumed = self.eval_output_with_lookahead(action, nodes, index, &mut next);
1410 index += consumed;
1411 }
1412 _ => {
1413 let mut bounded = *view;
1417 if let Some(region_start) = nodes
1418 .get(index + 1..)
1419 .into_iter()
1420 .flatten()
1421 .find_map(|next| match next.node {
1422 Node::Control(region) => Some(region.span.start),
1423 _ => None,
1424 })
1425 {
1426 bounded.child_limit = Some(
1427 bounded
1428 .child_limit
1429 .map_or(region_start, |limit| limit.min(region_start)),
1430 );
1431 }
1432 next.extend(self.eval_node(bounded));
1433 }
1434 }
1435 self.active_predicates.truncate(entry_predicates);
1436 let exit_condition = next.loop_control.exit_condition();
1437 next.guard_all(&remaining);
1438 out.extend(next);
1439 remaining = match exit_condition {
1440 Predicate::False => remaining,
1441 Predicate::True => Predicate::False,
1442 exit => and_conditions(remaining, exit.negated()),
1443 };
1444 index += 1;
1445 }
1446 out
1447 }
1448
1449 fn eval_output_with_lookahead(
1454 &mut self,
1455 action: &syntax::OutputAction,
1456 nodes: &[NodeView<'_>],
1457 index: usize,
1458 out: &mut Contributions,
1459 ) -> usize {
1460 let key_line = nodes.get(index + 1).and_then(|next| match next.node {
1461 Node::Opaque(opaque) if opaque.kind == OpaqueKind::ActionLineText => {
1462 let text = self.text(opaque.span);
1463 syntax::structural_mapping_colon(text).map(|colon| {
1464 (
1465 opaque.span,
1466 text.get(..colon).unwrap_or("").to_string(),
1467 text.get(colon + 1..).unwrap_or("").to_string(),
1468 )
1469 })
1470 }
1471 _ => None,
1472 });
1473 let Some((text_span, key_suffix, rest)) = key_line else {
1474 let (value, width) = self.eval_output_action(action.span);
1475 match width {
1476 Some(width) => out.floating.push(FloatingOutput {
1477 width,
1478 origin: action.span.start,
1479 value,
1480 }),
1481 None => out.values.extend(value),
1482 }
1483 return 0;
1484 };
1485
1486 let previous_site = self.enter_hole_site(action.span);
1491 let mut key_string = self.hole_string(action.span);
1492 if !key_suffix.is_empty() {
1493 key_string
1494 .parts
1495 .push(StringPart::Text([key_suffix].into_iter().collect()));
1496 }
1497 let key = EntryKey::Dynamic(key_string);
1498 self.push_key_reads(&key);
1499 self.restore_site(previous_site);
1500 let mut consumed = 1;
1501 let value = if rest.trim().is_empty() {
1502 match nodes.get(index + 2).map(|view| view.node) {
1503 Some(Node::Output(value_action))
1504 if self.same_line(text_span.end, value_action.span.start) =>
1505 {
1506 consumed = 2;
1507 self.eval_entire_hole(value_action.span)
1508 }
1509 _ => Guarded::empty(),
1510 }
1511 } else if rest.trim().starts_with('|') || rest.trim().starts_with('>') {
1512 let key_indent = self.line_indent(action.span.start);
1517 let (block, block_consumed) =
1518 self.consume_dynamic_block_body(nodes, index + 2, key_indent);
1519 consumed += block_consumed;
1520 block
1521 } else {
1522 Guarded::unconditional(AbstractFragment::Scalar(AbstractString::literal(
1523 rest.trim().to_string(),
1524 )))
1525 };
1526 out.merge_entry(key, value);
1527 consumed
1528 }
1529
1530 fn consume_dynamic_block_body(
1535 &mut self,
1536 nodes: &[NodeView<'_>],
1537 start_index: usize,
1538 key_indent: usize,
1539 ) -> (Guarded<AbstractFragment>, usize) {
1540 let mut parts: Vec<StringPart> = Vec::new();
1541 let mut consumed = 0;
1542 while let Some(next) = nodes.get(start_index + consumed) {
1543 let node = next.node;
1544 if self.line_indent(node.span_start()) <= key_indent {
1545 break;
1546 }
1547 match node {
1548 Node::Output(action) => {
1549 for (_, hole_parts) in self.eval_hole_parts(action.span) {
1550 parts.extend(hole_parts);
1551 }
1552 }
1553 Node::Scalar(line) => {
1554 for part in &line.content.parts {
1555 match part {
1556 ScalarPart::Text(span) => {
1557 let text = self.text(*span);
1558 if !text.is_empty() {
1559 parts.push(StringPart::Text(
1560 [text.to_string()].into_iter().collect(),
1561 ));
1562 }
1563 }
1564 ScalarPart::Hole(span) => {
1565 for (_, hole_parts) in self.eval_hole_parts(*span) {
1566 parts.extend(hole_parts);
1567 }
1568 }
1569 }
1570 }
1571 }
1572 Node::Opaque(opaque) if opaque.kind == OpaqueKind::ActionLineText => {
1573 let text = self.text(opaque.span);
1574 if !text.is_empty() {
1575 parts.push(StringPart::Text([text.to_string()].into_iter().collect()));
1576 }
1577 }
1578 _ => break,
1579 }
1580 consumed += 1;
1581 }
1582 let value = Guarded::unconditional(AbstractFragment::Scalar(AbstractString {
1583 parts,
1584 suppressed: true,
1585 }));
1586 (value, consumed)
1587 }
1588
1589 fn same_line(&self, from: usize, to: usize) -> bool {
1590 self.source
1591 .get(from..to)
1592 .is_some_and(|between| !between.contains('\n') && between.trim().is_empty())
1593 }
1594
1595 pub(super) fn dynamic_entry_render_indent(&self, span: Span) -> usize {
1598 let line_start = self
1599 .source
1600 .get(..span.start)
1601 .and_then(|prefix| prefix.rfind('\n'))
1602 .map_or(0, |newline| newline + 1);
1603 let line_end = self
1604 .source
1605 .get(span.start..)
1606 .and_then(|rest| rest.find('\n'))
1607 .map_or(self.source.len(), |offset| span.start + offset);
1608 let line = self.source.get(line_start..line_end).unwrap_or("");
1609 for expr in helm_schema_ast::parse_action_expressions(line) {
1610 if let Some(width) = expr.fragment_indent_width() {
1611 return width;
1612 }
1613 }
1614 self.line_indent(span.start)
1615 }
1616
1617 pub(super) fn structural_content_indent(&self, node: &Node) -> Option<usize> {
1625 match node {
1626 Node::Mapping(entry) => Some(entry.indent),
1627 Node::Sequence(item) => Some(item.indent),
1628 Node::Scalar(line) => Some(line.indent),
1629 Node::Control(region) => region
1630 .branches
1631 .iter()
1632 .flat_map(|branch| &branch.body)
1633 .filter_map(|child| self.structural_content_indent(child))
1634 .min(),
1635 Node::Output(action) => {
1636 let width = parse_expr_text(self.text(action.span))
1637 .iter()
1638 .rev()
1639 .find_map(TemplateExpr::fragment_indent_width);
1640 match width {
1641 Some(_) => None,
1642 None => Some(self.line_indent(action.span.start)),
1643 }
1644 }
1645 Node::Comment(_) | Node::Opaque(_) => None,
1646 }
1647 }
1648
1649 pub(super) fn line_indent(&self, byte: usize) -> usize {
1651 let line_start = self
1652 .source
1653 .get(..byte)
1654 .and_then(|prefix| prefix.rfind('\n'))
1655 .map_or(0, |newline| newline + 1);
1656 self.source
1657 .get(line_start..)
1658 .map_or(0, |line| line.len() - line.trim_start_matches(' ').len())
1659 }
1660
1661 #[expect(
1662 clippy::too_many_lines,
1663 reason = "keeping this semantic operation together makes its state transitions easier to audit"
1664 )]
1665 fn eval_node(&mut self, view: NodeView<'_>) -> Contributions {
1666 let mut out = Contributions::default();
1667 match view.node {
1668 Node::Mapping(entry) => {
1669 let previous_site = self.enter_hole_site(entry.key.span);
1670 let key = self.entry_key(&entry.key);
1671 self.push_key_reads(&key);
1672 self.restore_site(previous_site);
1673 let mut value = Guarded::empty();
1674 if let Some(block) = &entry.block {
1675 value.extend(self.eval_block_scalar(block));
1676 }
1677 if let Some(parts) = &entry.value {
1678 let evaluated = self.eval_scalar_parts(parts);
1679 if evaluated.is_empty() {
1684 value.extend(Guarded::unconditional(AbstractFragment::Opaque(
1685 Opaque::default(),
1686 )));
1687 } else {
1688 value.extend(evaluated);
1689 }
1690 }
1691 let (children, siblings) = self.split_structural_children(view, entry.indent);
1692 if !children.is_empty() {
1693 let mut child = self.eval_node_list(&children);
1694 let opened_empty = entry.value.is_none() && entry.block.is_none();
1695 let marked_at = content_child_mark(&entry.children, entry.indent);
1696 value.extend(child.take_floating_below(entry.indent, opened_empty, marked_at));
1697 out.floating.append(&mut child.floating);
1698 out.loop_control.extend(child.take_loop_control());
1699 value.extend(child.assemble());
1700 }
1701 let siblings = if entry.block.is_some() {
1707 let (adopted, rest): (Vec<_>, Vec<_>) = siblings
1708 .into_iter()
1709 .partition(|child| matches!(child.node, Node::Output(_)));
1710 for adopted_view in adopted {
1711 if let Node::Output(action) = adopted_view.node {
1712 value.extend(self.eval_block_adopted_output(action.span));
1713 }
1714 }
1715 rest
1716 } else {
1717 siblings
1718 };
1719 out.merge_entry(key, value);
1720 if !siblings.is_empty() {
1721 out.extend(self.eval_node_list(&siblings));
1722 }
1723 }
1724 Node::Sequence(item) => {
1725 let mut value = Guarded::empty();
1726 if let Some(block) = &item.block {
1727 value.extend(self.eval_block_scalar(block));
1728 }
1729 if let Some(parts) = &item.value {
1730 value.extend(self.eval_scalar_parts(parts));
1731 }
1732 let (children, siblings) = self.split_structural_children(view, item.indent);
1733 if !children.is_empty() {
1734 let mut child = self.eval_node_list(&children);
1735 value.extend(child.take_floating_below(item.indent, false, None));
1738 out.floating.append(&mut child.floating);
1739 out.loop_control.extend(child.take_loop_control());
1740 value.extend(child.assemble());
1741 }
1742 let siblings = if item.block.is_some() {
1745 let (adopted, rest): (Vec<_>, Vec<_>) = siblings
1746 .into_iter()
1747 .partition(|child| matches!(child.node, Node::Output(_)));
1748 for adopted_view in adopted {
1749 if let Node::Output(action) = adopted_view.node {
1750 value.extend(self.eval_block_adopted_output(action.span));
1751 }
1752 }
1753 rest
1754 } else {
1755 siblings
1756 };
1757 out.items.push(value);
1758 if !siblings.is_empty() {
1759 out.extend(self.eval_node_list(&siblings));
1760 }
1761 }
1762 Node::Scalar(line) => {
1763 if line
1764 .content
1765 .parts
1766 .iter()
1767 .any(|part| matches!(part, ScalarPart::Hole(_)))
1768 {
1769 let value = self.eval_scalar_parts(&line.content);
1770 out.values.extend(value);
1771 }
1772 }
1773 Node::Opaque(opaque) if opaque.kind == OpaqueKind::Assignment => {
1774 self.eval_assignment_span(opaque.span);
1775 }
1776 Node::Opaque(opaque) if opaque.kind == OpaqueKind::Break => {
1777 out.loop_control.breaks.push(Predicate::True);
1778 }
1779 Node::Opaque(opaque) if opaque.kind == OpaqueKind::Continue => {
1780 out.loop_control.continues.push(Predicate::True);
1781 }
1782 Node::Control(_) | Node::Output(_) | Node::Comment(_) | Node::Opaque(_) => {}
1783 }
1784 out
1785 }
1786
1787 fn split_structural_children<'n>(
1795 &self,
1796 view: NodeView<'n>,
1797 container_indent: usize,
1798 ) -> (Vec<NodeView<'n>>, Vec<NodeView<'n>>) {
1799 view.in_scope_children()
1800 .into_iter()
1801 .partition(|child| self.node_belongs_inside(child.node, container_indent))
1802 }
1803
1804 fn node_belongs_inside(&self, node: &Node, container_indent: usize) -> bool {
1805 match node {
1806 Node::Mapping(entry) => entry.indent > container_indent,
1807 Node::Sequence(item) => item.indent >= container_indent,
1808 Node::Scalar(line) => line.indent > container_indent,
1809 Node::Control(region) => region
1810 .branches
1811 .iter()
1812 .flat_map(|branch| &branch.body)
1813 .all(|child| self.node_belongs_inside(child, container_indent)),
1814 Node::Output(action) => {
1815 self.line_indent(action.span.start) > container_indent
1818 || parse_expr_text(self.text(action.span))
1819 .iter()
1820 .rev()
1821 .any(|expr| expr.fragment_indent_width().is_some())
1822 }
1823 Node::Comment(_) | Node::Opaque(_) => true,
1824 }
1825 }
1826
1827 pub(super) fn entry_key(&mut self, parts: &ScalarParts) -> EntryKey {
1828 let has_hole = parts
1829 .parts
1830 .iter()
1831 .any(|part| matches!(part, ScalarPart::Hole(_)));
1832 if !has_hole {
1833 let key = syntax::unquote_yaml_scalar(self.text(parts.span).trim()).to_string();
1834 if !key.is_empty() {
1835 return EntryKey::Literal(key);
1836 }
1837 }
1838 let mut string = AbstractString::default();
1839 for part in &parts.parts {
1840 match part {
1841 ScalarPart::Text(span) => {
1842 let text = self.text(*span);
1843 if !text.is_empty() {
1844 string
1845 .parts
1846 .push(StringPart::Text([text.to_string()].into_iter().collect()));
1847 }
1848 }
1849 ScalarPart::Hole(span) => {
1850 let hole = self.hole_string(*span);
1851 string.parts.extend(hole.parts);
1852 }
1853 }
1854 }
1855 EntryKey::Dynamic(string)
1856 }
1857
1858 fn hole_string(&mut self, span: Span) -> AbstractString {
1862 let arms = self.eval_hole_parts(span);
1863 AbstractString {
1864 parts: arms.into_iter().flat_map(|(_, parts)| parts).collect(),
1865 suppressed: false,
1866 }
1867 }
1868}