use super::*;
pub(super) fn schemas_definitely_disjoint_for_negation(
left: &SchemaNode,
right: &SchemaNode,
context: &mut SubschemaCheckContext,
) -> bool {
fn inner(
left: &SchemaNode,
right: &SchemaNode,
context: &mut SubschemaCheckContext,
active: &mut HashSet<(NodeId, NodeId)>,
) -> bool {
if !active.insert((left.id(), right.id())) {
return false;
}
let left_mask = possible_json_type_mask(left);
let right_mask = possible_json_type_mask(right);
if left_mask == 0 || right_mask == 0 || (left_mask & right_mask) == 0 {
active.remove(&(left.id(), right.id()));
return true;
}
if let Some(values) = finite_schema_value_superset(left)
&& values.iter().all(|value| {
context.schema_definitely_rejects_value(left, value)
|| context.schema_definitely_rejects_value(right, value)
})
{
active.remove(&(left.id(), right.id()));
return true;
}
if let Some(values) = finite_schema_value_superset(right)
&& values.iter().all(|value| {
context.schema_definitely_rejects_value(right, value)
|| context.schema_definitely_rejects_value(left, value)
})
{
active.remove(&(left.id(), right.id()));
return true;
}
use SchemaNodeKind::*;
let result = match (left.kind(), right.kind()) {
(BoolSchema(false), _) | (_, BoolSchema(false)) => true,
(AllOf(conjuncts), AnyOf(branches)) | (AllOf(conjuncts), OneOf(branches)) => {
branches.iter().all(|branch| {
conjuncts
.iter()
.any(|conjunct| inner(conjunct, branch, context, active))
})
}
(AnyOf(branches), AllOf(conjuncts)) | (OneOf(branches), AllOf(conjuncts)) => {
branches.iter().all(|branch| {
conjuncts
.iter()
.any(|conjunct| inner(branch, conjunct, context, active))
})
}
(Not(excluded), _) => {
analyze_subschema_with_context(
right,
excluded,
context,
ExplanationMode::VerdictOnly,
)
.is_subschema
}
(_, Not(excluded)) => {
analyze_subschema_with_context(
left,
excluded,
context,
ExplanationMode::VerdictOnly,
)
.is_subschema
}
(AnyOf(children), _) | (OneOf(children), _) => children
.iter()
.all(|child| inner(child, right, context, active)),
(AllOf(children), _) => children
.iter()
.any(|child| inner(child, right, context, active)),
(_, AnyOf(children)) | (_, OneOf(children)) => children
.iter()
.all(|child| inner(left, child, context, active)),
(_, AllOf(children)) => children
.iter()
.any(|child| inner(left, child, context, active)),
_ => {
let left_mask = possible_json_type_mask(left);
left_mask == 0
|| schemas_definitely_disjoint_for_partition(left, left_mask, right, context)
}
};
active.remove(&(left.id(), right.id()));
result
}
inner(left, right, context, &mut HashSet::new())
}
pub(super) fn negated_allof_covered_by_anyof(
conjuncts: &[SchemaNode],
branches: &[SchemaNode],
context: &mut SubschemaCheckContext,
) -> bool {
conjuncts.iter().all(|conjunct| {
branches.iter().any(|branch| match branch.kind() {
SchemaNodeKind::Any | SchemaNodeKind::BoolSchema(true) => true,
SchemaNodeKind::Not(excluded) => {
analyze_subschema_with_context(
excluded,
conjunct,
context,
ExplanationMode::VerdictOnly,
)
.is_subschema
}
_ => false,
})
})
}
pub(super) fn negated_anyof_finite_complement_arm_subset_of(
children: &[SchemaNode],
target: &SchemaNode,
context: &mut SubschemaCheckContext,
) -> bool {
children.iter().enumerate().any(|(neg_index, child)| {
let SchemaNodeKind::Not(core) = child.kind() else {
return false;
};
let Some(values) = finite_schema_value_superset(core) else {
return false;
};
values.iter().all(|value| {
context.schema_definitely_rejects_value(core, value)
|| children.iter().enumerate().any(|(index, sibling)| {
index != neg_index && context.superset_contains_value(sibling, value)
})
|| context.superset_contains_value(target, value)
})
})
}
pub(super) fn negated_exclusion_covered_by_anyof_finite_gap(
excluded: &SchemaNode,
branches: &[SchemaNode],
context: &mut SubschemaCheckContext,
) -> bool {
branches.iter().any(|branch| {
let SchemaNodeKind::Not(coarser_excluded) = branch.kind() else {
return false;
};
let Some(values) = finite_schema_value_superset(coarser_excluded) else {
return false;
};
values.iter().all(|value| {
context.schema_definitely_rejects_value(coarser_excluded, value)
|| context.superset_contains_value(excluded, value)
|| branches
.iter()
.any(|cover| context.superset_contains_value(cover, value))
})
})
}
pub(super) fn schema_contains_explicit_not(schema: &SchemaNode) -> bool {
fn inner(schema: &SchemaNode, active: &mut HashSet<NodeId>) -> bool {
if !active.insert(schema.id()) {
return false;
}
let result = match schema.kind() {
SchemaNodeKind::Not(_) => true,
SchemaNodeKind::AllOf(children)
| SchemaNodeKind::AnyOf(children)
| SchemaNodeKind::OneOf(children) => children.iter().any(|child| inner(child, active)),
SchemaNodeKind::IfThenElse {
if_schema,
then_schema,
else_schema,
} => {
inner(if_schema, active)
|| then_schema
.as_ref()
.is_some_and(|child| inner(child, active))
|| else_schema
.as_ref()
.is_some_and(|child| inner(child, active))
}
_ => false,
};
active.remove(&schema.id());
result
}
inner(schema, &mut HashSet::new())
}
pub(super) fn schemas_definitely_disjoint_for_partition(
sub: &SchemaNode,
sub_mask: u8,
other: &SchemaNode,
context: &mut SubschemaCheckContext,
) -> bool {
fn base(
sub: &SchemaNode,
sub_mask: u8,
other: &SchemaNode,
context: &mut SubschemaCheckContext,
) -> bool {
if schemas_definitely_disjoint_for_oneof(sub, sub_mask, other) {
return true;
}
if let Some(values) = finite_schema_value_superset(sub)
&& values
.iter()
.all(|value| context.schema_definitely_rejects_value(other, value))
{
return true;
}
if let Some(values) = finite_schema_value_superset(other)
&& values
.iter()
.all(|value| context.schema_definitely_rejects_value(sub, value))
{
return true;
}
let overlap = possible_json_type_mask(sub) & possible_json_type_mask(other);
if overlap != 0
&& overlap & !JSON_TYPE_OBJECT == 0
&& (finite_required_property_values_rejected_by_other(sub, other, context)
|| finite_required_property_values_rejected_by_other(other, sub, context))
{
return true;
}
if overlap != 0
&& overlap & !JSON_TYPE_ARRAY == 0
&& (finite_required_array_item_values_rejected_by_other(sub, other, context)
|| finite_required_array_item_values_rejected_by_other(other, sub, context))
{
return true;
}
if (schema_contains_explicit_not(sub)
&& schema_definitely_excludes_schema(sub, other, context))
|| (schema_contains_explicit_not(other)
&& schema_definitely_excludes_schema(other, sub, context))
{
return true;
}
false
}
fn inner(
left: &SchemaNode,
right: &SchemaNode,
context: &mut SubschemaCheckContext,
active: &mut HashSet<(NodeId, NodeId)>,
) -> bool {
if !active.insert((left.id(), right.id())) {
return false;
}
use SchemaNodeKind::*;
let result = if base(left, possible_json_type_mask(left), right, context) {
true
} else {
match (left.kind(), right.kind()) {
(BoolSchema(false), _) | (_, BoolSchema(false)) => true,
(AnyOf(children), _) | (OneOf(children), _) => children
.iter()
.all(|child| inner(child, right, context, active)),
(AllOf(children), _) => children
.iter()
.any(|child| inner(child, right, context, active)),
(_, AnyOf(children)) | (_, OneOf(children)) => children
.iter()
.all(|child| inner(left, child, context, active)),
(_, AllOf(children)) => children
.iter()
.any(|child| inner(left, child, context, active)),
_ => false,
}
};
active.remove(&(left.id(), right.id()));
result
}
if !matches!(
sub.kind(),
SchemaNodeKind::AnyOf(_) | SchemaNodeKind::OneOf(_) | SchemaNodeKind::AllOf(_)
) && !matches!(
other.kind(),
SchemaNodeKind::AnyOf(_) | SchemaNodeKind::OneOf(_) | SchemaNodeKind::AllOf(_)
) {
return base(sub, sub_mask, other, context);
}
inner(sub, other, context, &mut HashSet::new())
}
pub(super) fn finite_required_property_values_rejected_by_other(
finite_side: &SchemaNode,
other: &SchemaNode,
context: &mut SubschemaCheckContext,
) -> bool {
for (name, values) in finite_required_property_value_bounds(finite_side) {
if values.is_empty() {
return true;
}
let object_only_overlap = (possible_json_type_mask(finite_side)
& possible_json_type_mask(other)
& !JSON_TYPE_OBJECT)
== 0;
if !(schema_guarantees_property_name(other, &name)
|| (object_only_overlap && schema_guarantees_property_name_for_objects(other, &name)))
{
continue;
}
if property_value_set_definitely_rejected(other, &name, &values, context) {
return true;
}
}
false
}
pub(super) fn property_value_set_definitely_rejected(
schema: &SchemaNode,
name: &str,
values: &[Value],
context: &mut SubschemaCheckContext,
) -> bool {
fn all_values_rejected(
constraint: &SchemaNode,
values: &[Value],
context: &mut SubschemaCheckContext,
) -> bool {
values
.iter()
.all(|value| context.schema_definitely_rejects_value(constraint, value))
}
fn inner(
schema: &SchemaNode,
name: &str,
values: &[Value],
context: &mut SubschemaCheckContext,
active: &mut HashSet<NodeId>,
) -> bool {
if !active.insert(schema.id()) {
return false;
}
let result = match schema.kind() {
SchemaNodeKind::BoolSchema(false) => true,
SchemaNodeKind::BoolSchema(true) => false,
SchemaNodeKind::Object {
properties,
pattern_properties,
additional,
property_names,
..
} => {
if string_literal_definitely_rejected(property_names, name) {
true
} else {
let mut matched = false;
let direct_rejects = properties.get(name).is_some_and(|property_schema| {
matched = true;
all_values_rejected(property_schema, values, context)
});
if direct_rejects {
true
} else {
let mut pattern_rejects = false;
let mut unsupported_pattern = false;
for pattern_property in pattern_properties.values() {
if pattern_property.pattern.support() != PatternSupport::Supported {
unsupported_pattern = true;
continue;
}
if pattern_property.pattern.is_match(name) {
matched = true;
if all_values_rejected(&pattern_property.schema, values, context) {
pattern_rejects = true;
break;
}
}
}
pattern_rejects
|| (!matched
&& !unsupported_pattern
&& all_values_rejected(additional, values, context))
}
}
}
SchemaNodeKind::AllOf(children) => children
.iter()
.any(|child| inner(child, name, values, context, active)),
SchemaNodeKind::AnyOf(children) | SchemaNodeKind::OneOf(children) => {
!children.is_empty()
&& children
.iter()
.all(|child| inner(child, name, values, context, active))
}
SchemaNodeKind::IfThenElse {
if_schema,
then_schema,
else_schema,
} => match (then_schema.as_ref(), else_schema.as_ref()) {
(Some(then_schema), Some(else_schema)) => {
inner(then_schema, name, values, context, active)
&& inner(else_schema, name, values, context, active)
}
(Some(then_schema), None)
if whole_json_types_accepted_mask(if_schema) & JSON_TYPE_OBJECT != 0 =>
{
inner(then_schema, name, values, context, active)
}
(None, Some(else_schema))
if possible_json_type_mask(if_schema) & JSON_TYPE_OBJECT == 0 =>
{
inner(else_schema, name, values, context, active)
}
_ => false,
},
_ => false,
};
active.remove(&schema.id());
result
}
inner(schema, name, values, context, &mut HashSet::new())
}
pub(super) fn finite_required_array_item_values_rejected_by_other(
finite_side: &SchemaNode,
other: &SchemaNode,
context: &mut SubschemaCheckContext,
) -> bool {
const MAX_TRACKED_INDEX: u64 = 32;
let Some(finite_len) = array_length_interval_bound(finite_side) else {
return false;
};
let Some(other_len) = array_length_interval_bound(other) else {
return false;
};
let shared = finite_len.lower.min(other_len.lower).min(MAX_TRACKED_INDEX);
for index in 0..shared {
let Ok(index_usize) = usize::try_from(index) else {
break;
};
let Some(values) = finite_item_value_bound_at(finite_side, index_usize) else {
continue;
};
if values.is_empty()
|| array_item_value_set_definitely_rejected(other, index_usize, &values, context)
{
return true;
}
}
false
}
pub(super) fn finite_item_value_bound_at(schema: &SchemaNode, index: usize) -> Option<Vec<Value>> {
fn dedup_extend(out: &mut Vec<Value>, values: Vec<Value>) {
for value in values {
if !out.iter().any(|seen| json_values_equal(seen, &value)) {
out.push(value);
}
}
}
fn inner(
schema: &SchemaNode,
index: usize,
active: &mut HashSet<NodeId>,
) -> Option<Vec<Value>> {
if !active.insert(schema.id()) {
return None;
}
let result = match schema.kind() {
SchemaNodeKind::BoolSchema(false) => Some(Vec::new()),
SchemaNodeKind::Array {
prefix_items,
items,
..
} => {
let item_schema = prefix_items.get(index).unwrap_or(items);
finite_schema_value_superset(item_schema)
}
SchemaNodeKind::AllOf(children) => children
.iter()
.find_map(|child| inner(child, index, active)),
SchemaNodeKind::AnyOf(children) | SchemaNodeKind::OneOf(children) => {
if children.is_empty() {
None
} else {
let mut out = Vec::new();
let mut complete = true;
for child in children {
match inner(child, index, active) {
Some(values) => dedup_extend(&mut out, values),
None => {
complete = false;
break;
}
}
}
complete.then_some(out)
}
}
SchemaNodeKind::IfThenElse {
if_schema,
then_schema,
else_schema,
} => match (then_schema.as_ref(), else_schema.as_ref()) {
(Some(then_schema), Some(else_schema)) => {
match (
inner(then_schema, index, active),
inner(else_schema, index, active),
) {
(Some(mut then_values), Some(else_values)) => {
dedup_extend(&mut then_values, else_values);
Some(then_values)
}
_ => None,
}
}
(Some(then_schema), None)
if whole_json_types_accepted_mask(if_schema) & JSON_TYPE_ARRAY != 0 =>
{
inner(then_schema, index, active)
}
(None, Some(else_schema))
if possible_json_type_mask(if_schema) & JSON_TYPE_ARRAY == 0 =>
{
inner(else_schema, index, active)
}
_ => None,
},
SchemaNodeKind::Const(value) => value
.as_array()
.and_then(|array| array.get(index).cloned())
.map(|value| vec![value]),
SchemaNodeKind::Enum(values) => {
let mut out = Vec::new();
let mut complete = !values.is_empty();
for value in values {
let Some(array) = value.as_array() else {
complete = false;
break;
};
let Some(item) = array.get(index) else {
complete = false;
break;
};
if !out.iter().any(|seen| json_values_equal(seen, item)) {
out.push(item.clone());
}
}
complete.then_some(out)
}
_ => None,
};
active.remove(&schema.id());
result
}
inner(schema, index, &mut HashSet::new())
}
pub(super) fn array_item_value_set_definitely_rejected(
schema: &SchemaNode,
index: usize,
values: &[Value],
context: &mut SubschemaCheckContext,
) -> bool {
fn all_values_rejected(
constraint: &SchemaNode,
values: &[Value],
context: &mut SubschemaCheckContext,
) -> bool {
values
.iter()
.all(|value| context.schema_definitely_rejects_value(constraint, value))
}
fn inner(
schema: &SchemaNode,
index: usize,
values: &[Value],
context: &mut SubschemaCheckContext,
active: &mut HashSet<NodeId>,
) -> bool {
if !active.insert(schema.id()) {
return false;
}
let result = match schema.kind() {
SchemaNodeKind::BoolSchema(false) => true,
SchemaNodeKind::BoolSchema(true) => false,
SchemaNodeKind::Array {
prefix_items,
items,
..
} => {
let item_schema = prefix_items.get(index).unwrap_or(items);
all_values_rejected(item_schema, values, context)
}
SchemaNodeKind::AllOf(children) => children
.iter()
.any(|child| inner(child, index, values, context, active)),
SchemaNodeKind::AnyOf(children) | SchemaNodeKind::OneOf(children) => {
!children.is_empty()
&& children
.iter()
.all(|child| inner(child, index, values, context, active))
}
SchemaNodeKind::IfThenElse {
if_schema,
then_schema,
else_schema,
} => match (then_schema.as_ref(), else_schema.as_ref()) {
(Some(then_schema), Some(else_schema)) => {
inner(then_schema, index, values, context, active)
&& inner(else_schema, index, values, context, active)
}
(Some(then_schema), None)
if whole_json_types_accepted_mask(if_schema) & JSON_TYPE_ARRAY != 0 =>
{
inner(then_schema, index, values, context, active)
}
(None, Some(else_schema))
if possible_json_type_mask(if_schema) & JSON_TYPE_ARRAY == 0 =>
{
inner(else_schema, index, values, context, active)
}
_ => false,
},
_ => false,
};
active.remove(&schema.id());
result
}
inner(schema, index, values, context, &mut HashSet::new())
}
pub(super) fn schema_definitely_excludes_schema(
schema: &SchemaNode,
excluded: &SchemaNode,
context: &mut SubschemaCheckContext,
) -> bool {
fn inner(
schema: &SchemaNode,
excluded: &SchemaNode,
context: &mut SubschemaCheckContext,
active: &mut HashSet<(NodeId, NodeId)>,
) -> bool {
if !active.insert((schema.id(), excluded.id())) {
return false;
}
use SchemaNodeKind::*;
let result = match schema.kind() {
BoolSchema(false) => true,
AllOf(children) => children
.iter()
.any(|child| inner(child, excluded, context, active)),
AnyOf(children) | OneOf(children) => children
.iter()
.all(|child| inner(child, excluded, context, active)),
IfThenElse {
then_schema,
else_schema,
..
} => match (then_schema.as_ref(), else_schema.as_ref()) {
(Some(then_schema), Some(else_schema)) => {
inner(then_schema, excluded, context, active)
&& inner(else_schema, excluded, context, active)
}
_ => false,
},
Not(negated) => {
analyze_subschema_with_context(
excluded,
negated,
context,
ExplanationMode::VerdictOnly,
)
.is_subschema
}
_ => false,
};
active.remove(&(schema.id(), excluded.id()));
result
}
inner(schema, excluded, context, &mut HashSet::new())
}
pub(super) fn schemas_definitely_disjoint_for_oneof(
sub: &SchemaNode,
sub_mask: u8,
other: &SchemaNode,
) -> bool {
let other_mask = possible_json_type_mask(other);
let overlap = sub_mask & other_mask;
if overlap == 0 {
return true;
}
if overlap == JSON_TYPE_NUMBER
&& (numeric_intervals_are_disjoint(sub, other) || integer_lattices_are_disjoint(sub, other))
{
return true;
}
if overlap == JSON_TYPE_STRING && string_length_intervals_are_disjoint(sub, other) {
return true;
}
if overlap == JSON_TYPE_ARRAY && array_length_intervals_are_disjoint(sub, other) {
return true;
}
if overlap == JSON_TYPE_ARRAY && required_array_item_shapes_are_disjoint(sub, other) {
return true;
}
if overlap == JSON_TYPE_OBJECT && object_property_count_intervals_are_disjoint(sub, other) {
return true;
}
if overlap & !JSON_TYPE_OBJECT != 0 {
return false;
}
required_property_values_are_disjoint(sub, other)
}
pub(super) fn schemas_definitely_disjoint_by_shape(left: &SchemaNode, right: &SchemaNode) -> bool {
let left_mask = possible_json_type_mask(left);
left_mask == 0 || schemas_definitely_disjoint_for_oneof(left, left_mask, right)
}