From e498c3c58c25bdbb62bb433174d4db5c06a13d0a Mon Sep 17 00:00:00 2001 From: thives Date: Sat, 15 Jun 2024 00:14:06 +0200 Subject: [PATCH 1/5] cJSON - split generated code into source and header files --- .../src/language/CJSON/CJSONRenderer.ts | 176 +++++++++++++----- test/languages.ts | 2 +- 2 files changed, 130 insertions(+), 48 deletions(-) diff --git a/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts b/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts index 6f9dc33b9b..b458b7b669 100644 --- a/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts +++ b/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts @@ -28,7 +28,9 @@ import { } from "./utils"; export class CJSONRenderer extends ConvenienceRenderer { - private currentFilename: string | undefined; /* Current filename */ + private currentHeaderFilename: string | undefined; /* Current header filename */ + + private currentSourceFilename: string | undefined; /* Current source filename */ private readonly memberNameStyle: NameStyle; /* Member name style */ @@ -170,8 +172,8 @@ export class CJSONRenderer extends ConvenienceRenderer { } /** - * Function called to create header file(s) - * @param proposedFilename: source filename provided from stdin + * Function called to create header and source file(s) + * @param proposedFilename: source filename provided from stdin (without extensions) */ protected emitSourceStructure(proposedFilename: string): void { /* Depending of source style option, generate a unique header or multiple header files */ @@ -183,12 +185,12 @@ export class CJSONRenderer extends ConvenienceRenderer { } /** - * Function called to create a single header file with types and generators + * Function called to create a single pair of header/source files with types and generators * @param proposedFilename: source filename provided from stdin */ protected emitSingleSourceStructure(proposedFilename: string): void { - /* Create file */ - this.startFile(proposedFilename); + /* Create header file */ + this.startHeaderFile(proposedFilename); /* Create types */ this.forEachDeclaration("leading-and-interposing", decl => { @@ -237,6 +239,12 @@ export class CJSONRenderer extends ConvenienceRenderer { type => this.namedTypeToNameForTopLevel(type) === undefined ); + /* Close header file */ + this.finishHeaderFile(); + + /* Create source file */ + this.startSourceFile(proposedFilename); + /* Create enum functions */ this.forEachEnum("leading-and-interposing", (enumType: EnumType, _enumName: Name) => this.emitEnumFunctions(enumType) @@ -257,8 +265,8 @@ export class CJSONRenderer extends ConvenienceRenderer { type => this.namedTypeToNameForTopLevel(type) === undefined ); - /* Close file */ - this.finishFile(); + /* Close source file */ + this.finishSourceFile(); } /** @@ -298,12 +306,13 @@ export class CJSONRenderer extends ConvenienceRenderer { protected emitEnum(enumType: EnumType, includes: string[]): void { /* Create file */ const enumName = this.nameForNamedType(enumType); - const filename = this.sourcelikeToString(enumName).concat(".h"); - includes.push(filename); - this.startFile(filename); + const headerFilename = this.sourcelikeToString(enumName).concat(".h"); + const sourceFilename = this.getSourceNameFromHeaderName(headerFilename); + includes.push(headerFilename); + this.startHeaderFile(headerFilename); /* Create includes */ - this.emitIncludes(enumType, this.sourcelikeToString(filename)); + this.emitIncludes(enumType, this.sourcelikeToString(headerFilename)); /* Create types */ this.emitEnumTypedef(enumType); @@ -311,11 +320,18 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create prototypes */ this.emitEnumPrototypes(enumType); + /* Close header file and create source file */ + this.finishHeaderFile(); + this.startSourceFile(sourceFilename); + + /* Include corresponding header file */ + this.emitIncludeLine(headerFilename, true); + /* Create functions */ this.emitEnumFunctions(enumType); - /* Close file */ - this.finishFile(); + /* Close source file */ + this.finishSourceFile(); } /** @@ -426,12 +442,13 @@ export class CJSONRenderer extends ConvenienceRenderer { protected emitUnion(unionType: UnionType, includes: string[]): void { /* Create file */ const unionName = this.nameForNamedType(unionType); - const filename = this.sourcelikeToString(unionName).concat(".h"); - includes.push(filename); - this.startFile(filename); + const headerFilename = this.sourcelikeToString(unionName).concat(".h"); + const sourceFilename = this.getSourceNameFromHeaderName(headerFilename); + includes.push(headerFilename); + this.startHeaderFile(headerFilename); /* Create includes */ - this.emitIncludes(unionType, this.sourcelikeToString(filename)); + this.emitIncludes(unionType, this.sourcelikeToString(headerFilename)); /* Create types */ this.emitUnionTypedef(unionType); @@ -439,11 +456,18 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create prototypes */ this.emitUnionPrototypes(unionType); + /* Close header file and create source file */ + this.finishHeaderFile(); + this.startSourceFile(sourceFilename); + + /* Include corresponding header file */ + this.emitIncludeLine(headerFilename, true); + /* Create functions */ this.emitUnionFunctions(unionType); - /* Close file */ - this.finishFile(); + /* Close source file */ + this.finishSourceFile(); } /** @@ -1329,12 +1353,13 @@ export class CJSONRenderer extends ConvenienceRenderer { protected emitClass(classType: ClassType, includes: string[]): void { /* Create file */ const className = this.nameForNamedType(classType); - const filename = this.sourcelikeToString(className).concat(".h"); - includes.push(filename); - this.startFile(filename); + const headerFilename = this.sourcelikeToString(className).concat(".h"); + const sourceFilename = this.getSourceNameFromHeaderName(headerFilename); + includes.push(headerFilename); + this.startHeaderFile(headerFilename); /* Create includes */ - this.emitIncludes(classType, this.sourcelikeToString(filename)); + this.emitIncludes(classType, this.sourcelikeToString(headerFilename)); /* Create types */ this.emitClassTypedef(classType); @@ -1342,11 +1367,18 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create prototypes */ this.emitClassPrototypes(classType); + /* Close header file and create source file */ + this.finishHeaderFile(); + this.startSourceFile(sourceFilename); + + /* Include corresponding header file */ + this.emitIncludeLine(headerFilename, true); + /* Create functions */ this.emitClassFunctions(classType); - /* Close file */ - this.finishFile(); + /* Close source file */ + this.finishSourceFile(); } /** @@ -2949,8 +2981,9 @@ export class CJSONRenderer extends ConvenienceRenderer { */ protected emitTopLevel(type: Type, className: Name, includes: string[]): void { /* Create file */ - const filename = this.sourcelikeToString(className).concat(".h"); - this.startFile(filename); + const headerFilename = this.sourcelikeToString(className).concat(".h"); + const sourceFilename = this.getSourceNameFromHeaderName(headerFilename); + this.startHeaderFile(headerFilename); /* Create includes - This create too much includes but this is safer because of specific corner cases */ includes.forEach(name => { @@ -2964,11 +2997,18 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create prototypes */ this.emitTopLevelPrototypes(type, className); + /* Close header file and create source file */ + this.finishHeaderFile(); + this.startSourceFile(sourceFilename); + + /* Include corresponding header file */ + this.emitIncludeLine(headerFilename, true); + /* Create functions */ this.emitTopLevelFunctions(type, className); - /* Close file */ - this.finishFile(); + /* Close source file */ + this.finishSourceFile(); } /** @@ -3564,21 +3604,21 @@ export class CJSONRenderer extends ConvenienceRenderer { } /** - * Function called to create a file + * Function called to create a header file * @param proposedFilename: source filename provided from stdin */ - protected startFile(proposedFilename: Sourcelike): void { - /* Check if previous file is closed, create a new file */ - assert(this.currentFilename === undefined, "Previous file wasn't finished"); + protected startHeaderFile(proposedFilename: Sourcelike): void { + /* Check if previous header file is closed, create a new file */ + assert(this.currentHeaderFilename === undefined, "Previous header file wasn't finished"); if (proposedFilename !== undefined) { - this.currentFilename = this.sourcelikeToString(proposedFilename); + this.currentHeaderFilename = this.sourcelikeToString(proposedFilename); } - /* Check if file has been created */ - if (this.currentFilename !== undefined) { + /* Check if header file has been created */ + if (this.currentHeaderFilename !== undefined) { /* Write header */ this.emitDescriptionBlock([ - this.currentFilename, + this.currentHeaderFilename, "This file has been autogenerated using quicktype https://github.com/quicktype/quicktype - DO NOT EDIT", "This file depends of https://github.com/DaveGamble/cJSON, https://github.com/joelguittet/c-list and https://github.com/joelguittet/c-hashtable", "To parse json data from json string use the following: struct * data = cJSON_Parse();", @@ -3592,12 +3632,12 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Write include guard */ this.emitLine( "#ifndef __", - allUpperWordStyle(this.currentFilename.replace(new RegExp(/[^a-zA-Z0-9]+/, "g"), "_")), + allUpperWordStyle(this.currentHeaderFilename.replace(new RegExp(/[^a-zA-Z0-9]+/, "g"), "_")), "__" ); this.emitLine( "#define __", - allUpperWordStyle(this.currentFilename.replace(new RegExp(/[^a-zA-Z0-9]+/, "g"), "_")), + allUpperWordStyle(this.currentHeaderFilename.replace(new RegExp(/[^a-zA-Z0-9]+/, "g"), "_")), "__" ); this.ensureBlankLine(); @@ -3632,12 +3672,36 @@ export class CJSONRenderer extends ConvenienceRenderer { } } + /** + * Function called to create a source file + * @param proposedFilename: source filename provided from stdin + */ + protected startSourceFile(proposedFilename: Sourcelike): void { + /* Check if previous source file is closed, create a new file */ + assert(this.currentSourceFilename === undefined, "Previous source file wasn't finished"); + if (proposedFilename !== undefined) { + this.currentSourceFilename = this.getSourceNameFromHeaderName(this.sourcelikeToString(proposedFilename)); + } + + /* Check if source file and corresponding header file has been created */ + if (this.currentSourceFilename !== undefined) { + /* Write header */ + this.emitDescriptionBlock([ + this.currentSourceFilename, + "This file has been autogenerated using quicktype https://github.com/quicktype/quicktype - DO NOT EDIT" + ]); + this.ensureBlankLine(); + this.emitIncludeLine(this.sourcelikeToString(proposedFilename), true); + this.ensureBlankLine(); + } + } + /** * Function called to close current file */ - protected finishFile(): void { - /* Check if file has been created */ - if (this.currentFilename !== undefined) { + protected finishHeaderFile(): void { + /* Check if header file has been created */ + if (this.currentHeaderFilename !== undefined) { /* Write C++ guard */ this.emitLine("#ifdef __cplusplus"); this.emitLine("}"); @@ -3647,14 +3711,28 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Write include guard */ this.emitLine( "#endif /* __", - allUpperWordStyle(this.currentFilename.replace(new RegExp(/[^a-zA-Z0-9]+/, "g"), "_")), + allUpperWordStyle(this.currentHeaderFilename.replace(new RegExp(/[^a-zA-Z0-9]+/, "g"), "_")), "__ */" ); this.ensureBlankLine(); - /* Close file */ - super.finishFile(defined(this.currentFilename)); - this.currentFilename = undefined; + /* Close headeerfile */ + super.finishFile(defined(this.currentHeaderFilename)); + this.currentHeaderFilename = undefined; + } + } + + /** + * Function called to close current source file + */ + protected finishSourceFile(): void { + /* Check if source file has been created */ + if (this.currentSourceFilename !== undefined) { + this.ensureBlankLine(); + + /* Close source file */ + super.finishFile(defined(this.currentSourceFilename)); + this.currentSourceFilename = undefined; } } @@ -3886,4 +3964,8 @@ export class CJSONRenderer extends ConvenienceRenderer { recur(false, false, 0, type); return result; } + + protected getSourceNameFromHeaderName(headerName: string): string { + return headerName.replace(".h", ".c"); + } } diff --git a/test/languages.ts b/test/languages.ts index f1890a7d54..5fcc476989 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -413,7 +413,7 @@ export const CJSONLanguage: Language = { base: "test/fixtures/cjson", setupCommand: "curl -o cJSON.c https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.15/cJSON.c && curl -o cJSON.h https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.15/cJSON.h && curl -o list.h https://raw.githubusercontent.com/joelguittet/c-list/master/include/list.h && curl -o list.c https://raw.githubusercontent.com/joelguittet/c-list/master/src/list.c && curl -o hashtable.h https://raw.githubusercontent.com/joelguittet/c-hashtable/master/include/hashtable.h && curl -o hashtable.c https://raw.githubusercontent.com/joelguittet/c-hashtable/master/src/hashtable.c", - compileCommand: "gcc -O0 -o quicktype -I. cJSON.c hashtable.c list.c main.c -lpthread", + compileCommand: "gcc -O0 -o quicktype -I. cJSON.c hashtable.c list.c main.c TopLevel.c -lpthread", runCommand(sample: string) { return `valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --error-exitcode=1 ./quicktype "${sample}"`; }, From 103bead9294e5c7fe63e39511bc94c0393e019b8 Mon Sep 17 00:00:00 2001 From: thives Date: Sat, 15 Jun 2024 20:50:39 +0200 Subject: [PATCH 2/5] cJSON - add header-only option (default true) to opt in split header and source code generation --- .../src/language/CJSON/CJSONRenderer.ts | 95 ++++++++++++++----- .../src/language/CJSON/language.ts | 15 ++- test/languages.ts | 4 +- 3 files changed, 84 insertions(+), 30 deletions(-) diff --git a/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts b/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts index b458b7b669..c3e2b416fc 100644 --- a/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts +++ b/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts @@ -239,11 +239,13 @@ export class CJSONRenderer extends ConvenienceRenderer { type => this.namedTypeToNameForTopLevel(type) === undefined ); - /* Close header file */ - this.finishHeaderFile(); + if (!this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); - /* Create source file */ - this.startSourceFile(proposedFilename); + /* Create source file */ + this.startSourceFile(proposedFilename); + } /* Create enum functions */ this.forEachEnum("leading-and-interposing", (enumType: EnumType, _enumName: Name) => @@ -265,8 +267,13 @@ export class CJSONRenderer extends ConvenienceRenderer { type => this.namedTypeToNameForTopLevel(type) === undefined ); - /* Close source file */ - this.finishSourceFile(); + if (this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + } else { + /* Close source file */ + this.finishSourceFile(); + } } /** @@ -320,9 +327,13 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create prototypes */ this.emitEnumPrototypes(enumType); - /* Close header file and create source file */ - this.finishHeaderFile(); - this.startSourceFile(sourceFilename); + if (!this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + + /* Create source file */ + this.startSourceFile(sourceFilename); + } /* Include corresponding header file */ this.emitIncludeLine(headerFilename, true); @@ -330,8 +341,13 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create functions */ this.emitEnumFunctions(enumType); - /* Close source file */ - this.finishSourceFile(); + if (this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + } else { + /* Close source file */ + this.finishSourceFile(); + } } /** @@ -456,9 +472,13 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create prototypes */ this.emitUnionPrototypes(unionType); - /* Close header file and create source file */ - this.finishHeaderFile(); - this.startSourceFile(sourceFilename); + if (!this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + + /* Create source file */ + this.startSourceFile(sourceFilename); + } /* Include corresponding header file */ this.emitIncludeLine(headerFilename, true); @@ -466,8 +486,13 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create functions */ this.emitUnionFunctions(unionType); - /* Close source file */ - this.finishSourceFile(); + if (this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + } else { + /* Close source file */ + this.finishSourceFile(); + } } /** @@ -1367,9 +1392,13 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create prototypes */ this.emitClassPrototypes(classType); - /* Close header file and create source file */ - this.finishHeaderFile(); - this.startSourceFile(sourceFilename); + if (!this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + + /* Create source file */ + this.startSourceFile(sourceFilename); + } /* Include corresponding header file */ this.emitIncludeLine(headerFilename, true); @@ -1377,8 +1406,13 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create functions */ this.emitClassFunctions(classType); - /* Close source file */ - this.finishSourceFile(); + if (this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + } else { + /* Close source file */ + this.finishSourceFile(); + } } /** @@ -2997,9 +3031,13 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create prototypes */ this.emitTopLevelPrototypes(type, className); - /* Close header file and create source file */ - this.finishHeaderFile(); - this.startSourceFile(sourceFilename); + if (!this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + + /* Create source file */ + this.startSourceFile(sourceFilename); + } /* Include corresponding header file */ this.emitIncludeLine(headerFilename, true); @@ -3007,8 +3045,13 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create functions */ this.emitTopLevelFunctions(type, className); - /* Close source file */ - this.finishSourceFile(); + if (this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + } else { + /* Close source file */ + this.finishSourceFile(); + } } /** diff --git a/packages/quicktype-core/src/language/CJSON/language.ts b/packages/quicktype-core/src/language/CJSON/language.ts index 82e32ffd90..376be3ee74 100644 --- a/packages/quicktype-core/src/language/CJSON/language.ts +++ b/packages/quicktype-core/src/language/CJSON/language.ts @@ -110,7 +110,17 @@ export const cJSONOptions = { camelValue, pascalUpperAcronymsValue, camelUpperAcronymsValue - ]) + ]), + headerOnly: new EnumOption( + "header-only", + "Generate headers only", + [ + ["true", true], + ["false", false] + ], + "true", + "secondary" + ) }; /* cJSON generator target language */ @@ -138,7 +148,8 @@ export class CJSONTargetLanguage extends TargetLanguage { cJSONOptions.hashtableSize, cJSONOptions.typeNamingStyle, cJSONOptions.memberNamingStyle, - cJSONOptions.enumeratorNamingStyle + cJSONOptions.enumeratorNamingStyle, + cJSONOptions.headerOnly ]; } diff --git a/test/languages.ts b/test/languages.ts index 5fcc476989..96a43c91dc 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -481,8 +481,8 @@ export const CJSONLanguage: Language = { /* Other cases not supported */ "implicit-class-array-union.schema" ], - rendererOptions: {}, - quickTestRendererOptions: [{ "source-style": "single-source" }], + rendererOptions: { "header-only": "false" }, + quickTestRendererOptions: [{ "source-style": "single-source", "header-only": "false" }], sourceFiles: ["src/language/CJSON/index.ts"] }; From 0043ecc507167892f16d3d031284f36b0e328758 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 18:01:20 -0400 Subject: [PATCH 3/5] fix(cjson): correct includes in generated source/header split Fixes for the header/source split feature (header-only=false): - startSourceFile now takes the header filename at every call site and derives the source filename itself. Previously the multi-source call sites passed the source filename, so every generated .c file began with an unguarded self-include (#include ) that made compilation recurse infinitely. - The redundant unconditional emitIncludeLine(headerFilename, true) after startSourceFile is gone. In header-only mode it made every generated header include itself (an unintended change to the pre-existing multi-source output); in split mode it duplicated the include emitted by startSourceFile. - The source file's include of its own header is now a quoted include, matching the existing convention: quoted includes for generated files, angle brackets only for system and vendored headers. - getSourceNameFromHeaderName anchors the extension swap (/\.h$/), so a header named my.house.h maps to my.house.c, not my.couse.h. - The header-only option is a BooleanOption instead of a string EnumOption, giving the usual --[no-]header-only CLI flags. - The five duplicated finishHeaderFile/finishSourceFile blocks are factored into finishCurrentFile, and a comment typo is fixed. Co-Authored-By: Claude Fable 5 --- .../src/language/CJSON/CJSONRenderer.ts | 126 ++++++++---------- .../src/language/CJSON/language.ts | 9 +- 2 files changed, 57 insertions(+), 78 deletions(-) diff --git a/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts b/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts index 3216bfc04b..ec5559a92a 100644 --- a/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts +++ b/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts @@ -57,9 +57,13 @@ import { } from "./utils"; export class CJSONRenderer extends ConvenienceRenderer { - private currentHeaderFilename: string | undefined; /* Current header filename */ + private currentHeaderFilename: + | string + | undefined; /* Current header filename */ - private currentSourceFilename: string | undefined; /* Current source filename */ + private currentSourceFilename: + | string + | undefined; /* Current source filename */ private readonly memberNameStyle: NameStyle; /* Member name style */ @@ -343,13 +347,7 @@ export class CJSONRenderer extends ConvenienceRenderer { (type) => this.namedTypeToNameForTopLevel(type) === undefined, ); - if (this._options.headerOnly) { - /* Close header file */ - this.finishHeaderFile(); - } else { - /* Close source file */ - this.finishSourceFile(); - } + this.finishCurrentFile(); } /** @@ -387,8 +385,6 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create file */ const enumName = this.nameForNamedType(enumType); const headerFilename = this.sourcelikeToString(enumName).concat(".h"); - const sourceFilename = - this.getSourceNameFromHeaderName(headerFilename); this.includes.push(headerFilename); this.startHeaderFile(headerFilename); @@ -406,22 +402,13 @@ export class CJSONRenderer extends ConvenienceRenderer { this.finishHeaderFile(); /* Create source file */ - this.startSourceFile(sourceFilename); + this.startSourceFile(headerFilename); } - /* Include corresponding header file */ - this.emitIncludeLine(headerFilename, true); - /* Create functions */ this.emitEnumFunctions(enumType); - if (this._options.headerOnly) { - /* Close header file */ - this.finishHeaderFile(); - } else { - /* Close source file */ - this.finishSourceFile(); - } + this.finishCurrentFile(); } /** @@ -582,8 +569,6 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create file */ const unionName = this.nameForNamedType(unionType); const headerFilename = this.sourcelikeToString(unionName).concat(".h"); - const sourceFilename = - this.getSourceNameFromHeaderName(headerFilename); this.includes.push(headerFilename); this.startHeaderFile(headerFilename); @@ -601,22 +586,13 @@ export class CJSONRenderer extends ConvenienceRenderer { this.finishHeaderFile(); /* Create source file */ - this.startSourceFile(sourceFilename); + this.startSourceFile(headerFilename); } - /* Include corresponding header file */ - this.emitIncludeLine(headerFilename, true); - /* Create functions */ this.emitUnionFunctions(unionType); - if (this._options.headerOnly) { - /* Close header file */ - this.finishHeaderFile(); - } else { - /* Close source file */ - this.finishSourceFile(); - } + this.finishCurrentFile(); } /** @@ -2049,8 +2025,6 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create file */ const className = this.nameForNamedType(classType); const headerFilename = this.sourcelikeToString(className).concat(".h"); - const sourceFilename = - this.getSourceNameFromHeaderName(headerFilename); this.includes.push(headerFilename); this.startHeaderFile(headerFilename); @@ -2068,22 +2042,13 @@ export class CJSONRenderer extends ConvenienceRenderer { this.finishHeaderFile(); /* Create source file */ - this.startSourceFile(sourceFilename); + this.startSourceFile(headerFilename); } - /* Include corresponding header file */ - this.emitIncludeLine(headerFilename, true); - /* Create functions */ this.emitClassFunctions(classType); - if (this._options.headerOnly) { - /* Close header file */ - this.finishHeaderFile(); - } else { - /* Close source file */ - this.finishSourceFile(); - } + this.finishCurrentFile(); } /** @@ -4686,7 +4651,6 @@ export class CJSONRenderer extends ConvenienceRenderer { ): void { /* Create file */ const headerFilename = this.sourcelikeToString(className).concat(".h"); - const sourceFilename = this.getSourceNameFromHeaderName(headerFilename); this.startHeaderFile(headerFilename); /* Create includes - This create too much includes but this is safer because of specific corner cases */ @@ -4706,22 +4670,13 @@ export class CJSONRenderer extends ConvenienceRenderer { this.finishHeaderFile(); /* Create source file */ - this.startSourceFile(sourceFilename); + this.startSourceFile(headerFilename); } - /* Include corresponding header file */ - this.emitIncludeLine(headerFilename, true); - /* Create functions */ this.emitTopLevelFunctions(type, className); - if (this._options.headerOnly) { - /* Close header file */ - this.finishHeaderFile(); - } else { - /* Close source file */ - this.finishSourceFile(); - } + this.finishCurrentFile(); } /** @@ -5649,7 +5604,8 @@ export class CJSONRenderer extends ConvenienceRenderer { "Previous header file wasn't finished", ); if (proposedFilename !== undefined) { - this.currentHeaderFilename = this.sourcelikeToString(proposedFilename); + this.currentHeaderFilename = + this.sourcelikeToString(proposedFilename); } /* Check if header file has been created */ @@ -5722,30 +5678,37 @@ export class CJSONRenderer extends ConvenienceRenderer { /** * Function called to create a source file - * @param proposedFilename: source filename provided from stdin + * @param headerFilename: filename of the header file corresponding to this source file */ - protected startSourceFile(proposedFilename: Sourcelike): void { + protected startSourceFile(headerFilename: Sourcelike): void { /* Check if previous source file is closed, create a new file */ - assert(this.currentSourceFilename === undefined, "Previous source file wasn't finished"); - if (proposedFilename !== undefined) { - this.currentSourceFilename = this.getSourceNameFromHeaderName(this.sourcelikeToString(proposedFilename)); + assert( + this.currentSourceFilename === undefined, + "Previous source file wasn't finished", + ); + if (headerFilename !== undefined) { + this.currentSourceFilename = this.getSourceNameFromHeaderName( + this.sourcelikeToString(headerFilename), + ); } - /* Check if source file and corresponding header file has been created */ + /* Check if source file has been created */ if (this.currentSourceFilename !== undefined) { /* Write header */ this.emitDescriptionBlock([ this.currentSourceFilename, - "This file has been autogenerated using quicktype https://github.com/quicktype/quicktype - DO NOT EDIT" + "This file has been autogenerated using quicktype https://github.com/quicktype/quicktype - DO NOT EDIT", ]); this.ensureBlankLine(); - this.emitIncludeLine(this.sourcelikeToString(proposedFilename), true); + + /* Include corresponding header file */ + this.emitIncludeLine(this.sourcelikeToString(headerFilename)); this.ensureBlankLine(); } } /** - * Function called to close current file + * Function called to close current header file */ protected finishHeaderFile(): void { /* Check if header file has been created */ @@ -5769,7 +5732,7 @@ export class CJSONRenderer extends ConvenienceRenderer { ); this.ensureBlankLine(); - /* Close headeerfile */ + /* Close header file */ super.finishFile(defined(this.currentHeaderFilename)); this.currentHeaderFilename = undefined; } @@ -5789,6 +5752,20 @@ export class CJSONRenderer extends ConvenienceRenderer { } } + /** + * Function called to close the current file, either the header file when + * generating headers only, or the source file otherwise + */ + protected finishCurrentFile(): void { + if (this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + } else { + /* Close source file */ + this.finishSourceFile(); + } + } + /** * Check if type need declaration before use * @note If returning true, canBeForwardDeclared must be declared @@ -6056,7 +6033,12 @@ export class CJSONRenderer extends ConvenienceRenderer { return result; } + /** + * Get the name of the source file corresponding to a header file + * @param headerName: header filename + * @return Source filename + */ protected getSourceNameFromHeaderName(headerName: string): string { - return headerName.replace(".h", ".c"); + return headerName.replace(/\.h$/, ".c"); } } diff --git a/packages/quicktype-core/src/language/CJSON/language.ts b/packages/quicktype-core/src/language/CJSON/language.ts index a5355b69cb..a7bfde1bc4 100644 --- a/packages/quicktype-core/src/language/CJSON/language.ts +++ b/packages/quicktype-core/src/language/CJSON/language.ts @@ -23,6 +23,7 @@ import type { RenderContext } from "../../Renderer"; import { + BooleanOption, EnumOption, StringOption, getOptionValues, @@ -110,14 +111,10 @@ export const cJSONOptions = { namingStyles, "upper-underscore-case", ), - headerOnly: new EnumOption( + headerOnly: new BooleanOption( "header-only", "Generate headers only", - { - true: true, - false: false, - } as const, - "true", + true, "secondary", ), }; From 2d1c3dddf61741393f79dcec795532763ef75ba3 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 18:01:34 -0400 Subject: [PATCH 4/5] test(cjson): overhaul fixtures for the source/header split modes - The vendored dependencies (cJSON, hashtable, list) are downloaded into a deps/ subdirectory and included with -isystem instead of -I.. Angle-bracket includes now resolve only in deps/, while quoted includes resolve relative to the including file, so a generated #include fails to compile and the include-style convention is enforced by the build. - second.c is a second translation unit including TopLevel.h. It is compiled and linked in the split-mode compile commands, verifying that split output supports multi-TU builds (issue #2617). - Three minimal mode fixtures run one complex input (nbl-stats.json, which produces enums, unions and 22 type pairs) through the remaining option combinations: - cjson-default: single-source, header-only (the pre-existing default output mode) - cjson-multi-header: multi-source, header-only (single TU; header-only output cannot link from multiple TUs) - cjson-multi-split: multi-source with header/source pairs, compiled with *.c so every generated source file is linked - The cjson comment-injection target's output is TopLevel.h; with header-only=false a proposed filename of TopLevel.c would have named both generated files TopLevel.c. Co-Authored-By: Claude Fable 5 --- .github/workflows/test-pr.yaml | 1 + .gitignore | 4 +- test/fixtures.ts | 7 ++- test/fixtures/cjson/second.c | 8 +++ test/languages.ts | 100 +++++++++++++++++++++++++++++++-- 5 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 test/fixtures/cjson/second.c diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index 6f67d78118..07af9b9bd3 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -24,6 +24,7 @@ jobs: - javascript,schema-javascript - golang,schema-golang - cjson,schema-cjson + - cjson-default,cjson-multi-header,cjson-multi-split - cplusplus,schema-cplusplus - flow,schema-flow - java,schema-java diff --git a/.gitignore b/.gitignore index 7178f4122b..747cce08bd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,7 @@ test/golang/schema-from-schema.json test/elm/elm-stuff/ test/elm/elm.js test/elm/QuickType.elm -test/fixtures/cjson/cJSON.* -test/fixtures/cjson/hashtable.* -test/fixtures/cjson/list.* +test/fixtures/cjson/deps/ test/fixtures/rust/target test/fixtures/java/target test/fixtures/java-lombok/target diff --git a/test/fixtures.ts b/test/fixtures.ts index 0472762b33..adea033712 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -947,7 +947,9 @@ const commentInjectionTreeSitterTargets: TreeSitterTarget[] = [ { displayName: "cjson", language: languages.CJSONLanguage, - output: "TopLevel.c", + // CJSONLanguage renders with header-only=false, so this produces + // both TopLevel.h and TopLevel.c; both are collected and parsed. + output: "TopLevel.h", wasmModule: "tree-sitter-c/tree-sitter-c.wasm", extensions: [".c", ".h"], schema: commentInjectionSchema, @@ -1550,6 +1552,9 @@ export const allFixtures: Fixture[] = [ new JSONFixture(languages.JavaLanguageWithLombok, "java-lombok"), new JSONFixture(languages.GoLanguage), new JSONFixture(languages.CJSONLanguage), + new JSONFixture(languages.CJSONDefaultLanguage, "cjson-default"), + new JSONFixture(languages.CJSONMultiHeaderLanguage, "cjson-multi-header"), + new JSONFixture(languages.CJSONMultiSplitLanguage, "cjson-multi-split"), new JSONFixture(languages.CPlusPlusLanguage), new JSONFixture(languages.PHPLanguage), new JSONFixture(languages.RustLanguage), diff --git a/test/fixtures/cjson/second.c b/test/fixtures/cjson/second.c new file mode 100644 index 0000000000..69df0916a6 --- /dev/null +++ b/test/fixtures/cjson/second.c @@ -0,0 +1,8 @@ +/* Second translation unit: including the generated header from more than + * one .c file must compile and link (no duplicate symbol definitions). */ + +#include "TopLevel.h" + +int quicktypeSecondTranslationUnit(void) { + return 0; +} diff --git a/test/languages.ts b/test/languages.ts index 07d52c3a1e..365d120912 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -490,16 +490,26 @@ export const GoLanguage: Language = { sourceFiles: ["src/language/Golang/index.ts"], }; +/* The vendored dependencies are downloaded into deps/ and included via + * -isystem so that generated cross-file includes must be quoted includes + * (resolved relative to the including file): a generated + * `#include ` fails to compile. */ +const cJSONSetupCommand = + "mkdir -p deps && curl -o deps/cJSON.c https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.15/cJSON.c && curl -o deps/cJSON.h https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.15/cJSON.h && curl -o deps/list.h https://raw.githubusercontent.com/joelguittet/c-list/master/include/list.h && curl -o deps/list.c https://raw.githubusercontent.com/joelguittet/c-list/master/src/list.c && curl -o deps/hashtable.h https://raw.githubusercontent.com/joelguittet/c-hashtable/master/include/hashtable.h && curl -o deps/hashtable.c https://raw.githubusercontent.com/joelguittet/c-hashtable/master/src/hashtable.c"; + +function cJSONRunCommand(sample: string): string { + return `valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --error-exitcode=1 ./quicktype "${sample}"`; +} + export const CJSONLanguage: Language = { name: "cjson", base: "test/fixtures/cjson", - setupCommand: - "curl -o cJSON.c https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.15/cJSON.c && curl -o cJSON.h https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.15/cJSON.h && curl -o list.h https://raw.githubusercontent.com/joelguittet/c-list/master/include/list.h && curl -o list.c https://raw.githubusercontent.com/joelguittet/c-list/master/src/list.c && curl -o hashtable.h https://raw.githubusercontent.com/joelguittet/c-hashtable/master/include/hashtable.h && curl -o hashtable.c https://raw.githubusercontent.com/joelguittet/c-hashtable/master/src/hashtable.c", + setupCommand: cJSONSetupCommand, + /* second.c is a second translation unit including TopLevel.h; it verifies + * that the generated header/source split supports multi-TU builds. */ compileCommand: - "gcc -O0 -o quicktype -I. cJSON.c hashtable.c list.c main.c TopLevel.c -lpthread", - runCommand(sample: string) { - return `valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --error-exitcode=1 ./quicktype "${sample}"`; - }, + "gcc -O0 -o quicktype -isystem deps deps/cJSON.c deps/hashtable.c deps/list.c main.c second.c TopLevel.c -lpthread", + runCommand: cJSONRunCommand, diffViaSchema: true, skipDiffViaSchema: [ /* Enum constants are different when generating with schema */ @@ -579,6 +589,84 @@ export const CJSONLanguage: Language = { sourceFiles: ["src/language/CJSON/index.ts"], }; +/* Minimal fixtures covering the remaining source-style / header-only mode + * combinations on a single complex input (enums, unions, many classes). + * They share the cjson driver directory and setup. */ + +/* Default options: single-source, header-only. Single translation unit, + * no generated TopLevel.c — the pre-existing output mode. */ +export const CJSONDefaultLanguage: Language = { + name: "cjson", + base: "test/fixtures/cjson", + setupCommand: cJSONSetupCommand, + compileCommand: + "gcc -O0 -o quicktype -isystem deps deps/cJSON.c deps/hashtable.c deps/list.c main.c -lpthread", + runCommand: cJSONRunCommand, + diffViaSchema: false, + skipDiffViaSchema: [], + allowMissingNull: false, + features: [], + output: "TopLevel.h", + topLevel: "TopLevel", + includeJSON: ["nbl-stats.json"], + skipMiscJSON: true, + skipSchema: [], + rendererOptions: {}, + quickTestRendererOptions: [], + sourceFiles: ["src/language/CJSON/index.ts"], +}; + +/* Multi-source, header-only. One header per type; still a single + * translation unit, since header-only output defines functions in the + * headers and cannot link from multiple translation units. */ +export const CJSONMultiHeaderLanguage: Language = { + name: "cjson", + base: "test/fixtures/cjson", + setupCommand: cJSONSetupCommand, + compileCommand: + "gcc -O0 -o quicktype -isystem deps deps/cJSON.c deps/hashtable.c deps/list.c main.c -lpthread", + runCommand: cJSONRunCommand, + diffViaSchema: false, + skipDiffViaSchema: [], + allowMissingNull: false, + features: [], + output: "TopLevel.h", + topLevel: "TopLevel", + includeJSON: ["nbl-stats.json"], + skipMiscJSON: true, + skipSchema: [], + rendererOptions: { "source-style": "multi-source" }, + quickTestRendererOptions: [], + sourceFiles: ["src/language/CJSON/index.ts"], +}; + +/* Multi-source, split header/source pairs. The wildcard picks up main.c, + * second.c and every generated .c file; linking the two translation units + * verifies the core promise of the split mode (issue #2617). */ +export const CJSONMultiSplitLanguage: Language = { + name: "cjson", + base: "test/fixtures/cjson", + setupCommand: cJSONSetupCommand, + compileCommand: + "gcc -O0 -o quicktype -isystem deps deps/cJSON.c deps/hashtable.c deps/list.c *.c -lpthread", + runCommand: cJSONRunCommand, + diffViaSchema: false, + skipDiffViaSchema: [], + allowMissingNull: false, + features: [], + output: "TopLevel.h", + topLevel: "TopLevel", + includeJSON: ["nbl-stats.json"], + skipMiscJSON: true, + skipSchema: [], + rendererOptions: { + "source-style": "multi-source", + "header-only": "false", + }, + quickTestRendererOptions: [], + sourceFiles: ["src/language/CJSON/index.ts"], +}; + export const CPlusPlusLanguage: Language = { name: "cplusplus", base: "test/fixtures/cplusplus", From 9726a94e7c2d9af686dde478c347517063e5fa8a Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 18:05:39 -0400 Subject: [PATCH 5/5] test(cjson): unit-test include structure of split output Renders a small input through the quicktype API and asserts, for the multi-source modes, that every header gets a source file, that no generated file includes itself, that generated files are referenced with quoted includes (never angle brackets), that each source file includes its own header first, and that header-only mode emits no source files. Also pins the output filenames for a proposed filename with an inner ".h" (my.house.h -> my.house.c). Co-Authored-By: Claude Fable 5 --- test/unit/cjson-split-sources.test.ts | 124 ++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 test/unit/cjson-split-sources.test.ts diff --git a/test/unit/cjson-split-sources.test.ts b/test/unit/cjson-split-sources.test.ts new file mode 100644 index 0000000000..b640c41e23 --- /dev/null +++ b/test/unit/cjson-split-sources.test.ts @@ -0,0 +1,124 @@ +// cJSON can split its output into header/source pairs (header-only=false) +// and can emit one file per type (source-style=multi-source). The first +// version of the split emitted a `#include ` self-include at +// the top of every generated source file (an unguarded self-include that +// recurses at compile time), referenced generated headers with angle +// brackets instead of the quoted-include convention, and made every header +// include itself in the pre-existing multi-source header-only mode. These +// tests pin down the include structure of the generated files. +import { describe, expect, test } from "vitest"; + +import { + InputData, + type RendererOptions, + jsonInputForTargetLanguage, + quicktypeMultiFile, +} from "quicktype-core"; + +async function cJSONFiles( + rendererOptions: RendererOptions, + outputFilename = "TopLevel.h", +): Promise> { + const jsonInput = jsonInputForTargetLanguage("cjson"); + await jsonInput.addSource({ + name: "TopLevel", + samples: [ + '{"child": {"n": 1}, "color": "red", "value": 1}', + '{"child": {"n": 2}, "color": "green", "value": "s"}', + ], + }); + const inputData = new InputData(); + inputData.addInput(jsonInput); + const result = await quicktypeMultiFile({ + inputData, + lang: "cjson", + outputFilename, + rendererOptions, + }); + return new Map( + Array.from(result, ([filename, serialized]) => [ + filename, + serialized.lines.join("\n"), + ]), + ); +} + +function includesIn(source: string): string[] { + return source.match(/#include [<"][^>"]+[>"]/g) ?? []; +} + +describe("cJSON multi-source header/source pairs", () => { + const rendererOptions: RendererOptions = { + "source-style": "multi-source", + "header-only": false, + }; + + test("every header gets a source file", async () => { + const files = await cJSONFiles(rendererOptions); + const names = Array.from(files.keys()); + const headers = names.filter((name) => name.endsWith(".h")); + expect(headers.length).toBeGreaterThan(2); + for (const header of headers) { + expect(names).toContain(header.replace(/\.h$/, ".c")); + } + }); + + test("no generated file includes itself", async () => { + const files = await cJSONFiles(rendererOptions); + for (const [filename, source] of files) { + expect(includesIn(source)).not.toContain(`#include "${filename}"`); + expect(includesIn(source)).not.toContain(`#include <${filename}>`); + } + }); + + test("generated files are included with quotes, not angle brackets", async () => { + const files = await cJSONFiles(rendererOptions); + for (const [, source] of files) { + for (const include of includesIn(source)) { + const match = /#include <([^>]+)>/.exec(include); + if (match === null) { + continue; + } + + // Angle brackets are reserved for system and vendored + // headers; a generated file must never appear in them. + expect(files.has(match[1])).toBe(false); + } + } + }); + + test("each source file includes its own header first", async () => { + const files = await cJSONFiles(rendererOptions); + for (const [filename, source] of files) { + if (!filename.endsWith(".c")) { + continue; + } + + const header = filename.replace(/\.c$/, ".h"); + expect(includesIn(source)[0]).toBe(`#include "${header}"`); + } + }); +}); + +describe("cJSON multi-source header-only mode", () => { + test("emits no source files and no header includes itself", async () => { + const files = await cJSONFiles({ "source-style": "multi-source" }); + expect(files.size).toBeGreaterThan(2); + for (const [filename, source] of files) { + expect(filename).toMatch(/\.h$/); + expect(includesIn(source)).not.toContain(`#include "${filename}"`); + expect(includesIn(source)).not.toContain(`#include <${filename}>`); + } + }); +}); + +describe("cJSON source filename derivation", () => { + test("only a trailing .h is swapped for .c", async () => { + // `.replace(".h", ".c")` would have produced "my.couse.h". + const files = await cJSONFiles({ "header-only": false }, "my.house.h"); + expect(Array.from(files.keys()).sort()).toEqual([ + "my.house.c", + "my.house.h", + ]); + }); +});