Skip to content

Commit 10cdaef

Browse files
authored
Merge pull request #3046 from glideapps/agent/fix-issue-273
fix(cplusplus): forward-declare recursive unions instead of self-referential aliases
2 parents 7b2fe4e + 407faab commit 10cdaef

3 files changed

Lines changed: 193 additions & 10 deletions

File tree

packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts

Lines changed: 190 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,58 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
255255
return !this.isCycleBreakerType(t);
256256
}
257257

258+
private typeContainsUnion(t: Type, visited = new Set<Type>()): boolean {
259+
if (t instanceof UnionType) return true;
260+
if (visited.has(t)) return false;
261+
262+
visited.add(t);
263+
return iterableSome(t.getChildren(), (child) =>
264+
this.typeContainsUnion(child, visited),
265+
);
266+
}
267+
268+
private typeReaches(
269+
t: Type,
270+
target: Type,
271+
visited = new Set<Type>(),
272+
): boolean {
273+
if (t === target) return true;
274+
if (visited.has(t)) return false;
275+
276+
visited.add(t);
277+
return iterableSome(t.getChildren(), (child) =>
278+
this.typeReaches(child, target, visited),
279+
);
280+
}
281+
282+
private isRecursiveUnion(u: UnionType): boolean {
283+
return iterableSome(u.getChildren(), (child) =>
284+
this.typeReaches(child, u),
285+
);
286+
}
287+
288+
private recursiveUnionUsesStackOptional(u: UnionType): boolean {
289+
const [maybeNull] = removeNullFromUnion(u, true);
290+
return (
291+
this.isRecursiveUnion(u) &&
292+
maybeNull !== null &&
293+
this.isOptionalAsValuePossible(u)
294+
);
295+
}
296+
258297
public isImplicitCycleBreaker(t: Type): boolean {
298+
// Containers break completeness cycles for classes, but not for type
299+
// aliases: an alias name is not in scope on its own right-hand side.
259300
const kind = t.kind;
260-
return kind === "array" || kind === "map";
301+
if (kind !== "array" && kind !== "map") return false;
302+
303+
return !iterableSome(t.getChildren(), (child) =>
304+
this.typeContainsUnion(child),
305+
);
306+
}
307+
308+
protected canBreakCycles(t: Type): boolean {
309+
return t instanceof ClassType || t instanceof UnionType;
261310
}
262311

263312
// Is likely to return std::optional or boost::optional
@@ -534,7 +583,13 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
534583
this.emitLine("#pragma once");
535584
this.ensureBlankLine();
536585

