Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/extension/alterschema/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema
linter/duplicate_examples.h
linter/enum_to_const.h
linter/equal_numeric_bounds_to_const.h
linter/forbid_empty_enum.h
linter/items_array_default.h
linter/items_schema_default.h
linter/multiple_of_default.h
Expand Down
2 changes: 2 additions & 0 deletions src/extension/alterschema/alterschema.cc
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ inline auto APPLIES_TO_POINTERS(std::vector<Pointer> &&keywords)
#include "linter/duplicate_examples.h"
#include "linter/enum_to_const.h"
#include "linter/equal_numeric_bounds_to_const.h"
#include "linter/forbid_empty_enum.h"
#include "linter/items_array_default.h"
#include "linter/items_schema_default.h"
#include "linter/multiple_of_default.h"
Expand Down Expand Up @@ -222,6 +223,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void {
bundle.add<UnsatisfiableMaxContains>();
bundle.add<UnsatisfiableMinProperties>();
bundle.add<EnumToConst>();
bundle.add<ForbidEmptyEnum>();
bundle.add<TopLevelTitle>();
bundle.add<TopLevelDescription>();
bundle.add<TopLevelExamples>();
Expand Down
35 changes: 35 additions & 0 deletions src/extension/alterschema/linter/forbid_empty_enum.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
class ForbidEmptyEnum final : public SchemaTransformRule {
public:
using mutates = std::true_type;
using reframe_after_transform = std::true_type;
ForbidEmptyEnum()
: SchemaTransformRule{"forbid_empty_enum",
"An empty `enum` validates nothing and is "
"equivalent to `not: {}`"} {};

[[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,
Vocabularies::Known::JSON_Schema_Draft_7,
Vocabularies::Known::JSON_Schema_Draft_6,
Vocabularies::Known::JSON_Schema_Draft_4}) &&
schema.is_object() && schema.defines("enum") &&
!schema.defines("not") && schema.at("enum").is_array() &&
schema.at("enum").empty());
return APPLIES_TO_KEYWORDS("enum");
}

auto transform(JSON &schema, const Result &) const -> void override {
Copy link
Member

@jviotti jviotti Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a LINT_AND_FIX test where you have a $ref pointing directly at the subschema with the empty enum and also one pointing to something INSIDE the subschema with empty enum (maybe by having a $defs sibling to the empty enum)?

I suspect the second will break at the moment.

Also what if you have an empty enum on a subschema that has its own nested $id? By turning the entire thing to false, we would end up getting rid of the identifier, which can break a reference to the subschema by URI.

Can you try to encode as many of these edge cases as you can possibly think of? These are the tricky cases that show up in practice and end up in incorrect linter behaviour or crashes

Given all the above, it might be easier to add not: {} instead of turning the entire thing to false. And you can always update the condition to apply for Draft 4 and later (which will address https://github.com/sourcemeta/core/pull/2287/changes#r2931230208) and only apply of not is not there already (a fine compromise for now?)

schema.erase("enum");
schema.assign("not", JSON::make_object());
}
};
246 changes: 246 additions & 0 deletions test/alterschema/alterschema_lint_2019_09_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4494,3 +4494,249 @@ TEST(AlterSchema_lint_2019_09, empty_object_as_true_1) {

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2019_09, forbid_empty_enum_1) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the rule is always fixable, let's always exercise it with LINT_AND_FIX. Also closely follow the conventions of the other tests. We have expected after the check for result.first, and we don't usually check the traces anymore.

sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [1],
"enum": []
})JSON");

LINT_AND_FIX(document, result, traces);

EXPECT_TRUE(result.first);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [1],
"not": true
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2019_09, forbid_empty_enum_2) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [1],
"enum": [1, 2]
})JSON");

LINT_AND_FIX(document, result, traces);

EXPECT_TRUE(result.first);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [1],
"enum": [1, 2]
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2019_09, forbid_empty_enum_3) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [1],
"type": "string"
})JSON");

LINT_AND_FIX(document, result, traces);

EXPECT_TRUE(result.first);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [1],
"type": "string"
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2019_09, forbid_empty_enum_4) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [{}],
"properties": {
"foo": {
"enum": []
}
}
})JSON");
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [{}],
"properties": {
"foo": {
"not": true
}
}
})JSON");

LINT_AND_FIX(document, result, traces);

EXPECT_TRUE(result.first);
EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2019_09, forbid_empty_enum_5) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [{}],
"$defs": {
"A": {
"enum": []
}
},
"$ref": "#/$defs/A"
})JSON");

LINT_AND_FIX(document, result, traces);

EXPECT_TRUE(result.first);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [{}],
"$defs": {
"A": {
"not": true
}
},
"$ref": "#/$defs/A"
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2019_09, forbid_empty_enum_6) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [{}],
"$defs": {
"A": {
"enum": [],
"$defs": {
"inner": { "type": "string" }
}
}
},
"$ref": "#/$defs/A/$defs/inner"
})JSON");

LINT_AND_FIX(document, result, traces);

EXPECT_TRUE(result.first);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [{}],
"$defs": {
"A": {
"$defs": {
"inner": { "type": "string" }
},
"not": true
}
},
"$ref": "#/$defs/A/$defs/inner"
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2019_09, forbid_empty_enum_7) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [{}],
"$defs": {
"A": {
"$id": "https://example.com/schemas/A",
"enum": []
}
},
"$ref": "https://example.com/schemas/A"
})JSON");

LINT_AND_FIX(document, result, traces);

EXPECT_TRUE(result.first);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [{}],
"$defs": {
"A": {
"$id": "https://example.com/schemas/A",
"not": true
}
},
"$ref": "https://example.com/schemas/A"
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2019_09, forbid_empty_enum_8) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [{}],
"properties": {
"foo": {
"x-note": "placeholder",
"enum": []
}
}
})JSON");

LINT_AND_FIX(document, result, traces);

EXPECT_TRUE(result.first);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Example",
"description": "Example schema",
"examples": [{}],
"properties": {
"foo": {
"x-note": "placeholder",
"not": true
}
}
})JSON");

EXPECT_EQ(document, expected);
}
Loading
Loading