Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 10 additions & 21 deletions src/compiler/compile.cc
Original file line number Diff line number Diff line change
Expand Up @@ -429,28 +429,17 @@ auto compile(const sourcemeta::core::JSON &schema,
const std::string_view entrypoint,
const std::optional<Tweaks> &tweaks) -> Template {
assert(is_schema(schema));
const auto effective_tweaks{tweaks.value_or(Tweaks{})};

if (effective_tweaks.assume_bundled) {
sourcemeta::core::SchemaFrame frame{
sourcemeta::core::SchemaFrame::Mode::References};
frame.analyse(schema, walker, resolver, default_dialect, default_id);
return compile(schema, walker, resolver, compiler, frame,
entrypoint.empty() ? frame.root() : entrypoint, mode,
tweaks);
} else {
// Make sure the input schema is bundled, otherwise we won't be able to
// resolve remote references here
const sourcemeta::core::JSON result{sourcemeta::core::bundle(
schema, walker, resolver, default_dialect, default_id)};

sourcemeta::core::SchemaFrame frame{
sourcemeta::core::SchemaFrame::Mode::References};
frame.analyse(result, walker, resolver, default_dialect, default_id);
return compile(result, walker, resolver, compiler, frame,
entrypoint.empty() ? frame.root() : entrypoint, mode,
tweaks);
}
// Make sure the input schema is bundled, otherwise we won't be able to
// resolve remote references here
const sourcemeta::core::JSON result{sourcemeta::core::bundle(
schema, walker, resolver, default_dialect, default_id)};

sourcemeta::core::SchemaFrame frame{
sourcemeta::core::SchemaFrame::Mode::References};
frame.analyse(result, walker, resolver, default_dialect, default_id);
return compile(result, walker, resolver, compiler, frame,
entrypoint.empty() ? frame.root() : entrypoint, mode, tweaks);
}

auto compile(const Context &context, const SchemaContext &schema_context,
Expand Down
3 changes: 0 additions & 3 deletions src/compiler/include/sourcemeta/blaze/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,6 @@ struct Tweaks {
bool properties_reorder{true};
/// Inline jump targets with fewer instructions than this threshold
std::size_t target_inline_threshold{50};
/// Assume the schema is already bundled with no pending unresolved external
/// references
bool assume_bundled{false};
};

/// @ingroup compiler
Expand Down
3 changes: 3 additions & 0 deletions src/linter/include/sourcemeta/blaze/linter.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#endif

#include <sourcemeta/blaze/compiler.h>
#include <sourcemeta/blaze/evaluator.h>
#include <sourcemeta/core/jsonschema.h>

#include <type_traits> // std::true_type
Expand Down Expand Up @@ -51,6 +52,7 @@ class SOURCEMETA_BLAZE_LINTER_EXPORT ValidExamples final
#pragma warning(disable : 4251)
#endif
const Compiler compiler_;
mutable Evaluator evaluator_;
#if defined(_MSC_VER)
#pragma warning(default : 4251)
#endif
Expand Down Expand Up @@ -86,6 +88,7 @@ class SOURCEMETA_BLAZE_LINTER_EXPORT ValidDefault final
#pragma warning(disable : 4251)
#endif
const Compiler compiler_;
mutable Evaluator evaluator_;
#if defined(_MSC_VER)
#pragma warning(default : 4251)
#endif
Expand Down
41 changes: 31 additions & 10 deletions src/linter/valid_default.cc
Original file line number Diff line number Diff line change
Expand Up @@ -45,31 +45,52 @@ auto ValidDefault::condition(
}
}

const auto &instance{schema.at("default")};

if (frame.standalone()) {
const auto base{frame.uri(location.pointer)};
assert(base.has_value());
const auto schema_template{compile(root, walker, resolver, this->compiler_,
frame, base.value().get(),
Mode::Exhaustive)};
SimpleOutput output{instance};
const auto result{
this->evaluator_.validate(schema_template, instance, std::ref(output))};
if (result) {
return false;
}

std::ostringstream message;
for (const auto &entry : output) {
message << entry.message << "\n";
message << " at instance location \"";
sourcemeta::core::stringify(entry.instance_location, message);
message << "\"\n";
message << " at evaluate path \"";
sourcemeta::core::stringify(entry.evaluate_path, message);
message << "\"\n";
}

return {{{"default"}}, std::move(message).str()};
}

const auto &root_base_dialect{
frame.traverse(frame.root()).value_or(location).get().base_dialect};
std::string_view default_id{location.base};
if (!sourcemeta::core::identify(root, root_base_dialect).empty() ||
default_id.empty()) {
// We want to only set a default identifier if the root schema does not
// have an explicit identifier. Otherwise, we can get into corner case
// when wrapping the schema
default_id = "";
}

sourcemeta::core::WeakPointer base;
const auto subschema{
sourcemeta::core::wrap(root, frame, location, resolver, base)};
// To avoid bundling twice in vain
Tweaks tweaks{.assume_bundled = frame.standalone()};
const auto schema_template{compile(subschema, walker, resolver,
this->compiler_, Mode::Exhaustive,
location.dialect, default_id, "", tweaks)};

