Skip to content

Commit fc0f132

Browse files
committed
lint: add incoherent_exclusive_limits rule
Signed-off-by: AcE <kintan0108@gmail.com>
1 parent bf77f60 commit fc0f132

11 files changed

Lines changed: 1240 additions & 0 deletions

src/alterschema/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema
9696
common/exclusive_minimum_number_and_minimum.h
9797
common/if_without_then_else.h
9898
common/ignored_metaschema.h
99+
common/incoherent_exclusive_limits.h
99100
common/max_contains_without_contains.h
100101
common/maximum_real_for_integer.h
101102
common/min_contains_without_contains.h

src/alterschema/alterschema.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame,
202202
#include "common/flatten_nested_extends.h"
203203
#include "common/if_without_then_else.h"
204204
#include "common/ignored_metaschema.h"
205+
#include "common/incoherent_exclusive_limits.h"
205206
#include "common/max_contains_without_contains.h"
206207
#include "common/maximum_real_for_integer.h"
207208
#include "common/min_contains_without_contains.h"
@@ -339,6 +340,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void {
339340
if (mode == AlterSchemaMode::Canonicalizer) {
340341
bundle.add<ExclusiveMinimumBooleanIntegerFold>();
341342
bundle.add<ExclusiveMaximumBooleanIntegerFold>();
343+
bundle.add<IncoherentExclusiveLimits>();
342344
bundle.add<UnsatisfiableExclusiveEqualBounds>();
343345
bundle.add<MinimumCanEqualIntegerFold>();
344346
bundle.add<MaximumCanEqualIntegerFold>();
@@ -464,6 +466,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void {
464466
bundle.add<UnevaluatedItemsDefault>();
465467
bundle.add<UnevaluatedPropertiesDefault>();
466468
bundle.add<UnsatisfiableMaxContains>();
469+
bundle.add<IncoherentExclusiveLimits>();
467470
bundle.add<IncoherentMinMaxContains>();
468471
bundle.add<UnsatisfiableMinProperties>();
469472
bundle.add<EnumToConst>();
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
class IncoherentExclusiveLimits final : public SchemaTransformRule {
2+
public:
3+
using mutates = std::true_type;
4+
using reframe_after_transform = std::true_type;
5+
IncoherentExclusiveLimits()
6+
: SchemaTransformRule{
7+
"incoherent_exclusive_limits",
8+
"`exclusiveMinimum` greater than or equal to "
9+
"`exclusiveMaximum` makes the schema unsatisfiable"} {};
10+
11+
[[nodiscard]] auto
12+
condition(const ::sourcemeta::core::JSON &schema,
13+
const ::sourcemeta::core::JSON &,
14+
const ::sourcemeta::core::Vocabularies &vocabularies,
15+
const ::sourcemeta::core::SchemaFrame &,
16+
const ::sourcemeta::core::SchemaFrame::Location &,
17+
const ::sourcemeta::core::SchemaWalker &,
18+
const ::sourcemeta::core::SchemaResolver &) const
19+
-> SchemaTransformRule::Result override {
20+
using Known = ::sourcemeta::core::Vocabularies::Known;
21+
ONLY_CONTINUE_IF(schema.is_object());
22+
23+
const bool is_2020_12 =
24+
vocabularies.contains(Known::JSON_Schema_2020_12_Validation) &&
25+
vocabularies.contains(Known::JSON_Schema_2020_12_Applicator);
26+
const bool is_2019_09 =
27+
vocabularies.contains(Known::JSON_Schema_2019_09_Validation) &&
28+
vocabularies.contains(Known::JSON_Schema_2019_09_Applicator);
29+
const bool is_draft7 = vocabularies.contains(Known::JSON_Schema_Draft_7);
30+
const bool is_draft6 = vocabularies.contains(Known::JSON_Schema_Draft_6);
31+
32+
ONLY_CONTINUE_IF(is_2020_12 || is_2019_09 || is_draft7 || is_draft6);
33+
34+
const auto *exclusive_minimum{schema.try_at("exclusiveMinimum")};
35+
ONLY_CONTINUE_IF(exclusive_minimum && exclusive_minimum->is_number());
36+
const auto *exclusive_maximum{schema.try_at("exclusiveMaximum")};
37+
ONLY_CONTINUE_IF(exclusive_maximum && exclusive_maximum->is_number() &&
38+
*exclusive_minimum >= *exclusive_maximum);
39+
return APPLIES_TO_KEYWORDS("exclusiveMinimum", "exclusiveMaximum");
40+
}
41+
42+
auto transform(JSON &schema, const Result &) const -> void override {
43+
bool needs_not = true;
44+
if (schema.defines("type")) {
45+
const auto &type_node = schema.at("type");
46+
if (type_node.is_string()) {
47+
const std::string type_str = type_node.to_string();
48+
if (type_str == "number" || type_str == "integer") {
49+
schema.assign("not", JSON{true});
50+
schema.erase("type");
51+
schema.erase("exclusiveMinimum");
52+
schema.erase("exclusiveMaximum");
53+
return;
54+
} else {
55+
needs_not = false;
56+
}
57+
} else if (type_node.is_array()) {
58+
auto remaining_types = JSON::make_array();
59+
for (const auto &element : type_node.as_array()) {
60+
if (element.is_string()) {
61+
const std::string element_str = element.to_string();
62+
if (element_str != "number" && element_str != "integer") {
63+
remaining_types.push_back(element);
64+
}
65+
} else {
66+
remaining_types.push_back(element);
67+
}
68+
}
69+
if (remaining_types.empty()) {
70+
schema.assign("not", JSON{true});
71+
schema.erase("type");
72+
schema.erase("exclusiveMinimum");
73+
schema.erase("exclusiveMaximum");
74+
return;
75+
} else if (remaining_types.size() == 1) {
76+
schema.assign("type", remaining_types.front());
77+
needs_not = false;
78+
} else {
79+
schema.assign("type", std::move(remaining_types));
80+
needs_not = false;
81+
}
82+
}
83+
}
84+
85+
if (needs_not) {
86+
auto inner = JSON::make_object();
87+
inner.assign("type", JSON{"number"});
88+
89+
const std::string target_key = schema.defines("exclusiveMinimum")
90+
? "exclusiveMinimum"
91+
: "exclusiveMaximum";
92+
93+
if (!schema.defines("not") && !schema.defines("allOf")) {
94+
schema.try_assign_before("not", std::move(inner), target_key);
95+
} else {
96+
auto new_entry = JSON::make_object();
97+
new_entry.assign("not", std::move(inner));
98+
99+
if (schema.defines("allOf") && schema.at("allOf").is_array()) {
100+
schema.at("allOf").push_back(std::move(new_entry));
101+
} else {
102+
auto all_of = JSON::make_array();
103+
if (schema.defines("not")) {
104+
auto existing_not = JSON::make_object();
105+
existing_not.assign("not", schema.at("not"));
106+
all_of.push_back(std::move(existing_not));
107+
all_of.push_back(std::move(new_entry));
108+
109+
schema.try_assign_before("allOf", std::move(all_of), "not");
110+
schema.erase("not");
111+
} else {
112+
all_of.push_back(std::move(new_entry));
113+
schema.try_assign_before("allOf", std::move(all_of), target_key);
114+
}
115+
}
116+
}
117+
}
118+
119+
schema.erase("exclusiveMinimum");
120+
schema.erase("exclusiveMaximum");
121+
}
122+
};

test/alterschema/alterschema_canonicalize_2019_09_test.cc

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,199 @@ TEST_F(Canonicalizer201909Test, exclusive_minimum_integer_to_minimum_3) {
323323
CANONICALIZE_AND_VALIDATE(document, expected, *compiled_meta_);
324324
}
325325

326+
TEST_F(Canonicalizer201909Test, incoherent_exclusive_limits_1) {
327+
auto document = sourcemeta::core::parse_json(R"JSON({
328+
"$schema": "https://json-schema.org/draft/2019-09/schema",
329+
"exclusiveMinimum": 3,
330+
"exclusiveMaximum": 3
331+
})JSON");
332+
333+
const auto expected = sourcemeta::core::parse_json(R"JSON({
334+
"$schema": "https://json-schema.org/draft/2019-09/schema",
335+
"not": { "type": "number" }
336+
})JSON");
337+
338+
CANONICALIZE_AND_VALIDATE(document, expected, *compiled_meta_);
339+
}
340+
341+
TEST_F(Canonicalizer201909Test, incoherent_exclusive_limits_2) {
342+
auto document = sourcemeta::core::parse_json(R"JSON({
343+
"$schema": "https://json-schema.org/draft/2019-09/schema",
344+
"exclusiveMinimum": 5,
345+
"exclusiveMaximum": 2
346+
})JSON");
347+
348+
const auto expected = sourcemeta::core::parse_json(R"JSON({
349+
"$schema": "https://json-schema.org/draft/2019-09/schema",
350+
"not": { "type": "number" }
351+
})JSON");
352+
353+
CANONICALIZE_AND_VALIDATE(document, expected, *compiled_meta_);
354+
}
355+
356+
TEST_F(Canonicalizer201909Test, incoherent_exclusive_limits_3) {
357+
auto document = sourcemeta::core::parse_json(R"JSON({
358+
"$schema": "https://json-schema.org/draft/2019-09/schema",
359+
"exclusiveMinimum": 5,
360+
"exclusiveMaximum": 2,
361+
"not": { "type": "string" }
362+
})JSON");
363+
364+
const auto expected = sourcemeta::core::parse_json(R"JSON({
365+
"$schema": "https://json-schema.org/draft/2019-09/schema",
366+
"allOf": [
367+
{
368+
"not": { "type": "string", "minLength": 0 }
369+
},
370+
{
371+
"not": { "type": "number" }
372+
}
373+
]
374+
})JSON");
375+
376+
CANONICALIZE_AND_VALIDATE(document, expected, *compiled_meta_);
377+
}
378+
379+
TEST_F(Canonicalizer201909Test, incoherent_exclusive_limits_4) {
380+
auto document = sourcemeta::core::parse_json(R"JSON({
381+
"$schema": "https://json-schema.org/draft/2019-09/schema",
382+
"$ref": "#/$defs/test/additionalProperties",
383+
"$defs": {
384+
"test": {
385+
"additionalProperties": {
386+
"type": "string"
387+
},
388+
"exclusiveMaximum": 5,
389+
"exclusiveMinimum": 8
390+
}
391+
}
392+
})JSON");
393+
394+
const auto expected = sourcemeta::core::parse_json(R"JSON({
395+
"$schema": "https://json-schema.org/draft/2019-09/schema",
396+
"$defs": {
397+
"test": {
398+
"allOf": [
399+
{
400+
"not": { "type": "number" }
401+
},
402+
{
403+
"anyOf": [
404+
{
405+
"enum": [ null ]
406+
},
407+
{
408+
"enum": [ false, true ]
409+
},
410+
{
411+
"type": "object",
412+
"additionalProperties": {
413+
"type": "string",
414+
"minLength": 0
415+
},
416+
"patternProperties": {},
417+
"propertyNames": true,
418+
"minProperties": 0,
419+
"properties": {}
420+
},
421+
{
422+
"type": "array",
423+
"uniqueItems": false,
424+
"minItems": 0,
425+
"contains": true,
426+
"minContains": 0,
427+
"items": true
428+
},
429+
{
430+
"type": "string",
431+
"minLength": 0
432+
},
433+
{
434+
"type": "number"
435+
}
436+
]
437+
}
438+
]
439+
}
440+
},
441+
"allOf": [
442+
{
443+
"$ref": "#/$defs/test/allOf/1/anyOf/2/additionalProperties"
444+
}
445+
]
446+
})JSON");
447+
448+
CANONICALIZE_AND_VALIDATE(document, expected, *compiled_meta_);
449+
}
450+
451+
TEST_F(Canonicalizer201909Test, incoherent_exclusive_limits_5) {
452+
auto document = sourcemeta::core::parse_json(R"JSON({
453+
"$schema": "https://json-schema.org/draft/2019-09/schema",
454+
"type": "number",
455+
"exclusiveMinimum": 5,
456+
"exclusiveMaximum": 2
457+
})JSON");
458+
459+
const auto expected = sourcemeta::core::parse_json(R"JSON({
460+
"$schema": "https://json-schema.org/draft/2019-09/schema",
461+
"not": true
462+
})JSON");
463+
464+
CANONICALIZE_AND_VALIDATE(document, expected, *compiled_meta_);
465+
}
466+
467+
TEST_F(Canonicalizer201909Test, incoherent_exclusive_limits_6) {
468+
auto document = sourcemeta::core::parse_json(R"JSON({
469+
"$schema": "https://json-schema.org/draft/2019-09/schema",
470+
"type": "integer",
471+
"exclusiveMinimum": 5,
472+
"exclusiveMaximum": 2
473+
})JSON");
474+
475+
const auto expected = sourcemeta::core::parse_json(R"JSON({
476+
"$schema": "https://json-schema.org/draft/2019-09/schema",
477+
"not": true
478+
})JSON");
479+
480+
CANONICALIZE_AND_VALIDATE(document, expected, *compiled_meta_);
481+
}
482+
483+
TEST_F(Canonicalizer201909Test, incoherent_exclusive_limits_7) {
484+
auto document = sourcemeta::core::parse_json(R"JSON({
485+
"$schema": "https://json-schema.org/draft/2019-09/schema",
486+
"type": ["string", "number"],
487+
"exclusiveMinimum": 5,
488+
"exclusiveMaximum": 2
489+
})JSON");
490+
491+
const auto expected = sourcemeta::core::parse_json(R"JSON({
492+
"$schema": "https://json-schema.org/draft/2019-09/schema",
493+
"type": "string",
494+
"minLength": 0
495+
})JSON");
496+
497+
CANONICALIZE_AND_VALIDATE(document, expected, *compiled_meta_);
498+
}
499+
500+
TEST_F(Canonicalizer201909Test, incoherent_exclusive_limits_8) {
501+
auto document = sourcemeta::core::parse_json(R"JSON({
502+
"$schema": "https://json-schema.org/draft/2019-09/schema",
503+
"allOf": [ { "type": "string" } ],
504+
"exclusiveMinimum": 5,
505+
"exclusiveMaximum": 2
506+
})JSON");
507+
508+
const auto expected = sourcemeta::core::parse_json(R"JSON({
509+
"$schema": "https://json-schema.org/draft/2019-09/schema",
510+
"allOf": [
511+
{ "type": "string", "minLength": 0 },
512+
{ "not": { "type": "number" } }
513+
]
514+
})JSON");
515+
516+
CANONICALIZE_AND_VALIDATE(document, expected, *compiled_meta_);
517+
}
518+
326519
TEST_F(Canonicalizer201909Test, exclusive_minimum_integer_to_minimum_5) {
327520
auto document = sourcemeta::core::parse_json(R"JSON({
328521
"$schema": "https://json-schema.org/draft/2019-09/schema",

0 commit comments

Comments
 (0)