diff --git a/src/alterschema/CMakeLists.txt b/src/alterschema/CMakeLists.txt index 22da55e73..98c4dcf16 100644 --- a/src/alterschema/CMakeLists.txt +++ b/src/alterschema/CMakeLists.txt @@ -155,6 +155,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema linter/properties_default.h linter/property_names_default.h linter/property_names_type_default.h + linter/require_schema_declaration.h linter/simple_properties_identifiers.h linter/then_empty.h linter/title_description_equal.h diff --git a/src/alterschema/alterschema.cc b/src/alterschema/alterschema.cc index 9b32e78f2..681411476 100644 --- a/src/alterschema/alterschema.cc +++ b/src/alterschema/alterschema.cc @@ -269,6 +269,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "linter/properties_default.h" #include "linter/property_names_default.h" #include "linter/property_names_type_default.h" +#include "linter/require_schema_declaration.h" #include "linter/simple_properties_identifiers.h" #include "linter/title_description_equal.h" #include "linter/title_trailing_period.h" @@ -482,6 +483,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/alterschema/linter/require_schema_declaration.h b/src/alterschema/linter/require_schema_declaration.h new file mode 100644 index 000000000..9a7842ca8 --- /dev/null +++ b/src/alterschema/linter/require_schema_declaration.h @@ -0,0 +1,32 @@ +class RequireSchemaDeclaration final : public SchemaTransformRule { +public: + using mutates = std::false_type; + using reframe_after_transform = std::false_type; + RequireSchemaDeclaration() + : SchemaTransformRule{ + "require_schema_declaration", + "A schema should declare its dialect using the `$schema` " + "keyword"} {}; + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(location.pointer.empty()); + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Core, + Vocabularies::Known::JSON_Schema_2019_09_Core, + 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_3_Hyper})); + ONLY_CONTINUE_IF(schema.is_object()); + ONLY_CONTINUE_IF(!schema.defines("$schema")); + return APPLIES_TO_KEYWORDS("$schema"); + } +}; diff --git a/test/alterschema/alterschema_lint_2019_09_test.cc b/test/alterschema/alterschema_lint_2019_09_test.cc index e3232b0f5..af0130b2c 100644 --- a/test/alterschema/alterschema_lint_2019_09_test.cc +++ b/test/alterschema/alterschema_lint_2019_09_test.cc @@ -5982,3 +5982,47 @@ TEST(AlterSchema_lint_2019_09, EXPECT_EQ(sourcemeta::core::to_string(outcome.locations.at(1)), "/patternProperties/[[:digit:]]"); } + +TEST(AlterSchema_lint_2019_09, require_schema_declaration_1) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "My Schema", + "description": "A schema", + "examples": [ "hello" ], + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2019_09, require_schema_declaration_2) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "My Schema", + "description": "A schema", + "examples": [ "hello" ], + "type": "string" + })JSON"); + + std::vector> + traces; + sourcemeta::blaze::SchemaTransformer bundle; + sourcemeta::blaze::add(bundle, sourcemeta::blaze::AlterSchemaMode::Linter); + const auto result = bundle.check( + document, sourcemeta::blaze::schema_walker, alterschema_test_resolver, + [&traces](const auto &pointer, const auto &name, const auto &message, + const auto &outcome, const auto &fixable) { + traces.emplace_back(pointer, name, message, outcome, fixable); + }, + "https://json-schema.org/draft/2019-09/schema"); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "require_schema_declaration", + "A schema should declare its dialect using the `$schema` " + "keyword", + false); +} diff --git a/test/alterschema/alterschema_lint_2020_12_test.cc b/test/alterschema/alterschema_lint_2020_12_test.cc index d40efbf56..90559222b 100644 --- a/test/alterschema/alterschema_lint_2020_12_test.cc +++ b/test/alterschema/alterschema_lint_2020_12_test.cc @@ -13583,3 +13583,47 @@ TEST(AlterSchema_lint_2020_12, embedded_custom_metaschema) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_2020_12, require_schema_declaration_1) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "My Schema", + "description": "A schema", + "examples": [ "hello" ], + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_2020_12, require_schema_declaration_2) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "My Schema", + "description": "A schema", + "examples": [ "hello" ], + "type": "string" + })JSON"); + + std::vector> + traces; + sourcemeta::blaze::SchemaTransformer bundle; + sourcemeta::blaze::add(bundle, sourcemeta::blaze::AlterSchemaMode::Linter); + const auto result = bundle.check( + document, sourcemeta::blaze::schema_walker, alterschema_test_resolver, + [&traces](const auto &pointer, const auto &name, const auto &message, + const auto &outcome, const auto &fixable) { + traces.emplace_back(pointer, name, message, outcome, fixable); + }, + "https://json-schema.org/draft/2020-12/schema"); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "require_schema_declaration", + "A schema should declare its dialect using the `$schema` " + "keyword", + false); +} diff --git a/test/alterschema/alterschema_lint_draft3_test.cc b/test/alterschema/alterschema_lint_draft3_test.cc index 07e5802ee..512ea7a63 100644 --- a/test/alterschema/alterschema_lint_draft3_test.cc +++ b/test/alterschema/alterschema_lint_draft3_test.cc @@ -3436,3 +3436,45 @@ TEST(AlterSchema_lint_draft3, EXPECT_EQ(sourcemeta::core::to_string(outcome.locations.at(1)), "/patternProperties/[[:digit:]]"); } + +TEST(AlterSchema_lint_draft3, require_schema_declaration_1) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-03/schema#", + "title": "My Schema", + "description": "A schema", + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft3, require_schema_declaration_2) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "My Schema", + "description": "A schema", + "type": "string" + })JSON"); + + std::vector> + traces; + sourcemeta::blaze::SchemaTransformer bundle; + sourcemeta::blaze::add(bundle, sourcemeta::blaze::AlterSchemaMode::Linter); + const auto result = bundle.check( + document, sourcemeta::blaze::schema_walker, alterschema_test_resolver, + [&traces](const auto &pointer, const auto &name, const auto &message, + const auto &outcome, const auto &fixable) { + traces.emplace_back(pointer, name, message, outcome, fixable); + }, + "http://json-schema.org/draft-03/schema#"); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "require_schema_declaration", + "A schema should declare its dialect using the `$schema` " + "keyword", + false); +} diff --git a/test/alterschema/alterschema_lint_draft4_test.cc b/test/alterschema/alterschema_lint_draft4_test.cc index d0e9e846d..c4045e00c 100644 --- a/test/alterschema/alterschema_lint_draft4_test.cc +++ b/test/alterschema/alterschema_lint_draft4_test.cc @@ -3704,3 +3704,45 @@ TEST(AlterSchema_lint_draft4, EXPECT_EQ(sourcemeta::core::to_string(outcome.locations.at(1)), "/patternProperties/[[:digit:]]"); } + +TEST(AlterSchema_lint_draft4, require_schema_declaration_1) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "My Schema", + "description": "A schema", + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft4, require_schema_declaration_2) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "My Schema", + "description": "A schema", + "type": "string" + })JSON"); + + std::vector> + traces; + sourcemeta::blaze::SchemaTransformer bundle; + sourcemeta::blaze::add(bundle, sourcemeta::blaze::AlterSchemaMode::Linter); + const auto result = bundle.check( + document, sourcemeta::blaze::schema_walker, alterschema_test_resolver, + [&traces](const auto &pointer, const auto &name, const auto &message, + const auto &outcome, const auto &fixable) { + traces.emplace_back(pointer, name, message, outcome, fixable); + }, + "http://json-schema.org/draft-04/schema#"); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "require_schema_declaration", + "A schema should declare its dialect using the `$schema` " + "keyword", + false); +} diff --git a/test/alterschema/alterschema_lint_draft6_test.cc b/test/alterschema/alterschema_lint_draft6_test.cc index 4a3ad83ad..7935864df 100644 --- a/test/alterschema/alterschema_lint_draft6_test.cc +++ b/test/alterschema/alterschema_lint_draft6_test.cc @@ -3812,3 +3812,47 @@ TEST(AlterSchema_lint_draft6, EXPECT_EQ(sourcemeta::core::to_string(outcome.locations.at(1)), "/patternProperties/[[:digit:]]"); } + +TEST(AlterSchema_lint_draft6, require_schema_declaration_1) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "My Schema", + "description": "A schema", + "examples": [ "hello" ], + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft6, require_schema_declaration_2) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "My Schema", + "description": "A schema", + "examples": [ "hello" ], + "type": "string" + })JSON"); + + std::vector> + traces; + sourcemeta::blaze::SchemaTransformer bundle; + sourcemeta::blaze::add(bundle, sourcemeta::blaze::AlterSchemaMode::Linter); + const auto result = bundle.check( + document, sourcemeta::blaze::schema_walker, alterschema_test_resolver, + [&traces](const auto &pointer, const auto &name, const auto &message, + const auto &outcome, const auto &fixable) { + traces.emplace_back(pointer, name, message, outcome, fixable); + }, + "http://json-schema.org/draft-06/schema#"); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "require_schema_declaration", + "A schema should declare its dialect using the `$schema` " + "keyword", + false); +} diff --git a/test/alterschema/alterschema_lint_draft7_test.cc b/test/alterschema/alterschema_lint_draft7_test.cc index be902c1b0..f9b9f1577 100644 --- a/test/alterschema/alterschema_lint_draft7_test.cc +++ b/test/alterschema/alterschema_lint_draft7_test.cc @@ -5352,3 +5352,47 @@ TEST(AlterSchema_lint_draft7, EXPECT_EQ(sourcemeta::core::to_string(outcome.locations.at(1)), "/patternProperties/[[:digit:]]"); } + +TEST(AlterSchema_lint_draft7, require_schema_declaration_1) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "My Schema", + "description": "A schema", + "examples": [ "hello" ], + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(AlterSchema_lint_draft7, require_schema_declaration_2) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "My Schema", + "description": "A schema", + "examples": [ "hello" ], + "type": "string" + })JSON"); + + std::vector> + traces; + sourcemeta::blaze::SchemaTransformer bundle; + sourcemeta::blaze::add(bundle, sourcemeta::blaze::AlterSchemaMode::Linter); + const auto result = bundle.check( + document, sourcemeta::blaze::schema_walker, alterschema_test_resolver, + [&traces](const auto &pointer, const auto &name, const auto &message, + const auto &outcome, const auto &fixable) { + traces.emplace_back(pointer, name, message, outcome, fixable); + }, + "http://json-schema.org/draft-07/schema#"); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "require_schema_declaration", + "A schema should declare its dialect using the `$schema` " + "keyword", + false); +}