[clang][extract-api] Add enum support

Add support for enum records
- Add `EnumConstantRecord` and `EnumRecord` to store API information for
  enums
- Implement `VisitEnumDecl` in `ExtractAPIVisitor`
- Implement serializatin for enum records and `MemberOf` relationship
- Add test case for enum records
- Few other improvements

Depends on D122160

Differential Revision: https://reviews.llvm.org/D121873
This commit is contained in:
Zixu Wang 2022-03-16 17:49:31 -07:00
parent 5a2e56b70e
commit 71b4c22612
8 changed files with 784 additions and 16 deletions

View File

@ -30,6 +30,25 @@
#include "llvm/Support/Casting.h"
#include <memory>
namespace {
/// \brief A custom deleter used for ``std::unique_ptr`` to APIRecords stored
/// in the BumpPtrAllocator.
///
/// \tparam T the exact type of the APIRecord subclass.
template <typename T> struct UniquePtrBumpPtrAllocatorDeleter {
void operator()(T *Instance) { Instance->~T(); }
};
/// A unique pointer to an APIRecord stored in the BumpPtrAllocator.
///
/// \tparam T the exact type of the APIRecord subclass.
template <typename T>
using APIRecordUniquePtr =
std::unique_ptr<T, UniquePtrBumpPtrAllocatorDeleter<T>>;
} // anonymous namespace
namespace clang {
namespace extractapi {
@ -73,6 +92,8 @@ struct APIRecord {
/// Discriminator for LLVM-style RTTI (dyn_cast<> et al.)
enum RecordKind {
RK_Global,
RK_EnumConstant,
RK_Enum,
};
private:
@ -125,6 +146,36 @@ private:
virtual void anchor();
};
/// This holds information associated with enum constants.
struct EnumConstantRecord : APIRecord {
EnumConstantRecord(StringRef Name, StringRef USR, PresumedLoc Loc,
const AvailabilityInfo &Availability,
const DocComment &Comment,
DeclarationFragments Declaration,
DeclarationFragments SubHeading)
: APIRecord(RK_EnumConstant, Name, USR, Loc, Availability,
LinkageInfo::none(), Comment, Declaration, SubHeading) {}
static bool classof(const APIRecord *Record) {
return Record->getKind() == RK_EnumConstant;
}
};
/// This holds information associated with enums.
struct EnumRecord : APIRecord {
SmallVector<APIRecordUniquePtr<EnumConstantRecord>> Constants;
EnumRecord(StringRef Name, StringRef USR, PresumedLoc Loc,
const AvailabilityInfo &Availability, const DocComment &Comment,
DeclarationFragments Declaration, DeclarationFragments SubHeading)
: APIRecord(RK_Enum, Name, USR, Loc, Availability, LinkageInfo::none(),
Comment, Declaration, SubHeading) {}
static bool classof(const APIRecord *Record) {
return Record->getKind() == RK_Enum;
}
};
/// APISet holds the set of API records collected from given inputs.
class APISet {
public:
@ -166,28 +217,41 @@ public:
DeclarationFragments SubHeading,
FunctionSignature Signature);
private:
/// \brief A custom deleter used for ``std::unique_ptr`` to APIRecords stored
/// in the BumpPtrAllocator.
/// Create and add an enum constant record into the API set.
///
/// \tparam T the exact type of the APIRecord subclass.
template <typename T> struct UniquePtrBumpPtrAllocatorDeleter {
void operator()(T *Instance) { Instance->~T(); }
};
/// Note: the caller is responsible for keeping the StringRef \p Name and
/// \p USR alive. APISet::copyString provides a way to copy strings into
/// APISet itself, and APISet::recordUSR(const Decl *D) is a helper method
/// to generate the USR for \c D and keep it alive in APISet.
EnumConstantRecord *addEnumConstant(EnumRecord *Enum, StringRef Name,
StringRef USR, PresumedLoc Loc,
const AvailabilityInfo &Availability,
const DocComment &Comment,
DeclarationFragments Declaration,
DeclarationFragments SubHeading);
public:
/// A unique pointer to an APIRecord stored in the BumpPtrAllocator.
/// Create and add an enum record into the API set.
///
/// \tparam T the exact type of the APIRecord subclass.
template <typename T>
using APIRecordUniquePtr =
std::unique_ptr<T, UniquePtrBumpPtrAllocatorDeleter<T>>;
/// Note: the caller is responsible for keeping the StringRef \p Name and
/// \p USR alive. APISet::copyString provides a way to copy strings into
/// APISet itself, and APISet::recordUSR(const Decl *D) is a helper method
/// to generate the USR for \c D and keep it alive in APISet.
EnumRecord *addEnum(StringRef Name, StringRef USR, PresumedLoc Loc,
const AvailabilityInfo &Availability,
const DocComment &Comment,
DeclarationFragments Declaration,
DeclarationFragments SubHeading);
/// A map to store the set of GlobalRecord%s with the declaration name as the
/// key.
using GlobalRecordMap =
llvm::MapVector<StringRef, APIRecordUniquePtr<GlobalRecord>>;
/// A map to store the set of EnumRecord%s with the declaration name as the
/// key.
using EnumRecordMap =
llvm::MapVector<StringRef, APIRecordUniquePtr<EnumRecord>>;
/// Get the target triple for the ExtractAPI invocation.
const llvm::Triple &getTarget() const { return Target; }
@ -195,6 +259,7 @@ public:
const LangOptions &getLangOpts() const { return LangOpts; }
const GlobalRecordMap &getGlobals() const { return Globals; }
const EnumRecordMap &getEnums() const { return Enums; }
/// Generate and store the USR of declaration \p D.
///
@ -219,6 +284,7 @@ private:
const LangOptions LangOpts;
GlobalRecordMap Globals;
EnumRecordMap Enums;
};
} // namespace extractapi