537-
if (this.haveOptionalProperties) {
586+
if (
587+
this.haveOptionalProperties ||
588+
(!this._options.codeFormat &&
589+
iterableSome(this.namedUnions, (u) =>
590+
this.recursiveUnionUsesStackOptional(u),
591+
))
592+
) {
538593
if (this._options.boost) {
539594
this.emitInclude(true, "boost/optional.hpp");
540595
} else {
@@ -1465,9 +1520,61 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
14651520
set.add(propType);
14661521
return [true, set];
14671522
})();
1523+
const isRecursiveNamedUnion =
1524+
propType instanceof UnionType &&
1525+
this.namedUnions.has(propType) &&
1526+
this.isRecursiveUnion(propType);
1527+
if (
1528+
isRecursiveNamedUnion &&
1529+
p.isOptional &&
1530+
removeNullFromUnion(propType, true)[0] !== null
1531+
) {
1532+
cppType = this.cppType(
1533+
propType,
1534+
{
1535+
needsForwardIndirection: true,
1536+
needsOptionalIndirection: true,
1537+
inJsonNamespace: false,
1538+
},
1539+
false,
1540+
true,
1541+
true,
1542+
);
1543+
const propertyName =
1544+
this._stringType.wrapEncodingChange(
1545+
[ourQualifier],
1546+
this._stringType.getType(),
1547+
this.NarrowString.getType(),
1548+
this._stringType.createStringLiteral([
1549+
stringEscape(json),
1550+
]),
1551+
);
1552+
this.emitLine(
1553+
assignment.wrap(
1554+
[],
1555+
[
1556+
"j.find(",
1557+
propertyName,
1558+
") != j.end() ? j.at(",
1559+
propertyName,
1560+
").get<",
1561+
cppType,
1562+
">() : ",
1563+
cppType,
1564+
"()",
1565+
],
1566+
),
1567+
";",
1568+
);
1569+
return;
1570+
}
1571+
14681572
if (nullOrOptional) {
1573+
const optionalTypes = isRecursiveNamedUnion
1574+
? new Set<Type>([propType])
1575+
: typeSet;
14691576
cppType = this.cppTypeInOptional(
1470-
typeSet,
1577+
optionalTypes,
14711578
{
14721579
needsForwardIndirection: false,
14731580
needsOptionalIndirection: false,
@@ -1477,7 +1584,7 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
14771584
true,
14781585
);
14791586
toType = this.cppTypeInOptional(
1480-
typeSet,
1587+
optionalTypes,
14811588
{
14821589
needsForwardIndirection: false,
14831590
needsOptionalIndirection: false,
@@ -1697,12 +1804,66 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
16971804
}
16981805

16991806
protected emitUnionTypedefs(u: UnionType, unionName: Name): void {
1807+
const variantType = this.variantType(u, false);
1808+
if (!this.isRecursiveUnion(u)) {
1809+
this.emitLine("using ", unionName, " = ", variantType, ";");
1810+
return;
1811+
}
1812+
1813+
// A struct introduces its name before the base type is parsed, allowing
1814+
// recursion through containers such as std::vector.
1815+
this.emitBlock(["struct ", unionName, " : ", variantType], true, () => {
1816+
this.emitLine("using base = ", variantType, ";");
1817+
this.emitLine("using base::base;");
1818+
this.emitLine("using base::operator=;");
1819+
});
1820+
}
1821+
1822+
protected emitUnionWrapperHeaders(u: UnionType, unionName: Name): void {
1823+
if (!this.isRecursiveUnion(u)) return;
1824+
17001825
this.emitLine(
1701-
"using ",
1826+
"void from_json(",
1827+
this.withConst("json"),
1828+
" & j, ",
17021829
unionName,
1703-
" = ",
1704-
this.variantType(u, false),
1705-
";",
1830+
" & x);",
1831+
);
1832+
this.emitLine(
1833+
"void to_json(json & j, ",
1834+
this.withConst(unionName),
1835+
" & x);",
1836+
);
1837+
}
1838+
1839+
protected emitUnionWrapperFunctions(u: UnionType, unionName: Name): void {
1840+
if (!this.isRecursiveUnion(u)) return;
1841+
1842+
this.emitBlock(
1843+
[
1844+
"inline void from_json(",
1845+
this.withConst("json"),
1846+
" & j, ",
1847+
unionName,
1848+
" & x)",
1849+
],
1850+
false,
1851+
() => this.emitLine("x = j.get<", unionName, "::base>();"),
1852+
);
1853+
this.ensureBlankLine();
1854+
this.emitBlock(
1855+
[
1856+
"inline void to_json(json & j, ",
1857+
this.withConst(unionName),
1858+
" & x)",
1859+
],
1860+
false,
1861+
() =>
1862+
this.emitLine(
1863+
"j = static_cast<",
1864+
this.withConst([unionName, "::base"]),
1865+
" &>(x);",
1866+
),
17061867
);
17071868
}
17081869

@@ -2881,6 +3042,15 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
28813042
this.emitHelperFunctions();
28823043
}
28833044

3045+
if (iterableSome(this.namedUnions, (u) => this.isRecursiveUnion(u))) {
3046+
this.forEachUnion("none", (u: UnionType, unionName: Name) => {
3047+
if (this.isRecursiveUnion(u)) {
3048+
this.emitLine("struct ", unionName, ";");
3049+
}
3050+
});
3051+
this.ensureBlankLine();
3052+
}
3053+
28843054
this.forEachDeclaration("interposing", (decl) =>
28853055
this.emitDeclaration(decl),
28863056
);
@@ -2904,6 +3074,12 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
29043074
"leading-and-interposing",
29053075
(_: unknown, enumName: Name) => this.emitEnumHeaders(enumName),
29063076
);
3077+
3078+
this.forEachUnion(
3079+
"leading-and-interposing",
3080+
(u: UnionType, unionName: Name) =>
3081+
this.emitUnionWrapperHeaders(u, unionName),
3082+
);
29073083
});
29083084
}
29093085

@@ -2933,6 +3109,12 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
29333109
(e: EnumType, enumName: Name) =>
29343110
this.emitEnumFunctions(e, enumName),
29353111
);
3112+
3113+
this.forEachUnion(
3114+
"leading-and-interposing",
3115+
(u: UnionType, unionName: Name) =>
3116+
this.emitUnionWrapperFunctions(u, unionName),
3117+
);
29363118
}
29373119

29383120
protected emitNlohmannNamespaceImpls(): void {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"value": "leaf"
3+
}

test/languages.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -833,8 +833,6 @@ export const CPlusPlusLanguage: Language = {
833833
"keyword-unions.schema",
834834
// The generated deserializer accepts non-object values when all class properties are optional.
835835
"nested-intersection-union.schema",
836-
// Recursive top-level unions produce aliases that can refer to later aliases.
837-
"recursive-union-flattening.schema",
838836
],
839837
rendererOptions: {},
840838
quickTestRendererOptions: [

0 commit comments

Comments
 (0)