diff --git a/src/linter/CMakeLists.txt b/src/linter/CMakeLists.txt index 1e125b5..0d0d177 100644 --- a/src/linter/CMakeLists.txt +++ b/src/linter/CMakeLists.txt @@ -7,6 +7,7 @@ noa_library(NAMESPACE sourcemeta PROJECT alterschema NAME linter # Antipattern antipattern/const_with_type.h antipattern/duplicate_enum_values.h + antipattern/duplicate_required_values.h antipattern/enum_with_type.h # Simplify diff --git a/src/linter/antipattern/duplicate_required_values.h b/src/linter/antipattern/duplicate_required_values.h new file mode 100644 index 0000000..bf49572 --- /dev/null +++ b/src/linter/antipattern/duplicate_required_values.h @@ -0,0 +1,31 @@ +class DuplicateRequiredValues final : public Rule { +public: + DuplicateRequiredValues() + : Rule("duplicate_required_values", + "Setting duplicate values in `required` is considered an " + "anti-pattern") {}; + + [[nodiscard]] auto + condition(const sourcemeta::jsontoolkit::JSON &schema, const std::string &, + const std::set &vocabularies, + const sourcemeta::jsontoolkit::Pointer &) const -> bool override { + return contains_any( + vocabularies, + {"https://json-schema.org/draft/2020-12/vocab/validation", + "https://json-schema.org/draft/2019-09/vocab/validation", + "http://json-schema.org/draft-07/schema#", + "http://json-schema.org/draft-06/schema#", + "http://json-schema.org/draft-04/schema#"}) && + schema.is_object() && schema.defines("required") && + schema.at("required").is_array() && !schema.at("required").unique(); + } + + auto transform(Transformer &transformer) const -> void override { + auto collection = transformer.schema().at("required"); + std::sort(collection.as_array().begin(), collection.as_array().end()); + auto last = + std::unique(collection.as_array().begin(), collection.as_array().end()); + collection.erase(last, collection.as_array().end()); + transformer.replace({"required"}, std::move(collection)); + } +}; diff --git a/src/linter/linter.cc b/src/linter/linter.cc index 7e60f79..050e9d5 100644 --- a/src/linter/linter.cc +++ b/src/linter/linter.cc @@ -18,6 +18,7 @@ auto contains_any(const T &container, const T &values) -> bool { // AntiPattern #include "antipattern/const_with_type.h" #include "antipattern/duplicate_enum_values.h" +#include "antipattern/duplicate_required_values.h" #include "antipattern/enum_with_type.h" // Simplify #include "simplify/single_type_array.h" @@ -46,6 +47,7 @@ auto add(Bundle &bundle, const LinterCategory category) -> void { case LinterCategory::AntiPattern: bundle.add(); bundle.add(); + bundle.add(); bundle.add(); break; case LinterCategory::Simplify: diff --git a/test/linter/2019_09_test.cc b/test/linter/2019_09_test.cc index 397a02e..8d2c8a8 100644 --- a/test/linter/2019_09_test.cc +++ b/test/linter/2019_09_test.cc @@ -568,3 +568,21 @@ TEST(Lint_2019_09, duplicate_enum_values_1) { EXPECT_EQ(document, expected); } + +TEST(Lint_2019_09, duplicate_required_values_1) { + sourcemeta::jsontoolkit::JSON document = + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "required": [ "foo", "bar", "baz", "foo" ] + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::jsontoolkit::JSON expected = + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "required": [ "bar", "baz", "foo" ] + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/linter/2020_12_test.cc b/test/linter/2020_12_test.cc index de52011..26d048e 100644 --- a/test/linter/2020_12_test.cc +++ b/test/linter/2020_12_test.cc @@ -551,3 +551,21 @@ TEST(Lint_2020_12, duplicate_enum_values_1) { EXPECT_EQ(document, expected); } + +TEST(Lint_2020_12, duplicate_required_values_1) { + sourcemeta::jsontoolkit::JSON document = + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "required": [ "foo", "bar", "baz", "foo" ] + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::jsontoolkit::JSON expected = + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "required": [ "bar", "baz", "foo" ] + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/linter/draft4_test.cc b/test/linter/draft4_test.cc index aeb8f5a..03f6981 100644 --- a/test/linter/draft4_test.cc +++ b/test/linter/draft4_test.cc @@ -179,3 +179,21 @@ TEST(Lint_draft4, duplicate_enum_values_4) { EXPECT_EQ(document, expected); } + +TEST(Lint_draft4, duplicate_required_values_1) { + sourcemeta::jsontoolkit::JSON document = + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "required": [ "foo", "bar", "baz", "foo" ] + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::jsontoolkit::JSON expected = + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "required": [ "bar", "baz", "foo" ] + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/linter/draft6_test.cc b/test/linter/draft6_test.cc index 04608d1..b4a2a1d 100644 --- a/test/linter/draft6_test.cc +++ b/test/linter/draft6_test.cc @@ -216,3 +216,21 @@ TEST(Lint_draft6, duplicate_enum_values_6) { EXPECT_EQ(document, expected); } + +TEST(Lint_draft6, duplicate_required_values_1) { + sourcemeta::jsontoolkit::JSON document = + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "required": [ "foo", "bar", "baz", "foo" ] + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::jsontoolkit::JSON expected = + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "required": [ "bar", "baz", "foo" ] + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/linter/draft7_test.cc b/test/linter/draft7_test.cc index cea1be8..ecbab33 100644 --- a/test/linter/draft7_test.cc +++ b/test/linter/draft7_test.cc @@ -307,3 +307,21 @@ TEST(Lint_draft7, duplicate_enum_values_7) { EXPECT_EQ(document, expected); } + +TEST(Lint_draft7, duplicate_required_values_1) { + sourcemeta::jsontoolkit::JSON document = + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ "foo", "bar", "baz", "foo" ] + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::jsontoolkit::JSON expected = + sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ "bar", "baz", "foo" ] + })JSON"); + + EXPECT_EQ(document, expected); +}