View File

@ -188,6 +188,14 @@ public:
/// Build DeclarationFragments for a function declaration FunctionDecl.
static DeclarationFragments getFragmentsForFunction(const FunctionDecl *);
/// Build DeclarationFragments for an enum constant declaration
/// EnumConstantDecl.
static DeclarationFragments
getFragmentsForEnumConstant(const EnumConstantDecl *);
/// Build DeclarationFragments for an enum declaration EnumDecl.
static DeclarationFragments getFragmentsForEnum(const EnumDecl *);
/// Build sub-heading fragments for a NamedDecl.
static DeclarationFragments getSubHeading(const NamedDecl *);

View File

@ -56,6 +56,17 @@ public:
/// write out the serialized JSON object to \p os.
void serialize(raw_ostream &os) override;
/// The kind of a relationship between two symbols.
enum RelationshipKind {
/// The source symbol is a member of the target symbol.
/// For example enum constants are members of the enum, class/instance
/// methods are members of the class, etc.
MemberOf,
};
/// Get the string representation of the relationship kind.
static StringRef getRelationshipString(RelationshipKind Kind);
private:
/// Synthesize the metadata section of the Symbol Graph format.
///
@ -86,9 +97,19 @@ private:
/// containing common symbol information of \p Record.
Optional<Object> serializeAPIRecord(const APIRecord &Record) const;
/// Serialize the \p Kind relationship between \p Source and \p Target.
///
/// Record the relationship between the two symbols in
/// SymbolGraphSerializer::Relationships.
void serializeRelationship(RelationshipKind Kind, const APIRecord &Source,
const APIRecord &Target);
/// Serialize a global record.
void serializeGlobalRecord(const GlobalRecord &Record);
/// Serialize an enum record.
void serializeEnumRecord(const EnumRecord &Record);
public:
SymbolGraphSerializer(const APISet &API, StringRef ProductName,
APISerializerOption Options = {})

View File