const auto &instance{schema.at("default")};
Evaluator evaluator;
location.dialect, default_id)};
SimpleOutput output{instance, base};
const auto result{
evaluator.validate(schema_template, instance, std::ref(output))};
this->evaluator_.validate(schema_template, instance, std::ref(output))};
if (result) {
return false;
}
Expand Down
48 changes: 39 additions & 9 deletions src/linter/valid_examples.cc
Original file line number Diff line number Diff line change
Expand Up @@ -49,32 +49,62 @@ auto ValidExamples::condition(
}
}

std::size_t cursor{0};

if (frame.standalone()) {
const auto base{frame.uri(location.pointer)};
assert(base.has_value());
const auto schema_template{compile(root, walker, resolver, this->compiler_,
frame, base.value().get(),
Mode::Exhaustive)};

for (const auto &example : schema.at("examples").as_array()) {
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This validation loop (lines 58-82) is identical to the one used later in the function (lines 112-132 in the modified file). This violation of the DRY principle increases maintenance burden and the risk of inconsistent behavior if one loop is updated but the other is not.

Consider extracting the loop into a helper lambda or private method that accepts the compiled schema and the optional base pointer.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/linter/valid_examples.cc, line 61:

<comment>This validation loop (lines 58-82) is identical to the one used later in the function (lines 112-132 in the modified file). This violation of the DRY principle increases maintenance burden and the risk of inconsistent behavior if one loop is updated but the other is not.

Consider extracting the loop into a helper lambda or private method that accepts the compiled schema and the optional base pointer.</comment>

<file context>
@@ -49,32 +49,62 @@ auto ValidExamples::condition(
+                                       frame, base.value().get(),
+                                       Mode::Exhaustive)};
+
+    for (const auto &example : schema.at("examples").as_array()) {
+      SimpleOutput output{example};
+      const auto result{this->evaluator_.validate(schema_template, example,
</file context>
Fix with Cubic

SimpleOutput output{example};
const auto result{this->evaluator_.validate(schema_template, example,
std::ref(output))};
if (!result) {
std::ostringstream message;
message << "Invalid example instance at index " << cursor << "\n";
for (const auto &entry : output) {
message << " " << entry.message << "\n";
message << " "
<< " at instance location \"";
sourcemeta::core::stringify(entry.instance_location, message);
message << "\"\n";
message << " "
<< " at evaluate path \"";
sourcemeta::core::stringify(entry.evaluate_path, message);
message << "\"\n";
}

return {{{"examples", cursor}}, std::move(message).str()};
}

cursor += 1;
}

return false;
}

const auto &root_base_dialect{
frame.traverse(frame.root()).value_or(location).get().base_dialect};
std::string_view default_id{location.base};
if (!sourcemeta::core::identify(root, root_base_dialect).empty() ||
default_id.empty()) {
// We want to only set a default identifier if the root schema does not
// have an explicit identifier. Otherwise, we can get into corner case
// when wrapping the schema
default_id = "";
}

sourcemeta::core::WeakPointer base;
const auto subschema{
sourcemeta::core::wrap(root, frame, location, resolver, base)};
// To avoid bundling twice in vain
Tweaks tweaks{.assume_bundled = frame.standalone()};
const auto schema_template{compile(subschema, walker, resolver,
this->compiler_, Mode::Exhaustive,
location.dialect, default_id, "", tweaks)};
location.dialect, default_id)};

Evaluator evaluator;
std::size_t cursor{0};
for (const auto &example : schema.at("examples").as_array()) {
SimpleOutput output{example, base};
const auto result{
evaluator.validate(schema_template, example, std::ref(output))};
this->evaluator_.validate(schema_template, example, std::ref(output))};
if (!result) {
std::ostringstream message;
message << "Invalid example instance at index " << cursor << "\n";
Expand Down
37 changes: 37 additions & 0 deletions test/linter/linter_valid_examples_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -806,3 +806,40 @@ TEST(Linter, valid_examples_16) {

EXPECT_EQ(schema, expected);
}

TEST(Linter, valid_examples_17) {
sourcemeta::core::SchemaTransformer bundle;
bundle.add<sourcemeta::blaze::ValidExamples>(
sourcemeta::blaze::default_schema_compiler);

auto schema{sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": { "examples": [ "10" ], "$ref": "ref.json" }
})JSON")};

auto resolver = [](const std::string_view identifier)
-> std::optional<sourcemeta::core::JSON> {
if (identifier == "https://example.com/ref.json") {
return sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/ref.json",
"type": "integer"
})JSON");
}

return sourcemeta::core::schema_resolver(identifier);
};

const auto result =
bundle.apply(schema, sourcemeta::core::schema_walker, resolver,
transformer_callback_error, "", "https://example.com/root");

EXPECT_TRUE(result.first);

const auto expected{sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": { "$ref": "ref.json" }
})JSON")};

EXPECT_EQ(schema, expected);
}