From f69ef04a33fef602d78bb41b70a818ce39f3dc3e Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Mon, 6 Apr 2026 14:42:25 -0400 Subject: [PATCH 1/2] [WIP] Fix resolution bug on `jsonschema.json` `resolve` with `$id` Signed-off-by: Juan Cruz Viotti --- src/resolver.h | 2 +- test/CMakeLists.txt | 2 + test/bundle/pass_resolve_config_match.sh | 2 +- ..._config_match_implicit_extension_config.sh | 2 +- ...lve_config_match_implicit_extension_ref.sh | 2 +- ...pass_lint_config_resolve_custom_dialect.sh | 55 ++++++++++++++++++ ...config_resolve_custom_dialect_with_path.sh | 56 +++++++++++++++++++ 7 files changed, 117 insertions(+), 4 deletions(-) create mode 100755 test/lint/pass_lint_config_resolve_custom_dialect.sh create mode 100755 test/lint/pass_lint_config_resolve_custom_dialect_with_path.sh diff --git a/src/resolver.h b/src/resolver.h index cefebe1df..64b9a6078 100644 --- a/src/resolver.h +++ b/src/resolver.h @@ -70,7 +70,7 @@ resolve_map_uri(const sourcemeta::blaze::Configuration &configuration, const sourcemeta::core::URI new_uri{match->second}; if (new_uri.is_relative()) { - return sourcemeta::core::URI::from_path(configuration.absolute_path / + return sourcemeta::core::URI::from_path(configuration.base_path / new_uri.to_path()) .recompose(); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 61cf39431..11f11ef8c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -714,6 +714,8 @@ add_jsonschema_test_unix(lint/pass_lint_config_rule_other_directory) add_jsonschema_test_unix(lint/fail_lint_config_rule_other_directory) add_jsonschema_test_unix(lint/fail_lint_config_rule_extension_mismatch) add_jsonschema_test_unix(lint/pass_lint_config_ignore) +add_jsonschema_test_unix(lint/pass_lint_config_resolve_custom_dialect) +add_jsonschema_test_unix(lint/pass_lint_config_resolve_custom_dialect_with_path) add_jsonschema_test_unix(lint/pass_stdin_lint) add_jsonschema_test_unix(lint/pass_stdin_lint_verbose) add_jsonschema_test_unix(lint/pass_stdin_fix) diff --git a/test/bundle/pass_resolve_config_match.sh b/test/bundle/pass_resolve_config_match.sh index 322ea696d..fd35aa0c1 100755 --- a/test/bundle/pass_resolve_config_match.sh +++ b/test/bundle/pass_resolve_config_match.sh @@ -11,7 +11,7 @@ cat << 'EOF' > "$TMP/jsonschema.json" { "path": "./schemas", "resolve": { - "https://example.com/my-external-schema.json": "../dependency.json" + "https://example.com/my-external-schema.json": "./dependency.json" } } EOF diff --git a/test/bundle/pass_resolve_config_match_implicit_extension_config.sh b/test/bundle/pass_resolve_config_match_implicit_extension_config.sh index 94206f8f8..c1e6a6cbc 100755 --- a/test/bundle/pass_resolve_config_match_implicit_extension_config.sh +++ b/test/bundle/pass_resolve_config_match_implicit_extension_config.sh @@ -11,7 +11,7 @@ cat << 'EOF' > "$TMP/jsonschema.json" { "path": "./schemas", "resolve": { - "https://example.com/my-external-schema": "../dependency.json" + "https://example.com/my-external-schema": "./dependency.json" } } EOF diff --git a/test/bundle/pass_resolve_config_match_implicit_extension_ref.sh b/test/bundle/pass_resolve_config_match_implicit_extension_ref.sh index ad8d2bffa..7a24b2fd7 100755 --- a/test/bundle/pass_resolve_config_match_implicit_extension_ref.sh +++ b/test/bundle/pass_resolve_config_match_implicit_extension_ref.sh @@ -11,7 +11,7 @@ cat << 'EOF' > "$TMP/jsonschema.json" { "path": "./schemas", "resolve": { - "https://example.com/my-external-schema.json": "../dependency.json" + "https://example.com/my-external-schema.json": "./dependency.json" } } EOF diff --git a/test/lint/pass_lint_config_resolve_custom_dialect.sh b/test/lint/pass_lint_config_resolve_custom_dialect.sh new file mode 100755 index 000000000..7eb9d62c7 --- /dev/null +++ b/test/lint/pass_lint_config_resolve_custom_dialect.sh @@ -0,0 +1,55 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +mkdir "$TMP/v2" + +cat << 'EOF' > "$TMP/v2/dialect.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/my-dialect", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true + }, + "type": "object" +} +EOF + +cat << 'EOF' > "$TMP/v2/person.json" +{ + "$schema": "https://example.com/my-dialect", + "title": "Person", + "description": "A person schema", + "type": "object", + "examples": [ { "name": "John" } ] +} +EOF + +cat << 'EOF' > "$TMP/jsonschema.json" +{ + "resolve": { + "https://example.com/my-dialect": "./v2/dialect.json" + } +} +EOF + +BIN="$(realpath "$1")" +cd "$TMP" +"$BIN" lint v2/person.json --verbose > "$TMP/output.txt" 2>&1 + +cat << EOF > "$TMP/expected.txt" +Linting: $(realpath "$TMP")/v2/person.json +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/lint/pass_lint_config_resolve_custom_dialect_with_path.sh b/test/lint/pass_lint_config_resolve_custom_dialect_with_path.sh new file mode 100755 index 000000000..baaaed0ad --- /dev/null +++ b/test/lint/pass_lint_config_resolve_custom_dialect_with_path.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +mkdir "$TMP/v2" + +cat << 'EOF' > "$TMP/v2/dialect.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/my-dialect", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true + }, + "type": "object" +} +EOF + +cat << 'EOF' > "$TMP/v2/person.json" +{ + "$schema": "https://example.com/my-dialect", + "title": "Person", + "description": "A person schema", + "type": "object", + "examples": [ { "name": "John" } ] +} +EOF + +cat << 'EOF' > "$TMP/jsonschema.json" +{ + "path": "v2", + "resolve": { + "https://example.com/my-dialect": "./v2/dialect.json" + } +} +EOF + +BIN="$(realpath "$1")" +cd "$TMP" +"$BIN" lint v2/person.json --verbose > "$TMP/output.txt" 2>&1 + +cat << EOF > "$TMP/expected.txt" +Linting: $(realpath "$TMP")/v2/person.json +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" From 13c8f5119e4763e7fd196c590d605ba198c6913b Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Mon, 6 Apr 2026 15:03:34 -0400 Subject: [PATCH 2/2] Better error Signed-off-by: Juan Cruz Viotti --- src/configuration.h | 48 ++++++++++++++- src/error.h | 60 +++++++++++++++++++ test/CMakeLists.txt | 1 + .../fail_lint_config_resolve_missing_file.sh | 60 +++++++++++++++++++ 4 files changed, 167 insertions(+), 2 deletions(-) create mode 100755 test/lint/fail_lint_config_resolve_missing_file.sh diff --git a/src/configuration.h b/src/configuration.h index 95cbe844d..6ccc5369a 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -3,13 +3,21 @@ #include #include +#include +#include #include +#include #include "error.h" #include "logger.h" +#include // assert +#include // std::size_t +#include // std::uint64_t +#include // std::deque #include // std::filesystem #include // std::map +#include // std::shared_ptr, std::make_shared #include // std::optional #include // std::ostringstream #include // std::string @@ -53,9 +61,28 @@ inline auto read_configuration( configuration_path.value()) .string() << "\n"; + sourcemeta::core::PointerPositionTracker positions; + auto property_storage = std::make_shared>(); try { - result = sourcemeta::blaze::Configuration::read_json( - configuration_path.value(), configuration_reader); + auto stream{sourcemeta::core::read_file(configuration_path.value())}; + std::ostringstream buffer; + buffer << stream.rdbuf(); + sourcemeta::core::JSON config_json{nullptr}; + sourcemeta::core::parse_json( + buffer.str(), config_json, + [&positions, &property_storage]( + const sourcemeta::core::JSON::ParsePhase phase, + const sourcemeta::core::JSON::Type type, const std::uint64_t line, + const std::uint64_t column, + const sourcemeta::core::JSON::ParseContext context, + const std::size_t index, + const sourcemeta::core::JSON::String &property) { + property_storage->emplace_back(property); + positions(phase, type, line, column, context, index, + property_storage->back()); + }); + result = sourcemeta::blaze::Configuration::from_json( + config_json, configuration_path.value().parent_path()); } catch (const sourcemeta::blaze::ConfigurationParseError &error) { throw sourcemeta::core::FileError< sourcemeta::blaze::ConfigurationParseError>( @@ -63,6 +90,23 @@ inline auto read_configuration( } assert(result.has_value()); + for (const auto &[resolve_uri, resolve_value] : result.value().resolve) { + const sourcemeta::core::URI value_uri{resolve_value}; + if (value_uri.is_relative()) { + const auto resolved_path{std::filesystem::weakly_canonical( + result.value().base_path / value_uri.to_path())}; + if (!std::filesystem::exists(resolved_path)) { + const sourcemeta::core::Pointer resolve_pointer{"resolve", + resolve_uri}; + const auto position{positions.get(resolve_pointer)}; + assert(position.has_value()); + throw ConfigurationResolveFileNotFoundError( + configuration_path.value(), resolve_pointer, resolved_path, + std::get<0>(position.value()), std::get<1>(position.value())); + } + } + } + if (schema_path.has_value() && !result.value().applies_to(schema_path.value())) { LOG_DEBUG(options) diff --git a/src/error.h b/src/error.h index faeb69930..75c5649c0 100644 --- a/src/error.h +++ b/src/error.h @@ -231,6 +231,48 @@ class InstallError : public std::runtime_error { std::string uri_; }; +class ConfigurationResolveFileNotFoundError : public std::runtime_error { +public: + ConfigurationResolveFileNotFoundError( + std::filesystem::path configuration_path, + sourcemeta::core::Pointer location, std::filesystem::path resolve_path, + std::uint64_t line, std::uint64_t column) + : std::runtime_error{"The resolve target does not exist on the " + "filesystem"}, + configuration_path_{std::move(configuration_path)}, + location_{std::move(location)}, resolve_path_{std::move(resolve_path)}, + line_{line}, column_{column} {} + + [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & { + return this->configuration_path_; + } + + [[nodiscard]] auto location() const noexcept + -> const sourcemeta::core::Pointer & { + return this->location_; + } + + [[nodiscard]] auto resolve_path() const noexcept + -> const std::filesystem::path & { + return this->resolve_path_; + } + + [[nodiscard]] auto line() const noexcept -> std::uint64_t { + return this->line_; + } + + [[nodiscard]] auto column() const noexcept -> std::uint64_t { + return this->column_; + } + +private: + std::filesystem::path configuration_path_; + sourcemeta::core::Pointer location_; + std::filesystem::path resolve_path_; + std::uint64_t line_; + std::uint64_t column_; +}; + class Fail : public std::runtime_error { public: Fail(int exit_code) : std::runtime_error{"Fail"}, exit_code_{exit_code} {} @@ -333,6 +375,20 @@ inline auto print_exception(const bool is_json, const Exception &exception) } } + if constexpr (requires(const Exception ¤t) { + { + current.resolve_path() + } -> std::convertible_to; + }) { + const auto &resolve_path_value{exception.resolve_path()}; + if (is_json) { + error_json.assign("resolvePath", + sourcemeta::core::JSON{resolve_path_value.string()}); + } else { + std::cerr << " at resolve path " << resolve_path_value.string() << "\n"; + } + } + if constexpr (requires(const Exception ¤t) { current.line(); }) { if (is_json) { error_json.assign("line", sourcemeta::core::JSON{static_cast( @@ -614,6 +670,10 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_SCHEMA_INPUT_ERROR; + } catch (const ConfigurationResolveFileNotFoundError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; } catch (const sourcemeta::core::FileError< sourcemeta::blaze::ConfigurationParseError> &error) { const auto is_json{options.contains("json")}; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 11f11ef8c..f0acd331b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -716,6 +716,7 @@ add_jsonschema_test_unix(lint/fail_lint_config_rule_extension_mismatch) add_jsonschema_test_unix(lint/pass_lint_config_ignore) add_jsonschema_test_unix(lint/pass_lint_config_resolve_custom_dialect) add_jsonschema_test_unix(lint/pass_lint_config_resolve_custom_dialect_with_path) +add_jsonschema_test_unix(lint/fail_lint_config_resolve_missing_file) add_jsonschema_test_unix(lint/pass_stdin_lint) add_jsonschema_test_unix(lint/pass_stdin_lint_verbose) add_jsonschema_test_unix(lint/pass_stdin_fix) diff --git a/test/lint/fail_lint_config_resolve_missing_file.sh b/test/lint/fail_lint_config_resolve_missing_file.sh new file mode 100755 index 000000000..8ba955434 --- /dev/null +++ b/test/lint/fail_lint_config_resolve_missing_file.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "Test schema", + "examples": [ { "name": "John" } ], + "$ref": "https://example.com/my-defs" +} +EOF + +cat << 'EOF' > "$TMP/jsonschema.json" +{ + "resolve": { + "https://example.com/my-defs": "./does-not-exist.json" + } +} +EOF + +BIN="$(realpath "$1")" +cd "$TMP" +"$BIN" lint schema.json > "$TMP/output.txt" 2>&1 && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +error: The resolve target does not exist on the filesystem + at resolve path $(realpath "$TMP")/does-not-exist.json + at line 3 + at column 5 + at file path $(realpath "$TMP")/jsonschema.json + at location "/resolve/https:~1~1example.com~1my-defs" +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" + +"$BIN" lint schema.json --json > "$TMP/output_json.txt" 2>&1 && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected_json.txt" +{ + "error": "The resolve target does not exist on the filesystem", + "resolvePath": "$(realpath "$TMP")/does-not-exist.json", + "line": 3, + "column": 5, + "filePath": "$(realpath "$TMP")/jsonschema.json", + "location": "/resolve/https:~1~1example.com~1my-defs" +} +EOF + +diff "$TMP/output_json.txt" "$TMP/expected_json.txt"