use super::{
BTreeMap, BTreeSet, ConditionalGuard, ContractPathAccumulator, ContractRequirementImplication,
ContractRequirementTarget, FailValueRequirement, Guard, GuardDnf, GuardValue,
MemberAccessConditions, Predicate, TruthCondition, lowerable_range_outer_guards,
member_local_truthy_selector, path_accumulator, path_contains_wildcard,
predicate_is_truthy_disjunction_over, predicate_to_guard, record_range_input_capture,
remove_redundant_approximate_conditions, terminal_clause_guard,
};
#[expect(
clippy::too_many_lines,
reason = "keeping this semantic operation together makes its state transitions easier to audit"
)]
pub(super) fn record_fail_conjunction(
paths: &mut BTreeMap<String, ContractPathAccumulator>,
terminal_clauses: &mut Vec<Vec<ConditionalGuard>>,
capture: &crate::eval_effect::FailCapture,
range_modes: &crate::range_modes::RangeModes,
) {
if let crate::eval_effect::CaptureKind::RangeKeyStrings {
paths: range_key_string_paths,
} = &capture.kind
{
record_range_key_string_requirements(paths, capture, range_key_string_paths, range_modes);
return;
}
if let crate::eval_effect::CaptureKind::CollectionItems {
paths: collection_paths,
schema_type,
pattern,
} = &capture.kind
{
record_collection_item_requirements(
paths,
capture,
collection_paths,
schema_type,
pattern.as_deref(),
);
return;
}
if let crate::eval_effect::CaptureKind::IndexAccess { path, index } = &capture.kind {
record_index_access_requirement(paths, capture, path, *index);
return;
}
if let crate::eval_effect::CaptureKind::SplitIndexAccess {
paths: source_paths,
separator,
index,
total_text_preimage,
} = &capture.kind
{
record_split_index_access_requirement(
paths,
capture,
source_paths,
separator,
*index,
*total_text_preimage,
);
return;
}
if let crate::eval_effect::CaptureKind::ValueType {
path,
schema_type,
null_aborts,
} = &capture.kind
{
record_value_requirement_capture(
paths,
capture,
path,
if *null_aborts {
FailValueRequirement::SchemaTypeEvenNull(schema_type.clone())
} else {
FailValueRequirement::SchemaType(schema_type.clone())
},
);
return;
}
if let crate::eval_effect::CaptureKind::StringRequirement {
path,
route,
selection,
} = &capture.kind
{
if *route == crate::eval_effect::StringRequirementRoute::Serialized {
return;
}
if *route == crate::eval_effect::StringRequirementRoute::Selected
&& let Some(requirement_capture) =
selected_member_requirement_capture(capture, path, selection)
{
record_value_requirement_capture(
paths,
&requirement_capture,
path,
FailValueRequirement::SchemaType("string".to_string()),
);
return;
}
let mut requirement_capture = capture.clone();
requirement_capture
.conjunction
.extend(selection.iter().cloned());
requirement_capture.conjunction.sort();
requirement_capture.conjunction.dedup();
if *route == crate::eval_effect::StringRequirementRoute::Direct
&& requirement_capture.conjunction.is_empty()
{
record_unconditional_string_requirement_facts(paths, path);
} else if string_requirement_has_execution_scope(&requirement_capture, path, range_modes) {
record_value_requirement_capture(
paths,
&requirement_capture,
path,
FailValueRequirement::SchemaType("string".to_string()),
);
if let Some(collection_path) = member_range_path(path)
&& requirement_capture.conjunction.iter().all(|predicate| {
matches!(
predicate,
Predicate::Guard(Guard::Range { path }) if path == &collection_path
)
})
&& requirement_capture
.ranged
.mode(&collection_path)
.member_identity
{
record_unconditional_string_requirement_facts(paths, path);
}
}
return;
}
if let crate::eval_effect::CaptureKind::RangeInput {
path,
destructured,
json_decoded,
} = &capture.kind
{
record_range_input_capture(paths, capture, path, *destructured, *json_decoded);
return;
}
if let crate::eval_effect::CaptureKind::RangeSelection {
path,
chain,
allow_integer,
} = &capture.kind
{
let mut capture = capture.clone();
let has_chain_disjunction = capture
.conjunction
.iter()
.any(|predicate| predicate_is_truthy_disjunction_over(predicate, chain));
let has_all_markers = chain.iter().all(|candidate| {
capture.conjunction.iter().any(|predicate| {
matches!(
predicate,
Predicate::Guard(Guard::Truthy { path } | Guard::With { path })
if path == candidate
)
})
});
if has_chain_disjunction && has_all_markers {
let mut remaining_markers = chain.iter().cloned().collect::<BTreeSet<_>>();
capture.conjunction.retain(|predicate| {
if predicate_is_truthy_disjunction_over(predicate, chain) {
return false;
}
let Predicate::Guard(
Guard::Truthy { path: marker } | Guard::With { path: marker },
) = predicate
else {
return true;
};
!remaining_markers.remove(marker)
});
}
record_value_requirement_capture(
paths,
&capture,
path,
FailValueRequirement::Iterable {
allow_integer: *allow_integer,
},
);
return;
}
if let crate::eval_effect::CaptureKind::DigSubject { path } = &capture.kind {
record_value_requirement_capture(
paths,
capture,
path,
FailValueRequirement::SchemaTypeEvenNull("object".to_string()),
);
return;
}
if let crate::eval_effect::CaptureKind::RequiredPresence { path } = &capture.kind {
let mut segments = helm_schema_core::split_value_path(path);
let Some(member) = segments.pop() else {
return;
};
if segments.is_empty() {
record_absence_abort_clause(terminal_clauses, capture, path);
return;
}
let parent = segments.join(".");
record_value_requirement_capture(
paths,
capture,
&parent,
FailValueRequirement::HasMemberEvenDefaulted(member),
);
return;
}
if let crate::eval_effect::CaptureKind::AbsenceAborts { path } = &capture.kind {
if path_contains_wildcard(path) {
let mut segments = helm_schema_core::split_value_path(path);
let Some(member) = segments.pop() else {
return;
};
if member == "*" || segments.is_empty() {
return;
}
if capture.conjunction.iter().any(|predicate| {
matches!(
predicate,
Predicate::Guard(Guard::Truthy { path: guard } | Guard::With { path: guard })
if guard == path
)
}) {
return;
}
record_value_requirement_capture(
paths,
capture,
&segments.join("."),
FailValueRequirement::HasMemberEvenDefaulted(member),
);
return;
}
record_absence_abort_clause(terminal_clauses, capture, path);
return;
}
if let crate::eval_effect::CaptureKind::ComparableKind { path, schema_type } = &capture.kind {
record_value_requirement_capture(
paths,
capture,
path,
FailValueRequirement::ComparableKind(schema_type.clone()),
);
return;
}
if let crate::eval_effect::CaptureKind::ValuePattern {
path,
pattern,
templated,
} = &capture.kind
{
record_value_requirement_capture(
paths,
capture,
path,
FailValueRequirement::MatchesPattern {
pattern: pattern.clone(),
templated: *templated,
},
);
return;
}
if let crate::eval_effect::CaptureKind::QuotedSerialization {
path,
style,
templated,
} = &capture.kind
{
record_value_requirement_capture(
paths,
capture,
path,
FailValueRequirement::QuotedSerializationSafe {
style: *style,
templated: *templated,
},
);
return;
}
if let crate::eval_effect::CaptureKind::PrintfStringOperand { path } = &capture.kind {
record_value_requirement_capture(
paths,
capture,
path,
FailValueRequirement::PrintfStringOperand,
);
return;
}
if let crate::eval_effect::CaptureKind::PlainSlotText {
path,
token_initial,
templated,
} = &capture.kind
{
record_value_requirement_capture(
paths,
capture,
path,
FailValueRequirement::PlainScalarSafe {
token_initial: *token_initial,
templated: *templated,
},
);
return;
}
if let crate::eval_effect::CaptureKind::RangeKeyPlainSlot {
paths: collection_paths,
} = &capture.kind
{
record_range_key_plain_slot_requirements(paths, capture, collection_paths, range_modes);
return;
}
if let crate::eval_effect::CaptureKind::MemberAccess { handled_kinds } = &capture.kind {
record_member_access_capture(paths, capture, handled_kinds, range_modes);
return;
}
let conjunction = remove_redundant_approximate_conditions(&capture.conjunction);
if conjunction.iter().any(|predicate| {
predicate.contains_approximation() && fail_outer_guard(predicate).is_none()
}) {
return;
}
if conjunction
.iter()
.flat_map(Predicate::value_paths)
.any(|path| path.starts_with('$'))
{
return;
}
if record_range_key_prefix_requirement(paths, &capture.kind, &conjunction) {
return;
}
if record_range_key_matches_requirement(paths, &capture.kind, &conjunction) {
return;
}
let or_covered: BTreeSet<&str> = capture
.conjunction
.iter()
.filter_map(|predicate| match predicate {
Predicate::Or(items) => Some(items.iter().filter_map(|item| match item {
Predicate::Guard(Guard::Truthy { path } | Guard::With { path }) => {
Some(path.as_str())
}
_ => None,
})),
_ => None,
})
.flatten()
.collect();
let conjunction: Vec<Predicate> = conjunction
.iter()
.filter(|predicate| {
!matches!(
predicate,
Predicate::Guard(Guard::With { path }) if or_covered.contains(path.as_str())
)
})
.cloned()
.collect();
let conjunction = &conjunction;
let execution_range_paths = conjunction.iter().filter_map(|predicate| match predicate {
Predicate::Guard(Guard::Range { path }) => Some(path.as_str()),
_ => None,
});
let test_candidate_paths = conjunction
.iter()
.filter(|predicate| predicate_is_negatable_test(predicate))
.flat_map(Predicate::value_paths)
.collect::<BTreeSet<_>>();
let ranged = execution_range_paths
.filter(|path| {
range_modes.mode(path).member_identity || capture.ranged.mode(path).member_identity
})
.chain(
capture
.ranged
.iter()
.filter_map(|(path, mode)| mode.member_identity.then_some(path)),
)
.filter(|path| {
let member = format!("{path}.*");
test_candidate_paths.iter().any(|candidate| {
candidate == &member
|| helm_schema_core::values_path_is_descendant(candidate, &member)
})
})
.max_by_key(|path| helm_schema_core::split_value_path(path).len());
let member_scope = ranged.map(|path| format!("{path}.*"));
let mut outer_guards = Vec::new();
let mut member_tests: Vec<&Predicate> = Vec::new();
let mut requirements = Vec::new();
let mut test_paths: BTreeSet<String> = BTreeSet::new();
for predicate in conjunction {
if let Predicate::Guard(Guard::Range { path }) = predicate {
if ranged == Some(path.as_str()) {
continue;
}
if capture.ranged.mode(path).input_identity {
outer_guards.push(ConditionalGuard::Truthy { path: path.clone() });
continue;
}
return;
}
let paths_of = predicate.value_paths();
if let Some(scope) = &member_scope {
if !paths_of.is_empty()
&& paths_of
.iter()
.all(|path| path == scope || path.starts_with(&format!("{scope}.")))
{
member_tests.push(predicate);
continue;
}
} else if paths_of.len() == 1 && predicate_is_negatable_test(predicate) {
let path = paths_of.iter().next().cloned().unwrap_or_default();
if let Some(required) =
requirements_from_negation(predicate, &path).filter(|required| !required.is_empty())
{
requirements.extend(required);
test_paths.insert(path);
continue;
}
}
let Some(guard) = fail_outer_guard(predicate) else {
return;
};
if guard
.value_paths()
.iter()
.any(|path| path_contains_wildcard(path))
{
return;
}
outer_guards.push(guard);
}
let mut member_field: Option<Vec<String>> = None;
if let Some(scope) = &member_scope
&& !member_tests.is_empty()
{
let requirements_at = |at: &str| -> Option<Vec<Vec<FailValueRequirement>>> {
member_tests
.iter()
.map(|predicate| {
requirements_from_negation(predicate, at)
.filter(|required| !required.is_empty())
})
.collect::<Option<Vec<_>>>()
};
let combine = |mut alternatives: Vec<Vec<FailValueRequirement>>| {
if alternatives.len() == 1 {
alternatives.remove(0)
} else {
alternatives.sort();
alternatives.dedup();
vec![FailValueRequirement::AnyOf(alternatives)]
}
};
if let Some(required) = requirements_at(scope) {
requirements.extend(combine(required));
test_paths.insert(scope.clone());
} else {
let field_path = {
let mut paths: BTreeSet<String> = member_tests
.iter()
.flat_map(|predicate| predicate.value_paths())
.collect();
match paths.pop_first() {
Some(path) if paths.is_empty() && !path[scope.len()..].contains('*') => {
Some(path)
}
_ => None,
}
};
let Some(field_path) = field_path.filter(|path| path != scope) else {
return;
};
let Some(required) = requirements_at(&field_path) else {
return;
};
if required
.iter()
.flatten()
.any(|requirement| matches!(requirement, FailValueRequirement::HelmFalsy))
{
return;
}
member_field = Some(helm_schema_core::split_value_path(
&field_path[scope.len() + 1..],
));
requirements.extend(combine(required));
test_paths.insert(field_path);
}
}
if requirements.is_empty() || test_paths.len() != 1 {
if ranged.is_none() && conjunction.is_empty() {
if !terminal_clauses.iter().any(Vec::is_empty) {
terminal_clauses.push(Vec::new());
}
} else if ranged.is_none()
&& !conjunction
.iter()
.any(|predicate| matches!(predicate, Predicate::Guard(Guard::Range { .. })))
{
let clause = conjunction
.iter()
.map(terminal_clause_guard)
.collect::<Option<Vec<_>>>();
if let Some(mut clause) = clause {
clause.sort();
clause.dedup();
if !clause.is_empty() && !terminal_clauses.contains(&clause) {
terminal_clauses.push(clause);
}
}
}
return;
}
let target = if let Some(path) = ranged {
path.to_string()
} else {
let Some(path) = test_paths.into_iter().next() else {
return;
};
path
};
requirements.sort();
requirements.dedup();
let contradictory = requirements.iter().any(|requirement| {
matches!(
requirement,
FailValueRequirement::SchemaType(schema_type)
if requirements
.contains(&FailValueRequirement::NotSchemaType(schema_type.clone()))
)
});
if contradictory {
return;
}
outer_guards.sort();
outer_guards.dedup();
let implication = ContractRequirementImplication {
outer_guards,
target: ranged.map_or(ContractRequirementTarget::Value, |path| {
let mode = range_modes.mode(path);
let allow_integer = !mode.destructured && !mode.json_decoded;
match member_field {
Some(target_path) => ContractRequirementTarget::MembersAt {
target_path,
allow_integer,
},
None => ContractRequirementTarget::Members { allow_integer },
}
}),
requirements,
};
let acc = path_accumulator(paths, &target);
acc.referenced = true;
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
}
fn selected_member_requirement_capture(
capture: &crate::eval_effect::FailCapture,
path: &str,
selection: &[Predicate],
) -> Option<crate::eval_effect::FailCapture> {
let collection_path = member_range_path(path)?;
if !selection.iter().any(|predicate| {
matches!(
predicate,
Predicate::Guard(Guard::Range { path }) if path == &collection_path
)
}) {
return None;
}
let mut capture = capture.clone();
capture.conjunction.push(Predicate::Guard(Guard::Range {
path: collection_path,
}));
capture.conjunction.sort();
capture.conjunction.dedup();
Some(capture)
}
fn member_range_path(path: &str) -> Option<String> {
let segments = helm_schema_core::split_value_path(path);
let wildcard = segments.iter().position(|segment| segment == "*")?;
Some(helm_schema_core::join_value_path(
segments.get(..wildcard)?.iter().cloned(),
))
}
fn string_requirement_has_execution_scope(
capture: &crate::eval_effect::FailCapture,
path: &str,
range_modes: &crate::range_modes::RangeModes,
) -> bool {
let segments = helm_schema_core::split_value_path(path);
let mut required_ranges = BTreeSet::new();
for (index, segment) in segments.iter().enumerate() {
if segment == "*" {
required_ranges.insert(helm_schema_core::join_value_path(
segments.get(..index).unwrap_or_default().iter().cloned(),
));
}
}
required_ranges.iter().all(|required| {
capture.ranged.mode(required).member_identity
|| range_modes.mode(required).member_identity
|| capture.conjunction.iter().any(|predicate| {
matches!(predicate, Predicate::Guard(Guard::Range { path }) if path == required)
})
})
}
fn record_unconditional_string_requirement_facts(
paths: &mut BTreeMap<String, ContractPathAccumulator>,
path: &str,
) {
if path.trim().is_empty() {
return;
}
let acc = path_accumulator(paths, path);
acc.referenced = true;
acc.type_hints.insert("string".to_string());
acc.facts.facts.has_string_contract = true;
acc.facts.facts.has_non_self_guarded_string_contract = true;
}
pub(super) fn record_range_key_prefix_requirement(
paths: &mut BTreeMap<String, ContractPathAccumulator>,
kind: &crate::eval_effect::CaptureKind,
conjunction: &[Predicate],
) -> bool {
if !matches!(kind, crate::eval_effect::CaptureKind::Fail) {
return false;
}
let prefixes = conjunction
.iter()
.filter_map(|predicate| match predicate {
Predicate::Guard(Guard::RangeKeyPrefix { path, prefix }) => {
Some((path.as_str(), prefix.as_str()))
}
_ => None,
})
.collect::<Vec<_>>();
let [(collection_path, prefix)] = prefixes.as_slice() else {
return !prefixes.is_empty();
};
let member_scope = format!("{collection_path}.*");
let has_matching_range = conjunction.iter().any(|predicate| {
matches!(predicate, Predicate::Guard(Guard::Range { path }) if path == collection_path)
});
if !has_matching_range {
return true;
}
let mut outer_guards = Vec::new();
let mut requirements = Vec::new();
for predicate in conjunction {
match predicate {
Predicate::Guard(Guard::RangeKeyPrefix {
path,
prefix: candidate,
}) if path == collection_path && candidate == prefix => {}
Predicate::Guard(Guard::Range { path }) if path == collection_path => {}
_ if {
let predicate_paths = predicate.value_paths();
!predicate_paths.is_empty()
&& predicate_paths.iter().all(|path| {
path == &member_scope
|| helm_schema_core::values_path_is_descendant(path, &member_scope)
})
} =>
{
let Some(mut required) = requirements_from_negation(predicate, &member_scope)
else {
return true;
};
requirements.append(&mut required);
}
_ => {
let Some(guard) = fail_outer_guard(predicate) else {
return true;
};
if guard
.value_paths()
.iter()
.any(|path| path_contains_wildcard(path))
{
return true;
}
outer_guards.push(guard);
}
}
}
if requirements.is_empty() {
return true;
}
outer_guards.sort();
outer_guards.dedup();
requirements.sort();
requirements.dedup();
let implication = ContractRequirementImplication {
outer_guards,
target: ContractRequirementTarget::MembersMatchingPrefix {
prefix: (*prefix).to_string(),
},
requirements,
};
let acc = path_accumulator(paths, collection_path);
acc.referenced = true;
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
true
}
pub(super) fn record_range_key_matches_requirement(
paths: &mut BTreeMap<String, ContractPathAccumulator>,
kind: &crate::eval_effect::CaptureKind,
conjunction: &[Predicate],
) -> bool {
fn key_match(predicate: &Predicate) -> Option<(bool, &str, &str)> {
match predicate {
Predicate::Guard(Guard::RangeKeyMatches { path, pattern }) => {
Some((false, path.as_str(), pattern.as_str()))
}
Predicate::Not(inner) => match inner.as_ref() {
Predicate::Guard(Guard::RangeKeyMatches { path, pattern }) => {
Some((true, path.as_str(), pattern.as_str()))
}
_ => None,
},
_ => None,
}
}
if !matches!(kind, crate::eval_effect::CaptureKind::Fail) {
return false;
}
let matches: Vec<(bool, &str, &str)> = conjunction.iter().filter_map(key_match).collect();
let [(negated, collection_path, pattern)] = matches.as_slice() else {
return !matches.is_empty();
};
let has_matching_range = conjunction.iter().any(|predicate| {
matches!(predicate, Predicate::Guard(Guard::Range { path }) if path == collection_path)
});
if !has_matching_range {
return true;
}
let member_scope = format!("{collection_path}.*");
let mut outer_guards = Vec::new();
for predicate in conjunction {
if key_match(predicate).is_some() {
continue;
}
match predicate {
Predicate::Guard(Guard::Range { path }) if path == collection_path => {}
_ if predicate.value_paths().iter().any(|path| {
path == &member_scope
|| helm_schema_core::values_path_is_descendant(path, &member_scope)
}) =>
{
return true;
}
_ => {
let Some(guard) = fail_outer_guard(predicate) else {
return true;
};
if guard
.value_paths()
.iter()
.any(|path| path_contains_wildcard(path))
{
return true;
}
outer_guards.push(guard);
}
}
}
outer_guards.sort();
outer_guards.dedup();
let requirement = if *negated {
FailValueRequirement::MatchesPattern {
pattern: (*pattern).to_string(),
templated: false,
}
} else {
FailValueRequirement::NotMatchesPattern {
pattern: (*pattern).to_string(),
}
};
let implication = ContractRequirementImplication {
outer_guards,
target: ContractRequirementTarget::Keys,
requirements: vec![requirement],
};
let acc = path_accumulator(paths, collection_path);
acc.referenced = true;
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
true
}
pub(super) fn capture_outer_guards(
capture: &crate::eval_effect::FailCapture,
) -> Option<Vec<ConditionalGuard>> {
let conjunction = remove_redundant_approximate_conditions(&capture.conjunction);
let key_equals_ranges: BTreeSet<&str> = conjunction
.iter()
.filter_map(|predicate| match predicate {
Predicate::Guard(Guard::RangeKeyEquals { path, .. }) => Some(path.as_str()),
_ => None,
})
.collect();
let mut guards = conjunction
.iter()
.filter(|predicate| {
!matches!(
predicate,
Predicate::Guard(Guard::Range { path }) if key_equals_ranges.contains(path.as_str())
)
})
.map(|predicate| match predicate {
Predicate::Guard(Guard::Range { path }) => capture
.ranged
.mode(path)
.input_identity
.then(|| ConditionalGuard::Truthy { path: path.clone() }),
predicate => fail_outer_guard(predicate),
})
.collect::<Option<Vec<_>>>()?;
if guards
.iter()
.flat_map(ConditionalGuard::value_paths)
.any(|path| path_contains_wildcard(&path))
{
return None;
}
guards.sort();
guards.dedup();
Some(guards)
}
pub(super) fn fail_outer_guard(predicate: &Predicate) -> Option<ConditionalGuard> {
if !predicate.contains_approximation() {
return predicate_to_guard(predicate, None);
}
match predicate {
Predicate::Approximate {
sound_subset: Some(sound_subset),
..
} => predicate_to_guard(sound_subset, None),
Predicate::Not(inner) => {
let Predicate::And(items) = inner.as_ref() else {
return None;
};
let mut decodable: Vec<ConditionalGuard> = items
.iter()
.filter(|item| !item.contains_approximation())
.filter_map(|item| predicate_to_guard(item, None))
.collect();
decodable.sort();
decodable.dedup();
let inner = match decodable.as_slice() {
[] => return None,
[guard] => guard.clone(),
_ => ConditionalGuard::AllOf(decodable),
};
Some(ConditionalGuard::Not(Box::new(inner)))
}
Predicate::Or(items) => {
let mut guards = items
.iter()
.filter_map(fail_outer_guard)
.collect::<Vec<_>>();
guards.sort();
guards.dedup();
match guards.as_slice() {
[] => None,
[guard] => Some(guard.clone()),
_ => Some(ConditionalGuard::AnyOf(guards)),
}
}
Predicate::And(items) => {
let mut guards = items
.iter()
.map(fail_outer_guard)
.collect::<Option<Vec<_>>>()?;
guards.sort();
guards.dedup();
match guards.as_slice() {
[] => None,
[guard] => Some(guard.clone()),
_ => Some(ConditionalGuard::AllOf(guards)),
}
}
_ => None,
}
}
pub(super) fn member_dispatch_escape(
predicate: &Predicate,
member_path: &str,
) -> Option<FailValueRequirement> {
let (negated, guard) = match predicate {
Predicate::Guard(guard) => (false, guard),
Predicate::Not(inner) => match inner.as_ref() {
Predicate::Guard(guard) => (true, guard),
_ => return None,
},
_ => return None,
};
let (schema_type, excluded) = match guard {
Guard::TypeIs { path, schema_type } if path == member_path => (schema_type, false),
Guard::NotTypeIs { path, schema_type } if path == member_path => (schema_type, true),
_ => return None,
};
if negated == excluded {
Some(FailValueRequirement::NotSchemaType(schema_type.clone()))
} else {
Some(FailValueRequirement::SchemaType(schema_type.clone()))
}
}
pub(super) fn complements_requirement(
escape: &FailValueRequirement,
requirement: &FailValueRequirement,
) -> bool {
match (escape, requirement) {
(
FailValueRequirement::NotSchemaType(excluded),
FailValueRequirement::SchemaType(required),
)
| (
FailValueRequirement::SchemaType(required),
FailValueRequirement::NotSchemaType(excluded),
) => excluded == required,
_ => false,
}
}
#[expect(
clippy::too_many_lines,
reason = "keeping this semantic operation together makes its state transitions easier to audit"
)]
pub(super) fn record_value_requirement_capture(
paths: &mut BTreeMap<String, ContractPathAccumulator>,
capture: &crate::eval_effect::FailCapture,
path: &str,
mut requirement: FailValueRequirement,
) {
if path.trim().is_empty() {
return;
}
let path_segments = helm_schema_core::split_value_path(path);
if let Some(first_wildcard) = path_segments.iter().position(|segment| segment == "*") {
let (collection_segments, wildcard_tail) = path_segments.split_at(first_wildcard);
let wildcard_count = wildcard_tail
.iter()
.take_while(|segment| segment.as_str() == "*")
.count();
let suffix = wildcard_tail.get(wildcard_count..).unwrap_or_default();
if wildcard_count >= 2 && !suffix.iter().any(|segment| segment == "*") {
let collection_path = helm_schema_core::join_value_path(collection_segments);
if collection_path.is_empty() {
return;
}
let ranged_collections = (0..wildcard_count)
.map(|depth| {
let mut segments = collection_segments.to_vec();
segments.extend(std::iter::repeat_n("*".to_string(), depth));
helm_schema_core::join_value_path(segments)
})
.collect::<BTreeSet<_>>();
if ranged_collections.iter().any(|path| {
let mode = capture.ranged.mode(path);
!mode.member_identity || (!mode.destructured && !mode.json_decoded)
}) {
return;
}
let conjunction = remove_redundant_approximate_conditions(&capture.conjunction);
let mut outer_guards = Vec::new();
for predicate in &conjunction {
if matches!(
predicate,
Predicate::Guard(Guard::Range { path })
if ranged_collections.contains(path)
) {
continue;
}
let Some(guard) = fail_outer_guard(predicate) else {
return;
};
if guard
.value_paths()
.iter()
.any(|path| path_contains_wildcard(path))
{
return;
}
outer_guards.push(guard);
}
outer_guards.sort();
outer_guards.dedup();
let mut target_path =
std::iter::repeat_n("*".to_string(), wildcard_count - 1).collect::<Vec<_>>();
target_path.extend(suffix.iter().cloned());
let implication = ContractRequirementImplication {
outer_guards,
target: ContractRequirementTarget::MembersAt {
target_path,
allow_integer: false,
},
requirements: vec![requirement],
};
let acc = path_accumulator(paths, &collection_path);
acc.referenced = true;
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
return;
}
}
let member_field_split = path.split_once(".*.").filter(|(collection, suffix)| {
!collection.contains('*') && !suffix.is_empty() && !suffix.contains('*')
});
if let Some((collection_path, member_suffix)) = member_field_split {
let conjunction = remove_redundant_approximate_conditions(&capture.conjunction);
let key_equals_ranges: BTreeSet<&str> = conjunction
.iter()
.filter_map(|predicate| match predicate {
Predicate::Guard(Guard::RangeKeyEquals { path, .. }) => Some(path.as_str()),
_ => None,
})
.collect();
let mut outer_guards = Vec::new();
let mut self_truthy_selected = false;
let mut member_selector = None;
for predicate in &conjunction {
match predicate {
Predicate::Guard(Guard::Range { path })
if path == collection_path || key_equals_ranges.contains(path.as_str()) => {}
Predicate::Guard(Guard::Truthy { path: guard_path }) if guard_path == path => {
self_truthy_selected = true;
}
_ => {
if let Some(relative) =
member_local_truthy_selector(collection_path, predicate, Some(path))
{
if member_selector.replace(relative).is_some() {
return;
}
continue;
}
let Some(guard) = fail_outer_guard(predicate) else {
return;
};
if guard
.value_paths()
.iter()
.any(|path| path_contains_wildcard(path))
{
return;
}
outer_guards.push(guard);
}
}
}
let requirement = if self_truthy_selected {
match requirement {
FailValueRequirement::SchemaType(schema_type) => {
FailValueRequirement::TruthyImpliesSchemaType(schema_type)
}
_ => return,
}
} else {
requirement
};
outer_guards.sort();
outer_guards.dedup();
let allow_integer = {
let mode = capture.ranged.mode(collection_path);
mode.member_identity && !mode.destructured && !mode.json_decoded
};
let target_path = helm_schema_core::split_value_path(member_suffix);
let implication = ContractRequirementImplication {
outer_guards,
target: match member_selector {
Some(guard_path) => ContractRequirementTarget::MembersAtWhereTruthy {
guard_path,
target_path,
allow_integer,
},
None => ContractRequirementTarget::MembersAt {
target_path,
allow_integer,
},
},
requirements: vec![requirement],
};
let acc = path_accumulator(paths, collection_path);
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
return;
}
let (target_path, target, outer_guards) = if let Some(collection_path) = path.strip_suffix(".*")
{
let mut outer_guards = Vec::new();
let mut prefix = None;
let mut member_selector = None;
let mut dispatch_escapes = Vec::new();
for predicate in &capture.conjunction {
match predicate {
Predicate::Guard(Guard::Range { path }) if path == collection_path => {}
Predicate::Guard(Guard::RangeKeyPrefix {
path,
prefix: candidate,
}) if path == collection_path => {
if prefix.replace(candidate.clone()).is_some() {
return;
}
}
_ => {
if let Some(escape) = member_dispatch_escape(predicate, path) {
dispatch_escapes.push(vec![escape]);
continue;
}
if let Some(relative) =
member_local_truthy_selector(collection_path, predicate, None)
{
if member_selector.replace(relative).is_some() {
return;
}
continue;
}
let Some(guard) = predicate_to_guard(predicate, None) else {
return;
};
if guard
.value_paths()
.iter()
.any(|path| path_contains_wildcard(path))
{
return;
}
outer_guards.push(guard);
}
}
}
if !dispatch_escapes.is_empty() {
if dispatch_escapes
.iter()
.flatten()
.any(|escape| complements_requirement(escape, &requirement))
{
return;
}
let mut alternatives = vec![vec![requirement]];
alternatives.append(&mut dispatch_escapes);
requirement = FailValueRequirement::AnyOf(alternatives);
}
outer_guards.sort();
outer_guards.dedup();
let allow_integer = {
let mode = capture.ranged.mode(collection_path);
mode.member_identity && !mode.destructured && !mode.json_decoded
};
let target = match (prefix, member_selector) {
(Some(prefix), _) => ContractRequirementTarget::MembersMatchingPrefix { prefix },
(None, Some(guard_path)) => ContractRequirementTarget::MembersAtWhereTruthy {
guard_path,
target_path: Vec::new(),
allow_integer,
},
(None, None) => ContractRequirementTarget::Members { allow_integer },
};
(collection_path, target, outer_guards)
} else {
if path_contains_wildcard(path) {
return;
}
let Some(outer_guards) = capture_outer_guards(capture) else {
return;
};
(path, ContractRequirementTarget::Value, outer_guards)
};
let implication = ContractRequirementImplication {
outer_guards,
target,
requirements: vec![requirement],
};
let acc = path_accumulator(paths, target_path);
acc.referenced = true;
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
}
pub(super) fn record_collection_item_requirements(
paths: &mut BTreeMap<String, ContractPathAccumulator>,
capture: &crate::eval_effect::FailCapture,
collection_paths: &BTreeSet<String>,
schema_type: &str,
pattern: Option<&str>,
) {
let Some(outer_guards) = capture_outer_guards(capture) else {
return;
};
for path in collection_paths {
if path_contains_wildcard(path) {
continue;
}
let mut requirements = vec![FailValueRequirement::SchemaType(schema_type.to_string())];
if let Some(pattern) = pattern {
requirements.push(FailValueRequirement::MatchesPattern {
pattern: pattern.to_string(),
templated: false,
});
}
let implication = ContractRequirementImplication {
outer_guards: outer_guards.clone(),
target: ContractRequirementTarget::Members {
allow_integer: false,
},
requirements,
};
let acc = path_accumulator(paths, path);
acc.referenced = true;
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
}
}
pub(super) fn record_absence_abort_clause(
terminal_clauses: &mut Vec<Vec<ConditionalGuard>>,
capture: &crate::eval_effect::FailCapture,
path: &str,
) {
let segments = helm_schema_core::split_value_path(path);
if segments.iter().any(|segment| segment == "*") {
return;
}
if segments
.iter()
.skip(2)
.any(|segment| segment == "global" || segment == "Values")
{
return;
}
let mut clause = Vec::new();
for predicate in &capture.conjunction {
let Some(guard) = terminal_clause_guard(predicate) else {
return;
};
if guard.value_paths().contains(path) {
return;
}
clause.push(guard);
}
clause.push(ConditionalGuard::Absent {
path: path.to_string(),
});
clause.sort();
clause.dedup();
if !terminal_clauses.contains(&clause) {
terminal_clauses.push(clause);
}
}
pub(super) fn record_index_access_requirement(
paths: &mut BTreeMap<String, ContractPathAccumulator>,
capture: &crate::eval_effect::FailCapture,
path: &str,
index: usize,
) {
if path.trim().is_empty() || path_contains_wildcard(path) {
return;
}
let Some(outer_guards) = capture_outer_guards(capture) else {
return;
};
let implication = ContractRequirementImplication {
outer_guards,
target: ContractRequirementTarget::Value,
requirements: vec![FailValueRequirement::IndexableAt(index)],
};
let acc = path_accumulator(paths, path);
acc.referenced = true;
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
}
pub(super) fn record_split_index_access_requirement(
paths: &mut BTreeMap<String, ContractPathAccumulator>,
capture: &crate::eval_effect::FailCapture,
source_paths: &BTreeSet<String>,
separator: &str,
index: usize,
allow_non_string: bool,
) {
if index == 0 || separator.is_empty() {
return;
}
let outer_guards = capture_outer_guards(capture);
for path in source_paths {
if path.trim().is_empty() {
continue;
}
if path_contains_wildcard(path) {
record_member_relative_split_requirement(
paths,
capture,
path,
separator,
index,
allow_non_string,
);
continue;
}
let Some(outer_guards) = outer_guards.clone() else {
continue;
};
let implication = ContractRequirementImplication {
outer_guards,
target: ContractRequirementTarget::Value,
requirements: vec![FailValueRequirement::SplitSegmentsAtLeast {
separator: separator.to_string(),
segments: index + 1,
allow_non_string,
}],
};
let acc = path_accumulator(paths, path);
acc.referenced = true;
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
}
}
pub(super) fn record_member_relative_split_requirement(
paths: &mut BTreeMap<String, ContractPathAccumulator>,
capture: &crate::eval_effect::FailCapture,
source_path: &str,
separator: &str,
index: usize,
allow_non_string: bool,
) {
let segments = helm_schema_core::split_value_path(source_path);
let Some(member_index) = segments.iter().rposition(|segment| segment == "*") else {
return;
};
if member_index == 0 || member_index + 1 >= segments.len() {
return;
}
let Some(collection_segments) = segments.get(..member_index) else {
return;
};
let Some(member_segments) = segments.get(..=member_index) else {
return;
};
let Some(target_path) = segments.get(member_index + 1..) else {
return;
};
let collection_path = helm_schema_core::join_value_path(collection_segments.to_vec());
let member_scope = helm_schema_core::join_value_path(member_segments.to_vec());
let target_path = target_path.to_vec();
let mut member_guards = Vec::new();
let mut outer_guards = Vec::new();
for predicate in &capture.conjunction {
if matches!(predicate, Predicate::Guard(Guard::Range { path })
if path == &collection_path
|| helm_schema_core::values_path_is_descendant(&member_scope, path))
{
continue;
}
if let Predicate::Guard(Guard::Eq { path, value }) = predicate
&& let Some(relative) =
helm_schema_core::split_value_path(path).strip_prefix(member_segments)
&& !relative.is_empty()
&& !relative.iter().any(|segment| segment == "*")
{
member_guards.push((relative.to_vec(), value.clone()));
continue;
}
let Some(guard) = predicate_to_guard(predicate, None) else {
return;
};
if guard
.value_paths()
.iter()
.any(|path| path_contains_wildcard(path))
{
return;
}
outer_guards.push(guard);
}
let [(guard_path, value)] = member_guards.as_slice() else {
return;
};
outer_guards.sort();
outer_guards.dedup();
let implication = ContractRequirementImplication {
outer_guards,
target: ContractRequirementTarget::MembersWhereEquals {
guard_path: guard_path.clone(),
value: value.clone(),
target_path,
},
requirements: vec![FailValueRequirement::SplitSegmentsAtLeast {
separator: separator.to_string(),
segments: index + 1,
allow_non_string,
}],
};
let acc = path_accumulator(paths, &collection_path);
acc.referenced = true;
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
}
pub(super) fn record_range_key_string_requirements(
paths: &mut BTreeMap<String, ContractPathAccumulator>,
capture: &crate::eval_effect::FailCapture,
range_key_string_paths: &BTreeSet<String>,
range_modes: &crate::range_modes::RangeModes,
) {
if capture.contains_approximation() {
return;
}
for path in range_key_string_paths {
if path_contains_wildcard(path)
|| (!range_modes.mode(path).member_identity
&& !capture.ranged.mode(path).member_identity)
{
continue;
}
let Some(outer_guards) = lowerable_range_outer_guards(path, &capture.conjunction) else {
continue;
};
let implication = ContractRequirementImplication {
outer_guards,
target: ContractRequirementTarget::Keys,
requirements: vec![FailValueRequirement::SchemaType("string".to_string())],
};
let acc = path_accumulator(paths, path);
acc.referenced = true;
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
}
}
pub(super) fn record_range_key_plain_slot_requirements(
paths: &mut BTreeMap<String, ContractPathAccumulator>,
capture: &crate::eval_effect::FailCapture,
collection_paths: &BTreeSet<String>,
range_modes: &crate::range_modes::RangeModes,
) {
if capture.contains_approximation() {
return;
}
for path in collection_paths {
if path_contains_wildcard(path)
|| (!range_modes.mode(path).member_identity
&& !capture.ranged.mode(path).member_identity)
{
continue;
}
let Some(outer_guards) = lowerable_range_outer_guards(path, &capture.conjunction) else {
continue;
};
let implication = ContractRequirementImplication {
outer_guards,
target: ContractRequirementTarget::Keys,
requirements: vec![FailValueRequirement::PlainScalarSafe {
token_initial: true,
templated: false,
}],
};
let acc = path_accumulator(paths, path);
acc.referenced = true;
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
}
}
pub(super) fn predicate_is_negatable_test(predicate: &Predicate) -> bool {
match predicate {
Predicate::Not(inner) => !matches!(inner.as_ref(), Predicate::Guard(Guard::Range { .. })),
Predicate::Guard(Guard::TypeIs { .. } | Guard::Absent { .. } | Guard::Eq { .. }) => true,
Predicate::Guard(Guard::NotEq { path, .. } | Guard::Truthy { path }) => path.contains(".*"),
Predicate::Or(items) => items.iter().all(predicate_is_negatable_test),
_ => false,
}
}
pub(super) fn requirements_from_negation(
predicate: &Predicate,
scope: &str,
) -> Option<Vec<FailValueRequirement>> {
match predicate {
Predicate::Not(inner) => requirements_from_holding(inner, scope),
Predicate::Or(items) => {
let mut requirements = Vec::new();
for item in items {
requirements.append(&mut requirements_from_negation(item, scope)?);
}
Some(requirements)
}
Predicate::Guard(Guard::TypeIs { path, schema_type }) if path == scope => {
Some(vec![FailValueRequirement::NotSchemaType(
schema_type.clone(),
)])
}
Predicate::Guard(Guard::Absent { path }) => {
let member = path.strip_prefix(&format!("{scope}."))?;
(!member.contains('.'))
.then(|| vec![FailValueRequirement::HasMember(member.to_string())])
}
Predicate::Guard(Guard::Truthy { path }) if scope.contains(".*") => {
if path == scope {
return Some(vec![FailValueRequirement::HelmFalsy]);
}
let field = path.strip_prefix(&format!("{scope}."))?;
(!field.contains('*')).then(|| {
vec![FailValueRequirement::FieldHelmFalsy {
path: helm_schema_core::split_value_path(field),
}]
})
}
Predicate::Guard(Guard::NotEq { path, value }) if scope.contains(".*") => {
let field = path.strip_prefix(&format!("{scope}."))?;
(!field.contains('*')).then(|| {
vec![FailValueRequirement::FieldEquals {
path: helm_schema_core::split_value_path(field),
value: value.clone(),
}]
})
}
Predicate::Guard(Guard::Eq { path, value }) => match value {
GuardValue::String(text)
if path == scope && scope.contains(".*") && !text.is_empty() =>
{
Some(vec![FailValueRequirement::NotEquals(value.clone())])
}
GuardValue::Int(_) | GuardValue::Bool(_) if path == scope && scope.contains(".*") => {
Some(vec![FailValueRequirement::NotEquals(value.clone())])
}
GuardValue::String(text) if scope.contains(".*") && path != scope => {
if text.is_empty() {
return Some(Vec::new());
}
let field = path.strip_prefix(&format!("{scope}."))?;
(!field.contains('*')).then(|| {
vec![FailValueRequirement::FieldNotEquals {
path: helm_schema_core::split_value_path(field),
value: value.clone(),
}]
})
}
GuardValue::Int(_) | GuardValue::Bool(_) if scope.contains(".*") && path != scope => {
let field = path.strip_prefix(&format!("{scope}."))?;
(!field.contains('*')).then(|| {
vec![FailValueRequirement::FieldNotEquals {
path: helm_schema_core::split_value_path(field),
value: value.clone(),
}]
})
}
_ => Some(Vec::new()),
},
_ => None,
}
}
pub(super) fn requirements_from_holding(
predicate: &Predicate,
scope: &str,
) -> Option<Vec<FailValueRequirement>> {
match predicate {
Predicate::Guard(Guard::TypeIs { path, schema_type }) if path == scope => {
Some(vec![FailValueRequirement::SchemaType(schema_type.clone())])
}
Predicate::Guard(Guard::MatchesPattern {
path,
pattern,
templated,
}) if path == scope => Some(vec![FailValueRequirement::MatchesPattern {
pattern: pattern.clone(),
templated: *templated,
}]),
Predicate::Guard(Guard::Absent { path }) if path == scope => Some(Vec::new()),
Predicate::Guard(Guard::Truthy { path }) if path == scope => {
if scope.contains(".*") {
Some(vec![FailValueRequirement::HelmTruthy])
} else {
Some(Vec::new())
}
}
Predicate::Guard(Guard::Truthy { path }) => {
let member = path.strip_prefix(&format!("{scope}."))?;
if scope.contains(".*") {
return (!member.contains('*')).then(|| {
vec![FailValueRequirement::FieldHelmTruthy {
path: helm_schema_core::split_value_path(member),
}]
});
}
(!member.contains('.'))
.then(|| vec![FailValueRequirement::HasMember(member.to_string())])
}
Predicate::Guard(Guard::Eq { path, value }) if scope.contains(".*") => {
let field = path.strip_prefix(&format!("{scope}."))?;
(!field.contains('*')).then(|| {
vec![FailValueRequirement::FieldEquals {
path: helm_schema_core::split_value_path(field),
value: value.clone(),
}]
})
}
Predicate::And(items) => {
let mut requirements = Vec::new();
for item in items {
requirements.append(&mut requirements_from_holding(item, scope)?);
}
Some(requirements)
}
Predicate::Not(inner) => match inner.as_ref() {
Predicate::Guard(Guard::Absent { path }) => {
let member = path.strip_prefix(&format!("{scope}."))?;
(!member.contains('.'))
.then(|| vec![FailValueRequirement::HasMember(member.to_string())])
}
_ => requirements_from_negation(inner, scope),
},
_ => None,
}
}
#[expect(
clippy::too_many_lines,
reason = "keeping this semantic operation together makes its state transitions easier to audit"
)]
pub(super) fn record_member_access_capture(
paths: &mut BTreeMap<String, ContractPathAccumulator>,
capture: &crate::eval_effect::FailCapture,
handled_kinds: &BTreeSet<String>,
range_modes: &crate::range_modes::RangeModes,
) {
let incomplete = capture.contains_approximation();
let mut target = None;
for predicate in &capture.conjunction {
if let Predicate::Not(inner) = predicate
&& let Predicate::Guard(Guard::TypeIs { path, schema_type }) = inner.as_ref()
&& schema_type == "object"
{
target = Some(path.clone());
}
}
let Some(target) = target else {
return;
};
if let Some(parent) = target.strip_suffix(".*")
&& !path_contains_wildcard(parent)
{
if incomplete {
return;
}
if !capture.ranged.mode(parent).member_identity {
return;
}
let mut outer_guards = Vec::new();
for predicate in &capture.conjunction {
if matches!(
predicate,
Predicate::Guard(Guard::Range { path }) if path == parent
) || matches!(
predicate,
Predicate::Not(inner)
if matches!(
inner.as_ref(),
Predicate::Guard(Guard::TypeIs { path, schema_type })
if path == &target && schema_type == "object"
)
) {
continue;
}
let Some(guard) = predicate_to_guard(predicate, None) else {
return;
};
if guard
.value_paths()
.iter()
.any(|path| path_contains_wildcard(path))
{
return;
}
outer_guards.push(guard);
}
outer_guards.sort();
outer_guards.dedup();
let implication = ContractRequirementImplication {
outer_guards,
target: ContractRequirementTarget::Members {
allow_integer: {
let mode = range_modes.mode(parent);
let capture_mode = capture.ranged.mode(parent);
!mode.destructured
&& !capture_mode.destructured
&& !mode.json_decoded
&& !capture_mode.json_decoded
},
},
requirements: vec![FailValueRequirement::SchemaType("object".to_string())],
};
let acc = path_accumulator(paths, parent);
acc.referenced = true;
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
return;
}
if path_contains_wildcard(&target) {
return;
}
let mut outer = Vec::new();
let mut condition_lowerable = true;
for predicate in &capture.conjunction {
match predicate {
Predicate::Not(inner)
if matches!(
inner.as_ref(),
Predicate::Guard(Guard::TypeIs { path, schema_type })
if path == &target && schema_type == "object"
) =>
{
continue;
}
Predicate::Guard(Guard::With { path }) if !path_contains_wildcard(path) => {
outer.push(Predicate::truthy_path(path.clone()));
continue;
}
_ => {}
}
let predicate = if predicate.contains_approximation() {
let subset = TruthCondition::from_predicate(predicate.clone()).when_true();
if subset == Predicate::False {
condition_lowerable = false;
break;
}
subset
} else {
predicate.clone()
};
let Some(guard) = predicate_to_guard(&predicate, None) else {
condition_lowerable = false;
break;
};
if guard
.value_paths()
.iter()
.any(|path| path_contains_wildcard(path))
{
condition_lowerable = false;
break;
}
outer.push(predicate);
}
let access_conditions = &mut path_accumulator(paths, &target).member_access_conditions;
if condition_lowerable {
access_conditions.record(
handled_kinds.iter().cloned().collect(),
GuardDnf::from_conjunction(outer),
!incomplete,
);
} else {
access_conditions.mark_incomplete();
}
}
type MemberAccessGuardSets = BTreeMap<Vec<String>, BTreeSet<Vec<ConditionalGuard>>>;
pub(super) fn lower_member_access_condition(
condition: &GuardDnf,
) -> Option<BTreeSet<Vec<ConditionalGuard>>> {
let mut guard_sets = BTreeSet::new();
for conjunction in condition.disjuncts() {
let mut guards = Vec::new();
for predicate in conjunction {
let guard = predicate_to_guard(predicate, None)?;
if guard
.value_paths()
.iter()
.any(|path| path_contains_wildcard(path))
{
return None;
}
guards.push(guard);
}
guards.sort();
guards.dedup();
guard_sets.insert(guards);
}
Some(factor_guard_sets(guard_sets))
}
pub(super) fn fold_member_access_arms(
guard_sets: BTreeSet<Vec<ConditionalGuard>>,
) -> Vec<ConditionalGuard> {
let mut outer_guards = Vec::new();
if guard_sets.contains(&Vec::new()) {
return outer_guards;
}
let mut arms: Vec<ConditionalGuard> = guard_sets
.into_iter()
.map(|mut set| {
if set.len() == 1 {
set.remove(0)
} else {
ConditionalGuard::AllOf(set)
}
})
.collect();
if arms.len() == 1 {
match arms.remove(0) {
ConditionalGuard::AllOf(set) => outer_guards.extend(set),
guard => outer_guards.push(guard),
}
} else {
outer_guards.push(ConditionalGuard::AnyOf(arms));
}
outer_guards.sort();
outer_guards.dedup();
outer_guards
}
pub(super) fn factor_guard_sets(
sets: BTreeSet<Vec<ConditionalGuard>>,
) -> BTreeSet<Vec<ConditionalGuard>> {
let mut sets: Vec<Vec<ConditionalGuard>> = sets.into_iter().collect();
loop {
let mut merged = None;
'search: for left in 0..sets.len() {
for right in left + 1..sets.len() {
let Some(left_set) = sets.get(left) else {
continue;
};
let Some(right_set) = sets.get(right) else {
continue;
};
if let Some(folded) = fold_activation_pair(left_set, right_set) {
merged = Some((left, right, folded));
break 'search;
}
}
}
let Some((left, right, folded)) = merged else {
break;
};
sets.remove(right);
if let Some(left_set) = sets.get_mut(left) {
*left_set = folded;
}
}
sets.into_iter().collect()
}
pub(super) fn fold_activation_pair(
left: &[ConditionalGuard],
right: &[ConditionalGuard],
) -> Option<Vec<ConditionalGuard>> {
let only_left: Vec<&ConditionalGuard> =
left.iter().filter(|guard| !right.contains(guard)).collect();
let only_right: Vec<&ConditionalGuard> =
right.iter().filter(|guard| !left.contains(guard)).collect();
let ([left_guard], [right_guard]) = (only_left.as_slice(), only_right.as_slice()) else {
return None;
};
let activation_pair = |a: &ConditionalGuard, b: &ConditionalGuard| match (a, b) {
(
ConditionalGuard::Truthy { path: truthy_path },
ConditionalGuard::Absent { path: absent_path },
) => (truthy_path == absent_path).then(|| {
let mut alternatives = vec![a.clone(), b.clone()];
alternatives.sort();
ConditionalGuard::AnyOf(alternatives)
}),
_ => None,
};
let folded_guard = activation_pair(left_guard, right_guard)
.or_else(|| activation_pair(right_guard, left_guard))?;
let mut folded: Vec<ConditionalGuard> = left
.iter()
.filter(|guard| guard != left_guard)
.cloned()
.collect();
folded.push(folded_guard);
folded.sort();
folded.dedup();
Some(folded)
}
pub(super) fn guard_implies_present(guard: &ConditionalGuard, path: &str) -> bool {
match guard {
ConditionalGuard::Truthy { path: guarded } | ConditionalGuard::With { path: guarded } => {
guarded == path
}
ConditionalGuard::TypeIs {
path: guarded,
schema_type,
} => guarded == path && schema_type != "null",
ConditionalGuard::HasKey { path: host, key } => {
helm_schema_core::append_value_path(host, key) == path
}
ConditionalGuard::Not(inner) => {
matches!(inner.as_ref(), ConditionalGuard::Absent { path: guarded } if guarded == path)
}
ConditionalGuard::AllOf(set) => set.iter().any(|guard| guard_implies_present(guard, path)),
ConditionalGuard::AnyOf(set) => set.iter().all(|guard| guard_implies_present(guard, path)),
_ => false,
}
}
pub(super) fn record_member_access_implications(
paths: &mut BTreeMap<String, ContractPathAccumulator>,
terminal_clauses: &mut Vec<Vec<ConditionalGuard>>,
) {
let dependency_roots: BTreeSet<String> = paths
.iter()
.filter(|(_, acc)| acc.facts.facts.accepted_dependency_values_root_fragment)
.map(|(path, _)| path.clone())
.collect();
let pending: Vec<(String, MemberAccessConditions)> = paths
.iter()
.filter(|(path, acc)| {
!acc.member_access_conditions.is_empty() && !path_contains_wildcard(path)
})
.map(|(path, acc)| (path.clone(), acc.member_access_conditions.clone()))
.collect();
for (path, conditions) in pending {
let mut exact_guard_sets = MemberAccessGuardSets::new();
for (kinds, condition) in &conditions.exact_by_handled_kinds {
if let Some(guard_sets) = lower_member_access_condition(condition) {
exact_guard_sets.insert(kinds.clone(), guard_sets);
}
}
let mut partial_guard_sets = MemberAccessGuardSets::new();
for (kinds, condition) in &conditions.partial_by_handled_kinds {
if let Some(guard_sets) = lower_member_access_condition(condition) {
partial_guard_sets.insert(kinds.clone(), guard_sets);
}
}
let exact_domain_is_complete = !conditions.saw_incomplete_access
&& exact_guard_sets.len() == conditions.exact_by_handled_kinds.len();
for (guard_sets_by_kind, complete_domain) in [
(&exact_guard_sets, exact_domain_is_complete),
(&partial_guard_sets, false),
] {
for (handled_kinds, guard_sets) in guard_sets_by_kind {
let outer_guards = fold_member_access_arms(guard_sets.clone());
let implication = ContractRequirementImplication {
outer_guards,
target: ContractRequirementTarget::Value,
requirements: vec![FailValueRequirement::MemberHost {
handled_kinds: handled_kinds.clone(),
complete_domain,
}],
};
let acc = path_accumulator(paths, &path);
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
}
}
if dependency_roots.contains(&path) {
continue;
}
let mut combined_condition = GuardDnf::never();
for condition in conditions
.exact_by_handled_kinds
.into_values()
.chain(conditions.partial_by_handled_kinds.into_values())
{
combined_condition.union_absorbing(condition);
}
let absent_abort_sets = lower_member_access_condition(&combined_condition)
.unwrap_or_default()
.into_iter()
.filter(|guards| {
!guards
.iter()
.any(|guard| guard_implies_present(guard, &path))
})
.collect::<BTreeSet<_>>();
if absent_abort_sets.is_empty() {
continue;
}
let mut clause = fold_member_access_arms(absent_abort_sets);
clause.push(ConditionalGuard::Absent { path });
clause.sort();
clause.dedup();
if !terminal_clauses.contains(&clause) {
terminal_clauses.push(clause);
}
}
}