diff --git a/src/extension/alterschema/CMakeLists.txt b/src/extension/alterschema/CMakeLists.txt index 506936b59..ee37e8cf2 100644 --- a/src/extension/alterschema/CMakeLists.txt +++ b/src/extension/alterschema/CMakeLists.txt @@ -90,7 +90,8 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema linter/unevaluated_items_default.h linter/unevaluated_properties_default.h linter/unsatisfiable_max_contains.h - linter/unsatisfiable_min_properties.h) + linter/unsatisfiable_min_properties.h + linter/then_false.h) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME alterschema) diff --git a/src/extension/alterschema/alterschema.cc b/src/extension/alterschema/alterschema.cc index bd2af6027..23ee04633 100644 --- a/src/extension/alterschema/alterschema.cc +++ b/src/extension/alterschema/alterschema.cc @@ -109,6 +109,7 @@ inline auto APPLIES_TO_POINTERS(std::vector &&keywords) #include "linter/property_names_default.h" #include "linter/property_names_type_default.h" #include "linter/simple_properties_identifiers.h" +#include "linter/then_false.h" #include "linter/title_description_equal.h" #include "linter/title_trailing_period.h" #include "linter/title_trim.h" @@ -150,6 +151,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/then_false.h b/src/extension/alterschema/linter/then_false.h new file mode 100644 index 000000000..1fcd0279d --- /dev/null +++ b/src/extension/alterschema/linter/then_false.h @@ -0,0 +1,52 @@ +class ThenFalse final : public SchemaTransformRule { +public: + ThenFalse() + : SchemaTransformRule{ + "then_false", + "`Setting the `then` keyword to the false schema is a convoluted " + "way of negating the `if` subschema, which you can more cleanly " + "represent using the `not` keyword"} {}; + + [[nodiscard]] auto + condition(const JSON &schema, const JSON &, const Vocabularies &vocabularies, + const SchemaFrame &, const SchemaFrame::Location &, + const SchemaWalker &, const SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7}) && + schema.is_object() && schema.defines("if") && schema.defines("then") && + schema.at("then").is_boolean() && !schema.at("then").to_boolean() && + is_schema(schema.at("if")) && + !(schema.at("if").is_boolean() && schema.at("if").to_boolean()) && + (!schema.defines("else") || + (schema.at("else").is_boolean() && schema.at("else").to_boolean())) && + (!schema.at("if").is_object() || + std::all_of(schema.at("if").as_object().begin(), + schema.at("if").as_object().end(), + [&schema](const auto &entry) { + return !schema.defines(entry.first); + })) && + std::all_of(schema.as_object().begin(), schema.as_object().end(), + [](const auto &entry) { + return entry.first == "$schema" || entry.first == "if" || + entry.first == "then" || entry.first == "else"; + })); + + if (schema.defines("else")) { + return APPLIES_TO_KEYWORDS("then", "if", "else"); + } else { + return APPLIES_TO_KEYWORDS("then", "if"); + } + } + + auto transform(JSON &schema, const Result &) const -> void override { + auto if_schema = schema.at("if"); + schema.erase("if"); + schema.erase("then"); + schema.erase("else"); + schema.assign("not", std::move(if_schema)); + } +}; diff --git a/test/alterschema/alterschema_lint_2019_09_test.cc b/test/alterschema/alterschema_lint_2019_09_test.cc index deb3544a2..fb48cc152 100644 --- a/test/alterschema/alterschema_lint_2019_09_test.cc +++ b/test/alterschema/alterschema_lint_2019_09_test.cc @@ -3997,3 +3997,179 @@ TEST(AlterSchema_lint_2019_09, EXPECT_TRUE(result.first); EXPECT_EQ(traces.size(), 0); } + +TEST(AlterSchema_lint_2019_09, then_false_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "if": { + "properties": { + "flag": { + "const": true + } + } + }, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "not": { + "properties": { + "flag": { + "const": true + } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, then_false_2) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "if": { + "properties": { + "flag": { + "const": true + } + } + }, + "then": false, + "else": true + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "not": { + "properties": { + "flag": { + "const": true + } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, then_false_3) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "if": true, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "if": true, + "then": false + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, then_false_4) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "if": false, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema" + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, then_false_5) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "if": { + "properties": { + "name": { "type": "number" } + } + }, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "if": { + "properties": { + "name": { "type": "number" } + } + }, + "then": false + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, then_false_6) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "if": { + "properties": { + "flag": { "const": true } + } + }, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "not": { + "properties": { + "flag": { "const": true } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, then_false_7) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "if": { + "type": "object" + }, + "then": false, + "else": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "if": { + "type": "object" + }, + "then": false, + "else": false + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/alterschema/alterschema_lint_2020_12_test.cc b/test/alterschema/alterschema_lint_2020_12_test.cc index 911217cdb..225cb8539 100644 --- a/test/alterschema/alterschema_lint_2020_12_test.cc +++ b/test/alterschema/alterschema_lint_2020_12_test.cc @@ -4452,6 +4452,182 @@ TEST(AlterSchema_lint_2020_12, EXPECT_EQ(document, expected); } +TEST(AlterSchema_lint_2020_12, then_false_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": { + "properties": { + "flag": { + "const": true + } + } + }, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { + "properties": { + "flag": { + "const": true + } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, then_false_2) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": { + "properties": { + "flag": { + "const": true + } + } + }, + "then": false, + "else": true + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { + "properties": { + "flag": { + "const": true + } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, then_false_3) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": true, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": true, + "then": false + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, then_false_4) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": false, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema" + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, then_false_5) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "if": { + "properties": { + "name": { "type": "number" } + } + }, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "if": { + "properties": { + "name": { "type": "number" } + } + }, + "then": false + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, then_false_6) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": { + "properties": { + "flag": { "const": true } + } + }, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { + "properties": { + "flag": { "const": true } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, then_false_7) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": { + "type": "object" + }, + "then": false, + "else": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": { + "type": "object" + }, + "then": false, + "else": false + })JSON"); + + EXPECT_EQ(document, expected); +} + TEST(AlterSchema_lint_2020_12, embedded_resource_draft7) { sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", diff --git a/test/alterschema/alterschema_lint_draft7_test.cc b/test/alterschema/alterschema_lint_draft7_test.cc index f7c05ab3a..b5bad5e0b 100644 --- a/test/alterschema/alterschema_lint_draft7_test.cc +++ b/test/alterschema/alterschema_lint_draft7_test.cc @@ -2664,6 +2664,182 @@ TEST(AlterSchema_lint_draft7, orphan_definitions_1) { EXPECT_EQ(document, expected); } +TEST(AlterSchema_lint_draft7, then_false_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "if": { + "properties": { + "flag": { + "const": true + } + } + }, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "not": { + "properties": { + "flag": { + "const": true + } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft7, then_false_2) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "if": { + "properties": { + "flag": { + "const": true + } + } + }, + "then": false, + "else": true + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "not": { + "properties": { + "flag": { + "const": true + } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft7, then_false_3) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "if": true, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "if": true, + "then": false + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft7, then_false_4) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "if": false, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#" + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft7, then_false_5) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "if": { + "properties": { + "name": { "type": "number" } + } + }, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "if": { + "properties": { + "name": { "type": "number" } + } + }, + "then": false + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft7, then_false_6) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "if": { + "properties": { + "flag": { "const": true } + } + }, + "then": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "not": { + "properties": { + "flag": { "const": true } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft7, then_false_7) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "if": { + "type": "object" + }, + "then": false, + "else": false + })JSON"); + + LINT_AND_FIX(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "if": { + "type": "object" + }, + "then": false, + "else": false + })JSON"); + + EXPECT_EQ(document, expected); +} + TEST(AlterSchema_lint_draft7, orphan_definitions_2) { sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-07/schema#",