Skip to content

Commit 7dcec23

Browse files
committed
Allow dynamically creating linter rules based on schemas
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 8611dfd commit 7dcec23

File tree

6 files changed

+1101
-1
lines changed

6 files changed

+1101
-1
lines changed

src/linter/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME linter
22
FOLDER "Blaze/Linter"
3+
PRIVATE_HEADERS error.h
34
SOURCES
5+
schema.cc
46
valid_default.cc
57
valid_examples.cc)
68

@@ -16,3 +18,5 @@ target_link_libraries(sourcemeta_blaze_linter PRIVATE
1618
sourcemeta::blaze::evaluator)
1719
target_link_libraries(sourcemeta_blaze_linter PRIVATE
1820
sourcemeta::blaze::output)
21+
target_link_libraries(sourcemeta_blaze_linter PRIVATE
22+
sourcemeta::core::regex)

src/linter/include/sourcemeta/blaze/linter.h

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55
#include <sourcemeta/blaze/linter_export.h>
66
#endif
77

8+
#include <sourcemeta/blaze/linter_error.h>
9+
810
#include <sourcemeta/blaze/compiler.h>
911
#include <sourcemeta/blaze/evaluator.h>
1012
#include <sourcemeta/core/jsonschema.h>
1113

12-
#include <type_traits> // std::true_type
14+
#include <optional> // std::optional, std::nullopt
15+
#include <string_view> // std::string_view
16+
#include <type_traits> // std::true_type, std::false_type
1317

1418
/// @defgroup linter Linter
1519
/// @brief A set of JSON Schema linter extensions powered by Blaze
@@ -94,6 +98,49 @@ class SOURCEMETA_BLAZE_LINTER_EXPORT ValidDefault final
9498
#endif
9599
};
96100

101+
/// @ingroup linter
102+
///
103+
/// A linter rule driven by a JSON Schema. Every subschema in the document
104+
/// under inspection is validated as a JSON instance against the provided
105+
/// rule schema. When a subschema does not conform, the rule fires and
106+
/// reports the validation errors. The rule name is extracted from the
107+
/// `title` keyword of the rule schema, and the rule description from the
108+
/// `description` keyword. The title must consist only of lowercase ASCII
109+
/// letters, digits, underscores, or slashes.
110+
class SOURCEMETA_BLAZE_LINTER_EXPORT SchemaRule final
111+
: public sourcemeta::core::SchemaTransformRule {
112+
public:
113+
using mutates = std::false_type;
114+
using reframe_after_transform = std::false_type;
115+
SchemaRule(const sourcemeta::core::JSON &schema,
116+
const sourcemeta::core::SchemaWalker &walker,
117+
const sourcemeta::core::SchemaResolver &resolver,
118+
const Compiler &compiler,
119+
const std::string_view default_dialect = "",
120+
const std::optional<Tweaks> &tweaks = std::nullopt);
121+
[[nodiscard]] auto condition(const sourcemeta::core::JSON &,
122+
const sourcemeta::core::JSON &,
123+
const sourcemeta::core::Vocabularies &,
124+
const sourcemeta::core::SchemaFrame &,
125+
const sourcemeta::core::SchemaFrame::Location &,
126+
const sourcemeta::core::SchemaWalker &,
127+
const sourcemeta::core::SchemaResolver &) const
128+
-> sourcemeta::core::SchemaTransformRule::Result override;
129+
130+
private:
131+
// Exporting symbols that depends on the standard C++ library is considered
132+
// safe.
133+
// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN
134+
#if defined(_MSC_VER)
135+
#pragma warning(disable : 4251)
136+
#endif
137+
Template template_;
138+
mutable Evaluator evaluator_;
139+
#if defined(_MSC_VER)
140+
#pragma warning(default : 4251)
141+
#endif
142+
};
143+
97144
} // namespace sourcemeta::blaze
98145

99146
#endif
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#ifndef SOURCEMETA_BLAZE_LINTER_ERROR_H_
2+
#define SOURCEMETA_BLAZE_LINTER_ERROR_H_
3+
4+
#ifndef SOURCEMETA_BLAZE_LINTER_EXPORT
5+
#include <sourcemeta/blaze/linter_export.h>
6+
#endif
7+
8+
#include <exception> // std::exception
9+
#include <string> // std::string
10+
#include <string_view> // std::string_view
11+
12+
namespace sourcemeta::blaze {
13+
14+
// Exporting symbols that depends on the standard C++ library is considered
15+
// safe.
16+
// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN
17+
#if defined(_MSC_VER)
18+
#pragma warning(disable : 4251 4275)
19+
#endif
20+
21+
/// @ingroup linter
22+
/// An error that represents an invalid schema rule name. The name must
23+
/// consist only of lowercase ASCII letters, digits, underscores, or slashes.
24+
class SOURCEMETA_BLAZE_LINTER_EXPORT LinterInvalidNameError
25+
: public std::exception {
26+
public:
27+
LinterInvalidNameError(const std::string_view name) : name_{name} {}
28+
29+
[[nodiscard]] auto what() const noexcept -> const char * override {
30+
return "The schema rule name is invalid";
31+
}
32+
33+
[[nodiscard]] auto name() const noexcept -> const std::string & {
34+
return this->name_;
35+
}
36+
37+
private:
38+
std::string name_;
39+
};
40+
41+
#if defined(_MSC_VER)
42+
#pragma warning(default : 4251 4275)
43+
#endif
44+
45+
} // namespace sourcemeta::blaze
46+
47+
#endif

