Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b1db205
create rule to remove other validation keywords when enum is present …
Karan-Palan Jul 27, 2025
acd7254
use type to remove keyword chekcs, use walker types instead of manual…
Karan-Palan Jul 28, 2025
16d4a1d
Merge branch 'main' of github.com:Karan-Palan/core into linter/enum-v…
Karan-Palan Jul 30, 2025
4e0891e
attempt to fix macos ci
Karan-Palan Jul 30, 2025
a016a15
Merge branch 'main' of github.com:Karan-Palan/core into linter/enum-v…
Karan-Palan Aug 4, 2025
c106652
add whitepsace for consistency
Karan-Palan Aug 4, 2025
7210d07
Merge branch 'main' of github.com:Karan-Palan/core into linter/enum-v…
Karan-Palan Aug 14, 2025
c443b4c
add more tests
Karan-Palan Aug 15, 2025
7b39def
chore: rename rule
Karan-Palan Aug 18, 2025
98c44c1
chore: remove draft0 and update tests with relevant keywords
Karan-Palan Aug 18, 2025
85b1205
fix: remove ref and add tests for that
Karan-Palan Aug 18, 2025
fc8d8c9
chore: update classname to match filename
Karan-Palan Aug 22, 2025
7efff1a
Merge branch 'main' of github.com:Karan-Palan/core into linter/enum-v…
Karan-Palan Aug 22, 2025
82315c2
feat:print offending keywords
Karan-Palan Aug 22, 2025
0ef2c83
feeat: add test with anchor
Karan-Palan Aug 22, 2025
c67df4c
chore: fix consistency
Karan-Palan Aug 22, 2025
0fbe0ad
chore: remove checks that aren't required
Karan-Palan Aug 22, 2025
215d85f
Merge branch 'main' of github.com:Karan-Palan/core into linter/enum-v…
Karan-Palan Aug 26, 2025
d525085
fix: compilation
Karan-Palan Aug 26, 2025
61893b7
chore: use ONLY_IF_CONTINUE
Karan-Palan Aug 29, 2025
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
3 changes: 2 additions & 1 deletion src/extension/alterschema/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema
linter/then_without_if.h
linter/property_names_type_default.h
linter/property_names_default.h
linter/draft_ref_siblings.h)
linter/draft_ref_siblings.h
linter/enum_validation_keywords_default.h)

if(SOURCEMETA_CORE_INSTALL)
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME alterschema)
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 @@ -54,6 +54,7 @@ contains_any(const Vocabularies &container,
#include "linter/else_empty.h"
#include "linter/else_without_if.h"
#include "linter/enum_to_const.h"
#include "linter/enum_validation_keywords_default.h"
#include "linter/enum_with_type.h"
#include "linter/equal_numeric_bounds_to_const.h"
#include "linter/equal_numeric_bounds_to_enum.h"
Expand Down Expand Up @@ -114,6 +115,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode)
bundle.add<MinimumRealForInteger>();
bundle.add<SingleTypeArray>();
bundle.add<EnumWithType>();
bundle.add<EnumValidationKeywordsDefault>();
bundle.add<DuplicateEnumValues>();
bundle.add<DuplicateRequiredValues>();
bundle.add<ConstWithType>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
class EnumValidationKeywordsDefault final : public SchemaTransformRule {
public:
EnumValidationKeywordsDefault()
: SchemaTransformRule{
"enum_validation_keywords_default",
"Setting validation keywords alongside `enum` is considered an "
"anti-pattern, as the enumeration choices already imply their "
"respective constraints"} {};

[[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 &walker,
const sourcemeta::core::SchemaResolver &) const
-> sourcemeta::core::SchemaTransformRule::Result override {
if (!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#",
"http://json-schema.org/draft-03/schema#",
"http://json-schema.org/draft-02/schema#",
"http://json-schema.org/draft-02/hyper-schema#",
"http://json-schema.org/draft-01/schema#",
"http://json-schema.org/draft-01/hyper-schema#",
"http://json-schema.org/draft-00/schema#",
"http://json-schema.org/draft-00/hyper-schema#"}) ||
!schema.is_object() || !schema.defines("enum") ||
!schema.at("enum").is_array() || schema.defines("$ref")) {
return false;
}

std::set<sourcemeta::core::JSON::Type> enum_types;
for (const auto &value : schema.at("enum").as_array()) {
enum_types.emplace(value.type());
}

if (enum_types.empty()) {
return false;
}

this->blacklist.clear();
for (const auto &entry : schema.as_object()) {
const auto metadata = walker(entry.first, vocabularies);
if (metadata.type == sourcemeta::core::SchemaKeywordType::Other ||
metadata.type == sourcemeta::core::SchemaKeywordType::Reference) {
continue;
}

// If keyword applies to any type, we can't determine type applicability
if (metadata.instances.empty()) {
continue;
}

// If none of the types that the keyword applies to match the enum types,
// then this keyword is redundant and can be removed
if (std::ranges::none_of(metadata.instances.cbegin(),
metadata.instances.cend(),
[&enum_types](const auto keyword_type) {
return enum_types.contains(keyword_type);
})) {
this->blacklist.emplace_back(entry.first);
}
}

return !this->blacklist.empty();
}

