Skip to content

Commit

Permalink
Port existing linter rules from JSON Toolkit (#194)
Browse files Browse the repository at this point in the history
Port existing linter rules from JSON Toolkit

Signed-off-by: Juan Cruz Viotti <[email protected]>
  • Loading branch information
jviotti authored Aug 23, 2024
1 parent 0c9ea7d commit acb83cd
Show file tree
Hide file tree
Showing 32 changed files with 2,867 additions and 1 deletion.
11 changes: 10 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ include(vendor/noa/cmake/noa.cmake)

# Options
option(ALTERSCHEMA_ENGINE "Build the Alterschema Engine library" ON)
option(ALTERSCHEMA_LINTER "Build the Alterschema Linter library" ON)
option(ALTERSCHEMA_TESTS "Build the Alterschema tests" OFF)
option(ALTERSCHEMA_DOCS "Build the Alterschema docs" OFF)
option(ALTERSCHEMA_INSTALL "Install the Alterschema library" ON)
Expand All @@ -19,11 +20,15 @@ if(ALTERSCHEMA_INSTALL)
# TODO
endif()

if(ALTERSCHEMA_ENGINE)
if(ALTERSCHEMA_ENGINE OR ALTERSCHEMA_LINTER)
find_package(JSONToolkit REQUIRED)
add_subdirectory(src/engine)
endif()

if(ALTERSCHEMA_LINTER)
add_subdirectory(src/linter)
endif()

if(ALTERSCHEMA_ADDRESS_SANITIZER)
noa_sanitizer(TYPE address)
elseif(ALTERSCHEMA_UNDEFINED_SANITIZER)
Expand All @@ -50,4 +55,8 @@ if(ALTERSCHEMA_TESTS)
if(ALTERSCHEMA_ENGINE)
add_subdirectory(test/engine)
endif()

if(ALTERSCHEMA_LINTER)
add_subdirectory(test/linter)
endif()
endif()
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ configure: .always
-DCMAKE_BUILD_TYPE:STRING=$(PRESET) \
-DCMAKE_COMPILE_WARNING_AS_ERROR:BOOL=ON \
-DALTERSCHEMA_ENGINE:BOOL=ON \
-DALTERSCHEMA_LINTER:BOOL=ON \
-DALTERSCHEMA_TESTS:BOOL=ON \
-DBUILD_SHARED_LIBS:BOOL=$(SHARED)

Expand Down
33 changes: 33 additions & 0 deletions src/linter/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
noa_library(NAMESPACE sourcemeta PROJECT alterschema NAME linter
FOLDER "Alterschema/Linter"
SOURCES linter.cc
# Modernize
modernize/enum_to_const.h

# Antipattern
antipattern/const_with_type.h
antipattern/enum_with_type.h

# Simplify
simplify/single_type_array.h

# Redundant
redundant/additional_properties_default.h
redundant/content_media_type_without_encoding.h
redundant/content_schema_default.h
redundant/content_schema_without_media_type.h
redundant/else_without_if.h
redundant/items_array_default.h
redundant/items_schema_default.h
redundant/max_contains_without_contains.h
redundant/min_contains_without_contains.h
redundant/then_without_if.h
redundant/unevaluated_items_default.h
redundant/unevaluated_properties_default.h)

if(ALTERSCHEMA_INSTALL)
noa_library_install(NAMESPACE sourcemeta PROJECT alterschema NAME linter)
endif()

target_link_libraries(sourcemeta_alterschema_linter PUBLIC
sourcemeta::alterschema::engine)
25 changes: 25 additions & 0 deletions src/linter/antipattern/const_with_type.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class ConstWithType final : public Rule {
public:
ConstWithType()
: Rule{"const_with_type",
"Setting `type` alongside `const` is considered an anti-pattern, "
"as the constant already implies its respective 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 {
return 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#"}) &&
schema.is_object() && schema.defines("type") &&
schema.defines("const");
}

auto transform(Transformer &transformer) const -> void override {
transformer.erase("type");
}
};
30 changes: 30 additions & 0 deletions src/linter/antipattern/enum_with_type.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class EnumWithType final : public Rule {
public:
EnumWithType()
: Rule{
"enum_with_type",
"Setting `type` alongside `enum` is considered an anti-pattern, as "
"the enumeration choices already imply their respective types"} {};

[[nodiscard]] auto
condition(const sourcemeta::jsontoolkit::JSON &schema, const std::string &,
const std::set<std::string> &vocabularies,
const sourcemeta::jsontoolkit::Pointer &) const -> bool override {
return 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#"}) &&
schema.is_object() && schema.defines("type") &&
schema.defines("enum");
}

auto transform(Transformer &transformer) const -> void override {
transformer.erase("type");
}
};
32 changes: 32 additions & 0 deletions src/linter/include/sourcemeta/alterschema/linter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#ifndef SOURCEMETA_ALTERSCHEMA_LINTER_H_
#define SOURCEMETA_ALTERSCHEMA_LINTER_H_

#include "linter_export.h"

#include <sourcemeta/alterschema/engine.h>

namespace sourcemeta::alterschema {

/// @ingroup linter
/// The category of a built-in transformation rule
enum class LinterCategory {
/// Rules that make use of newer features within the same dialect
Modernize,

/// Rules that detect common anti-patterns
AntiPattern,

/// Rules that simplify the given schema
Simplify,

/// Rules that remove schema redundancies
Redundant
};

/// Add a set of built-in linter rules given a category
SOURCEMETA_ALTERSCHEMA_LINTER_EXPORT
auto add(Bundle &bundle, const LinterCategory category) -> void;

} // namespace sourcemeta::alterschema

#endif
73 changes: 73 additions & 0 deletions src/linter/linter.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#include <sourcemeta/alterschema/linter.h>

#include <cassert> // assert

// For built-in rules
#include <algorithm> // std::any_of
#include <iterator> // std::cbegin, std::cend
namespace sourcemeta::alterschema {
template <typename T>
auto contains_any(const T &container, const T &values) -> bool {
return std::any_of(
std::cbegin(container), std::cend(container),
[&values](const auto &element) { return values.contains(element); });
}

// Modernize
#include "modernize/enum_to_const.h"
// AntiPattern
#include "antipattern/const_with_type.h"
#include "antipattern/enum_with_type.h"
// Simplify
#include "simplify/single_type_array.h"
// Redundant
#include "redundant/additional_properties_default.h"
#include "redundant/content_media_type_without_encoding.h"
#include "redundant/content_schema_default.h"
#include "redundant/content_schema_without_media_type.h"
#include "redundant/else_without_if.h"
#include "redundant/items_array_default.h"
#include "redundant/items_schema_default.h"
#include "redundant/max_contains_without_contains.h"
#include "redundant/min_contains_without_contains.h"
#include "redundant/then_without_if.h"
#include "redundant/unevaluated_items_default.h"
#include "redundant/unevaluated_properties_default.h"
} // namespace sourcemeta::alterschema

namespace sourcemeta::alterschema {

auto add(Bundle &bundle, const LinterCategory category) -> void {
switch (category) {
case LinterCategory::Modernize:
bundle.add<EnumToConst>();
break;
case LinterCategory::AntiPattern:
bundle.add<EnumWithType>();
bundle.add<ConstWithType>();
break;
case LinterCategory::Simplify:
bundle.add<SingleTypeArray>();
break;
case LinterCategory::Redundant:
bundle.add<AdditionalPropertiesDefault>();
bundle.add<ContentMediaTypeWithoutEncoding>();
bundle.add<ContentSchemaDefault>();
bundle.add<ContentSchemaWithoutMediaType>();
bundle.add<ElseWithoutIf>();
bundle.add<ItemsArrayDefault>();
bundle.add<ItemsSchemaDefault>();
bundle.add<MaxContainsWithoutContains>();
bundle.add<MinContainsWithoutContains>();
bundle.add<ThenWithoutIf>();
bundle.add<UnevaluatedItemsDefault>();
bundle.add<UnevaluatedPropertiesDefault>();
break;
default:
// We should never get here
assert(false);
break;
}
}

} // namespace sourcemeta::alterschema
26 changes: 26 additions & 0 deletions src/linter/modernize/enum_to_const.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class EnumToConst final : public Rule {
public:
EnumToConst()
: Rule("enum_to_const",
"An `enum` of a single value can be expressed as `const`") {};

[[nodiscard]] auto
condition(const sourcemeta::jsontoolkit::JSON &schema, const std::string &,
const std::set<std::string> &vocabularies,
const sourcemeta::jsontoolkit::Pointer &) const -> bool override {
return 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#"}) &&
schema.is_object() && !schema.defines("const") &&
schema.defines("enum") && schema.at("enum").is_array() &&
schema.at("enum").size() == 1;
}

auto transform(Transformer &transformer) const -> void override {
transformer.assign("const", transformer.schema().at("enum").front());
transformer.erase("enum");
}
};
32 changes: 32 additions & 0 deletions src/linter/redundant/additional_properties_default.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class AdditionalPropertiesDefault final : public Rule {
public:
AdditionalPropertiesDefault()
: Rule{"additional_properties_default",
"Setting the `additionalProperties` keyword to the true schema "
"does not add any further constraint"} {};

[[nodiscard]] auto
condition(const sourcemeta::jsontoolkit::JSON &schema, const std::string &,
const std::set<std::string> &vocabularies,
const sourcemeta::jsontoolkit::Pointer &) const -> bool override {
return contains_any(
vocabularies,
{"https://json-schema.org/draft/2020-12/vocab/applicator",
"https://json-schema.org/draft/2019-09/vocab/applicator",
"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#"}) &&
schema.is_object() && schema.defines("additionalProperties") &&
((schema.at("additionalProperties").is_boolean() &&
schema.at("additionalProperties").to_boolean()) ||
(schema.at("additionalProperties").is_object() &&
schema.at("additionalProperties").empty()));
}

auto transform(Transformer &transformer) const -> void override {
transformer.erase("additionalProperties");
}
};
23 changes: 23 additions & 0 deletions src/linter/redundant/content_media_type_without_encoding.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class ContentMediaTypeWithoutEncoding final : public Rule {
public:
ContentMediaTypeWithoutEncoding()
: Rule{"content_media_type_without_encoding",
"The `contentMediaType` keyword is meaningless "
"without the presence of the `contentEncoding` keyword"} {};

[[nodiscard]] auto
condition(const sourcemeta::jsontoolkit::JSON &schema, const std::string &,
const std::set<std::string> &vocabularies,
const sourcemeta::jsontoolkit::Pointer &) const -> bool override {
return contains_any(vocabularies,
{"https://json-schema.org/draft/2020-12/vocab/content",
"https://json-schema.org/draft/2019-09/vocab/content",
"http://json-schema.org/draft-07/schema#"}) &&
schema.is_object() && schema.defines("contentMediaType") &&
!schema.defines("contentEncoding");
}

auto transform(Transformer &transformer) const -> void override {
transformer.erase("contentMediaType");
}
};
26 changes: 26 additions & 0 deletions src/linter/redundant/content_schema_default.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class ContentSchemaDefault final : public Rule {
public:
ContentSchemaDefault()
: Rule{"content_schema_default",
"Setting the `contentSchema` keyword to the true schema "
"does not add any further constraint"} {};

[[nodiscard]] auto
condition(const sourcemeta::jsontoolkit::JSON &schema, const std::string &,
const std::set<std::string> &vocabularies,
const sourcemeta::jsontoolkit::Pointer &) const -> bool override {
return contains_any(
vocabularies,
{"https://json-schema.org/draft/2020-12/vocab/content",
"https://json-schema.org/draft/2019-09/vocab/content"}) &&
schema.is_object() && schema.defines("contentSchema") &&
((schema.at("contentSchema").is_boolean() &&
schema.at("contentSchema").to_boolean()) ||
(schema.at("contentSchema").is_object() &&
schema.at("contentSchema").empty()));
}

auto transform(Transformer &transformer) const -> void override {
transformer.erase("contentSchema");
}
};
23 changes: 23 additions & 0 deletions src/linter/redundant/content_schema_without_media_type.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class ContentSchemaWithoutMediaType final : public Rule {
public:
ContentSchemaWithoutMediaType()
: Rule{"content_schema_without_media_type",
"The `contentSchema` keyword is meaningless without the presence "
"of the `contentMediaType` keyword"} {};

[[nodiscard]] auto
condition(const sourcemeta::jsontoolkit::JSON &schema, const std::string &,
const std::set<std::string> &vocabularies,
const sourcemeta::jsontoolkit::Pointer &) const -> bool override {
return contains_any(
vocabularies,
{"https://json-schema.org/draft/2020-12/vocab/content",
"https://json-schema.org/draft/2019-09/vocab/content"}) &&
schema.is_object() && schema.defines("contentSchema") &&
!schema.defines("contentMediaType");
}

auto transform(Transformer &transformer) const -> void override {
transformer.erase("contentSchema");
}
};
23 changes: 23 additions & 0 deletions src/linter/redundant/else_without_if.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class ElseWithoutIf final : public Rule {
public:
ElseWithoutIf()
: Rule{"else_without_if", "The `else` keyword is meaningless "
"without the presence of the `if` keyword"} {};

[[nodiscard]] auto
condition(const sourcemeta::jsontoolkit::JSON &schema, const std::string &,
const std::set<std::string> &vocabularies,
const sourcemeta::jsontoolkit::Pointer &) const -> bool override {
return contains_any(
vocabularies,
{"https://json-schema.org/draft/2020-12/vocab/applicator",
"https://json-schema.org/draft/2019-09/vocab/applicator",
"http://json-schema.org/draft-07/schema#"}) &&
schema.is_object() && schema.defines("else") &&
!schema.defines("if");
}

auto transform(Transformer &transformer) const -> void override {
transformer.erase("else");
}
};
Loading

0 comments on commit acb83cd

Please sign in to comment.