src/linter/schema.cc

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#include <sourcemeta/blaze/evaluator.h>
2+
#include <sourcemeta/blaze/linter.h>
3+
#include <sourcemeta/blaze/output.h>
4+
5+
#include <sourcemeta/core/regex.h>
6+
7+
#include <cassert> // assert
8+
#include <functional> // std::ref
9+
#include <sstream> // std::ostringstream
10+
#include <string> // std::string
11+
#include <string_view> // std::string_view
12+
#include <utility> // std::move
13+
14+
namespace sourcemeta::blaze {
15+
16+
static auto validate_name(const std::string_view name) -> void {
17+
static const auto pattern{sourcemeta::core::to_regex("^[a-z0-9_/]+$")};
18+
assert(pattern.has_value());
19+
if (name.empty() ||
20+
!sourcemeta::core::matches(pattern.value(), std::string{name})) {
21+
throw LinterInvalidNameError(name);
22+
}
23+
}
24+
25+
static auto extract_description(const sourcemeta::core::JSON &schema)
26+
-> std::string {
27+
if (!schema.defines("description")) {
28+
return "";
29+
}
30+
31+
if (schema.at("description").is_string()) {
32+
return schema.at("description").to_string();
33+
}
34+
35+
std::ostringstream result;
36+
sourcemeta::core::stringify(schema.at("description"), result);
37+
return std::move(result).str();
38+
}
39+
40+
static auto extract_title(const sourcemeta::core::JSON &schema) -> std::string {
41+
if (!schema.defines("title") || !schema.at("title").is_string()) {
42+
throw LinterInvalidNameError("");
43+
}
44+
45+
auto title{schema.at("title").to_string()};
46+
validate_name(title);
47+
return title;
48+
}
49+
50+
SchemaRule::SchemaRule(const sourcemeta::core::JSON &schema,
51+
const sourcemeta::core::SchemaWalker &walker,
52+
const sourcemeta::core::SchemaResolver &resolver,
53+
const Compiler &compiler,
54+
const std::string_view default_dialect,
55+
const std::optional<Tweaks> &tweaks)
56+
: sourcemeta::core::SchemaTransformRule{extract_title(schema),
57+
extract_description(schema)},
58+
template_{compile(schema, walker, resolver, compiler, Mode::Exhaustive,
59+
default_dialect, "", "", tweaks)} {};
60+
61+
auto SchemaRule::condition(const sourcemeta::core::JSON &schema,
62+
const sourcemeta::core::JSON &,
63+
const sourcemeta::core::Vocabularies &,
64+
const sourcemeta::core::SchemaFrame &,
65+
const sourcemeta::core::SchemaFrame::Location &,
66+
const sourcemeta::core::SchemaWalker &,
67+
const sourcemeta::core::SchemaResolver &) const
68+
-> sourcemeta::core::SchemaTransformRule::Result {
69+
SimpleOutput output{schema};
70+
const auto result{
71+
this->evaluator_.validate(this->template_, schema, std::ref(output))};
72+
if (result) {
73+
return false;
74+
}
75+
76+
std::ostringstream message;
77+
for (const auto &entry : output) {
78+
message << entry.message << "\n";
79+
message << " at instance location \"";
80+
sourcemeta::core::stringify(entry.instance_location, message);
81+
message << "\"\n";
82+
message << " at evaluate path \"";
83+
sourcemeta::core::stringify(entry.evaluate_path, message);
84+
message << "\"\n";
85+
}
86+
87+
return {{}, std::move(message).str()};
88+
}
89+
90+
} // namespace sourcemeta::blaze

test/linter/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
sourcemeta_googletest(NAMESPACE sourcemeta PROJECT blaze NAME linter
22
FOLDER "Blaze/Linter"
33
SOURCES
4+
linter_schema_test.cc
45
linter_valid_default_test.cc
56
linter_valid_examples_test.cc)
67

0 commit comments

Comments
 (0)