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
198 changes: 190 additions & 8 deletions packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,58 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
return !this.isCycleBreakerType(t);
}

private typeContainsUnion(t: Type, visited = new Set<Type>()): boolean {
if (t instanceof UnionType) return true;
if (visited.has(t)) return false;

visited.add(t);
return iterableSome(t.getChildren(), (child) =>
this.typeContainsUnion(child, visited),
);
}

private typeReaches(
t: Type,
target: Type,
visited = new Set<Type>(),
): boolean {
if (t === target) return true;
if (visited.has(t)) return false;

visited.add(t);
return iterableSome(t.getChildren(), (child) =>
this.typeReaches(child, target, visited),
);
}

private isRecursiveUnion(u: UnionType): boolean {
return iterableSome(u.getChildren(), (child) =>
this.typeReaches(child, u),
);
}

private recursiveUnionUsesStackOptional(u: UnionType): boolean {
const [maybeNull] = removeNullFromUnion(u, true);
return (
this.isRecursiveUnion(u) &&
maybeNull !== null &&
this.isOptionalAsValuePossible(u)
);
}

public isImplicitCycleBreaker(t: Type): boolean {
// Containers break completeness cycles for classes, but not for type
// aliases: an alias name is not in scope on its own right-hand side.
const kind = t.kind;
return kind === "array" || kind === "map";
if (kind !== "array" && kind !== "map") return false;

return !iterableSome(t.getChildren(), (child) =>
this.typeContainsUnion(child),
);
}

protected canBreakCycles(t: Type): boolean {
return t instanceof ClassType || t instanceof UnionType;
}

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

if (this.haveOptionalProperties) {
if (
this.haveOptionalProperties ||
(!this._options.codeFormat &&
iterableSome(this.namedUnions, (u) =>
this.recursiveUnionUsesStackOptional(u),
))
) {
if (this._options.boost) {
this.emitInclude(true, "boost/optional.hpp");
} else {
Expand Down Expand Up @@ -1465,9 +1520,61 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
set.add(propType);
return [true, set];
})();
const isRecursiveNamedUnion =
propType instanceof UnionType &&
this.namedUnions.has(propType) &&
this.isRecursiveUnion(propType);
if (
isRecursiveNamedUnion &&
p.isOptional &&
removeNullFromUnion(propType, true)[0] !== null
) {
cppType = this.cppType(
propType,
{
needsForwardIndirection: true,
needsOptionalIndirection: true,
inJsonNamespace: false,
},
false,
true,
true,
);
const propertyName =
this._stringType.wrapEncodingChange(
[ourQualifier],
this._stringType.getType(),
this.NarrowString.getType(),
this._stringType.createStringLiteral([
stringEscape(json),
]),
);
this.emitLine(
assignment.wrap(
[],
[
"j.find(",
propertyName,
") != j.end() ? j.at(",
propertyName,
").get<",
cppType,
">() : ",
cppType,
"()",
],
),
";",
);
return;
}

if (nullOrOptional) {
const optionalTypes = isRecursiveNamedUnion
? new Set<Type>([propType])
: typeSet;
cppType = this.cppTypeInOptional(
typeSet,
optionalTypes,
{
needsForwardIndirection: false,
needsOptionalIndirection: false,
Expand All @@ -1477,7 +1584,7 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
true,
);
toType = this.cppTypeInOptional(
typeSet,
optionalTypes,
{
needsForwardIndirection: false,
needsOptionalIndirection: false,
Expand Down Expand Up @@ -1697,12 +1804,66 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
}

protected emitUnionTypedefs(u: UnionType, unionName: Name): void {
const variantType = this.variantType(u, false);
if (!this.isRecursiveUnion(u)) {
this.emitLine("using ", unionName, " = ", variantType, ";");
return;
}

// A struct introduces its name before the base type is parsed, allowing
// recursion through containers such as std::vector.
this.emitBlock(["struct ", unionName, " : ", variantType], true, () => {
this.emitLine("using base = ", variantType, ";");
this.emitLine("using base::base;");
this.emitLine("using base::operator=;");
});
}

protected emitUnionWrapperHeaders(u: UnionType, unionName: Name): void {
if (!this.isRecursiveUnion(u)) return;

this.emitLine(
"using ",
"void from_json(",
this.withConst("json"),
" & j, ",
unionName,
" = ",
this.variantType(u, false),
";",
" & x);",
);
this.emitLine(
"void to_json(json & j, ",
this.withConst(unionName),
" & x);",
);
}

protected emitUnionWrapperFunctions(u: UnionType, unionName: Name): void {
if (!this.isRecursiveUnion(u)) return;

this.emitBlock(
[
"inline void from_json(",
this.withConst("json"),
" & j, ",
unionName,
" & x)",
],
false,
() => this.emitLine("x = j.get<", unionName, "::base>();"),
);
this.ensureBlankLine();
this.emitBlock(
[
"inline void to_json(json & j, ",
this.withConst(unionName),
" & x)",
],
false,
() =>
this.emitLine(
"j = static_cast<",
this.withConst([unionName, "::base"]),
" &>(x);",
),
);
}

Expand Down Expand Up @@ -2881,6 +3042,15 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
this.emitHelperFunctions();
}

if (iterableSome(this.namedUnions, (u) => this.isRecursiveUnion(u))) {
this.forEachUnion("none", (u: UnionType, unionName: Name) => {
if (this.isRecursiveUnion(u)) {
this.emitLine("struct ", unionName, ";");
}
});
this.ensureBlankLine();
}

this.forEachDeclaration("interposing", (decl) =>
this.emitDeclaration(decl),
);
Expand All @@ -2904,6 +3074,12 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
"leading-and-interposing",
(_: unknown, enumName: Name) => this.emitEnumHeaders(enumName),
);

this.forEachUnion(
"leading-and-interposing",
(u: UnionType, unionName: Name) =>
this.emitUnionWrapperHeaders(u, unionName),
);
});
}

Expand Down Expand Up @@ -2933,6 +3109,12 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
(e: EnumType, enumName: Name) =>
this.emitEnumFunctions(e, enumName),
);

this.forEachUnion(
"leading-and-interposing",
(u: UnionType, unionName: Name) =>
this.emitUnionWrapperFunctions(u, unionName),
);
}

protected emitNlohmannNamespaceImpls(): void {
Expand Down
3 changes: 3 additions & 0 deletions test/inputs/schema/rust-cycle-breaker-union.2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"value": "leaf"
}
2 changes: 0 additions & 2 deletions test/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,8 +833,6 @@ export const CPlusPlusLanguage: Language = {
"keyword-unions.schema",
// The generated deserializer accepts non-object values when all class properties are optional.
"nested-intersection-union.schema",
// Recursive top-level unions produce aliases that can refer to later aliases.
"recursive-union-flattening.schema",
],
rendererOptions: {},
quickTestRendererOptions: [
Expand Down
Loading