From 1c3320cdde8f846a28afd2f2a83ca23edb25e92b Mon Sep 17 00:00:00 2001 From: Erick Velez Date: Tue, 10 Jun 2025 08:39:42 -0700 Subject: [PATCH] [clang-doc] add a JSON generator (#142483) Adds a JSON generator backend to emit mapped information as JSON. This will enable a better testing format for upcoming changes. It can also potentially serve to feed our other backend generators in the future, like Mustache which already serializes information to JSON before emitting as HTML. This patch contains functionality to emit classes and provides most of the basis of the generator. --- clang-tools-extra/clang-doc/CMakeLists.txt | 1 + clang-tools-extra/clang-doc/Generators.cpp | 2 + clang-tools-extra/clang-doc/Generators.h | 1 + clang-tools-extra/clang-doc/JSONGenerator.cpp | 508 ++++++++++++++++++ .../clang-doc/tool/ClangDocMain.cpp | 8 +- .../test/clang-doc/json/class-template.cpp | 29 + .../test/clang-doc/json/class.cpp | 193 +++++++ .../test/clang-doc/json/method-template.cpp | 40 ++ .../unittests/clang-doc/CMakeLists.txt | 1 + .../unittests/clang-doc/JSONGeneratorTest.cpp | 175 ++++++ 10 files changed, 956 insertions(+), 2 deletions(-) create mode 100644 clang-tools-extra/clang-doc/JSONGenerator.cpp create mode 100644 clang-tools-extra/test/clang-doc/json/class-template.cpp create mode 100644 clang-tools-extra/test/clang-doc/json/class.cpp create mode 100644 clang-tools-extra/test/clang-doc/json/method-template.cpp create mode 100644 clang-tools-extra/unittests/clang-doc/JSONGeneratorTest.cpp diff --git a/clang-tools-extra/clang-doc/CMakeLists.txt b/clang-tools-extra/clang-doc/CMakeLists.txt index 79563c41435e..5989e5fe60cf 100644 --- a/clang-tools-extra/clang-doc/CMakeLists.txt +++ b/clang-tools-extra/clang-doc/CMakeLists.txt @@ -17,6 +17,7 @@ add_clang_library(clangDoc STATIC Serialize.cpp YAMLGenerator.cpp HTMLMustacheGenerator.cpp + JSONGenerator.cpp DEPENDS omp_gen diff --git a/clang-tools-extra/clang-doc/Generators.cpp b/clang-tools-extra/clang-doc/Generators.cpp index a3c2773412cf..3fb5b63c403a 100644 --- a/clang-tools-extra/clang-doc/Generators.cpp +++ b/clang-tools-extra/clang-doc/Generators.cpp @@ -105,5 +105,7 @@ static int LLVM_ATTRIBUTE_UNUSED HTMLGeneratorAnchorDest = HTMLGeneratorAnchorSource; static int LLVM_ATTRIBUTE_UNUSED MHTMLGeneratorAnchorDest = MHTMLGeneratorAnchorSource; +static int LLVM_ATTRIBUTE_UNUSED JSONGeneratorAnchorDest = + JSONGeneratorAnchorSource; } // namespace doc } // namespace clang diff --git a/clang-tools-extra/clang-doc/Generators.h b/clang-tools-extra/clang-doc/Generators.h index aee04b9d58d9..92d3006e6002 100644 --- a/clang-tools-extra/clang-doc/Generators.h +++ b/clang-tools-extra/clang-doc/Generators.h @@ -58,6 +58,7 @@ extern volatile int YAMLGeneratorAnchorSource; extern volatile int MDGeneratorAnchorSource; extern volatile int HTMLGeneratorAnchorSource; extern volatile int MHTMLGeneratorAnchorSource; +extern volatile int JSONGeneratorAnchorSource; } // namespace doc } // namespace clang diff --git a/clang-tools-extra/clang-doc/JSONGenerator.cpp b/clang-tools-extra/clang-doc/JSONGenerator.cpp new file mode 100644 index 000000000000..086da0916532 --- /dev/null +++ b/clang-tools-extra/clang-doc/JSONGenerator.cpp @@ -0,0 +1,508 @@ +#include "Generators.h" +#include "clang/Basic/Specifiers.h" +#include "llvm/Support/JSON.h" + +using namespace llvm; +using namespace llvm::json; + +namespace clang { +namespace doc { + +class JSONGenerator : public Generator { +public: + static const char *Format; + + Error generateDocs(StringRef RootDir, + llvm::StringMap> Infos, + const ClangDocContext &CDCtx) override; + Error createResources(ClangDocContext &CDCtx) override; + Error generateDocForInfo(Info *I, llvm::raw_ostream &OS, + const ClangDocContext &CDCtx) override; +}; + +const char *JSONGenerator::Format = "json"; + +static void serializeInfo(const TypedefInfo &I, json::Object &Obj, + std::optional RepositoryUrl); +static void serializeInfo(const EnumInfo &I, json::Object &Obj, + std::optional RepositoryUrl); + +static json::Object serializeLocation(const Location &Loc, + std::optional RepositoryUrl) { + Object LocationObj = Object(); + LocationObj["LineNumber"] = Loc.StartLineNumber; + LocationObj["Filename"] = Loc.Filename; + + if (!Loc.IsFileInRootDir || !RepositoryUrl) + return LocationObj; + SmallString<128> FileURL(*RepositoryUrl); + sys::path::append(FileURL, sys::path::Style::posix, Loc.Filename); + FileURL += "#" + std::to_string(Loc.StartLineNumber); + LocationObj["FileURL"] = FileURL; + return LocationObj; +} + +static json::Value serializeComment(const CommentInfo &I) { + // taken from PR #142273 + Object Obj = Object(); + + json::Value ChildVal = Object(); + Object &Child = *ChildVal.getAsObject(); + + json::Value ChildArr = Array(); + auto &CARef = *ChildArr.getAsArray(); + CARef.reserve(I.Children.size()); + for (const auto &C : I.Children) + CARef.emplace_back(serializeComment(*C)); + + switch (I.Kind) { + case CommentKind::CK_TextComment: { + Obj.insert({commentKindToString(I.Kind), I.Text}); + return Obj; + } + + case CommentKind::CK_BlockCommandComment: { + Child.insert({"Command", I.Name}); + Child.insert({"Children", ChildArr}); + Obj.insert({commentKindToString(I.Kind), ChildVal}); + return Obj; + } + + case CommentKind::CK_InlineCommandComment: { + json::Value ArgsArr = Array(); + auto &ARef = *ArgsArr.getAsArray(); + ARef.reserve(I.Args.size()); + for (const auto &Arg : I.Args) + ARef.emplace_back(Arg); + Child.insert({"Command", I.Name}); + Child.insert({"Args", ArgsArr}); + Child.insert({"Children", ChildArr}); + Obj.insert({commentKindToString(I.Kind), ChildVal}); + return Obj; + } + + case CommentKind::CK_ParamCommandComment: + case CommentKind::CK_TParamCommandComment: { + Child.insert({"ParamName", I.ParamName}); + Child.insert({"Direction", I.Direction}); + Child.insert({"Explicit", I.Explicit}); + Child.insert({"Children", ChildArr}); + Obj.insert({commentKindToString(I.Kind), ChildVal}); + return Obj; + } + + case CommentKind::CK_VerbatimBlockComment: { + Child.insert({"Text", I.Text}); + if (!I.CloseName.empty()) + Child.insert({"CloseName", I.CloseName}); + Child.insert({"Children", ChildArr}); + Obj.insert({commentKindToString(I.Kind), ChildVal}); + return Obj; + } + + case CommentKind::CK_VerbatimBlockLineComment: + case CommentKind::CK_VerbatimLineComment: { + Child.insert({"Text", I.Text}); + Child.insert({"Children", ChildArr}); + Obj.insert({commentKindToString(I.Kind), ChildVal}); + return Obj; + } + + case CommentKind::CK_HTMLStartTagComment: { + json::Value AttrKeysArray = json::Array(); + json::Value AttrValuesArray = json::Array(); + auto &KeyArr = *AttrKeysArray.getAsArray(); + auto &ValArr = *AttrValuesArray.getAsArray(); + KeyArr.reserve(I.AttrKeys.size()); + ValArr.reserve(I.AttrValues.size()); + for (const auto &K : I.AttrKeys) + KeyArr.emplace_back(K); + for (const auto &V : I.AttrValues) + ValArr.emplace_back(V); + Child.insert({"Name", I.Name}); + Child.insert({"SelfClosing", I.SelfClosing}); + Child.insert({"AttrKeys", AttrKeysArray}); + Child.insert({"AttrValues", AttrValuesArray}); + Child.insert({"Children", ChildArr}); + Obj.insert({commentKindToString(I.Kind), ChildVal}); + return Obj; + } + + case CommentKind::CK_HTMLEndTagComment: { + Child.insert({"Name", I.Name}); + Child.insert({"Children", ChildArr}); + Obj.insert({commentKindToString(I.Kind), ChildVal}); + return Obj; + } + + case CommentKind::CK_FullComment: + case CommentKind::CK_ParagraphComment: { + Child.insert({"Children", ChildArr}); + Obj.insert({commentKindToString(I.Kind), ChildVal}); + return Obj; + } + + case CommentKind::CK_Unknown: { + Obj.insert({commentKindToString(I.Kind), I.Text}); + return Obj; + } + } + llvm_unreachable("Unknown comment kind encountered."); +} + +static void serializeCommonAttributes(const Info &I, json::Object &Obj, + std::optional RepositoryUrl) { + Obj["Name"] = I.Name; + Obj["USR"] = toHex(toStringRef(I.USR)); + + if (!I.Path.empty()) + Obj["Path"] = I.Path; + + if (!I.Namespace.empty()) { + Obj["Namespace"] = json::Array(); + for (const auto &NS : I.Namespace) + Obj["Namespace"].getAsArray()->push_back(NS.Name); + } + + if (!I.Description.empty()) { + json::Value DescArray = json::Array(); + auto &DescArrayRef = *DescArray.getAsArray(); + DescArrayRef.reserve(I.Description.size()); + for (const auto &Comment : I.Description) + DescArrayRef.push_back(serializeComment(Comment)); + Obj["Description"] = DescArray; + } + + // Namespaces aren't SymbolInfos, so they dont have a DefLoc + if (I.IT != InfoType::IT_namespace) { + const auto *Symbol = static_cast(&I); + if (Symbol->DefLoc) + Obj["Location"] = + serializeLocation(Symbol->DefLoc.value(), RepositoryUrl); + } +} + +static void serializeReference(const Reference &Ref, Object &ReferenceObj) { + ReferenceObj["Path"] = Ref.Path; + ReferenceObj["Name"] = Ref.Name; + ReferenceObj["QualName"] = Ref.QualName; + ReferenceObj["USR"] = toHex(toStringRef(Ref.USR)); +} + +static void serializeReference(const SmallVector &References, + Object &Obj, std::string Key) { + json::Value ReferencesArray = Array(); + json::Array &ReferencesArrayRef = *ReferencesArray.getAsArray(); + ReferencesArrayRef.reserve(References.size()); + for (const auto &Reference : References) { + json::Value ReferenceVal = Object(); + auto &ReferenceObj = *ReferenceVal.getAsObject(); + serializeReference(Reference, ReferenceObj); + ReferencesArrayRef.push_back(ReferenceVal); + } + Obj[Key] = ReferencesArray; +} + +// Although namespaces and records both have ScopeChildren, they serialize them +// differently. Only enums, records, and typedefs are handled here. +static void serializeCommonChildren(const ScopeChildren &Children, + json::Object &Obj, + std::optional RepositoryUrl) { + if (!Children.Enums.empty()) { + json::Value EnumsArray = Array(); + auto &EnumsArrayRef = *EnumsArray.getAsArray(); + EnumsArrayRef.reserve(Children.Enums.size()); + for (const auto &Enum : Children.Enums) { + json::Value EnumVal = Object(); + auto &EnumObj = *EnumVal.getAsObject(); + serializeInfo(Enum, EnumObj, RepositoryUrl); + EnumsArrayRef.push_back(EnumVal); + } + Obj["Enums"] = EnumsArray; + } + + if (!Children.Typedefs.empty()) { + json::Value TypedefsArray = Array(); + auto &TypedefsArrayRef = *TypedefsArray.getAsArray(); + TypedefsArrayRef.reserve(Children.Typedefs.size()); + for (const auto &Typedef : Children.Typedefs) { + json::Value TypedefVal = Object(); + auto &TypedefObj = *TypedefVal.getAsObject(); + serializeInfo(Typedef, TypedefObj, RepositoryUrl); + TypedefsArrayRef.push_back(TypedefVal); + } + Obj["Typedefs"] = TypedefsArray; + } + + if (!Children.Records.empty()) { + json::Value RecordsArray = Array(); + auto &RecordsArrayRef = *RecordsArray.getAsArray(); + RecordsArrayRef.reserve(Children.Records.size()); + for (const auto &Record : Children.Records) { + json::Value RecordVal = Object(); + auto &RecordObj = *RecordVal.getAsObject(); + serializeReference(Record, RecordObj); + RecordsArrayRef.push_back(RecordVal); + } + Obj["Records"] = RecordsArray; + } +} + +static void serializeInfo(const TemplateInfo &Template, Object &Obj) { + json::Value TemplateVal = Object(); + auto &TemplateObj = *TemplateVal.getAsObject(); + + if (Template.Specialization) { + json::Value TemplateSpecializationVal = Object(); + auto &TemplateSpecializationObj = *TemplateSpecializationVal.getAsObject(); + TemplateSpecializationObj["SpecializationOf"] = + toHex(toStringRef(Template.Specialization->SpecializationOf)); + if (!Template.Specialization->Params.empty()) { + json::Value ParamsArray = Array(); + auto &ParamsArrayRef = *ParamsArray.getAsArray(); + ParamsArrayRef.reserve(Template.Specialization->Params.size()); + for (const auto &Param : Template.Specialization->Params) + ParamsArrayRef.push_back(Param.Contents); + TemplateSpecializationObj["Parameters"] = ParamsArray; + } + TemplateObj["Specialization"] = TemplateSpecializationVal; + } + + if (!Template.Params.empty()) { + json::Value ParamsArray = Array(); + auto &ParamsArrayRef = *ParamsArray.getAsArray(); + ParamsArrayRef.reserve(Template.Params.size()); + for (const auto &Param : Template.Params) + ParamsArrayRef.push_back(Param.Contents); + TemplateObj["Parameters"] = ParamsArray; + } + + Obj["Template"] = TemplateVal; +} + +static void serializeInfo(const TypeInfo &I, Object &Obj) { + Obj["Name"] = I.Type.Name; + Obj["QualName"] = I.Type.QualName; + Obj["USR"] = toHex(toStringRef(I.Type.USR)); + Obj["IsTemplate"] = I.IsTemplate; + Obj["IsBuiltIn"] = I.IsBuiltIn; +} + +static void serializeInfo(const FunctionInfo &F, json::Object &Obj, + std::optional RepositoryURL) { + serializeCommonAttributes(F, Obj, RepositoryURL); + Obj["IsStatic"] = F.IsStatic; + + auto ReturnTypeObj = Object(); + serializeInfo(F.ReturnType, ReturnTypeObj); + Obj["ReturnType"] = std::move(ReturnTypeObj); + + if (!F.Params.empty()) { + json::Value ParamsArray = json::Array(); + auto &ParamsArrayRef = *ParamsArray.getAsArray(); + ParamsArrayRef.reserve(F.Params.size()); + for (const auto &Param : F.Params) { + json::Value ParamVal = Object(); + auto &ParamObj = *ParamVal.getAsObject(); + ParamObj["Name"] = Param.Name; + ParamObj["Type"] = Param.Type.Name; + ParamsArrayRef.push_back(ParamVal); + } + Obj["Params"] = ParamsArray; + } + + if (F.Template) + serializeInfo(F.Template.value(), Obj); +} + +static void serializeInfo(const EnumInfo &I, json::Object &Obj, + std::optional RepositoryUrl) { + serializeCommonAttributes(I, Obj, RepositoryUrl); + Obj["Scoped"] = I.Scoped; + + if (I.BaseType) { + json::Value BaseTypeVal = Object(); + auto &BaseTypeObj = *BaseTypeVal.getAsObject(); + BaseTypeObj["Name"] = I.BaseType->Type.Name; + BaseTypeObj["QualName"] = I.BaseType->Type.QualName; + BaseTypeObj["USR"] = toHex(toStringRef(I.BaseType->Type.USR)); + Obj["BaseType"] = BaseTypeVal; + } + + if (!I.Members.empty()) { + json::Value MembersArray = Array(); + auto &MembersArrayRef = *MembersArray.getAsArray(); + MembersArrayRef.reserve(I.Members.size()); + for (const auto &Member : I.Members) { + json::Value MemberVal = Object(); + auto &MemberObj = *MemberVal.getAsObject(); + MemberObj["Name"] = Member.Name; + if (!Member.ValueExpr.empty()) + MemberObj["ValueExpr"] = Member.ValueExpr; + else + MemberObj["Value"] = Member.Value; + MembersArrayRef.push_back(MemberVal); + } + Obj["Members"] = MembersArray; + } +} + +static void serializeInfo(const TypedefInfo &I, json::Object &Obj, + std::optional RepositoryUrl) { + serializeCommonAttributes(I, Obj, RepositoryUrl); + Obj["TypeDeclaration"] = I.TypeDeclaration; + Obj["IsUsing"] = I.IsUsing; + json::Value TypeVal = Object(); + auto &TypeObj = *TypeVal.getAsObject(); + serializeInfo(I.Underlying, TypeObj); + Obj["Underlying"] = TypeVal; +} + +static void serializeInfo(const RecordInfo &I, json::Object &Obj, + std::optional RepositoryUrl) { + serializeCommonAttributes(I, Obj, RepositoryUrl); + Obj["FullName"] = I.FullName; + Obj["TagType"] = getTagType(I.TagType); + Obj["IsTypedef"] = I.IsTypeDef; + + if (!I.Children.Functions.empty()) { + json::Value PubFunctionsArray = Array(); + json::Array &PubFunctionsArrayRef = *PubFunctionsArray.getAsArray(); + json::Value ProtFunctionsArray = Array(); + json::Array &ProtFunctionsArrayRef = *ProtFunctionsArray.getAsArray(); + + for (const auto &Function : I.Children.Functions) { + json::Value FunctionVal = Object(); + auto &FunctionObj = *FunctionVal.getAsObject(); + serializeInfo(Function, FunctionObj, RepositoryUrl); + AccessSpecifier Access = Function.Access; + if (Access == AccessSpecifier::AS_public) + PubFunctionsArrayRef.push_back(FunctionVal); + else if (Access == AccessSpecifier::AS_protected) + ProtFunctionsArrayRef.push_back(FunctionVal); + } + + if (!PubFunctionsArrayRef.empty()) + Obj["PublicFunctions"] = PubFunctionsArray; + if (!ProtFunctionsArrayRef.empty()) + Obj["ProtectedFunctions"] = ProtFunctionsArray; + } + + if (!I.Members.empty()) { + json::Value PublicMembersArray = Array(); + json::Array &PubMembersArrayRef = *PublicMembersArray.getAsArray(); + json::Value ProtectedMembersArray = Array(); + json::Array &ProtMembersArrayRef = *ProtectedMembersArray.getAsArray(); + + for (const MemberTypeInfo &Member : I.Members) { + json::Value MemberVal = Object(); + auto &MemberObj = *MemberVal.getAsObject(); + MemberObj["Name"] = Member.Name; + MemberObj["Type"] = Member.Type.Name; + + if (Member.Access == AccessSpecifier::AS_public) + PubMembersArrayRef.push_back(MemberVal); + else if (Member.Access == AccessSpecifier::AS_protected) + ProtMembersArrayRef.push_back(MemberVal); + } + + if (!PubMembersArrayRef.empty()) + Obj["PublicMembers"] = PublicMembersArray; + if (!ProtMembersArrayRef.empty()) + Obj["ProtectedMembers"] = ProtectedMembersArray; + } + + if (!I.Bases.empty()) { + json::Value BasesArray = Array(); + json::Array &BasesArrayRef = *BasesArray.getAsArray(); + BasesArrayRef.reserve(I.Bases.size()); + for (const auto &BaseInfo : I.Bases) { + json::Value BaseInfoVal = Object(); + auto &BaseInfoObj = *BaseInfoVal.getAsObject(); + serializeInfo(BaseInfo, BaseInfoObj, RepositoryUrl); + BaseInfoObj["IsVirtual"] = BaseInfo.IsVirtual; + BaseInfoObj["Access"] = getAccessSpelling(BaseInfo.Access); + BaseInfoObj["IsParent"] = BaseInfo.IsParent; + BasesArrayRef.push_back(BaseInfoVal); + } + Obj["Bases"] = BasesArray; + } + + if (!I.Parents.empty()) + serializeReference(I.Parents, Obj, "Parents"); + + if (!I.VirtualParents.empty()) + serializeReference(I.VirtualParents, Obj, "VirtualParents"); + + if (I.Template) + serializeInfo(I.Template.value(), Obj); + + serializeCommonChildren(I.Children, Obj, RepositoryUrl); +} + +Error JSONGenerator::generateDocs( + StringRef RootDir, llvm::StringMap> Infos, + const ClangDocContext &CDCtx) { + StringSet<> CreatedDirs; + StringMap> FileToInfos; + for (const auto &Group : Infos) { + Info *Info = Group.getValue().get(); + + SmallString<128> Path; + sys::path::native(RootDir, Path); + sys::path::append(Path, Info->getRelativeFilePath("")); + if (!CreatedDirs.contains(Path)) { + if (std::error_code Err = sys::fs::create_directories(Path); + Err != std::error_code()) + return createFileError(Twine(Path), Err); + CreatedDirs.insert(Path); + } + + sys::path::append(Path, Info->getFileBaseName() + ".json"); + FileToInfos[Path].push_back(Info); + } + + for (const auto &Group : FileToInfos) { + std::error_code FileErr; + raw_fd_ostream InfoOS(Group.getKey(), FileErr, sys::fs::OF_Text); + if (FileErr) + return createFileError("cannot open file " + Group.getKey(), FileErr); + + for (const auto &Info : Group.getValue()) + if (Error Err = generateDocForInfo(Info, InfoOS, CDCtx)) + return Err; + } + + return Error::success(); +} + +Error JSONGenerator::generateDocForInfo(Info *I, raw_ostream &OS, + const ClangDocContext &CDCtx) { + json::Object Obj = Object(); + + switch (I->IT) { + case InfoType::IT_namespace: + break; + case InfoType::IT_record: + serializeInfo(*static_cast(I), Obj, CDCtx.RepositoryUrl); + break; + case InfoType::IT_enum: + case InfoType::IT_function: + case InfoType::IT_typedef: + break; + case InfoType::IT_default: + return createStringError(inconvertibleErrorCode(), "unexpected info type"); + } + OS << llvm::formatv("{0:2}", llvm::json::Value(std::move(Obj))); + return Error::success(); +} + +Error JSONGenerator::createResources(ClangDocContext &CDCtx) { + return Error::success(); +} + +static GeneratorRegistry::Add JSON(JSONGenerator::Format, + "Generator for JSON output."); +volatile int JSONGeneratorAnchorSource = 0; +} // namespace doc +} // namespace clang diff --git a/clang-tools-extra/clang-doc/tool/ClangDocMain.cpp b/clang-tools-extra/clang-doc/tool/ClangDocMain.cpp index 15de031aa609..3bb67baf6573 100644 --- a/clang-tools-extra/clang-doc/tool/ClangDocMain.cpp +++ b/clang-tools-extra/clang-doc/tool/ClangDocMain.cpp @@ -110,7 +110,7 @@ Turn on time profiler. Generates clang-doc-tracing.json)"), llvm::cl::init(false), llvm::cl::cat(ClangDocCategory)); -enum OutputFormatTy { md, yaml, html, mustache }; +enum OutputFormatTy { md, yaml, html, mustache, json }; static llvm::cl::opt FormatEnum( "format", llvm::cl::desc("Format for outputted docs."), @@ -121,7 +121,9 @@ static llvm::cl::opt FormatEnum( clEnumValN(OutputFormatTy::html, "html", "Documentation in HTML format."), clEnumValN(OutputFormatTy::mustache, "mustache", - "Documentation in mustache HTML format")), + "Documentation in mustache HTML format"), + clEnumValN(OutputFormatTy::json, "json", + "Documentation in JSON format")), llvm::cl::init(OutputFormatTy::yaml), llvm::cl::cat(ClangDocCategory)); static llvm::ExitOnError ExitOnErr; @@ -136,6 +138,8 @@ static std::string getFormatString() { return "html"; case OutputFormatTy::mustache: return "mustache"; + case OutputFormatTy::json: + return "json"; } llvm_unreachable("Unknown OutputFormatTy"); } diff --git a/clang-tools-extra/test/clang-doc/json/class-template.cpp b/clang-tools-extra/test/clang-doc/json/class-template.cpp new file mode 100644 index 000000000000..e3ca086d1d9a --- /dev/null +++ b/clang-tools-extra/test/clang-doc/json/class-template.cpp @@ -0,0 +1,29 @@ +// RUN: rm -rf %t && mkdir -p %t +// RUN: clang-doc --output=%t --format=json --executor=standalone %s +// RUN: FileCheck %s < %t/GlobalNamespace/MyClass.json + +template struct MyClass { + T MemberTemplate; + T method(T Param); +}; + +// CHECK: "Name": "MyClass", +// CHECK: "Name": "method", +// CHECK: "Params": [ +// CHECK-NEXT: { +// CHECK-NEXT: "Name": "Param", +// CHECK-NEXT: "Type": "T" +// CHECK-NEXT: } +// CHECK-NEXT: ], +// CHECK-NEXT: "ReturnType": { +// CHECK-NEXT: "IsBuiltIn": false, +// CHECK-NEXT: "IsTemplate": false, +// CHECK-NEXT: "Name": "T", +// CHECK-NEXT: "QualName": "T" +// CHECK-NEXT: "USR": "0000000000000000000000000000000000000000" +// CHECK: "Name": "MemberTemplate", +// CHECK: "Type": "T" +// CHECK: "Template": { +// CHECK-NEXT: "Parameters": [ +// CHECK-NEXT: "typename T" +// CHECK-NEXT: ] diff --git a/clang-tools-extra/test/clang-doc/json/class.cpp b/clang-tools-extra/test/clang-doc/json/class.cpp new file mode 100644 index 000000000000..9ae1f14e2d2a --- /dev/null +++ b/clang-tools-extra/test/clang-doc/json/class.cpp @@ -0,0 +1,193 @@ +// RUN: rm -rf %t && mkdir -p %t +// RUN: clang-doc --output=%t --format=json --executor=standalone %s +// RUN: FileCheck %s < %t/GlobalNamespace/MyClass.json + +struct Foo; + +// This is a nice class. +// It has some nice methods and fields. +// @brief This is a brief description. +struct MyClass { + int PublicField; + + int myMethod(int MyParam); + static void staticMethod(); + const int& getConst(); + + enum Color { + RED, + GREEN, + BLUE = 5 + }; + + typedef int MyTypedef; + + class NestedClass; +protected: + int protectedMethod(); + + int ProtectedField; +}; + +// CHECK: { +// CHECK-NEXT: "Description": [ +// CHECK-NEXT: { +// CHECK-NEXT: "FullComment": { +// CHECK-NEXT: "Children": [ +// CHECK-NEXT: { +// CHECK-NEXT: "ParagraphComment": { +// CHECK-NEXT: "Children": [ +// CHECK-NEXT: { +// CHECK-NEXT: "TextComment": " This is a nice class." +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "TextComment": " It has some nice methods and fields." +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "TextComment": "" +// CHECK-NEXT: } +// CHECK-NEXT: ] +// CHECK: { +// CHECK-NEXT: "BlockCommandComment": { +// CHECK-NEXT: "Children": [ +// CHECK-NEXT: { +// CHECK-NEXT: "ParagraphComment": { +// CHECK-NEXT: "Children": [ +// CHECK-NEXT: { +// CHECK-NEXT: "TextComment": " This is a brief description." +// CHECK-NEXT: } +// CHECK: "Command": "brief" +// CHECK: "Enums": [ +// CHECK-NEXT: { +// CHECK-NEXT: "Location": { +// CHECK-NEXT: "Filename": "{{.*}}class.cpp", +// CHECK-NEXT: "LineNumber": 17 +// CHECK-NEXT: }, +// CHECK-NEXT: "Members": [ +// CHECK-NEXT: { +// CHECK-NEXT: "Name": "RED", +// CHECK-NEXT: "Value": "0" +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "Name": "GREEN", +// CHECK-NEXT: "Value": "1" +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "Name": "BLUE", +// CHECK-NEXT: "ValueExpr": "5" +// CHECK-NEXT: } +// CHECK-NEXT: ], +// CHECK-NEXT: "Name": "Color", +// CHECK-NEXT: "Namespace": [ +// CHECK-NEXT: "MyClass", +// CHECK-NEXT: "GlobalNamespace" +// CHECK-NEXT: ], +// CHECK-NEXT: "Scoped": false, +// CHECK-NEXT: "USR": "{{[0-9A-F]*}}" +// CHECK-NEXT: } +// CHECK-NEXT: ], +// COM: FIXME: FullName is not emitted correctly. +// CHECK-NEXT: "FullName": "", +// CHECK-NEXT: "IsTypedef": false, +// CHECK-NEXT: "Location": { +// CHECK-NEXT: "Filename": "{{.*}}class.cpp", +// CHECK-NEXT: "LineNumber": 10 +// CHECK-NEXT: }, +// CHECK-NEXT: "Name": "MyClass", +// CHECK-NEXT: "Namespace": [ +// CHECK-NEXT: "GlobalNamespace" +// CHECK-NEXT: ], +// CHECK-NEXT: "Path": "GlobalNamespace", +// CHECK-NEXT: "ProtectedFunctions": [ +// CHECK-NEXT: { +// CHECK-NEXT: "IsStatic": false, +// CHECK-NEXT: "Name": "protectedMethod", +// CHECK-NEXT: "Namespace": [ +// CHECK-NEXT: "MyClass", +// CHECK-NEXT: "GlobalNamespace" +// CHECK-NEXT: ], +// CHECK-NEXT: "ReturnType": { +// CHECK-NEXT: "IsBuiltIn": false, +// CHECK-NEXT: "IsTemplate": false, +// CHECK-NEXT: "Name": "int", +// CHECK-NEXT: "QualName": "int", +// CHECK-NEXT: "USR": "{{[0-9A-F]*}}" +// CHECK-NEXT: }, +// CHECK-NEXT: "USR": "{{[0-9A-F]*}}" +// CHECK-NEXT: } +// CHECK-NEXT: ], +// CHECK-NEXT: "ProtectedMembers": [ +// CHECK-NEXT: { +// CHECK-NEXT: "Name": "ProtectedField", +// CHECK-NEXT: "Type": "int" +// CHECK-NEXT: } +// CHECK-NEXT: ], +// CHECK-NEXT: "PublicFunctions": [ +// CHECK-NEXT: { +// CHECK-NEXT: "IsStatic": false, +// CHECK-NEXT: "Name": "myMethod", +// CHECK-NEXT: "Namespace": [ +// CHECK-NEXT: "MyClass", +// CHECK-NEXT: "GlobalNamespace" +// CHECK-NEXT: ], +// CHECK-NEXT: "Params": [ +// CHECK-NEXT: { +// CHECK-NEXT: "Name": "MyParam", +// CHECK-NEXT: "Type": "int" +// CHECK-NEXT: } +// CHECK-NEXT: ], +// CHECK-NEXT: "ReturnType": { +// CHECK-NEXT: "IsBuiltIn": false, +// CHECK-NEXT: "IsTemplate": false, +// CHECK-NEXT: "Name": "int", +// CHECK-NEXT: "QualName": "int", +// CHECK-NEXT: "USR": "{{[0-9A-F]*}}" +// CHECK-NEXT: }, +// CHECK-NEXT: "USR": "{{[0-9A-F]*}}" +// CHECK-NEXT: }, +// CHECK: "IsStatic": true, +// CHECK: "Name": "getConst", +// CHECK: "ReturnType": { +// CHECK-NEXT: "IsBuiltIn": false, +// CHECK-NEXT: "IsTemplate": false, +// CHECK-NEXT: "Name": "const int &", +// CHECK-NEXT: "QualName": "const int &", +// CHECK-NEXT: "USR": "{{[0-9A-F]*}}" +// CHECK-NEXT: }, +// CHECK: "PublicMembers": [ +// CHECK-NEXT: { +// CHECK-NEXT: "Name": "PublicField", +// CHECK-NEXT: "Type": "int" +// CHECK-NEXT: } +// CHECK-NEXT: ], +// CHECK-NEXT: "Records": [ +// CHECK-NEXT: { +// CHECK-NEXT: "Name": "NestedClass", +// CHECK-NEXT: "Path": "GlobalNamespace{{[\/]+}}MyClass", +// CHECK-NEXT: "QualName": "NestedClass", +// CHECK-NEXT: "USR": "{{[0-9A-F]*}}" +// CHECK-NEXT: } +// CHECK-NEXT: ] +// CHECK-NEXT: "TagType": "struct", +// CHECK-NEXT: "Typedefs": [ +// CHECK-NEXT: { +// CHECK-NEXT: "IsUsing": false, +// CHECK-NEXT: "Location": { +// CHECK-NEXT: "Filename": "{{.*}}class.cpp", +// CHECK-NEXT: "LineNumber": 23 +// CHECK-NEXT: }, +// CHECK-NEXT: "Name": "MyTypedef", +// CHECK-NEXT: "Namespace": [ +// CHECK-NEXT: "MyClass", +// CHECK-NEXT: "GlobalNamespace" +// CHECK-NEXT: ], +// CHECK-NEXT: "TypeDeclaration": "", +// CHECK-NEXT: "USR": "{{[0-9A-F]*}}", +// CHECK-NEXT: "Underlying": { +// CHECK-NEXT: "IsBuiltIn": false, +// CHECK-NEXT: "IsTemplate": false, +// CHECK-NEXT: "Name": "int", +// CHECK-NEXT: "QualName": "int", +// CHECK-NEXT: "USR": "0000000000000000000000000000000000000000" +// CHECK: "USR": "{{[0-9A-F]*}}" +// CHECK-NEXT: } diff --git a/clang-tools-extra/test/clang-doc/json/method-template.cpp b/clang-tools-extra/test/clang-doc/json/method-template.cpp new file mode 100644 index 000000000000..c51a2706d1c2 --- /dev/null +++ b/clang-tools-extra/test/clang-doc/json/method-template.cpp @@ -0,0 +1,40 @@ +// RUN: rm -rf %t && mkdir -p %t +// RUN: clang-doc --output=%t --format=json --executor=standalone %s +// RUN: FileCheck %s < %t/GlobalNamespace/MyClass.json + +struct MyClass { + template T methodTemplate(T param) { + } +}; + +// CHECK: "PublicFunctions": [ +// CHECK-NEXT: { +// CHECK-NEXT: "IsStatic": false, +// CHECK-NEXT: "Location": { +// CHECK-NEXT: "Filename": "{{.*}}method-template.cpp", +// CHECK-NEXT: "LineNumber": 6 +// CHECK-NEXT: }, +// CHECK-NEXT: "Name": "methodTemplate", +// CHECK-NEXT: "Namespace": [ +// CHECK-NEXT: "MyClass", +// CHECK-NEXT: "GlobalNamespace" +// CHECK-NEXT: ], +// CHECK-NEXT: "Params": [ +// CHECK-NEXT: { +// CHECK-NEXT: "Name": "param", +// CHECK-NEXT: "Type": "T" +// CHECK-NEXT: } +// CHECK-NEXT: ], +// CHECK-NEXT: "ReturnType": { +// CHECK-NEXT: "IsBuiltIn": false, +// CHECK-NEXT: "IsTemplate": false, +// CHECK-NEXT: "Name": "T", +// CHECK-NEXT: "QualName": "T", +// CHECK-NEXT: "USR": "0000000000000000000000000000000000000000" +// CHECK-NEXT: }, +// CHECK-NEXT: "Template": { +// CHECK-NEXT: "Parameters": [ +// CHECK-NEXT: "class T" +// CHECK-NEXT: ] +// CHECK-NEXT: }, +// CHECK-NEXT: "USR": "{{[0-9A-F]*}}" diff --git a/clang-tools-extra/unittests/clang-doc/CMakeLists.txt b/clang-tools-extra/unittests/clang-doc/CMakeLists.txt index 59a856ed987d..18166acf9bbc 100644 --- a/clang-tools-extra/unittests/clang-doc/CMakeLists.txt +++ b/clang-tools-extra/unittests/clang-doc/CMakeLists.txt @@ -31,6 +31,7 @@ add_extra_unittest(ClangDocTests MergeTest.cpp SerializeTest.cpp YAMLGeneratorTest.cpp + JSONGeneratorTest.cpp ) clang_target_link_libraries(ClangDocTests diff --git a/clang-tools-extra/unittests/clang-doc/JSONGeneratorTest.cpp b/clang-tools-extra/unittests/clang-doc/JSONGeneratorTest.cpp new file mode 100644 index 000000000000..24516cbc34a5 --- /dev/null +++ b/clang-tools-extra/unittests/clang-doc/JSONGeneratorTest.cpp @@ -0,0 +1,175 @@ +#include "ClangDocTest.h" +#include "Generators.h" +#include "Representation.h" +#include "gtest/gtest.h" + +namespace clang { +namespace doc { + +static std::unique_ptr getJSONGenerator() { + auto G = doc::findGeneratorByName("json"); + if (!G) + return nullptr; + return std::move(G.get()); +} + +TEST(JSONGeneratorTest, emitRecordJSON) { + RecordInfo I; + I.Name = "Foo"; + // FIXME: FullName is not emitted correctly. + I.FullName = ""; + I.IsTypeDef = false; + I.Namespace.emplace_back(EmptySID, "GlobalNamespace", InfoType::IT_namespace); + I.Path = "GlobalNamespace"; + I.DefLoc = Location(1, 1, "main.cpp"); + I.TagType = TagTypeKind::Class; + + I.Template = TemplateInfo(); + I.Template->Params.emplace_back("class T"); + + I.Children.Enums.emplace_back(); + I.Children.Enums.back().Name = "Color"; + I.Children.Enums.back().Scoped = false; + I.Children.Enums.back().Members.emplace_back(); + I.Children.Enums.back().Members.back().Name = "RED"; + I.Children.Enums.back().Members.back().Value = "0"; + + I.Members.emplace_back(TypeInfo("int"), "X", AccessSpecifier::AS_protected); + + I.Bases.emplace_back(EmptySID, "F", "path/to/F", true, + AccessSpecifier::AS_public, true); + I.Bases.back().Children.Functions.emplace_back(); + I.Bases.back().Children.Functions.back().Name = "InheritedFunctionOne"; + I.Bases.back().Members.emplace_back(TypeInfo("int"), "N", + AccessSpecifier::AS_public); + + // F is in the global namespace + I.Parents.emplace_back(EmptySID, "F", InfoType::IT_record, ""); + I.VirtualParents.emplace_back(EmptySID, "G", InfoType::IT_record, + "path::to::G::G", "path/to/G"); + + I.Children.Records.emplace_back(EmptySID, "ChildStruct", InfoType::IT_record, + "path::to::A::r::ChildStruct", "path/to/A/r"); + I.Children.Functions.emplace_back(); + I.Children.Functions.back().Name = "OneFunction"; + + auto G = getJSONGenerator(); + assert(G); + std::string Buffer; + llvm::raw_string_ostream Actual(Buffer); + auto Err = G->generateDocForInfo(&I, Actual, ClangDocContext()); + assert(!Err); + std::string Expected = R"raw({ + "Bases": [ + { + "Access": "public", + "FullName": "", + "IsParent": true, + "IsTypedef": false, + "IsVirtual": true, + "Name": "F", + "Path": "path/to/F", + "PublicFunctions": [ + { + "IsStatic": false, + "Name": "InheritedFunctionOne", + "ReturnType": { + "IsBuiltIn": false, + "IsTemplate": false, + "Name": "", + "QualName": "", + "USR": "0000000000000000000000000000000000000000" + }, + "USR": "0000000000000000000000000000000000000000" + } + ], + "PublicMembers": [ + { + "Name": "N", + "Type": "int" + } + ], + "TagType": "struct", + "USR": "0000000000000000000000000000000000000000" + } + ], + "Enums": [ + { + "Members": [ + { + "Name": "RED", + "Value": "0" + } + ], + "Name": "Color", + "Scoped": false, + "USR": "0000000000000000000000000000000000000000" + } + ], + "FullName": "", + "IsTypedef": false, + "Location": { + "Filename": "main.cpp", + "LineNumber": 1 + }, + "Name": "Foo", + "Namespace": [ + "GlobalNamespace" + ], + "Parents": [ + { + "Name": "F", + "Path": "", + "QualName": "", + "USR": "0000000000000000000000000000000000000000" + } + ], + "Path": "GlobalNamespace", + "ProtectedMembers": [ + { + "Name": "X", + "Type": "int" + } + ], + "PublicFunctions": [ + { + "IsStatic": false, + "Name": "OneFunction", + "ReturnType": { + "IsBuiltIn": false, + "IsTemplate": false, + "Name": "", + "QualName": "", + "USR": "0000000000000000000000000000000000000000" + }, + "USR": "0000000000000000000000000000000000000000" + } + ], + "Records": [ + { + "Name": "ChildStruct", + "Path": "path/to/A/r", + "QualName": "path::to::A::r::ChildStruct", + "USR": "0000000000000000000000000000000000000000" + } + ], + "TagType": "class", + "Template": { + "Parameters": [ + "class T" + ] + }, + "USR": "0000000000000000000000000000000000000000", + "VirtualParents": [ + { + "Name": "G", + "Path": "path/to/G", + "QualName": "path::to::G::G", + "USR": "0000000000000000000000000000000000000000" + } + ] +})raw"; + EXPECT_EQ(Expected, Actual.str()); +} +} // namespace doc +} // namespace clang