diff --git a/src/extension/alterschema/common/required_properties_in_properties.h b/src/extension/alterschema/common/required_properties_in_properties.h index 26739fae5..f4e7a1793 100644 --- a/src/extension/alterschema/common/required_properties_in_properties.h +++ b/src/extension/alterschema/common/required_properties_in_properties.h @@ -78,10 +78,12 @@ class RequiredPropertiesInProperties final : public SchemaTransformRule { const SchemaWalker &walker, const SchemaResolver &resolver, const JSON::String &property) const -> bool { - if (location.parent.has_value()) { - const auto &parent_pointer{location.parent.value()}; - const auto relative_pointer{ - location.pointer.resolve_from(parent_pointer)}; + auto current_pointer = location.pointer; + auto current_parent = location.parent; + + while (current_parent.has_value()) { + const auto &parent_pointer{current_parent.value()}; + const auto relative_pointer{current_pointer.resolve_from(parent_pointer)}; assert(!relative_pointer.empty() && relative_pointer.at(0).is_property()); const auto parent{ frame.traverse(frame.uri(parent_pointer).value().get())}; @@ -89,14 +91,21 @@ class RequiredPropertiesInProperties final : public SchemaTransformRule { const auto type{walker(relative_pointer.at(0).to_property(), frame.vocabularies(parent.value().get(), resolver)) .type}; - if (type == SchemaKeywordType::ApplicatorElementsInPlaceSome || - type == SchemaKeywordType::ApplicatorElementsInPlace || - type == SchemaKeywordType::ApplicatorValueInPlaceMaybe || - type == SchemaKeywordType::ApplicatorValueInPlaceNegate || - type == SchemaKeywordType::ApplicatorValueInPlaceOther) { - return this->defined_in_properties_sibling(get(root, parent_pointer), - property); + if (type != SchemaKeywordType::ApplicatorElementsInPlaceSome && + type != SchemaKeywordType::ApplicatorElementsInPlace && + type != SchemaKeywordType::ApplicatorValueInPlaceMaybe && + type != SchemaKeywordType::ApplicatorValueInPlaceNegate && + type != SchemaKeywordType::ApplicatorValueInPlaceOther) { + return false; + } + + if (this->defined_in_properties_sibling(get(root, parent_pointer), + property)) { + return true; } + + current_pointer = parent_pointer; + current_parent = parent.value().get().parent; } return false; diff --git a/test/alterschema/alterschema_lint_2020_12_test.cc b/test/alterschema/alterschema_lint_2020_12_test.cc index 4069963d4..ba496eff7 100644 --- a/test/alterschema/alterschema_lint_2020_12_test.cc +++ b/test/alterschema/alterschema_lint_2020_12_test.cc @@ -9512,3 +9512,155 @@ TEST(AlterSchema_lint_2020_12, empty_object_as_true_3) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_2020_12, object_oneof_required_not_required_1) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "A test", + "examples": [ {} ], + "properties": { + "term1": true, + "term2": true + }, + "oneOf": [ + { "required": [ "term1" ], "not": { "required": [ "term2" ] } }, + { "required": [ "term2" ], "not": { "required": [ "term1" ] } } + ] + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2020_12, object_oneof_required_not_required_2) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "A test", + "examples": [ [] ], + "properties": { + "term1": true, + "term2": true + }, + "items": { + "not": { "required": [ "term2" ] } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE( + traces, 0, "/items/not", "required_properties_in_properties", + "Every property listed in the `required` keyword must be explicitly " + "defined using the `properties` keyword", + true); +} + +TEST(AlterSchema_lint_2020_12, object_oneof_required_not_required_3) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "A test", + "examples": [ {} ], + "properties": { + "foo": true, + "bar": true + }, + "if": { "type": "object" }, + "then": { + "oneOf": [ + { "not": { "required": [ "foo" ] } }, + { "not": { "required": [ "bar" ] } } + ] + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2020_12, object_oneof_required_not_required_4) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "A test", + "examples": [ {} ], + "properties": { + "foo": true + }, + "if": { "required": [ "foo" ] }, + "then": { "not": { "required": [ "foo" ] } } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2020_12, object_oneof_required_not_required_5) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "A test", + "examples": [ [] ], + "properties": { + "foo": true + }, + "oneOf": [ + { + "items": { + "not": { "required": [ "foo" ] } + } + } + ] + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE( + traces, 0, "/oneOf/0/items/not", "required_properties_in_properties", + "Every property listed in the `required` keyword must be explicitly " + "defined using the `properties` keyword", + true); +} + +TEST(AlterSchema_lint_2020_12, object_oneof_required_not_required_6) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "A test", + "examples": [ {} ], + "properties": { + "foo": true, + "bar": true + }, + "allOf": [ + { + "oneOf": [ + { "not": { "required": [ "foo" ] } }, + { "not": { "required": [ "bar" ] } } + ] + } + ] + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE( + traces, 0, "", "unnecessary_allof_wrapper", + "Keywords inside `allOf` that do not conflict with the parent schema " + "can be elevated", + true); +}