[clang-doc] serialize friends (#146165)

Parse friends into a new FriendInfo and serialize them in JSON. We keep track of the friend declaration's template and function information if applicable.
This commit is contained in:
Erick Velez
2025-06-30 12:43:52 -07:00
committed by GitHub
parent 96b9b2e21d
commit a68e4470c1
13 changed files with 236 additions and 45 deletions

View File

@@ -94,6 +94,7 @@ static llvm::Error decodeRecord(const Record &R, InfoType &Field,
case InfoType::IT_typedef:
case InfoType::IT_concept:
case InfoType::IT_variable:
case InfoType::IT_friend:
Field = IT;
return llvm::Error::success();
}
@@ -111,6 +112,7 @@ static llvm::Error decodeRecord(const Record &R, FieldId &Field,
case FieldId::F_child_namespace:
case FieldId::F_child_record:
case FieldId::F_concept:
case FieldId::F_friend:
case FieldId::F_default:
Field = F;
return llvm::Error::success();
@@ -450,6 +452,15 @@ static llvm::Error parseRecord(const Record &R, unsigned ID,
}
}
static llvm::Error parseRecord(const Record &R, unsigned ID, StringRef Blob,
FriendInfo *F) {
if (ID == FRIEND_IS_CLASS) {
return decodeRecord(R, F->IsClass, Blob);
}
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"invalid field for Friend");
}
template <typename T> static llvm::Expected<CommentInfo *> getCommentInfo(T I) {
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"invalid type cannot contain CommentInfo");
@@ -525,6 +536,18 @@ template <> llvm::Error addTypeInfo(FunctionInfo *I, FieldTypeInfo &&T) {
return llvm::Error::success();
}
template <> llvm::Error addTypeInfo(FriendInfo *I, FieldTypeInfo &&T) {
if (!I->Params)
I->Params.emplace();
I->Params->emplace_back(std::move(T));
return llvm::Error::success();
}
template <> llvm::Error addTypeInfo(FriendInfo *I, TypeInfo &&T) {
I->ReturnType.emplace(std::move(T));
return llvm::Error::success();
}
template <> llvm::Error addTypeInfo(EnumInfo *I, TypeInfo &&T) {
I->BaseType = std::move(T);
return llvm::Error::success();
@@ -667,6 +690,16 @@ llvm::Error addReference(ConstraintInfo *I, Reference &&R, FieldId F) {
"ConstraintInfo cannot contain this Reference");
}
template <>
llvm::Error addReference(FriendInfo *Friend, Reference &&R, FieldId F) {
if (F == FieldId::F_friend) {
Friend->Ref = std::move(R);
return llvm::Error::success();
}
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"Friend cannot contain this Reference");
}
template <typename T, typename ChildInfoType>
static void addChild(T I, ChildInfoType &&R) {
llvm::errs() << "invalid child type for info";
@@ -700,6 +733,9 @@ template <> void addChild(RecordInfo *I, EnumInfo &&R) {
template <> void addChild(RecordInfo *I, TypedefInfo &&R) {
I->Children.Typedefs.emplace_back(std::move(R));
}
template <> void addChild(RecordInfo *I, FriendInfo &&R) {
I->Friends.emplace_back(std::move(R));
}
// Other types of children:
template <> void addChild(EnumInfo *I, EnumValueInfo &&R) {
@@ -741,6 +777,9 @@ template <> void addTemplate(FunctionInfo *I, TemplateInfo &&P) {
template <> void addTemplate(ConceptInfo *I, TemplateInfo &&P) {
I->Template = std::move(P);
}
template <> void addTemplate(FriendInfo *I, TemplateInfo &&P) {
I->Template.emplace(std::move(P));
}
// Template specializations go only into template records.
template <typename T>
@@ -921,6 +960,10 @@ llvm::Error ClangDocBitcodeReader::readSubBlock(unsigned ID, T I) {
case BI_VAR_BLOCK_ID: {
return handleSubBlock<VarInfo>(ID, I, CreateAddFunc(addChild<T, VarInfo>));
}
case BI_FRIEND_BLOCK_ID: {
return handleSubBlock<FriendInfo>(ID, I,
CreateAddFunc(addChild<T, FriendInfo>));
}
default:
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"invalid subblock type");
@@ -1032,6 +1075,8 @@ ClangDocBitcodeReader::readBlockToInfo(unsigned ID) {
return createInfo<FunctionInfo>(ID);
case BI_VAR_BLOCK_ID:
return createInfo<VarInfo>(ID);
case BI_FRIEND_BLOCK_ID:
return createInfo<FriendInfo>(ID);
default:
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"cannot create info");
@@ -1072,6 +1117,7 @@ ClangDocBitcodeReader::readBitcode() {
case BI_TYPEDEF_BLOCK_ID:
case BI_CONCEPT_BLOCK_ID:
case BI_VAR_BLOCK_ID:
case BI_FRIEND_BLOCK_ID:
case BI_FUNCTION_BLOCK_ID: {
auto InfoOrErr = readBlockToInfo(ID);
if (!InfoOrErr)

View File

@@ -131,7 +131,8 @@ static const llvm::IndexedMap<llvm::StringRef, BlockIdToIndexFunctor>
{BI_TEMPLATE_PARAM_BLOCK_ID, "TemplateParamBlock"},
{BI_CONSTRAINT_BLOCK_ID, "ConstraintBlock"},
{BI_CONCEPT_BLOCK_ID, "ConceptBlock"},
{BI_VAR_BLOCK_ID, "VarBlock"}};
{BI_VAR_BLOCK_ID, "VarBlock"},
{BI_FRIEND_BLOCK_ID, "FriendBlock"}};
assert(Inits.size() == BlockIdCount);
for (const auto &Init : Inits)
BlockIdNameMap[Init.first] = Init.second;
@@ -224,7 +225,8 @@ static const llvm::IndexedMap<RecordIdDsc, RecordIdToIndexFunctor>
{VAR_USR, {"USR", &genSymbolIdAbbrev}},
{VAR_NAME, {"Name", &genStringAbbrev}},
{VAR_DEFLOCATION, {"DefLocation", &genLocationAbbrev}},
{VAR_IS_STATIC, {"IsStatic", &genBoolAbbrev}}};
{VAR_IS_STATIC, {"IsStatic", &genBoolAbbrev}},
{FRIEND_IS_CLASS, {"IsClass", &genBoolAbbrev}}};
assert(Inits.size() == RecordIdCount);
for (const auto &Init : Inits) {
@@ -293,7 +295,8 @@ static const std::vector<std::pair<BlockId, std::vector<RecordId>>>
CONCEPT_CONSTRAINT_EXPRESSION}},
// Constraint Block
{BI_CONSTRAINT_BLOCK_ID, {CONSTRAINT_EXPRESSION}},
{BI_VAR_BLOCK_ID, {VAR_NAME, VAR_USR, VAR_DEFLOCATION, VAR_IS_STATIC}}};
{BI_VAR_BLOCK_ID, {VAR_NAME, VAR_USR, VAR_DEFLOCATION, VAR_IS_STATIC}},
{BI_FRIEND_BLOCK_ID, {FRIEND_IS_CLASS}}};
// AbbreviationMap
@@ -476,6 +479,19 @@ void ClangDocBitcodeWriter::emitBlock(const Reference &R, FieldId Field) {
emitRecord((unsigned)Field, REFERENCE_FIELD);
}
void ClangDocBitcodeWriter::emitBlock(const FriendInfo &R) {
StreamSubBlockGuard Block(Stream, BI_FRIEND_BLOCK_ID);
emitBlock(R.Ref, FieldId::F_friend);
emitRecord(R.IsClass, FRIEND_IS_CLASS);
if (R.Template)
emitBlock(*R.Template);
if (R.Params)
for (const auto &P : *R.Params)
emitBlock(P);
if (R.ReturnType)
emitBlock(*R.ReturnType);
}
void ClangDocBitcodeWriter::emitBlock(const TypeInfo &T) {
StreamSubBlockGuard Block(Stream, BI_TYPE_BLOCK_ID);
emitBlock(T.Type, FieldId::F_type);
@@ -628,6 +644,8 @@ void ClangDocBitcodeWriter::emitBlock(const RecordInfo &I) {
emitBlock(C);
if (I.Template)
emitBlock(*I.Template);
for (const auto &C : I.Friends)
emitBlock(C);
}
void ClangDocBitcodeWriter::emitBlock(const BaseRecordInfo &I) {
@@ -744,6 +762,9 @@ bool ClangDocBitcodeWriter::dispatchInfoForWrite(Info *I) {
case InfoType::IT_variable:
emitBlock(*static_cast<VarInfo *>(I));
break;
case InfoType::IT_friend:
emitBlock(*static_cast<FriendInfo *>(I));
break;
case InfoType::IT_default:
llvm::errs() << "Unexpected info, unable to write.\n";
return true;

View File

@@ -70,6 +70,7 @@ enum BlockId {
BI_TYPEDEF_BLOCK_ID,
BI_CONCEPT_BLOCK_ID,
BI_VAR_BLOCK_ID,
BI_FRIEND_BLOCK_ID,
BI_LAST,
BI_FIRST = BI_VERSION_BLOCK_ID
};
@@ -153,6 +154,7 @@ enum RecordId {
VAR_NAME,
VAR_DEFLOCATION,
VAR_IS_STATIC,
FRIEND_IS_CLASS,
RI_LAST,
RI_FIRST = VERSION
};
@@ -169,7 +171,8 @@ enum class FieldId {
F_type,
F_child_namespace,
F_child_record,
F_concept
F_concept,
F_friend
};
class ClangDocBitcodeWriter {
@@ -201,6 +204,7 @@ public:
void emitBlock(const ConceptInfo &T);
void emitBlock(const ConstraintInfo &T);
void emitBlock(const Reference &B, FieldId F);
void emitBlock(const FriendInfo &R);
void emitBlock(const VarInfo &B);
private:

View File

@@ -987,6 +987,7 @@ llvm::Error HTMLGenerator::generateDocForInfo(Info *I, llvm::raw_ostream &OS,
break;
case InfoType::IT_concept:
case InfoType::IT_variable:
case InfoType::IT_friend:
break;
case InfoType::IT_default:
return llvm::createStringError(llvm::inconvertibleErrorCode(),
@@ -1018,6 +1019,8 @@ static std::string getRefType(InfoType IT) {
return "concept";
case InfoType::IT_variable:
return "variable";
case InfoType::IT_friend:
return "friend";
}
llvm_unreachable("Unknown InfoType");
}

View File

@@ -588,6 +588,7 @@ Error MustacheHTMLGenerator::generateDocForInfo(Info *I, raw_ostream &OS,
case InfoType::IT_concept:
break;
case InfoType::IT_variable:
case InfoType::IT_friend:
break;
case InfoType::IT_default:
return createStringError(inconvertibleErrorCode(), "unexpected InfoType");

View File

@@ -39,8 +39,7 @@ static void serializeArray(const Container &Records, Object &Obj,
static auto SerializeInfoLambda = [](const auto &Info, Object &Object) {
serializeInfo(Info, Object);
};
static auto SerializeReferenceLambda = [](const Reference &Ref,
Object &Object) {
static auto SerializeReferenceLambda = [](const auto &Ref, Object &Object) {
serializeReference(Ref, Object);
};
@@ -365,6 +364,22 @@ static void serializeInfo(const BaseRecordInfo &I, Object &Obj,
Obj["IsParent"] = I.IsParent;
}
static void serializeInfo(const FriendInfo &I, Object &Obj) {
auto FriendRef = Object();
serializeReference(I.Ref, FriendRef);
Obj["Reference"] = std::move(FriendRef);
Obj["IsClass"] = I.IsClass;
if (I.Template)
serializeInfo(I.Template.value(), Obj);
if (I.Params)
serializeArray(I.Params.value(), Obj, "Params", SerializeInfoLambda);
if (I.ReturnType) {
auto ReturnTypeObj = Object();
serializeInfo(I.ReturnType.value(), ReturnTypeObj);
Obj["ReturnType"] = std::move(ReturnTypeObj);
}
}
static void serializeInfo(const RecordInfo &I, json::Object &Obj,
const std::optional<StringRef> &RepositoryUrl) {
serializeCommonAttributes(I, Obj, RepositoryUrl);
@@ -436,6 +451,9 @@ static void serializeInfo(const RecordInfo &I, json::Object &Obj,
if (I.Template)
serializeInfo(I.Template.value(), Obj);
if (!I.Friends.empty())
serializeArray(I.Friends, Obj, "Friends", SerializeInfoLambda);
serializeCommonChildren(I.Children, Obj, RepositoryUrl);
}
@@ -525,6 +543,7 @@ Error JSONGenerator::generateDocForInfo(Info *I, raw_ostream &OS,
case InfoType::IT_function:
case InfoType::IT_typedef:
case InfoType::IT_variable:
case InfoType::IT_friend:
break;
case InfoType::IT_default:
return createStringError(inconvertibleErrorCode(), "unexpected info type");

View File

@@ -378,6 +378,9 @@ static llvm::Error genIndex(ClangDocContext &CDCtx) {
case InfoType::IT_variable:
Type = "Variable";
break;
case InfoType::IT_friend:
Type = "Friend";
break;
case InfoType::IT_default:
Type = "Other";
}
@@ -472,6 +475,7 @@ llvm::Error MDGenerator::generateDocForInfo(Info *I, llvm::raw_ostream &OS,
break;
case InfoType::IT_concept:
case InfoType::IT_variable:
case InfoType::IT_friend:
break;
case InfoType::IT_default:
return createStringError(llvm::inconvertibleErrorCode(),

View File

@@ -147,6 +147,8 @@ mergeInfos(std::vector<std::unique_ptr<Info>> &Values) {
return reduce<ConceptInfo>(Values);
case InfoType::IT_variable:
return reduce<VarInfo>(Values);
case InfoType::IT_friend:
return reduce<FriendInfo>(Values);
case InfoType::IT_default:
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"unexpected info type");
@@ -247,6 +249,15 @@ void Reference::merge(Reference &&Other) {
Path = Other.Path;
}
bool FriendInfo::mergeable(const FriendInfo &Other) {
return Ref.USR == Other.Ref.USR && Ref.Name == Other.Ref.Name;
}
void FriendInfo::merge(FriendInfo &&Other) {
assert(mergeable(Other));
Ref.merge(std::move(Other.Ref));
}
void Info::mergeBase(Info &&Other) {
assert(mergeable(Other));
if (USR == EmptySID)
@@ -313,6 +324,8 @@ void RecordInfo::merge(RecordInfo &&Other) {
Parents = std::move(Other.Parents);
if (VirtualParents.empty())
VirtualParents = std::move(Other.VirtualParents);
if (Friends.empty())
Friends = std::move(Other.Friends);
// Reduce children if necessary.
reduceChildren(Children.Records, std::move(Other.Children.Records));
reduceChildren(Children.Functions, std::move(Other.Children.Functions));
@@ -422,6 +435,9 @@ llvm::SmallString<16> Info::extractName() const {
case InfoType::IT_variable:
return llvm::SmallString<16>("@nonymous_variable_" +
toHex(llvm::toStringRef(USR)));
case InfoType::IT_friend:
return llvm::SmallString<16>("@nonymous_friend_" +
toHex(llvm::toStringRef(USR)));
case InfoType::IT_default:
return llvm::SmallString<16>("@nonymous_" + toHex(llvm::toStringRef(USR)));
}

View File

@@ -46,7 +46,8 @@ enum class InfoType {
IT_enum,
IT_typedef,
IT_concept,
IT_variable
IT_variable,
IT_friend
};
enum class CommentKind {
@@ -379,6 +380,22 @@ struct SymbolInfo : public Info {
bool IsStatic = false;
};
struct FriendInfo : SymbolInfo {
FriendInfo() : SymbolInfo(InfoType::IT_friend) {}
FriendInfo(SymbolID USR) : SymbolInfo(InfoType::IT_friend, USR) {}
FriendInfo(const InfoType IT, const SymbolID &USR,
const StringRef Name = StringRef())
: SymbolInfo(IT, USR, Name) {}
bool mergeable(const FriendInfo &Other);
void merge(FriendInfo &&Other);
Reference Ref;
std::optional<TemplateInfo> Template;
std::optional<TypeInfo> ReturnType;
std::optional<SmallVector<FieldTypeInfo, 4>> Params;
bool IsClass = false;
};
struct VarInfo : SymbolInfo {
VarInfo() : SymbolInfo(InfoType::IT_variable) {}
explicit VarInfo(SymbolID USR) : SymbolInfo(InfoType::IT_variable, USR) {}
@@ -454,6 +471,8 @@ struct RecordInfo : public SymbolInfo {
Bases; // List of base/parent records; this includes inherited methods and
// attributes
std::vector<FriendInfo> Friends;
ScopeChildren Children;
};

View File

@@ -7,9 +7,12 @@
//===----------------------------------------------------------------------===//
#include "Serialize.h"
#include "../clangd/CodeCompletionStrings.h"
#include "BitcodeWriter.h"
#include "clang/AST/Attr.h"
#include "clang/AST/Comment.h"
#include "clang/AST/DeclFriend.h"
#include "clang/Index/USRGeneration.h"
#include "clang/Lex/Lexer.h"
#include "llvm/ADT/StringExtras.h"
@@ -403,6 +406,7 @@ std::string serialize(std::unique_ptr<Info> &I) {
return serialize(*static_cast<ConceptInfo *>(I.get()));
case InfoType::IT_variable:
return serialize(*static_cast<VarInfo *>(I.get()));
case InfoType::IT_friend:
case InfoType::IT_typedef:
case InfoType::IT_default:
return "";
@@ -556,6 +560,7 @@ static std::unique_ptr<Info> makeAndInsertIntoParent(ChildType Child) {
case InfoType::IT_typedef:
case InfoType::IT_concept:
case InfoType::IT_variable:
case InfoType::IT_friend:
break;
}
llvm_unreachable("Invalid reference type for parent namespace");
@@ -947,6 +952,55 @@ emitInfo(const NamespaceDecl *D, const FullComment *FC, Location Loc,
return {std::move(NSI), makeAndInsertIntoParent<const NamespaceInfo &>(*NSI)};
}
static void parseFriends(RecordInfo &RI, const CXXRecordDecl *D) {
if (!D->hasDefinition() || !D->hasFriends())
return;
for (const FriendDecl *FD : D->friends()) {
if (FD->isUnsupportedFriend())
continue;
FriendInfo F(InfoType::IT_friend, getUSRForDecl(FD));
const auto *ActualDecl = FD->getFriendDecl();
if (!ActualDecl) {
const auto *FriendTypeInfo = FD->getFriendType();
if (!FriendTypeInfo)
continue;
ActualDecl = FriendTypeInfo->getType()->getAsCXXRecordDecl();
if (!ActualDecl)
continue;
F.IsClass = true;
}
if (const auto *ActualTD = dyn_cast_or_null<TemplateDecl>(ActualDecl)) {
if (isa<RecordDecl>(ActualTD->getTemplatedDecl()))
F.IsClass = true;
F.Template.emplace();
for (const auto *Param : ActualTD->getTemplateParameters()->asArray())
F.Template->Params.emplace_back(
getSourceCode(Param, Param->getSourceRange()));
ActualDecl = ActualTD->getTemplatedDecl();
}
if (auto *FuncDecl = dyn_cast_or_null<FunctionDecl>(ActualDecl)) {
FunctionInfo TempInfo;
parseParameters(TempInfo, FuncDecl);
F.Params.emplace();
F.Params = std::move(TempInfo.Params);
F.ReturnType = getTypeInfoForType(FuncDecl->getReturnType(),
FuncDecl->getLangOpts());
}
F.Ref =
Reference(getUSRForDecl(ActualDecl), ActualDecl->getNameAsString(),
InfoType::IT_default, ActualDecl->getQualifiedNameAsString(),
getInfoRelativePath(ActualDecl));
RI.Friends.push_back(std::move(F));
}
}
std::pair<std::unique_ptr<Info>, std::unique_ptr<Info>>
emitInfo(const RecordDecl *D, const FullComment *FC, Location Loc,
bool PublicOnly) {
@@ -970,6 +1024,7 @@ emitInfo(const RecordDecl *D, const FullComment *FC, Location Loc,
// TODO: remove first call to parseBases, that function should be deleted
parseBases(*RI, C);
parseBases(*RI, C, /*IsFileInRootDir=*/true, PublicOnly, /*IsParent=*/true);
parseFriends(*RI, C);
}
RI->Path = getInfoRelativePath(RI->Namespace);

View File

@@ -410,6 +410,7 @@ llvm::Error YAMLGenerator::generateDocForInfo(Info *I, llvm::raw_ostream &OS,
break;
case InfoType::IT_concept:
case InfoType::IT_variable:
case InfoType::IT_friend:
break;
case InfoType::IT_default:
return llvm::createStringError(llvm::inconvertibleErrorCode(),

View File

@@ -89,44 +89,44 @@ protected:
// CHECK-NEXT: "USR": "{{[0-9A-F]*}}"
// CHECK-NEXT: }
// CHECK-NEXT: ],
// CHECK-NOT: "Friends": [
// CHECK-NOT: {
// CHECK-NOT: "IsClass": false,
// CHECK-NOT: "Params": [
// CHECK-NOT: {
// CHECK-NOT: "Name": "",
// CHECK-NOT: "Type": "int"
// CHECK-NOT: }
// CHECK-NOT: ],
// CHECK-NOT: "Reference": {
// CHECK-NOT: "Name": "friendFunction",
// CHECK-NOT: "Path": "",
// CHECK-NOT: "QualName": "friendFunction",
// CHECK-NOT: "USR": "{{[0-9A-F]*}}"
// CHECK-NOT: },
// CHECK-NOT: "ReturnType": {
// CHECK-NOT: "IsBuiltIn": true,
// CHECK-NOT: "IsTemplate": false,
// CHECK-NOT: "Name": "void",
// CHECK-NOT: "QualName": "void",
// CHECK-NOT: "USR": "0000000000000000000000000000000000000000"
// CHECK-NOT: },
// CHECK-NOT: "Template": {
// CHECK-NOT: "Parameters": [
// CHECK-NOT: "typename T"
// CHECK-NOT: ]
// CHECK-NOT: }
// CHECK-NOT: },
// CHECK-NOT: {
// CHECK-NOT: "IsClass": true,
// CHECK-NOT: "Reference": {
// CHECK-NOT: "Name": "Foo",
// CHECK-NOT: "Path": "GlobalNamespace",
// CHECK-NOT: "QualName": "Foo",
// CHECK-NOT: "USR": "{{[0-9A-F]*}}"
// CHECK-NOT: },
// CHECK-NOT: },
// CHECK-NOT: ],
// CHECK-NEXT: "Friends": [
// CHECK-NEXT: {
// CHECK-NEXT: "IsClass": false,
// CHECK-NEXT: "Params": [
// CHECK-NEXT: {
// CHECK-NEXT: "Name": "",
// CHECK-NEXT: "Type": "int"
// CHECK-NEXT: }
// CHECK-NEXT: ],
// CHECK-NEXT: "Reference": {
// CHECK-NEXT: "Name": "friendFunction",
// CHECK-NEXT: "Path": "",
// CHECK-NEXT: "QualName": "friendFunction",
// CHECK-NEXT: "USR": "{{[0-9A-F]*}}"
// CHECK-NEXT: },
// CHECK-NEXT: "ReturnType": {
// CHECK-NEXT: "IsBuiltIn": true,
// CHECK-NEXT: "IsTemplate": false,
// CHECK-NEXT: "Name": "void",
// CHECK-NEXT: "QualName": "void",
// CHECK-NEXT: "USR": "0000000000000000000000000000000000000000"
// CHECK-NEXT: },
// CHECK-NEXT: "Template": {
// CHECK-NEXT: "Parameters": [
// CHECK-NEXT: "typename T"
// CHECK-NEXT: ]
// CHECK-NEXT: }
// CHECK-NEXT: },
// CHECK-NEXT: {
// CHECK-NEXT: "IsClass": true,
// CHECK-NEXT: "Reference": {
// CHECK-NEXT: "Name": "Foo",
// CHECK-NEXT: "Path": "GlobalNamespace",
// CHECK-NEXT: "QualName": "Foo",
// CHECK-NEXT: "USR": "{{[0-9A-F]*}}"
// CHECK-NEXT: }
// CHECK-NEXT: }
// CHECK-NEXT: ],
// COM: FIXME: FullName is not emitted correctly.
// CHECK-NEXT: "FullName": "",
// CHECK-NEXT: "IsTypedef": false,

View File

@@ -41,6 +41,8 @@ static std::string writeInfo(Info *I) {
return writeInfo(*static_cast<ConceptInfo *>(I));
case InfoType::IT_variable:
return writeInfo(*static_cast<VarInfo *>(I));
case InfoType::IT_friend:
return writeInfo(*static_cast<FriendInfo *>(I));
case InfoType::IT_default:
return "";
}