diff --git a/DEPENDENCIES b/DEPENDENCIES index 3a23e029..bf50edc2 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,5 +1,5 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 core https://github.com/sourcemeta/core bb1c78e8fa148a2ece951bb776798a43fe328821 jsonbinpack https://github.com/sourcemeta/jsonbinpack e2f99ed5e69ab17b027c3d7bb0ef95b27953bb08 -blaze https://github.com/sourcemeta/blaze 04832d45bf4327d4ec874fa67f339797cd49b375 +blaze https://github.com/sourcemeta/blaze bcc61aee87455218aec48282a1ca63e5c39d9434 ctrf https://github.com/ctrf-io/ctrf 93ea827d951390190171d37443bff169cf47c808 diff --git a/vendor/blaze/schemas/canonical-draft3.json b/vendor/blaze/schemas/canonical-draft3.json index f9a35075..1e0836b5 100644 --- a/vendor/blaze/schemas/canonical-draft3.json +++ b/vendor/blaze/schemas/canonical-draft3.json @@ -70,7 +70,56 @@ } } }, - "schema": { + "extends": { + "x-lint-exclude": "simple_properties_identifiers", + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/metadata" + }, + { + "$ref": "#/$defs/core" + } + ], + "required": [ "extends" ], + "properties": { + "extends": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/schema" + } + }, + "required": { + "type": "boolean" + } + }, + "unevaluatedProperties": false + }, + "union": { + "x-lint-exclude": "simple_properties_identifiers", + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/metadata" + }, + { + "$ref": "#/$defs/core" + } + ], + "required": [ "type" ], + "properties": { + "type": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/schema" + } + } + }, + "unevaluatedProperties": false + }, + "leaf": { "anyOf": [ { "const": {} @@ -336,55 +385,19 @@ } }, "unevaluatedProperties": false + } + ] + }, + "schema": { + "anyOf": [ + { + "$ref": "#/$defs/leaf" }, { - "x-lint-exclude": "simple_properties_identifiers", - "type": "object", - "allOf": [ - { - "$ref": "#/$defs/metadata" - }, - { - "$ref": "#/$defs/core" - } - ], - "required": [ "type" ], - "properties": { - "type": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/$defs/schema" - } - } - }, - "unevaluatedProperties": false + "$ref": "#/$defs/union" }, { - "x-lint-exclude": "simple_properties_identifiers", - "type": "object", - "allOf": [ - { - "$ref": "#/$defs/metadata" - }, - { - "$ref": "#/$defs/core" - } - ], - "required": [ "extends" ], - "properties": { - "extends": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/$defs/schema" - } - }, - "required": { - "type": "boolean" - } - }, - "unevaluatedProperties": false + "$ref": "#/$defs/extends" }, { "x-lint-exclude": "simple_properties_identifiers", @@ -404,7 +417,18 @@ "maxItems": 1, "minItems": 1, "items": { - "$ref": "#/$defs/schema" + "$comment": "TODO: `disallow` should only ever wrap a single-kind leaf. We additionally permit `extends` and `type` unions here as an interim escape hatch: negating a conjunction or disjunction whose wrapper is targeted by a `$ref` cannot be pushed to the leaves, because doing so would dissolve the referenced node. The proper fix is to invert such references so the `disallow` wraps a `$ref` leaf instead, after which both can be dropped from this list", + "anyOf": [ + { + "$ref": "#/$defs/leaf" + }, + { + "$ref": "#/$defs/union" + }, + { + "$ref": "#/$defs/extends" + } + ] } } }, diff --git a/vendor/blaze/src/alterschema/CMakeLists.txt b/vendor/blaze/src/alterschema/CMakeLists.txt index 6fd7ad96..ed3d4f50 100644 --- a/vendor/blaze/src/alterschema/CMakeLists.txt +++ b/vendor/blaze/src/alterschema/CMakeLists.txt @@ -13,6 +13,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema canonicalizer/deprecated_false_drop.h canonicalizer/draft3_type_any.h canonicalizer/disallow_array_to_extends.h + canonicalizer/disallow_double_negation.h canonicalizer/disallow_extends_to_type.h canonicalizer/disallow_to_array_of_schemas.h canonicalizer/disallow_type_union_to_extends.h diff --git a/vendor/blaze/src/alterschema/alterschema.cc b/vendor/blaze/src/alterschema/alterschema.cc index efadc22f..0d7a23f2 100644 --- a/vendor/blaze/src/alterschema/alterschema.cc +++ b/vendor/blaze/src/alterschema/alterschema.cc @@ -119,6 +119,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "canonicalizer/dependent_schemas_to_any_of.h" #include "canonicalizer/deprecated_false_drop.h" #include "canonicalizer/disallow_array_to_extends.h" +#include "canonicalizer/disallow_double_negation.h" #include "canonicalizer/disallow_extends_to_type.h" #include "canonicalizer/disallow_to_array_of_schemas.h" #include "canonicalizer/disallow_type_union_to_extends.h" @@ -539,6 +540,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_double_negation.h b/vendor/blaze/src/alterschema/canonicalizer/disallow_double_negation.h new file mode 100644 index 00000000..4a6ffa81 --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/disallow_double_negation.h @@ -0,0 +1,117 @@ +class DisallowDoubleNegation final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + DisallowDoubleNegation() + : SchemaTransformRule{ + "disallow_double_negation", + "A `disallow` whose single negated schema is itself a `disallow` " + "of " + "a single schema is a double negation equivalent to the inner " + "schema"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + static const JSON::String KEYWORD{"disallow"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->size() == 1); + ONLY_CONTINUE_IF(is_single_negation(disallow->at(0))); + + // Lifting the inner schema merges its keywords into this node, so the node + // must assert nothing besides `disallow` (otherwise a sibling constraint + // sharing a key with the inner schema would be silently clobbered) + ONLY_CONTINUE_IF( + wraps_single_constraint(schema, "disallow", walker, vocabularies)); + + // Collapsing the chain dissolves every intermediate `disallow` wrapper, so + // a reference targeting any of them has no valid new home: bail. A + // reference into the surviving innermost schema relocates with it (handled + // by `rereference`) + auto wrapper{location.pointer}; + const sourcemeta::core::JSON *node{&disallow->at(0)}; + while (is_single_negation(*node)) { + wrapper.push_back(std::cref(KEYWORD)); + wrapper.push_back(static_cast(0)); + if (frame.has_references_to(wrapper)) { + return false; + } + + node = &node->at(KEYWORD).at(0); + } + + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + auto inner{schema.at("disallow").at(0).at("disallow").at(0)}; + schema.erase("disallow"); + + while (is_single_negation(inner) && + is_single_negation(inner.at("disallow").at(0))) { + auto next{inner.at("disallow").at(0).at("disallow").at(0)}; + inner = std::move(next); + } + + if (inner.is_object()) { + schema.merge(inner.as_object()); + } + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + auto old_prefix{current.concat({"disallow", 0, "disallow", 0})}; + while ( + target.starts_with(old_prefix.concat({"disallow", 0, "disallow", 0}))) { + old_prefix = old_prefix.concat({"disallow", 0, "disallow", 0}); + } + + if (!target.starts_with(old_prefix)) { + return target; + } + + return target.rebase(old_prefix, current); + } + +private: + static auto is_single_negation(const sourcemeta::core::JSON &schema) -> bool { + return schema.is_object() && schema.size() == 1 && + schema.defines("disallow") && schema.at("disallow").is_array() && + schema.at("disallow").size() == 1; + } + + static auto wraps_single_constraint( + const sourcemeta::core::JSON &schema, const std::string_view keyword, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::Vocabularies &vocabularies) -> bool { + for (const auto &entry : schema.as_object()) { + if (entry.first == keyword) { + continue; + } + + const auto type{walker(entry.first, vocabularies).type}; + if (type != SchemaKeywordType::Annotation && + type != SchemaKeywordType::Comment && + type != SchemaKeywordType::Other && + type != SchemaKeywordType::Unknown && + type != SchemaKeywordType::LocationMembers) { + return false; + } + } + + return true; + } +}; diff --git a/vendor/blaze/src/compiler/default_compiler_draft3.h b/vendor/blaze/src/compiler/default_compiler_draft3.h index 198d2d7a..2ed75e42 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft3.h +++ b/vendor/blaze/src/compiler/default_compiler_draft3.h @@ -204,10 +204,13 @@ auto compile_required_assertions(const Context &context, if (is_closed_properties_required(schema_context.schema, properties_set)) { if (context.mode == Mode::FastValidation && assume_object) { static const std::string properties_keyword{"properties"}; + // `SchemaContext::relative_pointer` is a reference, so the concatenated + // pointer must outlive `new_schema_context` + const auto properties_pointer{ + schema_context.relative_pointer.initial().concat( + sourcemeta::blaze::make_weak_pointer(properties_keyword))}; const SchemaContext new_schema_context{ - .relative_pointer = - schema_context.relative_pointer.initial().concat( - sourcemeta::blaze::make_weak_pointer(properties_keyword)), + .relative_pointer = properties_pointer, .schema = schema_context.schema, .vocabularies = schema_context.vocabularies, .base = schema_context.base, @@ -1073,6 +1076,56 @@ auto compiler_draft3_applicator_patternproperties( context, schema_context, dynamic_context, false, false); } +// Determine whether the `properties` keyword on its own enforces a closed +// object (i.e. compiles to one of the `LoopPropertiesExactly*` forms). This +// happens when every property subschema reduces to a single strict type +// assertion of the same type. In that case `additionalProperties: false` is +// already enforced by `properties` and does not need to emit anything. +inline auto +properties_enforce_closed_object(const Context &context, + const SchemaContext &schema_context) -> bool { + const bool assume_object{schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() == + "object"}; + if (!assume_object || !schema_context.schema.defines("properties") || + !schema_context.schema.at("properties").is_object()) { + return false; + } + + // `SchemaContext::relative_pointer` is a reference, so the concatenated + // pointer must outlive `new_schema_context` + const auto properties_pointer{ + schema_context.relative_pointer.initial().concat( + sourcemeta::blaze::make_weak_pointer(KEYWORD_PROPERTIES))}; + const SchemaContext new_schema_context{ + .relative_pointer = properties_pointer, + .schema = schema_context.schema, + .vocabularies = schema_context.vocabularies, + .base = schema_context.base, + .is_property_name = schema_context.is_property_name}; + const DynamicContext new_dynamic_context{ + .keyword = KEYWORD_PROPERTIES, + .base_schema_location = sourcemeta::core::empty_weak_pointer, + .base_instance_location = sourcemeta::core::empty_weak_pointer}; + const auto properties{ + compile_properties(context, new_schema_context, new_dynamic_context, {})}; + if (!std::ranges::all_of(properties, [](const auto &property) { + return property.second.size() == 1 && + property.second.front().type == + InstructionIndex::AssertionTypeStrict; + })) { + return false; + } + + std::set types; + for (const auto &property : properties) { + types.insert(std::get(property.second.front().value)); + } + + return types.size() == 1; +} + auto compiler_draft3_applicator_additionalproperties_with_options( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const bool annotate, @@ -1145,17 +1198,24 @@ auto compiler_draft3_applicator_additionalproperties_with_options( return {}; } - // When all properties are required and `additionalProperties: false`, - // the `required` keyword compiles to `AssertionDefinesExactly` which already - // checks that the object has exactly the required properties, so we don't - // need to emit anything for `additionalProperties` + // When all properties are required and `additionalProperties: false`, the + // object is closed by another keyword, so we don't need to emit anything for + // `additionalProperties`. This happens either because `required` compiles to + // an `AssertionDefinesExactly` variant (only when there is more than one + // required property) or because `properties` itself compiles to a closed + // form. With a single required property `required` only compiles to + // `AssertionDefinesStrict`, which does not reject unknown properties, so we + // must still emit the closure unless `properties` enforces it. if (context.mode == Mode::FastValidation && children.size() == 1 && children.front().type == InstructionIndex::AssertionFail && !filter_strings.empty() && filter_prefixes.empty() && - filter_regexes.empty() && - is_closed_properties_required(schema_context.schema, - required_properties(schema_context))) { - return {}; + filter_regexes.empty()) { + const auto required{required_properties(schema_context)}; + if (is_closed_properties_required(schema_context.schema, required) && + (required.size() > 1 || + properties_enforce_closed_object(context, schema_context))) { + return {}; + } } if (context.mode == Mode::FastValidation && filter_strings.empty() && diff --git a/vendor/blaze/src/compiler/postprocess.h b/vendor/blaze/src/compiler/postprocess.h index a959457a..cc256008 100644 --- a/vendor/blaze/src/compiler/postprocess.h +++ b/vendor/blaze/src/compiler/postprocess.h @@ -67,7 +67,10 @@ is_parent_to_children_instruction(const InstructionIndex type) noexcept inline auto convert_to_property_type_assertions(Instructions &instructions) -> void { for (auto &instruction : instructions) { - if (!instruction.relative_instance_location.empty()) { + if (!instruction.relative_instance_location.empty() && + std::ranges::all_of( + instruction.relative_instance_location, + [](const auto &token) { return token.is_property(); })) { switch (instruction.type) { case InstructionIndex::AssertionTypeStrict: instruction.type = InstructionIndex::AssertionPropertyTypeStrict;