Skip to content

[clang-doc] refactor JSON for better Mustache compatibility #149588

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 23, 2025
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
2 changes: 2 additions & 0 deletions clang-tools-extra/clang-doc/BitcodeReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@ static llvm::Error parseRecord(const Record &R, unsigned ID,
return decodeRecord(R, I->Path, Blob);
case REFERENCE_FIELD:
return decodeRecord(R, F, Blob);
case REFERENCE_FILE:
return decodeRecord(R, I->DocumentationFileName, Blob);
default:
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"invalid field for Reference");
Expand Down
4 changes: 3 additions & 1 deletion clang-tools-extra/clang-doc/BitcodeWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ static const llvm::IndexedMap<RecordIdDsc, RecordIdToIndexFunctor>
{REFERENCE_TYPE, {"RefType", &genIntAbbrev}},
{REFERENCE_PATH, {"Path", &genStringAbbrev}},
{REFERENCE_FIELD, {"Field", &genIntAbbrev}},
{REFERENCE_FILE, {"File", &genStringAbbrev}},
{TEMPLATE_PARAM_CONTENTS, {"Contents", &genStringAbbrev}},
{TEMPLATE_SPECIALIZATION_OF,
{"SpecializationOf", &genSymbolIdAbbrev}},
Expand Down Expand Up @@ -286,7 +287,7 @@ static const std::vector<std::pair<BlockId, std::vector<RecordId>>>
// Reference Block
{BI_REFERENCE_BLOCK_ID,
{REFERENCE_USR, REFERENCE_NAME, REFERENCE_QUAL_NAME, REFERENCE_TYPE,
REFERENCE_PATH, REFERENCE_FIELD}},
REFERENCE_PATH, REFERENCE_FIELD, REFERENCE_FILE}},
// Template Blocks.
{BI_TEMPLATE_BLOCK_ID, {}},
{BI_TEMPLATE_PARAM_BLOCK_ID, {TEMPLATE_PARAM_CONTENTS}},
Expand Down Expand Up @@ -479,6 +480,7 @@ void ClangDocBitcodeWriter::emitBlock(const Reference &R, FieldId Field) {
emitRecord((unsigned)R.RefType, REFERENCE_TYPE);
emitRecord(R.Path, REFERENCE_PATH);
emitRecord((unsigned)Field, REFERENCE_FIELD);
emitRecord(R.DocumentationFileName, REFERENCE_FILE);
}

