Skip to content

Commit

Permalink
Add an implicit linter rule for elevating unknown types (#233)
Browse files Browse the repository at this point in the history
Signed-off-by: Juan Cruz Viotti <[email protected]>
  • Loading branch information
jviotti authored Sep 11, 2024
1 parent 361377e commit 98ef4a5
Show file tree
Hide file tree
Showing 12 changed files with 224 additions and 15 deletions.
1 change: 1 addition & 0 deletions src/linter/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ noa_library(NAMESPACE sourcemeta PROJECT alterschema NAME linter
implicit/min_properties_implicit.h
implicit/multiple_of_implicit.h
implicit/properties_implicit.h
implicit/type_union_implicit.h

# Superfluous
superfluous/content_media_type_without_encoding.h
Expand Down
128 changes: 128 additions & 0 deletions src/linter/implicit/type_union_implicit.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
class TypeUnionImplicit final : public sourcemeta::alterschema::Rule {
public:
TypeUnionImplicit()
: Rule{"type_union_implicit",
"Not setting `type` is equivalent to accepting any type"} {};

[[nodiscard]] auto
condition(const sourcemeta::jsontoolkit::JSON &schema, const std::string &,
const std::set<std::string> &vocabularies,
const sourcemeta::jsontoolkit::Pointer &) const -> bool override {
if (!schema.is_object()) {
return false;
}

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/hyper-schema#",
"http://json-schema.org/draft-01/hyper-schema#",
"http://json-schema.org/draft-00/hyper-schema#"}) &&
schema.defines("type")) {
return false;
}

if (vocabularies.contains(
"https://json-schema.org/draft/2020-12/vocab/core") &&
schema.defines_any({"$ref", "$dynamicRef"})) {
return false;
}

if (vocabularies.contains(
"https://json-schema.org/draft/2020-12/vocab/applicator") &&
schema.defines_any(
{"anyOf", "oneOf", "allOf", "if", "then", "else", "not"})) {
return false;
}

if (vocabularies.contains(
"https://json-schema.org/draft/2020-12/vocab/validation") &&
schema.defines_any({"enum", "const"})) {
return false;
}

if (vocabularies.contains(
"https://json-schema.org/draft/2019-09/vocab/core") &&
schema.defines_any({"$ref", "$recursiveRef"})) {
return false;
}

if (vocabularies.contains(
"https://json-schema.org/draft/2019-09/vocab/applicator") &&
schema.defines_any(
{"anyOf", "oneOf", "allOf", "if", "then", "else", "not"})) {
return false;
}

if (vocabularies.contains(
"https://json-schema.org/draft/2019-09/vocab/validation") &&
schema.defines_any({"enum", "const"})) {
return false;
}

if (vocabularies.contains("http://json-schema.org/draft-07/schema#") &&
schema.defines_any({"$ref", "enum", "const", "anyOf", "oneOf", "allOf",
"if", "then", "else", "not"})) {
return false;
}

if (vocabularies.contains("http://json-schema.org/draft-06/schema#") &&
schema.defines_any(
{"$ref", "enum", "const", "anyOf", "oneOf", "allOf", "not"})) {
return false;
}

if (vocabularies.contains("http://json-schema.org/draft-04/schema#") &&
schema.defines_any(
{"$ref", "enum", "anyOf", "oneOf", "allOf", "not"})) {
return false;
}

if (vocabularies.contains("http://json-schema.org/draft-03/schema#") &&
schema.defines_any({"$ref", "enum", "disallow", "extends"})) {
return false;
}

if (vocabularies.contains(
"http://json-schema.org/draft-02/hyper-schema#") &&
schema.defines_any({"enum", "disallow", "extends"})) {
return false;
}

if (vocabularies.contains(
"http://json-schema.org/draft-01/hyper-schema#") &&
schema.defines_any({"enum", "disallow", "extends"})) {
return false;
}

if (vocabularies.contains(
"http://json-schema.org/draft-00/hyper-schema#") &&
schema.defines_any({"enum", "disallow", "extends"})) {
return false;
}

return true;
}