auto transform(sourcemeta::core::JSON &schema) const -> void override {
schema.erase_keys(this->blacklist.cbegin(), this->blacklist.cend());
}

private:
mutable std::vector<sourcemeta::core::JSON::String> blacklist;
};
53 changes: 53 additions & 0 deletions test/alterschema/alterschema_lint_2019_09_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,59 @@ TEST(AlterSchema_lint_2019_09, enum_with_type_1) {
EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2019_09, enum_validation_keywords_default_1) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"enum": [ "foo", "bar" ],
"minimum": 0
})JSON");

LINT_AND_FIX_FOR_READABILITY(document);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"enum": [ "foo", "bar" ]
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2019_09, enum_validation_keywords_default_2) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"enum": [ 1, 2, 3 ],
"minLength": 0,
"maxLength": 5
})JSON");

LINT_AND_FIX_FOR_READABILITY(document);

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

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2019_09, enum_validation_keywords_default_3) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"enum": [ {"a": 1}, {"b": 2} ],
"minLength": 3,
"minimum": 0
})JSON");

LINT_AND_FIX_FOR_READABILITY(document);

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

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2019_09, single_type_array_1) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
Expand Down
57 changes: 57 additions & 0 deletions test/alterschema/alterschema_lint_2020_12_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2583,3 +2583,60 @@ TEST(AlterSchema_lint_2020_12, non_applicable_type_specific_keywords_3) {

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2020_12, enum_validation_keywords_default_1) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [ 1, 2, 3 ],
"minLength": 3,
"pattern": "^[a-z]+$"
})JSON");

LINT_AND_FIX_FOR_READABILITY(document);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [ 1, 2, 3 ]
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2020_12, enum_validation_keywords_default_2) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [ "one", "two" ],
"minimum": 0,
"maximum": 100,
"multipleOf": 5
})JSON");

LINT_AND_FIX_FOR_READABILITY(document);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [ "one", "two" ]
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2020_12, enum_validation_keywords_default_3) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [ true, false ],
"minLength": 1,
"minimum": 0,
"minItems": 1,
"minProperties": 1
})JSON");

LINT_AND_FIX_FOR_READABILITY(document);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [ true, false ]
})JSON");

EXPECT_EQ(document, expected);
}
53 changes: 53 additions & 0 deletions test/alterschema/alterschema_lint_draft0_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,59 @@ TEST(AlterSchema_lint_draft0, single_type_array_1) {
EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_draft0, enum_validation_keywords_default_1) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-00/schema#",
"enum": [ "foo", "bar" ],
"minimum": 0
})JSON");

LINT_AND_FIX_FOR_READABILITY(document);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-00/schema#",
"enum": [ "foo", "bar" ]
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_draft0, enum_validation_keywords_default_2) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-00/schema#",
"enum": [ 1, 2, 3 ],
"minLength": 0,
"maxLength": 5
})JSON");

LINT_AND_FIX_FOR_READABILITY(document);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-00/schema#",
"enum": [ 1, 2, 3 ]
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_draft0, enum_validation_keywords_default_3) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-00/schema#",
"enum": [ {"a": 1}, {"b": 2} ],
"minLength": 3,
"minimum": 0
})JSON");

LINT_AND_FIX_FOR_READABILITY(document);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-00/schema#",
"enum": [ {"a": 1}, {"b": 2} ]
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_draft0, drop_non_array_keywords_1) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-00/schema#",
Expand Down
53 changes: 53 additions & 0 deletions test/alterschema/alterschema_lint_draft1_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,59 @@ TEST(AlterSchema_lint_draft1, enum_with_type_1) {
EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_draft1, enum_validation_keywords_default_1) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-01/schema#",
"enum": [ "foo", "bar" ],
"minimum": 0
})JSON");

LINT_AND_FIX_FOR_READABILITY(document);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-01/schema#",
"enum": [ "foo", "bar" ]
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_draft1, enum_validation_keywords_default_2) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-01/schema#",
"enum": [ 1, 2, 3 ],
"minLength": 0,
"maxLength": 5
})JSON");

LINT_AND_FIX_FOR_READABILITY(document);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-01/schema#",
"enum": [ 1, 2, 3 ]
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_draft1, enum_validation_keywords_default_3) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-01/schema#",
"enum": [ {"a": 1}, {"b": 2} ],
"minLength": 3,
"minimum": 0
})JSON");

LINT_AND_FIX_FOR_READABILITY(document);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-01/schema#",
"enum": [ {"a": 1}, {"b": 2} ]
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_draft1, single_type_array_1) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-01/schema#",
Expand Down
Loading
Loading