void ClangDocBitcodeWriter::emitBlock(const FriendInfo &R) {
Expand Down
1 change: 1 addition & 0 deletions clang-tools-extra/clang-doc/BitcodeWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ enum RecordId {
REFERENCE_TYPE,
REFERENCE_PATH,
REFERENCE_FIELD,
REFERENCE_FILE,
TEMPLATE_PARAM_CONTENTS,
TEMPLATE_SPECIALIZATION_OF,
TYPEDEF_USR,
Expand Down
60 changes: 50 additions & 10 deletions clang-tools-extra/clang-doc/JSONGenerator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,30 @@ static auto SerializeReferenceLambda = [](const auto &Ref, Object &Object) {
serializeReference(Ref, Object);
};

static std::string infoTypeToString(InfoType IT) {
switch (IT) {
case InfoType::IT_default:
return "default";
case InfoType::IT_namespace:
return "namespace";
case InfoType::IT_record:
return "record";
case InfoType::IT_function:
return "function";
case InfoType::IT_enum:
return "enum";
case InfoType::IT_typedef:
return "typedef";
case InfoType::IT_concept:
return "concept";
case InfoType::IT_variable:
return "variable";
case InfoType::IT_friend:
return "friend";
}
llvm_unreachable("Unknown InfoType encountered.");
}

static json::Object
serializeLocation(const Location &Loc,
const std::optional<StringRef> RepositoryUrl) {
Expand Down Expand Up @@ -172,6 +196,9 @@ serializeCommonAttributes(const Info &I, json::Object &Obj,
const std::optional<StringRef> RepositoryUrl) {
Obj["Name"] = I.Name;
Obj["USR"] = toHex(toStringRef(I.USR));
Obj["InfoType"] = infoTypeToString(I.IT);
if (!I.DocumentationFileName.empty())
Obj["DocumentationFileName"] = I.DocumentationFileName;

if (!I.Path.empty())
Obj["Path"] = I.Path;
Expand Down Expand Up @@ -205,6 +232,8 @@ static void serializeReference(const Reference &Ref, Object &ReferenceObj) {
ReferenceObj["Name"] = Ref.Name;
ReferenceObj["QualName"] = Ref.QualName;
ReferenceObj["USR"] = toHex(toStringRef(Ref.USR));
if (!Ref.DocumentationFileName.empty())
ReferenceObj["DocumentationFileName"] = Ref.DocumentationFileName;
}

// Although namespaces and records both have ScopeChildren, they serialize them
Expand All @@ -217,14 +246,18 @@ serializeCommonChildren(const ScopeChildren &Children, json::Object &Obj,
serializeInfo(Info, Object, RepositoryUrl);
};

if (!Children.Enums.empty())
if (!Children.Enums.empty()) {
serializeArray(Children.Enums, Obj, "Enums", SerializeInfo);
Obj["HasEnums"] = true;
}

if (!Children.Typedefs.empty())
serializeArray(Children.Typedefs, Obj, "Typedefs", SerializeInfo);

if (!Children.Records.empty())
if (!Children.Records.empty()) {
serializeArray(Children.Records, Obj, "Records", SerializeReferenceLambda);
Obj["HasRecords"] = true;
}
}

template <typename Container, typename SerializationFunc>
Expand All @@ -234,10 +267,12 @@ static void serializeArray(const Container &Records, Object &Obj,
json::Value RecordsArray = Array();
auto &RecordsArrayRef = *RecordsArray.getAsArray();
RecordsArrayRef.reserve(Records.size());
for (const auto &Item : Records) {
for (size_t Index = 0; Index < Records.size(); ++Index) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think enumerate() will make this nicer, but its available if you do. It's not the most widely used pattern, but I'd hate for you not to know its around (like I did for a couple years).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I saw that Peter had used it previously for the same function but I really didn't see the benefit over this if I couldn't use a range-based loop naturally. I think the old enumeration would actually serialize "End" = false everywhere else.

json::Value ItemVal = Object();
auto &ItemObj = *ItemVal.getAsObject();
SerializeInfo(Item, ItemObj);
SerializeInfo(Records[Index], ItemObj);
if (Index == Records.size() - 1)
ItemObj["End"] = true;
RecordsArrayRef.push_back(ItemVal);
}
Obj[Key] = RecordsArray;
Expand Down Expand Up @@ -380,6 +415,11 @@ static void serializeInfo(const FriendInfo &I, Object &Obj) {
}
}

static void insertArray(Object &Obj, json::Value &Array, StringRef Key) {
Obj[Key] = Array;
Obj["Has" + Key.str()] = true;
}

static void serializeInfo(const RecordInfo &I, json::Object &Obj,
const std::optional<StringRef> &RepositoryUrl) {
serializeCommonAttributes(I, Obj, RepositoryUrl);
Expand All @@ -406,7 +446,7 @@ static void serializeInfo(const RecordInfo &I, json::Object &Obj,
}

if (!PubFunctionsArrayRef.empty())
Obj["PublicFunctions"] = PubFunctionsArray;
insertArray(Obj, PubFunctionsArray, "PublicFunctions");
if (!ProtFunctionsArrayRef.empty())
Obj["ProtectedFunctions"] = ProtFunctionsArray;
}
Expand All @@ -430,7 +470,7 @@ static void serializeInfo(const RecordInfo &I, json::Object &Obj,
}

if (!PubMembersArrayRef.empty())
Obj["PublicMembers"] = PublicMembersArray;
insertArray(Obj, PublicMembersArray, "PublicMembers");
if (!ProtMembersArrayRef.empty())
Obj["ProtectedMembers"] = ProtectedMembersArray;
}
Expand Down Expand Up @@ -496,10 +536,7 @@ static SmallString<16> determineFileName(Info *I, SmallString<128> &Path) {
SmallString<16> FileName;
if (I->IT == InfoType::IT_record) {
auto *RecordSymbolInfo = static_cast<SymbolInfo *>(I);
if (RecordSymbolInfo->MangledName.size() < 255)
FileName = RecordSymbolInfo->MangledName;
else
FileName = toStringRef(toHex(RecordSymbolInfo->USR));
FileName = RecordSymbolInfo->MangledName;
} else if (I->IT == InfoType::IT_namespace && I->Name != "")
// Serialize the global namespace as index.json
FileName = I->Name;
Expand Down Expand Up @@ -527,7 +564,10 @@ Error JSONGenerator::generateDocs(
}

SmallString<16> FileName = determineFileName(Info, Path);
if (FileToInfos.contains(Path))
continue;
FileToInfos[Path].push_back(Info);
Info->DocumentationFileName = FileName;
}

for (const auto &Group : FileToInfos) {
Expand Down
2 changes: 2 additions & 0 deletions clang-tools-extra/clang-doc/Representation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ void Reference::merge(Reference &&Other) {
Name = Other.Name;
if (Path.empty())
Path = Other.Path;
if (DocumentationFileName.empty())
DocumentationFileName = Other.DocumentationFileName;
}

bool FriendInfo::mergeable(const FriendInfo &Other) {
Expand Down
10 changes: 10 additions & 0 deletions clang-tools-extra/clang-doc/Representation.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ struct Reference {
Reference(SymbolID USR, StringRef Name, InfoType IT, StringRef QualName,
StringRef Path = StringRef())
: USR(USR), Name(Name), QualName(QualName), RefType(IT), Path(Path) {}
Reference(SymbolID USR, StringRef Name, InfoType IT, StringRef QualName,
StringRef Path, SmallString<16> DocumentationFileName)
: USR(USR), Name(Name), QualName(QualName), RefType(IT), Path(Path),
DocumentationFileName(DocumentationFileName) {}

bool operator==(const Reference &Other) const {
return std::tie(USR, Name, QualName, RefType) ==
Expand Down Expand Up @@ -155,6 +159,7 @@ struct Reference {
// Path of directory where the clang-doc generated file will be saved
// (possibly unresolved)
llvm::SmallString<128> Path;
SmallString<16> DocumentationFileName;
};

// Holds the children of a record or namespace.
Expand Down Expand Up @@ -331,6 +336,11 @@ struct Info {
llvm::SmallString<128> Path; // Path of directory where the clang-doc
// generated file will be saved

// The name used for the file that this info is documented in.
// In the JSON generator, infos are documented in files with mangled names.
// Thus, we keep track of the physical filename for linking purposes.
SmallString<16> DocumentationFileName;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 16? I'm guessing no mangled name will be only 16 chars (unless its C and not C++). For now this is fine, but we should try and revisit our string usage and try to make it consistent. Some day, I'd like to start interning strings as well, and then just dropping them w/ an arena, ala UniqueStringSaver.


void mergeBase(Info &&I);
bool mergeable(const Info &Other);

Expand Down
11 changes: 9 additions & 2 deletions clang-tools-extra/clang-doc/Serialize.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,8 @@ static void InsertChild(ScopeChildren &Scope, const NamespaceInfo &Info) {

static void InsertChild(ScopeChildren &Scope, const RecordInfo &Info) {
Scope.Records.emplace_back(Info.USR, Info.Name, InfoType::IT_record,
Info.Name, getInfoRelativePath(Info.Namespace));
Info.Name, getInfoRelativePath(Info.Namespace),
Info.MangledName);
}

static void InsertChild(ScopeChildren &Scope, EnumInfo Info) {
Expand Down Expand Up @@ -777,7 +778,13 @@ static void populateSymbolInfo(SymbolInfo &I, const T *D, const FullComment *C,
Mangler->mangleCXXVTable(CXXD, MangledStream);
else
MangledStream << D->getNameAsString();
I.MangledName = MangledName;
if (MangledName.size() > 255)
// File creation fails if the mangled name is too long, so default to the
// USR. We should look for a better check since filesystems differ in
// maximum filename length
I.MangledName = llvm::toStringRef(llvm::toHex(I.USR));
else
I.MangledName = MangledName;
delete Mangler;
}

Expand Down
1 change: 1 addition & 0 deletions clang-tools-extra/test/clang-doc/json/class-requires.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct MyClass;
// CHECK-NEXT: "Template": {
// CHECK-NEXT: "Constraints": [
// CHECK-NEXT: {
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "Expression": "Addable<T>",
// CHECK-NEXT: "Name": "Addable",
// CHECK-NEXT: "Path": "",
Expand Down
1 change: 1 addition & 0 deletions clang-tools-extra/test/clang-doc/json/class-template.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ template<typename T> struct MyClass {
// CHECK: "Name": "method",
// CHECK: "Params": [
// CHECK-NEXT: {
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "Name": "Param",
// CHECK-NEXT: "Type": "T"
// CHECK-NEXT: }
Expand Down
18 changes: 18 additions & 0 deletions clang-tools-extra/test/clang-doc/json/class.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ struct MyClass {
// CHECK-NEXT: "TextComment": " This is a brief description."
// CHECK-NEXT: }
// CHECK: "Command": "brief"
// CHECK: "DocumentationFileName": "_ZTV7MyClass",
// CHECK: "Enums": [
// CHECK-NEXT: {
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "InfoType": "enum",
// CHECK-NEXT: "Location": {
// CHECK-NEXT: "Filename": "{{.*}}class.cpp",
// CHECK-NEXT: "LineNumber": 17
Expand All @@ -76,6 +79,7 @@ struct MyClass {
// CHECK-NEXT: "Value": "1"
// CHECK-NEXT: },
// CHECK-NEXT: {
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "Name": "BLUE",
// CHECK-NEXT: "ValueExpr": "5"
// CHECK-NEXT: }
Expand All @@ -94,6 +98,7 @@ struct MyClass {
// CHECK-NEXT: "IsClass": false,
// CHECK-NEXT: "Params": [
// CHECK-NEXT: {
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "Name": "",
// CHECK-NEXT: "Type": "int"
// CHECK-NEXT: }
Expand All @@ -118,6 +123,7 @@ struct MyClass {
// CHECK-NEXT: }
// CHECK-NEXT: },
// CHECK-NEXT: {
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "IsClass": true,
// CHECK-NEXT: "Reference": {
// CHECK-NEXT: "Name": "Foo",
Expand All @@ -129,6 +135,11 @@ struct MyClass {
// CHECK-NEXT: ],
// COM: FIXME: FullName is not emitted correctly.
// CHECK-NEXT: "FullName": "",
// CHECK-NEXT: "HasEnums": true,
// CHECK-NEXT: "HasPublicFunctions": true,
// CHECK-NEXT: "HasPublicMembers": true,
// CHECK-NEXT: "HasRecords": true,
// CHECK-NEXT: "InfoType": "record",
// CHECK-NEXT: "IsTypedef": false,
// CHECK-NEXT: "Location": {
// CHECK-NEXT: "Filename": "{{.*}}class.cpp",
Expand All @@ -142,6 +153,7 @@ struct MyClass {
// CHECK-NEXT: "Path": "GlobalNamespace",
// CHECK-NEXT: "ProtectedFunctions": [
// CHECK-NEXT: {
// CHECK-NEXT: "InfoType": "function",
// CHECK-NEXT: "IsStatic": false,
// CHECK-NEXT: "Name": "protectedMethod",
// CHECK-NEXT: "Namespace": [
Expand All @@ -166,6 +178,7 @@ struct MyClass {
// CHECK-NEXT: ],
// CHECK-NEXT: "PublicFunctions": [
// CHECK-NEXT: {
// CHECK-NEXT: "InfoType": "function",
// CHECK-NEXT: "IsStatic": false,
// CHECK-NEXT: "Name": "myMethod",
// CHECK-NEXT: "Namespace": [
Expand All @@ -174,6 +187,7 @@ struct MyClass {
// CHECK-NEXT: ],
// CHECK-NEXT: "Params": [
// CHECK-NEXT: {
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "Name": "MyParam",
// CHECK-NEXT: "Type": "int"
// CHECK-NEXT: }
Expand Down Expand Up @@ -204,6 +218,8 @@ struct MyClass {
// CHECK-NEXT: ],
// CHECK-NEXT: "Records": [
// CHECK-NEXT: {
// CHECK-NEXT: "DocumentationFileName": "_ZTVN7MyClass11NestedClassE",
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "Name": "NestedClass",
// CHECK-NEXT: "Path": "GlobalNamespace{{[\/]+}}MyClass",
// CHECK-NEXT: "QualName": "NestedClass",
Expand All @@ -213,6 +229,8 @@ struct MyClass {
// CHECK-NEXT: "TagType": "struct",
// CHECK-NEXT: "Typedefs": [
// CHECK-NEXT: {
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "InfoType": "typedef",
// CHECK-NEXT: "IsUsing": false,
// CHECK-NEXT: "Location": {
// CHECK-NEXT: "Filename": "{{.*}}class.cpp",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ template<typename T> requires (Incrementable<T> && Decrementable<T>) || PreIncre
// CHECK-NEXT: "USR": "{{[0-9A-F]*}}"
// CHECK-NEXT: },
// CHECK-NEXT: {
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "Expression": "Decrementable<T>",
// CHECK-NEXT: "Name": "Decrementable",
// CHECK-NEXT: "Path": "",
Expand All @@ -55,6 +56,7 @@ template<typename T> requires (Incrementable<T> && Decrementable<T>) || PreIncre
// CHECK-NEXT: "USR": "{{[0-9A-F]*}}"
// CHECK-NEXT: },
// CHECK-NEXT: {
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "Expression": "Decrementable<T>",
// CHECK-NEXT: "Name": "Decrementable",
// CHECK-NEXT: "Path": "",
Expand Down Expand Up @@ -87,6 +89,7 @@ template<typename T> requires (Incrementable<T> && Decrementable<T>) || PreIncre
// CHECK-NEXT: "USR": "{{[0-9A-F]*}}"
// CHECK-NEXT: },
// CHECK-NEXT: {
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "Expression": "PreDecrementable<T>",
// CHECK-NEXT: "Name": "PreDecrementable",
// CHECK-NEXT: "Path": "",
Expand All @@ -112,6 +115,7 @@ template<typename T> requires (Incrementable<T> && Decrementable<T>) || PreIncre
// CHECK-NEXT: "USR": "{{[0-9A-F]*}}"
// CHECK-NEXT: },
// CHECK-NEXT: {
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "Expression": "PreIncrementable<T>",
// CHECK-NEXT: "Name": "PreIncrementable",
// CHECK-NEXT: "Path": "",
Expand Down
2 changes: 2 additions & 0 deletions clang-tools-extra/test/clang-doc/json/concept.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ concept Incrementable = requires(T x) {
// CHECK-NEXT: {
// CHECK-NEXT: "TextComment": " Requires that T suports post and pre-incrementing."
// CHECK: ],
// CHECK: "End": true,
// CHECK-NEXT: "InfoType": "concept",
// CHECK-NEXT: "IsType": true,
// CHECK-NEXT: "Name": "Incrementable",
// CHECK-NEXT: "Template": {
Expand Down
Loading
Loading