auto transform(sourcemeta::alterschema::Transformer &transformer) const
-> void override {
auto types{sourcemeta::jsontoolkit::JSON::make_array()};

// All possible JSON Schema types
// See
// https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.1.1
types.push_back(sourcemeta::jsontoolkit::JSON{"null"});
types.push_back(sourcemeta::jsontoolkit::JSON{"boolean"});
types.push_back(sourcemeta::jsontoolkit::JSON{"object"});
types.push_back(sourcemeta::jsontoolkit::JSON{"array"});
types.push_back(sourcemeta::jsontoolkit::JSON{"string"});
types.push_back(sourcemeta::jsontoolkit::JSON{"number"});
types.push_back(sourcemeta::jsontoolkit::JSON{"integer"});

transformer.assign("type", std::move(types));
}
};
2 changes: 2 additions & 0 deletions src/linter/linter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ template <typename T> auto every_item_is_boolean(const T &container) -> bool {
#include "implicit/min_properties_implicit.h"
#include "implicit/multiple_of_implicit.h"
#include "implicit/properties_implicit.h"
#include "implicit/type_union_implicit.h"
// Superfluous
#include "superfluous/content_media_type_without_encoding.h"
#include "superfluous/content_schema_without_media_type.h"
Expand Down Expand Up @@ -230,6 +231,7 @@ auto add(Bundle &bundle, const LinterCategory category) -> void {
bundle.add<MinPropertiesImplicit>();
bundle.add<MultipleOfImplicit>();
bundle.add<PropertiesImplicit>();
bundle.add<TypeUnionImplicit>();
break;
case LinterCategory::Superfluous:
bundle.add<ContentMediaTypeWithoutEncoding>();
Expand Down
7 changes: 6 additions & 1 deletion test/linter/2019_09_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1362,6 +1362,7 @@ TEST(Lint_2019_09, exclusive_maximum_integer_to_maximum_5) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ],
"exclusiveMaximum": 1
})JSON");

Expand Down Expand Up @@ -1463,6 +1464,7 @@ TEST(Lint_2019_09, exclusive_minimum_integer_to_minimum_5) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ],
"exclusiveMinimum": 1
})JSON");

Expand Down Expand Up @@ -1553,8 +1555,11 @@ TEST(Lint_2019_09, boolean_true_1) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ],
"properties": {
"foo": {}
"foo": {
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ]
}
}
})JSON");

Expand Down
62 changes: 55 additions & 7 deletions test/linter/2020_12_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,7 @@ TEST(Lint_2020_12, dependent_required_tautology_3) {
sourcemeta::jsontoolkit::JSON document =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": [ "foo" ],
"dependentRequired": {
"foo": [ "bar" ],
Expand All @@ -860,6 +861,9 @@ TEST(Lint_2020_12, dependent_required_tautology_3) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {},
"minProperties": 3,
"required": [ "foo", "bar", "baz" ],
"dependentRequired": {}
})JSON");
Expand Down Expand Up @@ -1388,7 +1392,15 @@ TEST(Lint_2020_12, exclusive_maximum_integer_to_maximum_5) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"exclusiveMaximum": 1
"anyOf": [
{ "enum": [ null ] },
{ "enum": [ false, true ] },
{ "properties": {}, "minProperties": 0, "type": "object" },
{ "minItems": 0, "type": "array" },
{ "minLength": 0, "type": "string" },
{ "type": "number", "multipleOf": 1, "exclusiveMaximum": 1 },
{ "multipleOf": 1, "maximum": 0, "type": "integer" }
]
})JSON");

EXPECT_EQ(document, expected);
Expand Down Expand Up @@ -1489,7 +1501,15 @@ TEST(Lint_2020_12, exclusive_minimum_integer_to_minimum_5) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"exclusiveMinimum": 1
"anyOf": [
{ "enum": [ null ] },
{ "enum": [ false, true ] },
{ "properties": {}, "minProperties": 0, "type": "object" },
{ "minItems": 0, "type": "array" },
{ "minLength": 0, "type": "string" },
{ "multipleOf": 1, "type": "number", "exclusiveMinimum": 1 },
{ "multipleOf": 1, "minimum": 2, "type": "integer" }
]
})JSON");