@ -59,6 +59,31 @@ APISet::addFunction(StringRef Name, StringRef USR, PresumedLoc Loc,
Comment, Fragments, SubHeading, Signature);
}
EnumConstantRecord *APISet::addEnumConstant(
EnumRecord *Enum, StringRef Name, StringRef USR, PresumedLoc Loc,
const AvailabilityInfo &Availability, const DocComment &Comment,
DeclarationFragments Declaration, DeclarationFragments SubHeading) {
auto Record =
APIRecordUniquePtr<EnumConstantRecord>(new (Allocator) EnumConstantRecord{
Name, USR, Loc, Availability, Comment, Declaration, SubHeading});
return Enum->Constants.emplace_back(std::move(Record)).get();
}
EnumRecord *APISet::addEnum(StringRef Name, StringRef USR, PresumedLoc Loc,
const AvailabilityInfo &Availability,
const DocComment &Comment,
DeclarationFragments Declaration,
DeclarationFragments SubHeading) {
auto Result = Enums.insert({Name, nullptr});
if (Result.second) {
// Create the record if it does not already exist.
auto Record = APIRecordUniquePtr<EnumRecord>(new (Allocator) EnumRecord{
Name, USR, Loc, Availability, Comment, Declaration, SubHeading});
Result.first->second = std::move(Record);
}
return Result.first->second.get();
}
StringRef APISet::recordUSR(const Decl *D) {
SmallString<128> USR;
index::generateUSRForDecl(D, USR);

View File

@ -401,6 +401,35 @@ DeclarationFragmentsBuilder::getFragmentsForFunction(const FunctionDecl *Func) {
return Fragments;
}
DeclarationFragments DeclarationFragmentsBuilder::getFragmentsForEnumConstant(
const EnumConstantDecl *EnumConstDecl) {
DeclarationFragments Fragments;
return Fragments.append(EnumConstDecl->getName(),
DeclarationFragments::FragmentKind::Identifier);
}
DeclarationFragments
DeclarationFragmentsBuilder::getFragmentsForEnum(const EnumDecl *EnumDecl) {
// TODO: After we support typedef records, if there's a typedef for this enum
// just use the declaration fragments of the typedef decl.
DeclarationFragments Fragments, After;
Fragments.append("enum", DeclarationFragments::FragmentKind::Keyword);
if (!EnumDecl->getName().empty())
Fragments.appendSpace().append(
EnumDecl->getName(), DeclarationFragments::FragmentKind::Identifier);
QualType IntegerType = EnumDecl->getIntegerType();
if (!IntegerType.isNull())
Fragments.append(": ", DeclarationFragments::FragmentKind::Text)
.append(
getFragmentsForType(IntegerType, EnumDecl->getASTContext(), After))
.append(std::move(After));
return Fragments;
}
FunctionSignature
DeclarationFragmentsBuilder::getFunctionSignature(const FunctionDecl *Func) {
FunctionSignature Signature;

View File

@ -145,6 +145,40 @@ public:
return true;
}
bool VisitEnumDecl(const EnumDecl *Decl) {
if (!Decl->isComplete())
return true;
// Skip forward declaration.
if (!Decl->isThisDeclarationADefinition())
return true;
// Collect symbol information.
StringRef Name = Decl->getName();
StringRef USR = API.recordUSR(Decl);
PresumedLoc Loc =
Context.getSourceManager().getPresumedLoc(Decl->getLocation());
AvailabilityInfo Availability = getAvailability(Decl);
DocComment Comment;
if (auto *RawComment = Context.getRawCommentForDeclNoCache(Decl))
Comment = RawComment->getFormattedLines(Context.getSourceManager(),
Context.getDiagnostics());
// Build declaration fragments and sub-heading for the enum.
DeclarationFragments Declaration =
DeclarationFragmentsBuilder::getFragmentsForEnum(Decl);
DeclarationFragments SubHeading =
DeclarationFragmentsBuilder::getSubHeading(Decl);
EnumRecord *EnumRecord = API.addEnum(Name, USR, Loc, Availability, Comment,
Declaration, SubHeading);
// Now collect information about the enumerators in this enum.
recordEnumConstants(EnumRecord, Decl->enumerators());
return true;
}
private:
/// Get availability information of the declaration \p D.
AvailabilityInfo getAvailability(const Decl *D) const {
@ -177,6 +211,33 @@ private:
return Availability;
}
/// Collect API information for the enum constants and associate with the
/// parent enum.
void recordEnumConstants(EnumRecord *EnumRecord,
const EnumDecl::enumerator_range Constants) {
for (const auto *Constant : Constants) {
// Collect symbol information.
StringRef Name = Constant->getName();
StringRef USR = API.recordUSR(Constant);
PresumedLoc Loc =
Context.getSourceManager().getPresumedLoc(Constant->getLocation());
AvailabilityInfo Availability = getAvailability(Constant);
DocComment Comment;
if (auto *RawComment = Context.getRawCommentForDeclNoCache(Constant))
Comment = RawComment->getFormattedLines(Context.getSourceManager(),
Context.getDiagnostics());
// Build declaration fragments and sub-heading for the enum constant.
DeclarationFragments Declaration =
DeclarationFragmentsBuilder::getFragmentsForEnumConstant(Constant);
DeclarationFragments SubHeading =
DeclarationFragmentsBuilder::getSubHeading(Constant);
API.addEnumConstant(EnumRecord, Name, USR, Loc, Availability, Comment,
Declaration, SubHeading);
}
}
ASTContext &Context;
APISet API;
};

