diff --git a/DEPENDENCIES b/DEPENDENCIES index bccc1e9..d131e6f 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,4 +1,4 @@ vendorpull https://github.com/sourcemeta/vendorpull dea311b5bfb53b6926a4140267959ae334d3ecf4 noa https://github.com/sourcemeta/noa 5ff4024902642afc9cc2f9a9e02ae9dff9d15d4f -jsontoolkit https://github.com/sourcemeta/jsontoolkit 503146c9412c040bcbcdb9a09a6da42c300d3435 +jsontoolkit https://github.com/sourcemeta/jsontoolkit 72dde22434cfa827201dbde10af976c82bd99a43 hydra https://github.com/sourcemeta/hydra d5e0c314dae88b0bf2ac4eeff2c7395910e2c7e9 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9ef9418..52041e4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,8 +6,7 @@ add_executable(jsonschema_cli command_bundle.cc command_test.cc command_lint.cc - command_validate.cc - lint/enum_with_type.h) + command_validate.cc) intelligence_jsonschema_add_compile_options(jsonschema_cli) set_target_properties(jsonschema_cli PROPERTIES OUTPUT_NAME jsonschema) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::jsontoolkit::uri) diff --git a/src/command_lint.cc b/src/command_lint.cc index ca5bc0e..a780c7d 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -7,17 +7,18 @@ #include "command.h" #include "utils.h" -#include "lint/enum_with_type.h" - // TODO: Implement a --fix flag auto intelligence::jsonschema::cli::lint( const std::span &arguments) -> int { const auto options{parse_options(arguments, {})}; sourcemeta::jsontoolkit::SchemaTransformBundle bundle; - bundle.add(); - bool result{true}; + bundle.add( + sourcemeta::jsontoolkit::SchemaTransformBundle::Category::Modernize); + bundle.add( + sourcemeta::jsontoolkit::SchemaTransformBundle::Category::AntiPattern); + bool result{true}; for (const auto &entry : for_each_json(options.at(""))) { const bool subresult = bundle.check( entry.second, sourcemeta::jsontoolkit::default_schema_walker, @@ -29,7 +30,9 @@ auto intelligence::jsonschema::cli::lint( std::cout << " " << message << " (" << name << ")\n"; }); - result = result || subresult; + if (!subresult) { + result = false; + } } return result ? EXIT_SUCCESS : EXIT_FAILURE; diff --git a/src/lint/enum_with_type.h b/src/lint/enum_with_type.h deleted file mode 100644 index 6da45ce..0000000 --- a/src/lint/enum_with_type.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef INTELLIGENCE_JSONSCHEMA_CLI_LINT_ENUM_WITH_TYPE_H_ -#define INTELLIGENCE_JSONSCHEMA_CLI_LINT_ENUM_WITH_TYPE_H_ - -#include -#include - -namespace intelligence::jsonschema::cli { - -class EnumWithType final : public sourcemeta::jsontoolkit::SchemaTransformRule { -public: - EnumWithType() - : SchemaTransformRule{"enum_with_type", - "Enumeration declarations imply their own types"} { - }; - - [[nodiscard]] auto - condition(const sourcemeta::jsontoolkit::JSON &schema, const std::string &, - const std::set &vocabularies, - const sourcemeta::jsontoolkit::Pointer &) const -> bool override { - // TODO: This applies to older dialects too? - return vocabularies.contains( - "https://json-schema.org/draft/2020-12/vocab/validation") && - schema.is_object() && schema.defines("type") && - schema.defines("enum"); - } - - auto transform(sourcemeta::jsontoolkit::SchemaTransformer &transformer) const - -> void override { - transformer.erase("type"); - } -}; - -} // namespace intelligence::jsonschema::cli - -#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bc85714..3dee5ed 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -20,3 +20,5 @@ add_jsonschema_test_unix(bundle_remote_single_schema) add_jsonschema_test_unix(test_single_pass) add_jsonschema_test_unix(test_single_fail) add_jsonschema_test_unix(test_single_unsupported) +add_jsonschema_test_unix(lint_pass) +add_jsonschema_test_unix(lint_fail) diff --git a/test/lint_fail.sh b/test/lint_fail.sh new file mode 100755 index 0000000..19920df --- /dev/null +++ b/test/lint_fail.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +# shellcheck disable=SC2317 +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "string", + "enum": [ "foo" ] +} +EOF + +"$1" lint "$TMP/schema.json" && CODE="$?" || CODE="$?" + +if [ "$CODE" = "0" ] +then + echo "FAIL" 1>&2 + exit 1 +else + echo "PASS" 1>&2 + exit 0 +fi diff --git a/test/lint_pass.sh b/test/lint_pass.sh new file mode 100755 index 0000000..2caa83b --- /dev/null +++ b/test/lint_pass.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +# shellcheck disable=SC2317 +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "string" +} +EOF + +"$1" lint "$TMP/schema.json" diff --git a/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt b/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt index bd0d47e..334f9c2 100644 --- a/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt +++ b/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt @@ -12,6 +12,9 @@ noa_library(NAMESPACE sourcemeta PROJECT jsontoolkit NAME jsonschema compile_helpers.h default_compiler.cc default_compiler_draft6.h default_compiler_draft4.h + rules/enum_to_const.h + rules/enum_with_type.h + rules/const_with_type.h "${CMAKE_CURRENT_BINARY_DIR}/official_resolver.cc") if(JSONTOOLKIT_INSTALL) diff --git a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_transform_bundle.h b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_transform_bundle.h index e37a5c2..a399dfc 100644 --- a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_transform_bundle.h +++ b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_transform_bundle.h @@ -104,6 +104,18 @@ class SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT SchemaTransformBundle { this->rules.emplace(rule->name(), std::move(rule)); } + /// The category of a built-in transformation rule + enum class Category { + /// Rules that make use of newer features within the same dialect + Modernize, + + /// Rules that detect common anti-patterns + AntiPattern + }; + + /// Add a set of built-in rules given a category + auto add(const Category category) -> void; + /// Apply the bundle of rules to a schema auto apply(JSON &schema, const SchemaWalker &walker, const SchemaResolver &resolver, diff --git a/vendor/jsontoolkit/src/jsonschema/rules/const_with_type.h b/vendor/jsontoolkit/src/jsonschema/rules/const_with_type.h new file mode 100644 index 0000000..f93c166 --- /dev/null +++ b/vendor/jsontoolkit/src/jsonschema/rules/const_with_type.h @@ -0,0 +1,25 @@ +class ConstWithType final : public SchemaTransformRule { +public: + ConstWithType() + : SchemaTransformRule{ + "const_with_type", + "Setting `type` alongside `const` is considered an anti-pattern, " + "as the constant already implies its respective type"} {}; + + [[nodiscard]] auto condition(const JSON &schema, const std::string &, + const std::set &vocabularies, + const 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(SchemaTransformer &transformer) const -> void override { + transformer.erase("type"); + } +}; diff --git a/vendor/jsontoolkit/src/jsonschema/rules/enum_to_const.h b/vendor/jsontoolkit/src/jsonschema/rules/enum_to_const.h new file mode 100644 index 0000000..475ea37 --- /dev/null +++ b/vendor/jsontoolkit/src/jsonschema/rules/enum_to_const.h @@ -0,0 +1,26 @@ +class EnumToConst final : public SchemaTransformRule { +public: + EnumToConst() + : SchemaTransformRule( + "enum_to_const", + "An `enum` of a single value can be expressed as `const`") {}; + + [[nodiscard]] auto condition(const JSON &schema, const std::string &, + const std::set &vocabularies, + const 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(SchemaTransformer &transformer) const -> void override { + transformer.assign("const", transformer.schema().at("enum").front()); + transformer.erase("enum"); + } +}; diff --git a/vendor/jsontoolkit/src/jsonschema/rules/enum_with_type.h b/vendor/jsontoolkit/src/jsonschema/rules/enum_with_type.h new file mode 100644 index 0000000..618bc42 --- /dev/null +++ b/vendor/jsontoolkit/src/jsonschema/rules/enum_with_type.h @@ -0,0 +1,29 @@ +class EnumWithType final : public SchemaTransformRule { +public: + EnumWithType() + : SchemaTransformRule{ + "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 JSON &schema, const std::string &, + const std::set &vocabularies, + const 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(SchemaTransformer &transformer) const -> void override { + transformer.erase("type"); + } +}; diff --git a/vendor/jsontoolkit/src/jsonschema/transform_bundle.cc b/vendor/jsontoolkit/src/jsonschema/transform_bundle.cc index 236eaad..f0070da 100644 --- a/vendor/jsontoolkit/src/jsonschema/transform_bundle.cc +++ b/vendor/jsontoolkit/src/jsonschema/transform_bundle.cc @@ -4,6 +4,41 @@ #include // std::ostringstream #include // std::runtime_error +// For built-in rules +#include // std::any_of +#include // std::cbegin, std::cend +namespace sourcemeta::jsontoolkit { +template +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 "rules/const_with_type.h" +#include "rules/enum_to_const.h" +#include "rules/enum_with_type.h" +} // namespace sourcemeta::jsontoolkit + +auto sourcemeta::jsontoolkit::SchemaTransformBundle::add( + const sourcemeta::jsontoolkit::SchemaTransformBundle::Category category) + -> void { + switch (category) { + case sourcemeta::jsontoolkit::SchemaTransformBundle::Category::Modernize: + this->template add(); + break; + case sourcemeta::jsontoolkit::SchemaTransformBundle::Category::AntiPattern: + this->template add(); + this->template add(); + break; + default: + // We should never get here + assert(false); + break; + } +} + auto sourcemeta::jsontoolkit::SchemaTransformBundle::apply( sourcemeta::jsontoolkit::JSON &schema, const sourcemeta::jsontoolkit::SchemaWalker &walker,