EXPECT_EQ(document, expected);
Expand Down Expand Up @@ -1579,9 +1599,31 @@ TEST(Lint_2020_12, boolean_true_1) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {}
}
"anyOf": [
{ "enum": [ null ] },
{ "enum": [ false, true ] },
{
"minProperties": 0,
"type": "object",
"properties": {
"foo": {
"anyOf": [
{ "enum": [ null ] },
{ "enum": [ false, true ] },
{ "properties": {}, "minProperties": 0, "type": "object" },
{ "minItems": 0, "type": "array" },
{ "minLength": 0, "type": "string" },
{ "multipleOf": 1, "type": "number" },
{ "multipleOf": 1, "type": "integer" }
]
}
}
},
{ "minItems": 0, "type": "array" },
{ "minLength": 0, "type": "string" },
{ "multipleOf": 1, "type": "number" },
{ "multipleOf": 1, "type": "integer" }
]
})JSON");

EXPECT_EQ(document, expected);
Expand Down Expand Up @@ -1630,7 +1672,10 @@ TEST(Lint_2020_12, type_array_to_any_of_2) {
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": [ "integer", "number", "string" ],
"anyOf": [ { "minimum": 4 }, { "maximum": 8 } ]
"anyOf": [
{ "minimum": 4, "type": "integer" },
{ "maximum": 8, "type": "integer" }
]
})JSON");

LINT_AND_FIX_FOR_ANALYSIS(document);
Expand All @@ -1639,7 +1684,10 @@ TEST(Lint_2020_12, type_array_to_any_of_2) {
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": [ "integer", "number", "string" ],
"anyOf": [ { "minimum": 4 }, { "maximum": 8 } ]
"anyOf": [
{ "minimum": 4, "type": "integer", "multipleOf": 1 },
{ "maximum": 8, "type": "integer", "multipleOf": 1 }
]
})JSON");

EXPECT_EQ(document, expected);
Expand Down
5 changes: 4 additions & 1 deletion test/linter/draft0_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,11 @@ TEST(Lint_draft0, boolean_true_1) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "http://json-schema.org/draft-00/schema#",
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ],
"properties": {
"foo": {}
"foo": {
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ]
}
}
})JSON");

Expand Down
5 changes: 4 additions & 1 deletion test/linter/draft1_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,11 @@ TEST(Lint_draft1, boolean_true_1) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "http://json-schema.org/draft-01/schema#",
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ],
"properties": {
"foo": {}
"foo": {
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ]
}
}
})JSON");

Expand Down
5 changes: 4 additions & 1 deletion test/linter/draft2_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,11 @@ TEST(Lint_draft2, boolean_true_1) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "http://json-schema.org/draft-02/schema#",
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ],
"properties": {
"foo": {}
"foo": {
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ]
}
}
})JSON");

Expand Down
5 changes: 4 additions & 1 deletion test/linter/draft3_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -593,8 +593,11 @@ TEST(Lint_draft3, boolean_true_1) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "http://json-schema.org/draft-03/schema#",
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ],
"properties": {
"foo": {}
"foo": {
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ]
}
}
})JSON");

Expand Down
5 changes: 4 additions & 1 deletion test/linter/draft4_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -705,8 +705,11 @@ TEST(Lint_draft4, boolean_true_1) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "http://json-schema.org/draft-04/schema#",
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ],
"properties": {
"foo": {}
"foo": {
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ]
}
}
})JSON");

Expand Down
7 changes: 6 additions & 1 deletion test/linter/draft6_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,7 @@ TEST(Lint_draft6, exclusive_maximum_integer_to_maximum_5) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "http://json-schema.org/draft-06/schema#",
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ],
"exclusiveMaximum": 1
})JSON");

Expand Down Expand Up @@ -1074,6 +1075,7 @@ TEST(Lint_draft6, exclusive_minimum_integer_to_minimum_5) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "http://json-schema.org/draft-06/schema#",
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ],
"exclusiveMinimum": 1
})JSON");

Expand All @@ -1094,8 +1096,11 @@ TEST(Lint_draft6, boolean_true_1) {
const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "http://json-schema.org/draft-06/schema#",
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ],
"properties": {
"foo": {}
"foo": {
"type": [ "null", "boolean", "object", "array", "string", "number", "integer" ]
}
}
})JSON");

Expand Down
Loading

0 comments on commit 98ef4a5

Please sign in to comment.