diff --git a/src/extension/alterschema/CMakeLists.txt b/src/extension/alterschema/CMakeLists.txt index 5fe9f4e93..d17efb350 100644 --- a/src/extension/alterschema/CMakeLists.txt +++ b/src/extension/alterschema/CMakeLists.txt @@ -85,6 +85,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema linter/enum_to_const.h linter/equal_numeric_bounds_to_const.h linter/forbid_empty_enum.h + linter/incoherent_min_max_contains.h linter/invalid_external_ref.h linter/items_array_default.h linter/items_schema_default.h diff --git a/src/extension/alterschema/alterschema.cc b/src/extension/alterschema/alterschema.cc index a6e9744c9..d20eff011 100644 --- a/src/extension/alterschema/alterschema.cc +++ b/src/extension/alterschema/alterschema.cc @@ -114,6 +114,7 @@ inline auto APPLIES_TO_POINTERS(std::vector &&keywords) #include "linter/enum_to_const.h" #include "linter/equal_numeric_bounds_to_const.h" #include "linter/forbid_empty_enum.h" +#include "linter/incoherent_min_max_contains.h" #include "linter/invalid_external_ref.h" #include "linter/items_array_default.h" #include "linter/items_schema_default.h" @@ -231,6 +232,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/src/extension/alterschema/linter/incoherent_min_max_contains.h b/src/extension/alterschema/linter/incoherent_min_max_contains.h new file mode 100644 index 000000000..7ee0a0011 --- /dev/null +++ b/src/extension/alterschema/linter/incoherent_min_max_contains.h @@ -0,0 +1,33 @@ +class IncoherentMinMaxContains final : public SchemaTransformRule { +public: + using mutates = std::false_type; + using reframe_after_transform = std::false_type; + IncoherentMinMaxContains() + : SchemaTransformRule{ + "incoherent_min_max_contains", + "`minContains` greater than `maxContains` makes the schema " + "unsatisfiable"} {}; + + [[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 &, + const sourcemeta::core::SchemaResolver &) const + -> sourcemeta::core::SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation}) && + schema.is_object() && schema.defines("contains") && + schema.defines("minContains") && + schema.at("minContains").is_integer() && + schema.defines("maxContains") && + schema.at("maxContains").is_integer() && + schema.at("minContains").to_integer() > + schema.at("maxContains").to_integer()); + return APPLIES_TO_KEYWORDS("minContains", "maxContains"); + } +}; diff --git a/test/alterschema/alterschema_lint_2019_09_test.cc b/test/alterschema/alterschema_lint_2019_09_test.cc index 24c642295..e3ef2ef9c 100644 --- a/test/alterschema/alterschema_lint_2019_09_test.cc +++ b/test/alterschema/alterschema_lint_2019_09_test.cc @@ -2185,6 +2185,214 @@ TEST(AlterSchema_lint_2019_09, unsatisfiable_max_contains_3) { EXPECT_EQ(document, expected); } +TEST(AlterSchema_lint_2019_09, incoherent_min_max_contains_1) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Test", + "description": "A test schema", + "examples": [ [] ], + "type": "array", + "contains": { "type": "string" }, + "minContains": 5, + "maxContains": 3 + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "incoherent_min_max_contains", + "`minContains` greater than `maxContains` makes the " + "schema unsatisfiable", + false); +} + +TEST(AlterSchema_lint_2019_09, incoherent_min_max_contains_2) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Test", + "description": "A test schema", + "examples": [ [] ], + "type": "array", + "contains": { "type": "string" }, + "minContains": 3, + "maxContains": 3 + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2019_09, incoherent_min_max_contains_3) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Test", + "description": "A test schema", + "examples": [ [] ], + "type": "array", + "contains": { "type": "string" }, + "minContains": 1, + "maxContains": 5 + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2019_09, incoherent_min_max_contains_4) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { + "type": "array", + "contains": { "type": "string" }, + "minContains": 5, + "maxContains": 3 + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "incoherent_min_max_contains", + "`minContains` greater than `maxContains` makes the " + "schema unsatisfiable", + false); +} + +TEST(AlterSchema_lint_2019_09, incoherent_min_max_contains_5) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Test", + "description": "A test schema", + "examples": [ [] ], + "type": "array", + "contains": { "type": "string" }, + "minContains": 5 + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2019_09, incoherent_min_max_contains_6) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Test", + "description": "A test schema", + "examples": [ [] ], + "type": "array", + "items": { + "type": "array", + "contains": { "type": "string" }, + "minContains": 5, + "maxContains": 3 + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/items", "incoherent_min_max_contains", + "`minContains` greater than `maxContains` makes the " + "schema unsatisfiable", + false); +} + +TEST(AlterSchema_lint_2019_09, incoherent_min_max_contains_7) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Test", + "description": "A test schema", + "examples": [ [] ], + "$ref": "#/$defs/foo", + "$defs": { + "foo": { + "type": "array", + "contains": { "type": "string" }, + "minContains": 5, + "maxContains": 3 + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/$defs/foo", "incoherent_min_max_contains", + "`minContains` greater than `maxContains` makes the " + "schema unsatisfiable", + false); +} + +TEST(AlterSchema_lint_2019_09, incoherent_min_max_contains_8) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "$ref": "#/$defs/A/properties/bar", + "$defs": { + "A": { + "type": "array", + "contains": { "type": "string" }, + "minContains": 5, + "maxContains": 3, + "properties": { + "bar": { "type": "string" } + } + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/$defs/A", "incoherent_min_max_contains", + "`minContains` greater than `maxContains` makes the " + "schema unsatisfiable", + false); +} + +TEST(AlterSchema_lint_2019_09, incoherent_min_max_contains_9) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Test", + "description": "A test schema", + "examples": [ [] ], + "type": "array", + "minContains": 5, + "maxContains": 3 + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 2); + EXPECT_LINT_TRACE(traces, 0, "", "max_contains_without_contains", + "The `maxContains` keyword is meaningless " + "without the presence of the `contains` keyword", + true); + EXPECT_LINT_TRACE(traces, 1, "", "min_contains_without_contains", + "The `minContains` keyword is meaningless " + "without the presence of the `contains` keyword", + true); +} + TEST(AlterSchema_lint_2019_09, equal_numeric_bounds_to_const_1) { sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2019-09/schema", diff --git a/test/alterschema/alterschema_lint_2020_12_test.cc b/test/alterschema/alterschema_lint_2020_12_test.cc index ad409c356..b303726c3 100644 --- a/test/alterschema/alterschema_lint_2020_12_test.cc +++ b/test/alterschema/alterschema_lint_2020_12_test.cc @@ -2089,6 +2089,214 @@ TEST(AlterSchema_lint_2020_12, unsatisfiable_max_contains_3) { EXPECT_EQ(document, expected); } +TEST(AlterSchema_lint_2020_12, incoherent_min_max_contains_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 schema", + "examples": [ [] ], + "type": "array", + "contains": { "type": "string" }, + "minContains": 5, + "maxContains": 3 + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "incoherent_min_max_contains", + "`minContains` greater than `maxContains` makes the " + "schema unsatisfiable", + false); +} + +TEST(AlterSchema_lint_2020_12, incoherent_min_max_contains_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 schema", + "examples": [ [] ], + "type": "array", + "contains": { "type": "string" }, + "minContains": 3, + "maxContains": 3 + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2020_12, incoherent_min_max_contains_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 schema", + "examples": [ [] ], + "type": "array", + "contains": { "type": "string" }, + "minContains": 1, + "maxContains": 5 + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2020_12, incoherent_min_max_contains_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 schema", + "examples": [ {} ], + "properties": { + "foo": { + "type": "array", + "contains": { "type": "string" }, + "minContains": 5, + "maxContains": 3 + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "incoherent_min_max_contains", + "`minContains` greater than `maxContains` makes the " + "schema unsatisfiable", + false); +} + +TEST(AlterSchema_lint_2020_12, incoherent_min_max_contains_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 schema", + "examples": [ [] ], + "type": "array", + "contains": { "type": "string" }, + "minContains": 5 + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2020_12, incoherent_min_max_contains_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 schema", + "examples": [ [] ], + "type": "array", + "items": { + "type": "array", + "contains": { "type": "string" }, + "minContains": 5, + "maxContains": 3 + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/items", "incoherent_min_max_contains", + "`minContains` greater than `maxContains` makes the " + "schema unsatisfiable", + false); +} + +TEST(AlterSchema_lint_2020_12, incoherent_min_max_contains_7) { + 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 schema", + "examples": [ [] ], + "$ref": "#/$defs/foo", + "$defs": { + "foo": { + "type": "array", + "contains": { "type": "string" }, + "minContains": 5, + "maxContains": 3 + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/$defs/foo", "incoherent_min_max_contains", + "`minContains` greater than `maxContains` makes the " + "schema unsatisfiable", + false); +} + +TEST(AlterSchema_lint_2020_12, incoherent_min_max_contains_8) { + 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 schema", + "examples": [ {} ], + "$ref": "#/$defs/A/properties/bar", + "$defs": { + "A": { + "type": "array", + "contains": { "type": "string" }, + "minContains": 5, + "maxContains": 3, + "properties": { + "bar": { "type": "string" } + } + } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/$defs/A", "incoherent_min_max_contains", + "`minContains` greater than `maxContains` makes the " + "schema unsatisfiable", + false); +} + +TEST(AlterSchema_lint_2020_12, incoherent_min_max_contains_9) { + 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 schema", + "examples": [ [] ], + "type": "array", + "minContains": 5, + "maxContains": 3 + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 2); + EXPECT_LINT_TRACE(traces, 0, "", "max_contains_without_contains", + "The `maxContains` keyword is meaningless " + "without the presence of the `contains` keyword", + true); + EXPECT_LINT_TRACE(traces, 1, "", "min_contains_without_contains", + "The `minContains` keyword is meaningless " + "without the presence of the `contains` keyword", + true); +} + TEST(AlterSchema_lint_2020_12, equal_numeric_bounds_to_const_1) { sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema",