Skip to content

Commit d203239

Browse files
authored
Lint unnecessary allOf wrappers for $ref in 2019-09 and later (#1736)
Fixes: #1737 Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 1a00072 commit d203239

8 files changed

+357
-0
lines changed

src/extension/alterschema/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema
138138
linter/drop_non_string_keywords_unevaluated_2020_12.h
139139
linter/drop_non_string_keywords_validation_2019_09.h
140140
linter/drop_non_string_keywords_validation_2020_12.h
141+
linter/unnecessary_allof_ref_wrapper.h
141142
linter/duplicate_allof_branches.h
142143
linter/duplicate_anyof_branches.h
143144
linter/else_without_if.h

src/extension/alterschema/alterschema.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ template <typename T> auto every_item_is_boolean(const T &container) -> bool {
169169
#include "linter/then_without_if.h"
170170
#include "linter/unevaluated_items_default.h"
171171
#include "linter/unevaluated_properties_default.h"
172+
#include "linter/unnecessary_allof_ref_wrapper.h"
172173
#include "linter/unsatisfiable_max_contains.h"
173174
#include "linter/unsatisfiable_min_properties.h"
174175
} // namespace sourcemeta::core
@@ -273,6 +274,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode)
273274
bundle.add<DropNonStringKeywordsUnevaluated_2020_12>();
274275
bundle.add<DropNonStringKeywordsValidation_2019_09>();
275276
bundle.add<DropNonStringKeywordsValidation_2020_12>();
277+
bundle.add<UnnecessaryAllOfRefWrapper>();
276278
bundle.add<DuplicateAllOfBranches>();
277279
bundle.add<DuplicateAnyOfBranches>();
278280
bundle.add<ElseWithoutIf>();
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
class UnnecessaryAllOfRefWrapper final : public SchemaTransformRule {
2+
public:
3+
UnnecessaryAllOfRefWrapper()
4+
: SchemaTransformRule{"unnecessary_allof_ref_wrapper",
5+
"Wrapping `$ref` in `allOf` is unnecessary in JSON "
6+
"Schema 2019-09 and later versions as `$ref` does "
7+
"not override sibling keywords anymore"} {};
8+
9+
[[nodiscard]] auto
10+
condition(const sourcemeta::core::JSON &schema,
11+
const sourcemeta::core::JSON &,
12+
const sourcemeta::core::Vocabularies &vocabularies,
13+
const sourcemeta::core::SchemaFrame &,
14+
const sourcemeta::core::SchemaFrame::Location &,
15+
const sourcemeta::core::SchemaWalker &,
16+
const sourcemeta::core::SchemaResolver &) const
17+
-> sourcemeta::core::SchemaTransformRule::Result override {
18+
if (!((vocabularies.contains(
19+
"https://json-schema.org/draft/2020-12/vocab/core") &&
20+
vocabularies.contains(
21+
"https://json-schema.org/draft/2020-12/vocab/applicator")) ||
22+
(vocabularies.contains(
23+
"https://json-schema.org/draft/2019-09/vocab/core") &&
24+
vocabularies.contains(
25+
"https://json-schema.org/draft/2019-09/vocab/applicator")))) {
26+
return false;
27+
}
28+
29+
if (!schema.is_object() || schema.defines("$ref") ||
30+
!schema.defines("allOf") || !schema.at("allOf").is_array() ||
31+
schema.at("allOf").empty()) {
32+
return false;
33+
}
34+
35+
bool match{false};
36+
for (const auto &entry : schema.at("allOf").as_array()) {
37+
if (entry.is_object() && entry.defines("$ref")) {
38+
if (match) {
39+
return false;
40+
} else {
41+
match = true;
42+
}
43+
}
44+
}
45+
46+
return match;
47+
}
48+
49+
auto transform(JSON &schema) const -> void override {
50+
// TODO: We should have a way for the condition to pass data to the
51+
// transform, so we don't have to loop from scratch once more to figure out
52+
// what index to remove
53+
auto &array{schema.at("allOf").as_array()};
54+
auto iterator{array.begin()};
55+
for (; iterator != array.end(); ++iterator) {
56+
if (iterator->is_object() && iterator->defines("$ref")) {
57+
break;
58+
}
59+
}
60+
61+
assert(iterator != array.end());
62+
auto reference{std::move(iterator->at("$ref"))};
63+
if (iterator->size() == 1) {
64+
schema.at("allOf").erase(iterator);
65+
if (schema.at("allOf").empty()) {
66+
schema.erase("allOf");
67+
}
68+
} else {
69+
iterator->erase("$ref");
70+
}
71+
72+
schema.assign("$ref", std::move(reference));
73+
}
74+
};

test/alterschema/alterschema_lint_2019_09_test.cc

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1570,3 +1570,113 @@ TEST(AlterSchema_lint_2019_09, equal_numeric_bounds_to_enum_2) {
15701570

15711571
EXPECT_EQ(document, expected);
15721572
}
1573+
1574+
TEST(AlterSchema_lint_2019_09, unnecessary_allof_ref_wrapper_1) {
1575+
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
1576+
"$schema": "https://json-schema.org/draft/2019-09/schema",
1577+
"allOf": [
1578+
{ "$ref": "https://example.com" }
1579+
]
1580+
})JSON");
1581+
1582+
LINT_AND_FIX_FOR_READABILITY(document);
1583+
1584+
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
1585+
"$schema": "https://json-schema.org/draft/2019-09/schema",
1586+
"$ref": "https://example.com"
1587+
})JSON");
1588+
1589+
EXPECT_EQ(document, expected);
1590+
}
1591+
1592+
TEST(AlterSchema_lint_2019_09, unnecessary_allof_ref_wrapper_2) {
1593+
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
1594+
"$schema": "https://json-schema.org/draft/2019-09/schema",
1595+
"allOf": [
1596+
{ "$ref": "https://example.com/foo" },
1597+
{ "$ref": "https://example.com/bar" }
1598+
]
1599+
})JSON");
1600+
1601+
LINT_AND_FIX_FOR_READABILITY(document);
1602+
1603+
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
1604+
"$schema": "https://json-schema.org/draft/2019-09/schema",
1605+
"allOf": [
1606+
{ "$ref": "https://example.com/foo" },
1607+
{ "$ref": "https://example.com/bar" }
1608+
]
1609+
})JSON");
1610+
1611+
EXPECT_EQ(document, expected);
1612+
}
1613+
1614+
TEST(AlterSchema_lint_2019_09, unnecessary_allof_ref_wrapper_3) {
1615+
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
1616+
"$schema": "https://json-schema.org/draft/2019-09/schema",
1617+
"$ref": "https://example.com/foo",
1618+
"allOf": [
1619+
{ "$ref": "https://example.com/bar" }
1620+
]
1621+
})JSON");
1622+
1623+
LINT_AND_FIX_FOR_READABILITY(document);
1624+
1625+
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
1626+
"$schema": "https://json-schema.org/draft/2019-09/schema",
1627+
"$ref": "https://example.com/foo",
1628+
"allOf": [
1629+
{ "$ref": "https://example.com/bar" }
1630+
]
1631+
})JSON");
1632+
1633+
EXPECT_EQ(document, expected);
1634+
}
1635+
1636+
TEST(AlterSchema_lint_2019_09, unnecessary_allof_ref_wrapper_4) {
1637+
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
1638+
"$schema": "https://json-schema.org/draft/2019-09/schema",
1639+
"allOf": [
1640+
{ "type": "number" },
1641+
{ "$ref": "https://example.com" },
1642+
{ "type": "integer" }
1643+
]
1644+
})JSON");
1645+
1646+
LINT_AND_FIX_FOR_READABILITY(document);
1647+
1648+
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
1649+
"$schema": "https://json-schema.org/draft/2019-09/schema",
1650+
"$ref": "https://example.com",
1651+
"allOf": [
1652+
{ "type": "number" },
1653+
{ "type": "integer" }
1654+
]
1655+
})JSON");
1656+
1657+
EXPECT_EQ(document, expected);
1658+
}
1659+
1660+
TEST(AlterSchema_lint_2019_09, unnecessary_allof_ref_wrapper_5) {
1661+
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
1662+
"$schema": "https://json-schema.org/draft/2019-09/schema",
1663+
"allOf": [
1664+
{
1665+
"type": "integer",
1666+
"$ref": "https://example.com"
1667+
}
1668+
]
1669+
})JSON");
1670+
1671+
LINT_AND_FIX_FOR_READABILITY(document);
1672+
1673+
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
1674+
"$schema": "https://json-schema.org/draft/2019-09/schema",
1675+
"$ref": "https://example.com",
1676+
"allOf": [
1677+
{ "type": "integer" }
1678+
]
1679+
})JSON");
1680+
1681+
EXPECT_EQ(document, expected);
1682+
}

test/alterschema/alterschema_lint_2020_12_test.cc

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1732,3 +1732,113 @@ TEST(AlterSchema_lint_2020_12, equal_numeric_bounds_to_enum_2) {
17321732

17331733
EXPECT_EQ(document, expected);
17341734
}
1735+
1736+
TEST(AlterSchema_lint_2020_12, unnecessary_allof_ref_wrapper_1) {
1737+
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
1738+
"$schema": "https://json-schema.org/draft/2020-12/schema",
1739+
"allOf": [
1740+
{ "$ref": "https://example.com" }
1741+
]
1742+
})JSON");
1743+
1744+
LINT_AND_FIX_FOR_READABILITY(document);
1745+
1746+
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
1747+
"$schema": "https://json-schema.org/draft/2020-12/schema",
1748+
"$ref": "https://example.com"
1749+
})JSON");
1750+
1751+
EXPECT_EQ(document, expected);
1752+
}
1753+
1754+
TEST(AlterSchema_lint_2020_12, unnecessary_allof_ref_wrapper_2) {
1755+
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
1756+
"$schema": "https://json-schema.org/draft/2020-12/schema",
1757+
"allOf": [
1758+
{ "$ref": "https://example.com/foo" },
1759+
{ "$ref": "https://example.com/bar" }
1760+
]
1761+
})JSON");
1762+
1763+
LINT_AND_FIX_FOR_READABILITY(document);
1764+
1765+
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
1766+
"$schema": "https://json-schema.org/draft/2020-12/schema",
1767+
"allOf": [
1768+
{ "$ref": "https://example.com/foo" },
1769+
{ "$ref": "https://example.com/bar" }
1770+
]
1771+
})JSON");
1772+
1773+
EXPECT_EQ(document, expected);
1774+
}
1775+
1776+
TEST(AlterSchema_lint_2020_12, unnecessary_allof_ref_wrapper_3) {
1777+
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
1778+
"$schema": "https://json-schema.org/draft/2020-12/schema",
1779+
"$ref": "https://example.com/foo",
1780+
"allOf": [
1781+
{ "$ref": "https://example.com/bar" }
1782+
]
1783+
})JSON");
1784+
1785+
LINT_AND_FIX_FOR_READABILITY(document);
1786+
1787+
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
1788+
"$schema": "https://json-schema.org/draft/2020-12/schema",
1789+
"$ref": "https://example.com/foo",
1790+
"allOf": [
1791+
{ "$ref": "https://example.com/bar" }
1792+
]
1793+
})JSON");
1794+
1795+
EXPECT_EQ(document, expected);
1796+
}
1797+
1798+
TEST(AlterSchema_lint_2020_12, unnecessary_allof_ref_wrapper_4) {
1799+
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
1800+
"$schema": "https://json-schema.org/draft/2020-12/schema",
1801+
"allOf": [
1802+
{ "type": "number" },
1803+
{ "$ref": "https://example.com" },
1804+
{ "type": "integer" }
1805+
]
1806+
})JSON");
1807+
1808+
LINT_AND_FIX_FOR_READABILITY(document);
1809+
1810+
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
1811+
"$schema": "https://json-schema.org/draft/2020-12/schema",
1812+
"$ref": "https://example.com",
1813+
"allOf": [
1814+
{ "type": "number" },
1815+
{ "type": "integer" }
1816+
]
1817+
})JSON");
1818+
1819+
EXPECT_EQ(document, expected);
1820+
}
1821+
1822+
TEST(AlterSchema_lint_2020_12, unnecessary_allof_ref_wrapper_5) {
1823+
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
1824+
"$schema": "https://json-schema.org/draft/2020-12/schema",
1825+
"allOf": [
1826+
{
1827+
"type": "integer",
1828+
"$ref": "https://example.com"
1829+
}
1830+
]
1831+
})JSON");
1832+
1833+
LINT_AND_FIX_FOR_READABILITY(document);
1834+
1835+
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
1836+
"$schema": "https://json-schema.org/draft/2020-12/schema",
1837+
"$ref": "https://example.com",
1838+
"allOf": [
1839+
{ "type": "integer" }
1840+
]
1841+
})JSON");
1842+
1843+
EXPECT_EQ(document, expected);
1844+
}

test/alterschema/alterschema_lint_draft4_test.cc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,3 +741,23 @@ TEST(AlterSchema_lint_draft4, equal_numeric_bounds_to_enum_2) {
741741

742742
EXPECT_EQ(document, expected);
743743
}
744+
745+
TEST(AlterSchema_lint_draft4, unnecessary_allof_ref_wrapper_1) {
746+
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
747+
"$schema": "http://json-schema.org/draft-04/schema#",
748+
"allOf": [
749+
{ "$ref": "https://example.com" }
750+
]
751+
})JSON");
752+
753+
LINT_AND_FIX_FOR_READABILITY(document);
754+
755+
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
756+
"$schema": "http://json-schema.org/draft-04/schema#",
757+
"allOf": [
758+
{ "$ref": "https://example.com" }
759+
]
760+
})JSON");
761+
762+
EXPECT_EQ(document, expected);
763+
}

test/alterschema/alterschema_lint_draft6_test.cc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,3 +1092,23 @@ TEST(AlterSchema_lint_draft6, equal_numeric_bounds_to_enum_2) {
10921092

10931093
EXPECT_EQ(document, expected);
10941094
}
1095+
1096+
TEST(AlterSchema_lint_draft6, unnecessary_allof_ref_wrapper_1) {
1097+
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
1098+
"$schema": "http://json-schema.org/draft-06/schema#",
1099+
"allOf": [
1100+
{ "$ref": "https://example.com" }
1101+
]
1102+
})JSON");
1103+
1104+
LINT_AND_FIX_FOR_READABILITY(document);
1105+
1106+
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
1107+
"$schema": "http://json-schema.org/draft-06/schema#",
1108+
"allOf": [
1109+
{ "$ref": "https://example.com" }
1110+
]
1111+
})JSON");
1112+
1113+
EXPECT_EQ(document, expected);
1114+
}

test/alterschema/alterschema_lint_draft7_test.cc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,3 +1188,23 @@ TEST(AlterSchema_lint_draft7, equal_numeric_bounds_to_enum_2) {
11881188

11891189
EXPECT_EQ(document, expected);
11901190
}
1191+
1192+
TEST(AlterSchema_lint_draft7, unnecessary_allof_ref_wrapper_1) {
1193+
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
1194+
"$schema": "http://json-schema.org/draft-07/schema#",
1195+
"allOf": [
1196+
{ "$ref": "https://example.com" }
1197+
]
1198+
})JSON");
1199+
1200+
LINT_AND_FIX_FOR_READABILITY(document);
1201+
1202+
const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
1203+
"$schema": "http://json-schema.org/draft-07/schema#",
1204+
"allOf": [
1205+
{ "$ref": "https://example.com" }
1206+
]
1207+
})JSON");
1208+
1209+
EXPECT_EQ(document, expected);
1210+
}

0 commit comments

Comments
 (0)