diff --git a/DEPENDENCIES b/DEPENDENCIES index 405e213c..2ee6cf8c 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,5 +1,5 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core e4d7ae9358710fc138d2afd3179db6d850e4190f +core https://github.com/sourcemeta/core 9985d96665aae0a8dfabf63f59b0732b92c64771 jsonbinpack https://github.com/sourcemeta/jsonbinpack 8fae212dc7ec02af4bb0cd4e7fccd42a2471f1c1 blaze https://github.com/sourcemeta/blaze 8dba65f8aebfe1ac976168b76e01c20dd406c517 hydra https://github.com/sourcemeta/hydra af9f2c54709d620872ead0c3f8f683c15a0fa702 diff --git a/src/command_lint.cc b/src/command_lint.cc index c1ed34e4..0fbb4bfa 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -127,10 +127,6 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) bundle.add( sourcemeta::blaze::default_schema_compiler); - // TODO: This rule has known problems we are working hard to fix right now - // See https://github.com/sourcemeta/core/pull/2145 - bundle.remove("orphan_definitions"); - if (options.contains("only")) { if (options.contains("exclude")) { throw OptionConflictError{ diff --git a/test/lint/pass_lint_list_exclude.sh b/test/lint/pass_lint_list_exclude.sh index 492d8378..e62d9381 100755 --- a/test/lint/pass_lint_list_exclude.sh +++ b/test/lint/pass_lint_list_exclude.sh @@ -139,6 +139,9 @@ non_applicable_type_specific_keywords not_false Setting the `not` keyword to `false` imposes no constraints. Negating `false` yields the always-true schema +orphan_definitions + Schema definitions in `$defs` or `definitions` that are never internally referenced can be removed + pattern_properties_default Setting the `patternProperties` keyword to the empty object does not add any further constraint @@ -205,13 +208,16 @@ unnecessary_allof_ref_wrapper_modern unnecessary_allof_wrapper Keywords inside `allOf` that do not conflict with the parent schema can be elevated +unsatisfiable_in_place_applicator_type + An in-place applicator branch that defines a `type` with no overlap with the parent `type` can never be satisfied + unsatisfiable_max_contains Setting the `maxContains` keyword to a number greater than or equal to the array upper bound does not add any further constraint unsatisfiable_min_properties Setting `minProperties` to a number less than `required` does not add any further constraint -Number of rules: 67 +Number of rules: 69 EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/lint/pass_lint_list_long.sh b/test/lint/pass_lint_list_long.sh index 989944f6..543b3ed3 100755 --- a/test/lint/pass_lint_list_long.sh +++ b/test/lint/pass_lint_list_long.sh @@ -145,6 +145,9 @@ non_applicable_type_specific_keywords not_false Setting the `not` keyword to `false` imposes no constraints. Negating `false` yields the always-true schema +orphan_definitions + Schema definitions in `$defs` or `definitions` that are never internally referenced can be removed + pattern_properties_default Setting the `patternProperties` keyword to the empty object does not add any further constraint @@ -211,13 +214,16 @@ unnecessary_allof_ref_wrapper_modern unnecessary_allof_wrapper Keywords inside `allOf` that do not conflict with the parent schema can be elevated +unsatisfiable_in_place_applicator_type + An in-place applicator branch that defines a `type` with no overlap with the parent `type` can never be satisfied + unsatisfiable_max_contains Setting the `maxContains` keyword to a number greater than or equal to the array upper bound does not add any further constraint unsatisfiable_min_properties Setting `minProperties` to a number less than `required` does not add any further constraint -Number of rules: 69 +Number of rules: 71 EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/lint/pass_lint_list_short.sh b/test/lint/pass_lint_list_short.sh index 99c1902b..76cb78c2 100755 --- a/test/lint/pass_lint_list_short.sh +++ b/test/lint/pass_lint_list_short.sh @@ -145,6 +145,9 @@ non_applicable_type_specific_keywords not_false Setting the `not` keyword to `false` imposes no constraints. Negating `false` yields the always-true schema +orphan_definitions + Schema definitions in `$defs` or `definitions` that are never internally referenced can be removed + pattern_properties_default Setting the `patternProperties` keyword to the empty object does not add any further constraint @@ -211,13 +214,16 @@ unnecessary_allof_ref_wrapper_modern unnecessary_allof_wrapper Keywords inside `allOf` that do not conflict with the parent schema can be elevated +unsatisfiable_in_place_applicator_type + An in-place applicator branch that defines a `type` with no overlap with the parent `type` can never be satisfied + unsatisfiable_max_contains Setting the `maxContains` keyword to a number greater than or equal to the array upper bound does not add any further constraint unsatisfiable_min_properties Setting `minProperties` to a number less than `required` does not add any further constraint -Number of rules: 69 +Number of rules: 71 EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/vendor/core/src/core/jsonpointer/CMakeLists.txt b/vendor/core/src/core/jsonpointer/CMakeLists.txt index 20d4291a..2cb95a38 100644 --- a/vendor/core/src/core/jsonpointer/CMakeLists.txt +++ b/vendor/core/src/core/jsonpointer/CMakeLists.txt @@ -1,7 +1,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jsonpointer PRIVATE_HEADERS pointer.h position.h error.h token.h walker.h template.h - SOURCES jsonpointer.cc stringify.h parser.h grammar.h position.cc) + SOURCES jsonpointer.cc stringify.h parser.h grammar.h position.cc mangle.cc) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME jsonpointer) diff --git a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer.h b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer.h index 3a914313..7354ca70 100644 --- a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer.h +++ b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer.h @@ -21,6 +21,7 @@ #include // std::allocator #include // std::basic_ostream #include // std::basic_string +#include // std::string_view #include // std::is_same_v /// @defgroup jsonpointer JSON Pointer @@ -545,6 +546,42 @@ auto to_string(const PointerTemplate &pointer) -> std::basic_string>; +/// @ingroup jsonpointer +/// +/// Mangle a JSON Pointer template and prefix into a collision-free identifier. +/// +/// The encoding rules for ASCII characters (0x00-0x7F) are: +/// +/// - Lowercase at segment start (except x, u, z): capitalize (no marker) +/// - Lowercase x, u, z at segment start: hex escape (reserved characters) +/// - Uppercase at segment start (except X, U, Z): U + letter +/// - Uppercase X, U, Z at segment start: hex escape (reserved characters) +/// - Non-segment-start lowercase: as-is +/// - Non-segment-start uppercase (except X, U): as-is +/// - Non-segment-start X: X58, Non-segment-start U: X55 +/// - ASCII digits (0-9): as-is +/// - Other ASCII (space, punctuation, control): hex escape, starts new segment +/// - Z/z reserved for special token prefixes +/// +/// For non-ASCII bytes (0x80-0xFF, e.g. UTF-8 sequences): +/// +/// - Always hex escaped +/// - Do NOT start a new segment (preserves UTF-8 multi-byte sequences) +/// +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const sourcemeta::core::PointerTemplate pointer{"foo", "bar"}; +/// const auto result{sourcemeta::core::mangle(pointer, "schema")}; +/// assert(result == "Schema_Foo_Bar"); +/// ``` +SOURCEMETA_CORE_JSONPOINTER_EXPORT +auto mangle(const PointerTemplate &pointer, std::string_view prefix) + -> std::string; + /// @ingroup jsonpointer /// /// Stringify the input JSON Pointer into a properly escaped URI fragment. For diff --git a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h index cc6eaf33..a3057ece 100644 --- a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h +++ b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h @@ -492,6 +492,55 @@ template class GenericPointer { } } + /// Check whether a JSON Pointer starts with another JSON Pointer followed + /// by a property token. This is useful for checking container membership + /// without allocating a new pointer. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::Pointer pointer{"foo", "$defs", "bar"}; + /// const sourcemeta::core::Pointer prefix{"foo"}; + /// assert(pointer.starts_with(prefix, "$defs")); + /// assert(!pointer.starts_with(prefix, "other")); + /// ``` + template + requires(!std::is_same_v, Token>) + [[nodiscard]] auto starts_with(const GenericPointer &other, + const StringT &tail) const -> bool { + const auto prefix_size{other.size()}; + return this->size() > prefix_size && this->starts_with(other) && + this->data[prefix_size].is_property() && + this->data[prefix_size].to_property() == tail; + } + + /// Check whether a JSON Pointer starts with another JSON Pointer followed + /// by two property tokens. This is useful for checking nested container + /// membership without allocating a new pointer. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::Pointer pointer{"foo", "$defs", "bar", "baz"}; + /// const sourcemeta::core::Pointer prefix{"foo"}; + /// assert(pointer.starts_with(prefix, "$defs", "bar")); + /// assert(!pointer.starts_with(prefix, "$defs", "other")); + /// ``` + template + requires(!std::is_same_v, Token> && + !std::is_same_v, Token>) + [[nodiscard]] auto starts_with(const GenericPointer &other, + const StringLeftT &tail_left, + const StringRightT &tail_right) const -> bool { + const auto prefix_size{other.size()}; + return this->size() > prefix_size + 1 && + this->starts_with(other, tail_left) && + this->data[prefix_size + 1].is_property() && + this->data[prefix_size + 1].to_property() == tail_right; + } + /// Check whether a JSON Pointer starts with the initial part of another JSON /// Pointer. For example: /// diff --git a/vendor/core/src/core/jsonpointer/mangle.cc b/vendor/core/src/core/jsonpointer/mangle.cc new file mode 100644 index 00000000..eaab7be9 --- /dev/null +++ b/vendor/core/src/core/jsonpointer/mangle.cc @@ -0,0 +1,222 @@ +#include + +#include // assert +#include // std::setfill, std::setw +#include // std::ostringstream +#include // std::string_view +#include // std::visit + +namespace { + +// Special characters +constexpr auto ESCAPE_PREFIX = 'X'; +constexpr auto UPPERCASE_PREFIX = 'U'; +constexpr auto SEPARATOR = '_'; +constexpr auto HYPHEN = '-'; + +// Reserved characters that need escaping +constexpr auto RESERVED_X_UPPER = 'X'; +constexpr auto RESERVED_X_LOWER = 'x'; +constexpr auto RESERVED_U_UPPER = 'U'; +constexpr auto RESERVED_U_LOWER = 'u'; +constexpr auto RESERVED_Z_UPPER = 'Z'; +constexpr auto RESERVED_Z_LOWER = 'z'; + +// Special token markers +constexpr std::string_view TOKEN_EMPTY = "ZEmpty"; +constexpr std::string_view TOKEN_WILDCARD_PROPERTY = "ZAnyProperty"; +constexpr std::string_view TOKEN_WILDCARD_ITEM = "ZAnyItem"; +constexpr std::string_view TOKEN_WILDCARD_KEY = "ZAnyKey"; +constexpr std::string_view TOKEN_CONDITION = "ZMaybe"; +constexpr std::string_view TOKEN_NEGATION = "ZNot"; +constexpr std::string_view TOKEN_REGEX = "ZRegex"; + +constexpr auto ASCII_MAX = static_cast(0x80); + +// Locale-independent ASCII character classification +inline auto is_ascii_alpha(unsigned char character) noexcept -> bool { + return (character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z'); +} + +inline auto is_ascii_digit(unsigned char character) noexcept -> bool { + return character >= '0' && character <= '9'; +} + +inline auto is_ascii_lower(unsigned char character) noexcept -> bool { + return character >= 'a' && character <= 'z'; +} + +inline auto to_ascii_upper(unsigned char character) noexcept -> char { + if (character >= 'a' && character <= 'z') { + return static_cast(character - 'a' + 'A'); + } + return static_cast(character); +} + +inline auto hex_escape(std::ostringstream &output, char character) noexcept + -> void { + output << ESCAPE_PREFIX << std::uppercase << std::hex << std::setfill('0') + << std::setw(2) + << static_cast(static_cast(character)); +} + +inline auto is_reserved_at_start(char character) noexcept -> bool { + switch (character) { + case RESERVED_X_UPPER: + case RESERVED_X_LOWER: + case RESERVED_U_UPPER: + case RESERVED_U_LOWER: + case RESERVED_Z_UPPER: + case RESERVED_Z_LOWER: + return true; + default: + return false; + } +} + +inline auto encode_prefix(std::ostringstream &output, + std::string_view input) noexcept -> void { + bool capitalize_next{true}; + bool first{true}; + + for (const auto character : input) { + const auto unsigned_character{static_cast(character)}; + + if (is_ascii_alpha(unsigned_character)) { + if (capitalize_next && is_ascii_lower(unsigned_character)) { + output << to_ascii_upper(unsigned_character); + } else { + output << character; + } + capitalize_next = false; + } else if (is_ascii_digit(unsigned_character)) { + if (first) { + output << SEPARATOR; + } + output << character; + capitalize_next = false; + } else if (character == SEPARATOR || character == HYPHEN) { + capitalize_next = true; + } else { + hex_escape(output, character); + capitalize_next = true; + } + + first = false; + } +} + +inline auto encode_string(std::ostringstream &output, + const std::string &input) noexcept -> void { + bool segment_start{true}; + + for (const auto character : input) { + const auto unsigned_character{static_cast(character)}; + + if (is_ascii_alpha(unsigned_character)) { + const bool is_lower{is_ascii_lower(unsigned_character)}; + if (segment_start) { + if (is_reserved_at_start(character)) { + hex_escape(output, character); + } else if (is_lower) { + output << to_ascii_upper(unsigned_character); + } else { + output << UPPERCASE_PREFIX << character; + } + } else if (character == RESERVED_X_UPPER || + character == RESERVED_U_UPPER) { + hex_escape(output, character); + } else { + output << character; + } + segment_start = false; + } else if (is_ascii_digit(unsigned_character)) { + output << character; + segment_start = false; + } else { + hex_escape(output, character); + // Only ASCII non-alphanumeric starts a new segment + // Non-ASCII bytes (>= 0x80) do not start new segments (UTF-8 handling) + segment_start = (unsigned_character < ASCII_MAX); + } + } +} + +inline auto encode_string_or_empty(std::ostringstream &output, + const std::string &input) noexcept -> void { + if (input.empty()) { + output << TOKEN_EMPTY; + } else { + encode_string(output, input); + } +} + +class TokenVisitor { +public: + explicit TokenVisitor(std::ostringstream &output) noexcept + : output_{output} {} + + auto operator()(const sourcemeta::core::Pointer::Token &token) const noexcept + -> void { + this->output_ << SEPARATOR; + encode_string_or_empty(this->output_, token.to_property()); + } + + auto operator()(const sourcemeta::core::PointerTemplate::Wildcard &wildcard) + const noexcept -> void { + this->output_ << SEPARATOR; + switch (wildcard) { + case sourcemeta::core::PointerTemplate::Wildcard::Property: + this->output_ << TOKEN_WILDCARD_PROPERTY; + break; + case sourcemeta::core::PointerTemplate::Wildcard::Item: + this->output_ << TOKEN_WILDCARD_ITEM; + break; + case sourcemeta::core::PointerTemplate::Wildcard::Key: + this->output_ << TOKEN_WILDCARD_KEY; + break; + } + } + + auto operator()(const sourcemeta::core::PointerTemplate::Condition &condition) + const noexcept -> void { + this->output_ << SEPARATOR << TOKEN_CONDITION; + if (condition.suffix.has_value()) { + encode_string_or_empty(this->output_, condition.suffix.value()); + } + } + + auto + operator()(const sourcemeta::core::PointerTemplate::Negation &) const noexcept + -> void { + this->output_ << SEPARATOR << TOKEN_NEGATION; + } + + auto operator()(const sourcemeta::core::PointerTemplate::Regex ®ex) + const noexcept -> void { + this->output_ << SEPARATOR << TOKEN_REGEX; + encode_string_or_empty(this->output_, regex); + } + +private: + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + std::ostringstream &output_; +}; + +} // namespace + +namespace sourcemeta::core { + +auto mangle(const PointerTemplate &pointer, const std::string_view prefix) + -> std::string { + assert(!prefix.empty()); + std::ostringstream output; + encode_prefix(output, prefix); + for (const auto &token : pointer) { + std::visit(TokenVisitor{output}, token); + } + return output.str(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jsonschema/transformer.cc b/vendor/core/src/core/jsonschema/transformer.cc index 32287b52..4e297d5c 100644 --- a/vendor/core/src/core/jsonschema/transformer.cc +++ b/vendor/core/src/core/jsonschema/transformer.cc @@ -107,7 +107,7 @@ auto SchemaTransformer::check( const std::optional &default_dialect, const std::optional &default_id) const -> std::pair { - SchemaFrame frame{SchemaFrame::Mode::Instances}; + SchemaFrame frame{SchemaFrame::Mode::References}; // If we use the default id when there is already one, framing will duplicate // the locations leading to duplicate check reports @@ -173,7 +173,7 @@ auto SchemaTransformer::apply( std::size_t subschema_count{0}; std::size_t subschema_failures{0}; while (true) { - SchemaFrame frame{SchemaFrame::Mode::Instances}; + SchemaFrame frame{SchemaFrame::Mode::References}; frame.analyse(schema, walker, resolver, default_dialect, default_id); std::unordered_set visited; diff --git a/vendor/core/src/extension/alterschema/CMakeLists.txt b/vendor/core/src/extension/alterschema/CMakeLists.txt index 11ca6a60..506936b5 100644 --- a/vendor/core/src/extension/alterschema/CMakeLists.txt +++ b/vendor/core/src/extension/alterschema/CMakeLists.txt @@ -59,6 +59,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema common/unnecessary_allof_ref_wrapper_draft.h common/unnecessary_allof_ref_wrapper_modern.h common/unnecessary_allof_wrapper.h + common/unsatisfiable_in_place_applicator_type.h # Linter linter/additional_properties_default.h diff --git a/vendor/core/src/extension/alterschema/alterschema.cc b/vendor/core/src/extension/alterschema/alterschema.cc index 4e87f439..bd2af602 100644 --- a/vendor/core/src/extension/alterschema/alterschema.cc +++ b/vendor/core/src/extension/alterschema/alterschema.cc @@ -87,6 +87,7 @@ inline auto APPLIES_TO_POINTERS(std::vector &&keywords) #include "common/unnecessary_allof_ref_wrapper_draft.h" #include "common/unnecessary_allof_ref_wrapper_modern.h" #include "common/unnecessary_allof_wrapper.h" +#include "common/unsatisfiable_in_place_applicator_type.h" // Linter #include "linter/additional_properties_default.h" @@ -140,6 +141,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/core/src/extension/alterschema/common/orphan_definitions.h b/vendor/core/src/extension/alterschema/common/orphan_definitions.h index 812a7dc9..0ff2e3e6 100644 --- a/vendor/core/src/extension/alterschema/common/orphan_definitions.h +++ b/vendor/core/src/extension/alterschema/common/orphan_definitions.h @@ -15,6 +15,7 @@ class OrphanDefinitions final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> sourcemeta::core::SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(schema.is_object()); const bool has_modern_core{ vocabularies.contains(Vocabularies::Known::JSON_Schema_2020_12_Core) || vocabularies.contains(Vocabularies::Known::JSON_Schema_2019_09_Core)}; @@ -22,19 +23,41 @@ class OrphanDefinitions final : public SchemaTransformRule { vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_7) || vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_6) || vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_4)}; + const bool has_defs{has_modern_core && schema.defines("$defs")}; + const bool has_definitions{(has_modern_core || has_draft_definitions) && + schema.defines("definitions")}; + ONLY_CONTINUE_IF(has_defs || has_definitions); - ONLY_CONTINUE_IF(has_modern_core || has_draft_definitions); - ONLY_CONTINUE_IF(schema.is_object()); + const auto prefix_size{location.pointer.size()}; + bool has_external_to_defs{false}; + bool has_external_to_definitions{false}; + std::unordered_set outside_referenced_defs; + std::unordered_set outside_referenced_definitions; - std::vector orphans; + for (const auto &[key, reference] : frame.references()) { + const auto destination_location{frame.traverse(reference.destination)}; + if (destination_location.has_value()) { + if (has_defs) { + process_reference(key.second, destination_location->get().pointer, + location.pointer, prefix_size, "$defs", + has_external_to_defs, outside_referenced_defs); + } - if (has_modern_core) { - collect_orphans(frame, location, schema, "$defs", orphans); + if (has_definitions) { + process_reference(key.second, destination_location->get().pointer, + location.pointer, prefix_size, "definitions", + has_external_to_definitions, + outside_referenced_definitions); + } + } } - if (has_modern_core || has_draft_definitions) { - collect_orphans(frame, location, schema, "definitions", orphans); - } + std::vector orphans; + collect_orphans(schema, "$defs", has_defs, has_external_to_defs, + outside_referenced_defs, orphans); + collect_orphans(schema, "definitions", has_definitions, + has_external_to_definitions, outside_referenced_definitions, + orphans); ONLY_CONTINUE_IF(!orphans.empty()); return APPLIES_TO_POINTERS(std::move(orphans)); @@ -55,19 +78,43 @@ class OrphanDefinitions final : public SchemaTransformRule { private: static auto - collect_orphans(const sourcemeta::core::SchemaFrame &frame, - const sourcemeta::core::SchemaFrame::Location &root, - const JSON &schema, const JSON::String &container, - std::vector &orphans) -> void { - if (!schema.defines(container) || !schema.at(container).is_object()) { + process_reference(const Pointer &source_pointer, + const Pointer &destination_pointer, const Pointer &prefix, + const std::size_t prefix_size, std::string_view container, + bool &has_external, + std::unordered_set &referenced) -> void { + if (!destination_pointer.starts_with(prefix, container) || + destination_pointer.size() <= prefix_size + 1) { return; } - for (const auto &entry : schema.at(container).as_object()) { - auto entry_pointer{Pointer{container, entry.first}}; - const auto &entry_location{frame.traverse(root, entry_pointer)}; - if (frame.instance_locations(entry_location).empty()) { - orphans.push_back(std::move(entry_pointer)); + const auto &entry_token{destination_pointer.at(prefix_size + 1)}; + if (entry_token.is_property()) { + const auto &entry_name{entry_token.to_property()}; + if (!source_pointer.starts_with(prefix, container)) { + has_external = true; + referenced.insert(entry_name); + } else if (!source_pointer.starts_with(prefix, container, entry_name)) { + referenced.insert(entry_name); + } + } + } + + static auto + collect_orphans(const JSON &schema, const JSON::String &container, + const bool has_container, const bool has_external_reference, + const std::unordered_set &referenced, + std::vector &orphans) -> void { + if (has_container) { + const auto &maybe_object{schema.at(container)}; + if (maybe_object.is_object()) { + // If no external references to container, all definitions are orphans + // Otherwise, only unreferenced definitions are orphans + for (const auto &entry : maybe_object.as_object()) { + if (!has_external_reference || !referenced.contains(entry.first)) { + orphans.push_back(Pointer{container, entry.first}); + } + } } } } diff --git a/vendor/core/src/extension/alterschema/common/unsatisfiable_in_place_applicator_type.h b/vendor/core/src/extension/alterschema/common/unsatisfiable_in_place_applicator_type.h new file mode 100644 index 00000000..7c677d39 --- /dev/null +++ b/vendor/core/src/extension/alterschema/common/unsatisfiable_in_place_applicator_type.h @@ -0,0 +1,85 @@ +class UnsatisfiableInPlaceApplicatorType final : public SchemaTransformRule { +public: + UnsatisfiableInPlaceApplicatorType() + : SchemaTransformRule{ + "unsatisfiable_in_place_applicator_type", + "An in-place applicator branch that defines a `type` with no " + "overlap with the parent `type` can never be satisfied"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &) const + -> sourcemeta::core::SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(schema.is_object() && schema.defines("type")); + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_0})); + const auto parent_types{parse_schema_type(schema.at("type"))}; + + std::vector locations; + + for (const auto &entry : schema.as_object()) { + const auto &keyword{entry.first}; + const auto &keyword_type{walker(keyword, vocabularies).type}; + + if (keyword_type == SchemaKeywordType::ApplicatorElementsInPlace || + keyword_type == SchemaKeywordType::ApplicatorElementsInPlaceSome) { + if (!entry.second.is_array()) { + continue; + } + + const auto &branches{entry.second}; + for (std::size_t index = 0; index < branches.size(); ++index) { + const auto &branch{branches.at(index)}; + if (!branch.is_object() || !branch.defines("type")) { + continue; + } + + const auto branch_types{parse_schema_type(branch.at("type"))}; + if ((parent_types & branch_types).none()) { + locations.push_back(Pointer{keyword, index}); + } + } + } else if (keyword_type == + SchemaKeywordType::ApplicatorValueInPlaceMaybe) { + if (!entry.second.is_object() || !entry.second.defines("type")) { + continue; + } + + const auto branch_types{parse_schema_type(entry.second.at("type"))}; + if ((parent_types & branch_types).none()) { + locations.push_back(Pointer{keyword}); + } + } + } + + ONLY_CONTINUE_IF(!locations.empty()); + return APPLIES_TO_POINTERS(std::move(locations)); + } + + auto transform(JSON &schema, const Result &result) const -> void override { + for (const auto &location : result.locations) { + if (location.size() == 2) { + const auto &keyword{location.at(0).to_property()}; + const auto index{location.at(1).to_index()}; + schema.at(keyword).at(index).into(JSON{false}); + } else { + assert(location.size() == 1); + const auto &keyword{location.at(0).to_property()}; + schema.at(keyword).into(JSON{false}); + } + } + } +};