View File

@ -336,17 +336,21 @@ Object serializeNames(const APIRecord &Record) {
/// the kind, and a \c displayName for rendering human-readable names.
Object serializeSymbolKind(const APIRecord &Record,
const LangOptions &LangOpts) {
auto AddLangPrefix = [&LangOpts](StringRef S) -> std::string {
return (getLanguageName(LangOpts) + "." + S).str();
};
Object Kind;
switch (Record.getKind()) {
case APIRecord::RK_Global:
case APIRecord::RK_Global: {
auto *GR = dyn_cast<GlobalRecord>(&Record);
switch (GR->GlobalKind) {
case GVKind::Function:
Kind["identifier"] = (getLanguageName(LangOpts) + ".func").str();
Kind["identifier"] = AddLangPrefix("func");
Kind["displayName"] = "Function";
break;
case GVKind::Variable:
Kind["identifier"] = (getLanguageName(LangOpts) + ".var").str();
Kind["identifier"] = AddLangPrefix("var");
Kind["displayName"] = "Global Variable";
break;
case GVKind::Unknown:
@ -355,6 +359,15 @@ Object serializeSymbolKind(const APIRecord &Record,
}
break;
}
case APIRecord::RK_EnumConstant:
Kind["identifier"] = AddLangPrefix("enum.case");
Kind["displayName"] = "Enumeration Case";
break;
case APIRecord::RK_Enum:
Kind["identifier"] = AddLangPrefix("enum");
Kind["displayName"] = "Enumeration";
break;
}
return Kind;
}
@ -413,6 +426,25 @@ SymbolGraphSerializer::serializeAPIRecord(const APIRecord &Record) const {
return Obj;
}
StringRef SymbolGraphSerializer::getRelationshipString(RelationshipKind Kind) {
switch (Kind) {
case RelationshipKind::MemberOf:
return "memberOf";
}
llvm_unreachable("Unhandled relationship kind");
}
void SymbolGraphSerializer::serializeRelationship(RelationshipKind Kind,
const APIRecord &Source,
const APIRecord &Target) {
Object Relationship;
Relationship["source"] = Source.USR;
Relationship["target"] = Target.USR;
Relationship["kind"] = getRelationshipString(Kind);
Relationships.emplace_back(std::move(Relationship));
}
void SymbolGraphSerializer::serializeGlobalRecord(const GlobalRecord &Record) {
auto Obj = serializeAPIRecord(Record);
if (!Obj)
@ -425,6 +457,23 @@ void SymbolGraphSerializer::serializeGlobalRecord(const GlobalRecord &Record) {
Symbols.emplace_back(std::move(*Obj));
}
void SymbolGraphSerializer::serializeEnumRecord(const EnumRecord &Record) {
auto Enum = serializeAPIRecord(Record);
if (!Enum)
return;
Symbols.emplace_back(std::move(*Enum));
for (const auto &Constant : Record.Constants) {
auto EnumConstant = serializeAPIRecord(*Constant);
if (!EnumConstant)
continue;
Symbols.emplace_back(std::move(*EnumConstant));
serializeRelationship(RelationshipKind::MemberOf, *Constant, Record);
}
}
Object SymbolGraphSerializer::serialize() {
Object Root;
serializeObject(Root, "metadata", serializeMetadata());
@ -434,6 +483,10 @@ Object SymbolGraphSerializer::serialize() {
for (const auto &Global : API.getGlobals())
serializeGlobalRecord(*Global.second);
// Serialize enum records in the API set.
for (const auto &Enum : API.getEnums())
serializeEnumRecord(*Enum.second);
Root["symbols"] = std::move(Symbols);
Root["relationhips"] = std::move(Relationships);

View File

@ -0,0 +1,505 @@
// RUN: rm -rf %t
// RUN: split-file %s %t
// RUN: sed -e "s@INPUT_DIR@%/t@g" %t/reference.output.json.in >> \
// RUN: %t/reference.output.json
// RUN: %clang -extract-api -target arm64-apple-macosx \
// RUN: %t/input.h -o %t/output.json | FileCheck -allow-empty %s
// Generator version is not consistent across test runs, normalize it.
// RUN: sed -e "s@\"generator\": \".*\"@\"generator\": \"?\"@g" \
// RUN: %t/output.json >> %t/output-normalized.json
// RUN: diff %t/reference.output.json %t/output-normalized.json
// CHECK-NOT: error:
// CHECK-NOT: warning:
//--- input.h
/// Kinds of vehicles
enum Vehicle {
Bicycle,
Car,
Train, ///< Move this to the top! -Sheldon
Ship,
Airplane,
};
enum Direction : unsigned char {
North = 0,
East,
South,
West
};
//--- reference.output.json.in
{
"metadata": {
"formatVersion": {
"major": 0,
"minor": 5,
"patch": 3
},
"generator": "?"
},
"module": {
"name": "",
"platform": {
"architecture": "arm64",
"operatingSystem": {
"minimumVersion": {
"major": 11,
"minor": 0,
"patch": 0
},
"name": "macosx"
},
"vendor": "apple"
}
},
"relationhips": [
{
"kind": "memberOf",
"source": "c:@E@Vehicle@Bicycle",
"target": "c:@E@Vehicle"
},
{
"kind": "memberOf",
"source": "c:@E@Vehicle@Car",
"target": "c:@E@Vehicle"
},
{
"kind": "memberOf",
"source": "c:@E@Vehicle@Train",
"target": "c:@E@Vehicle"
},
{
"kind": "memberOf",
"source": "c:@E@Vehicle@Ship",
"target": "c:@E@Vehicle"
},
{
"kind": "memberOf",
"source": "c:@E@Vehicle@Airplane",
"target": "c:@E@Vehicle"
},
{
"kind": "memberOf",
"source": "c:@E@Direction@North",
"target": "c:@E@Direction"
},
{
"kind": "memberOf",
"source": "c:@E@Direction@East",
"target": "c:@E@Direction"
},
{
"kind": "memberOf",
"source": "c:@E@Direction@South",
"target": "c:@E@Direction"
},
{
"kind": "memberOf",
"source": "c:@E@Direction@West",
"target": "c:@E@Direction"
}
],
"symbols": [
{
"declarationFragments": [
{
"kind": "keyword",
"spelling": "enum"
},
{
"kind": "text",
"spelling": " "
},
{
"kind": "identifier",
"spelling": "Vehicle"
},
{
"kind": "text",
"spelling": ": "
},
{
"kind": "typeIdentifier",
"preciseIdentifier": "c:i",
"spelling": "unsigned int"
}
],
"docComment": {
"lines": [
{
"range": {
"end": {
"character": 22,
"line": 1
},
"start": {
"character": 5,
"line": 1
}
},
"text": "Kinds of vehicles"
}
]
},
"identifier": {
"interfaceLanguage": "c",
"precise": "c:@E@Vehicle"
},
"kind": {
"displayName": "Enumeration",
"identifier": "c.enum"
},
"location": {
"character": 6,
"line": 2,
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "identifier",
"spelling": "Vehicle"
}
],
"title": "Vehicle"
}
},
{
"declarationFragments": [
{
"kind": "identifier",
"spelling": "Bicycle"
}
],
"identifier": {
"interfaceLanguage": "c",
"precise": "c:@E@Vehicle@Bicycle"
},
"kind": {
"displayName": "Enumeration Case",
"identifier": "c.enum.case"
},
"location": {
"character": 3,
"line": 3,
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "identifier",
"spelling": "Bicycle"
}
],
"title": "Bicycle"
}
},
{
"declarationFragments": [
{
"kind": "identifier",
"spelling": "Car"
}
],
"identifier": {
"interfaceLanguage": "c",
"precise": "c:@E@Vehicle@Car"
},
"kind": {
"displayName": "Enumeration Case",
"identifier": "c.enum.case"
},
"location": {
"character": 3,
"line": 4,
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "identifier",
"spelling": "Car"
}
],
"title": "Car"
}
},
{
"declarationFragments": [
{
"kind": "identifier",
"spelling": "Train"
}
],
"docComment": {
"lines": [
{
"range": {
"end": {
"character": 45,
"line": 5
},
"start": {
"character": 15,
"line": 5
}
},
"text": "Move this to the top! -Sheldon"
}
]
},
"identifier": {
"interfaceLanguage": "c",
"precise": "c:@E@Vehicle@Train"
},
"kind": {
"displayName": "Enumeration Case",
"identifier": "c.enum.case"
},
"location": {
"character": 3,
"line": 5,
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "identifier",
"spelling": "Train"
}
],
"title": "Train"
}
},
{
"declarationFragments": [
{
"kind": "identifier",
"spelling": "Ship"
}
],
"identifier": {
"interfaceLanguage": "c",
"precise": "c:@E@Vehicle@Ship"
},
"kind": {
"displayName": "Enumeration Case",
"identifier": "c.enum.case"
},
"location": {
"character": 3,
"line": 6,
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "identifier",
"spelling": "Ship"
}
],
"title": "Ship"
}
},
{
"declarationFragments": [
{
"kind": "identifier",
"spelling": "Airplane"
}
],
"identifier": {
"interfaceLanguage": "c",
"precise": "c:@E@Vehicle@Airplane"
},
"kind": {
"displayName": "Enumeration Case",
"identifier": "c.enum.case"
},
"location": {
"character": 3,
"line": 7,
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "identifier",
"spelling": "Airplane"
}
],
"title": "Airplane"
}
},
{
"declarationFragments": [
{
"kind": "keyword",
"spelling": "enum"
},
{
"kind": "text",
"spelling": " "
},
{
"kind": "identifier",
"spelling": "Direction"
},
{
"kind": "text",
"spelling": ": "
},
{
"kind": "typeIdentifier",
"preciseIdentifier": "c:c",
"spelling": "unsigned char"
}
],
"identifier": {
"interfaceLanguage": "c",
"precise": "c:@E@Direction"
},
"kind": {
"displayName": "Enumeration",
"identifier": "c.enum"
},
"location": {
"character": 6,
"line": 10,
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "identifier",
"spelling": "Direction"
}
],
"title": "Direction"
}
},
{
"declarationFragments": [
{
"kind": "identifier",
"spelling": "North"
}
],
"identifier": {
"interfaceLanguage": "c",
"precise": "c:@E@Direction@North"
},
"kind": {
"displayName": "Enumeration Case",
"identifier": "c.enum.case"
},
"location": {
"character": 3,
"line": 11,
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "identifier",
"spelling": "North"
}
],
"title": "North"
}
},
{
"declarationFragments": [
{
"kind": "identifier",
"spelling": "East"
}
],
"identifier": {
"interfaceLanguage": "c",
"precise": "c:@E@Direction@East"
},
"kind": {
"displayName": "Enumeration Case",
"identifier": "c.enum.case"
},
"location": {
"character": 3,
"line": 12,
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "identifier",
"spelling": "East"
}
],
"title": "East"
}
},
{
"declarationFragments": [
{
"kind": "identifier",
"spelling": "South"
}
],
"identifier": {
"interfaceLanguage": "c",
"precise": "c:@E@Direction@South"
},
"kind": {
"displayName": "Enumeration Case",
"identifier": "c.enum.case"
},
"location": {
"character": 3,
"line": 13,
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "identifier",
"spelling": "South"
}
],
"title": "South"
}
},
{
"declarationFragments": [
{
"kind": "identifier",
"spelling": "West"
}
],
"identifier": {
"interfaceLanguage": "c",
"precise": "c:@E@Direction@West"
},
"kind": {
"displayName": "Enumeration Case",
"identifier": "c.enum.case"
},
"location": {
"character": 3,
"line": 14,
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "identifier",
"spelling": "West"
}
],
"title": "West"
}
}
]
}