[clang][extract-api] Add Objective-C Category support

Add (partial) support for Objective-C category records in ExtractAPI.
The current ExtractAPI collects everything for an Objective-C category,
but not fully serialized in the SymbolGraphSerializer. Categories
extending external interfaces are disgarded during serialization, and
categories extending known interfaces are merged (all members surfaced)
into the interfaces.

Differential Revision: https://reviews.llvm.org/D122774
This commit is contained in:
Zixu Wang 2022-03-30 17:21:33 -07:00
parent 6b306233f7
commit 178aad9b94
8 changed files with 473 additions and 60 deletions

View File

@ -86,6 +86,7 @@ struct APIRecord {
RK_ObjCIvar,
RK_ObjCMethod,
RK_ObjCInterface,
RK_ObjCCategory,
RK_ObjCProtocol,
RK_MacroDefinition,
RK_Typedef,
@ -340,9 +341,33 @@ struct ObjCContainerRecord : APIRecord {
virtual ~ObjCContainerRecord() = 0;
};
/// This holds information associated with Objective-C categories.
struct ObjCCategoryRecord : ObjCContainerRecord {
SymbolReference Interface;
ObjCCategoryRecord(StringRef Name, StringRef USR, PresumedLoc Loc,
const AvailabilityInfo &Availability,
const DocComment &Comment,
DeclarationFragments Declaration,
DeclarationFragments SubHeading, SymbolReference Interface)
: ObjCContainerRecord(RK_ObjCCategory, Name, USR, Loc, Availability,
LinkageInfo::none(), Comment, Declaration,
SubHeading),
Interface(Interface) {}
static bool classof(const APIRecord *Record) {
return Record->getKind() == RK_ObjCCategory;
}
private:
virtual void anchor();
};
/// This holds information associated with Objective-C interfaces/classes.
struct ObjCInterfaceRecord : ObjCContainerRecord {
SymbolReference SuperClass;
// ObjCCategoryRecord%s are stored in and owned by APISet.
SmallVector<ObjCCategoryRecord *> Categories;
ObjCInterfaceRecord(StringRef Name, StringRef USR, PresumedLoc Loc,
const AvailabilityInfo &Availability, LinkageInfo Linkage,
@ -512,6 +537,18 @@ public:
DeclarationFragments Declaration,
DeclarationFragments SubHeading);
/// Create and add an Objective-C category record into the API set.
///
/// 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.
ObjCCategoryRecord *
addObjCCategory(StringRef Name, StringRef USR, PresumedLoc Loc,
const AvailabilityInfo &Availability,
const DocComment &Comment, DeclarationFragments Declaration,
DeclarationFragments SubHeading, SymbolReference Interface);
/// Create and add an Objective-C interface record into the API set.
///
/// Note: the caller is responsible for keeping the StringRef \p Name and
@ -618,6 +655,9 @@ public:
const RecordMap<GlobalRecord> &getGlobals() const { return Globals; }
const RecordMap<EnumRecord> &getEnums() const { return Enums; }
const RecordMap<StructRecord> &getStructs() const { return Structs; }
const RecordMap<ObjCCategoryRecord> &getObjCCategories() const {
return ObjCCategories;
}
const RecordMap<ObjCInterfaceRecord> &getObjCInterfaces() const {
return ObjCInterfaces;
}
@ -662,6 +702,7 @@ private:
RecordMap<GlobalRecord> Globals;
RecordMap<EnumRecord> Enums;
RecordMap<StructRecord> Structs;
RecordMap<ObjCCategoryRecord> ObjCCategories;
RecordMap<ObjCInterfaceRecord> ObjCInterfaces;
RecordMap<ObjCProtocolRecord> ObjCProtocols;
RecordMap<MacroDefinitionRecord> Macros;

View File

@ -204,6 +204,11 @@ public:
/// Build DeclarationFragments for a struct record declaration RecordDecl.
static DeclarationFragments getFragmentsForStruct(const RecordDecl *);
/// Build DeclarationFragments for an Objective-C category declaration
/// ObjCCategoryDecl.
static DeclarationFragments
getFragmentsForObjCCategory(const ObjCCategoryDecl *);
/// Build DeclarationFragments for an Objective-C interface declaration
/// ObjCInterfaceDecl.
static DeclarationFragments

View File

@ -124,6 +124,12 @@ private:
/// containing common symbol information of \p Record.
Optional<Object> serializeAPIRecord(const APIRecord &Record) const;
/// Helper method to serialize second-level member records of \p Record and
/// the member-of relationships.
template <typename MemberTy>
void serializeMembers(const APIRecord &Record,
const SmallVector<std::unique_ptr<MemberTy>> &Members);
/// Serialize the \p Kind relationship between \p Source and \p Target.
///
/// Record the relationship between the two symbols in

View File

@ -109,6 +109,24 @@ StructRecord *APISet::addStruct(StringRef Name, StringRef USR, PresumedLoc Loc,
Declaration, SubHeading);
}
ObjCCategoryRecord *APISet::addObjCCategory(
StringRef Name, StringRef USR, PresumedLoc Loc,
const AvailabilityInfo &Availability, const DocComment &Comment,
DeclarationFragments Declaration, DeclarationFragments SubHeading,
SymbolReference Interface) {
// Create the category record.
auto *Record = addTopLevelRecord(ObjCCategories, Name, USR, Loc, Availability,
Comment, Declaration, SubHeading, Interface);
// If this category is extending a known interface, associate it with the
// ObjCInterfaceRecord.
auto It = ObjCInterfaces.find(Interface.Name);
if (It != ObjCInterfaces.end())
It->second->Categories.push_back(Record);
return Record;
}
ObjCInterfaceRecord *APISet::addObjCInterface(
StringRef Name, StringRef USR, PresumedLoc Loc,
const AvailabilityInfo &Availability, LinkageInfo Linkage,
@ -219,6 +237,7 @@ void StructRecord::anchor() {}
void ObjCPropertyRecord::anchor() {}
void ObjCInstanceVariableRecord::anchor() {}
void ObjCMethodRecord::anchor() {}
void ObjCCategoryRecord::anchor() {}
void ObjCInterfaceRecord::anchor() {}
void ObjCProtocolRecord::anchor() {}
void MacroDefinitionRecord::anchor() {}

View File

@ -526,6 +526,25 @@ DeclarationFragmentsBuilder::getFragmentsForMacro(StringRef Name,
return Fragments;
}
DeclarationFragments DeclarationFragmentsBuilder::getFragmentsForObjCCategory(
const ObjCCategoryDecl *Category) {
DeclarationFragments Fragments;
SmallString<128> InterfaceUSR;
index::generateUSRForDecl(Category->getClassInterface(), InterfaceUSR);
Fragments.append("@interface", DeclarationFragments::FragmentKind::Keyword)
.appendSpace()
.append(Category->getClassInterface()->getName(),
DeclarationFragments::FragmentKind::TypeIdentifier, InterfaceUSR)
.append(" (", DeclarationFragments::FragmentKind::Text)
.append(Category->getName(),
DeclarationFragments::FragmentKind::Identifier)
.append(")", DeclarationFragments::FragmentKind::Text);
return Fragments;
}
DeclarationFragments DeclarationFragmentsBuilder::getFragmentsForObjCInterface(
const ObjCInterfaceDecl *Interface) {
DeclarationFragments Fragments;

View File

@ -338,6 +338,39 @@ public:
return true;
}
bool VisitObjCCategoryDecl(const ObjCCategoryDecl *Decl) {
// 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 category.
DeclarationFragments Declaration =
DeclarationFragmentsBuilder::getFragmentsForObjCCategory(Decl);
DeclarationFragments SubHeading =
DeclarationFragmentsBuilder::getSubHeading(Decl);
const ObjCInterfaceDecl *InterfaceDecl = Decl->getClassInterface();
SymbolReference Interface(InterfaceDecl->getName(),
API.recordUSR(InterfaceDecl));
ObjCCategoryRecord *ObjCCategoryRecord =
API.addObjCCategory(Name, USR, Loc, Availability, Comment, Declaration,
SubHeading, Interface);
recordObjCMethods(ObjCCategoryRecord, Decl->methods());
recordObjCProperties(ObjCCategoryRecord, Decl->properties());
recordObjCInstanceVariables(ObjCCategoryRecord, Decl->ivars());
recordObjCProtocols(ObjCCategoryRecord, Decl->protocols());
return true;
}
private:
/// Get availability information of the declaration \p D.
AvailabilityInfo getAvailability(const Decl *D) const {

View File

@ -401,6 +401,11 @@ Object serializeSymbolKind(const APIRecord &Record, Language Lang) {
Kind["identifier"] = AddLangPrefix("class");
Kind["displayName"] = "Class";
break;
case APIRecord::RK_ObjCCategory:
// We don't serialize out standalone Objective-C category symbols yet.
llvm_unreachable("Serializing standalone Objective-C category symbols is "
"not supported.");
break;
case APIRecord::RK_ObjCProtocol:
Kind["identifier"] = AddLangPrefix("protocol");
Kind["displayName"] = "Protocol";
@ -476,6 +481,21 @@ SymbolGraphSerializer::serializeAPIRecord(const APIRecord &Record) const {
return Obj;
}
template <typename MemberTy>
void SymbolGraphSerializer::serializeMembers(
const APIRecord &Record,
const SmallVector<std::unique_ptr<MemberTy>> &Members) {
for (const auto &Member : Members) {
auto MemberPathComponentGuard = makePathComponentGuard(Member->Name);
auto MemberRecord = serializeAPIRecord(*Member);
if (!MemberRecord)
continue;
Symbols.emplace_back(std::move(*MemberRecord));
serializeRelationship(RelationshipKind::MemberOf, *Member, Record);
}
}
StringRef SymbolGraphSerializer::getRelationshipString(RelationshipKind Kind) {
switch (Kind) {
case RelationshipKind::MemberOf:
@ -520,18 +540,7 @@ void SymbolGraphSerializer::serializeEnumRecord(const EnumRecord &Record) {
return;
Symbols.emplace_back(std::move(*Enum));
for (const auto &Constant : Record.Constants) {
auto EnumConstantPathComponentGuard =
makePathComponentGuard(Constant->Name);
auto EnumConstant = serializeAPIRecord(*Constant);
if (!EnumConstant)
continue;
Symbols.emplace_back(std::move(*EnumConstant));
serializeRelationship(RelationshipKind::MemberOf, *Constant, Record);
}
serializeMembers(Record, Record.Constants);
}
void SymbolGraphSerializer::serializeStructRecord(const StructRecord &Record) {
@ -541,17 +550,7 @@ void SymbolGraphSerializer::serializeStructRecord(const StructRecord &Record) {
return;
Symbols.emplace_back(std::move(*Struct));
for (const auto &Field : Record.Fields) {
auto StructFieldPathComponentGuard = makePathComponentGuard(Field->Name);
auto StructField = serializeAPIRecord(*Field);
if (!StructField)
continue;
Symbols.emplace_back(std::move(*StructField));
serializeRelationship(RelationshipKind::MemberOf, *Field, Record);
}
serializeMembers(Record, Record.Fields);
}
void SymbolGraphSerializer::serializeObjCContainerRecord(
@ -563,53 +562,33 @@ void SymbolGraphSerializer::serializeObjCContainerRecord(
Symbols.emplace_back(std::move(*ObjCContainer));
// Record instance variables and that the instance variables are members of
// the container.
for (const auto &Ivar : Record.Ivars) {
auto IvarPathComponentGuard = makePathComponentGuard(Ivar->Name);
auto ObjCIvar = serializeAPIRecord(*Ivar);
if (!ObjCIvar)
continue;
Symbols.emplace_back(std::move(*ObjCIvar));
serializeRelationship(RelationshipKind::MemberOf, *Ivar, Record);
}
// Record methods and that the methods are members of the container.
for (const auto &Method : Record.Methods) {
auto MethodPathComponentGuard = makePathComponentGuard(Method->Name);
auto ObjCMethod = serializeAPIRecord(*Method);
if (!ObjCMethod)
continue;
Symbols.emplace_back(std::move(*ObjCMethod));
serializeRelationship(RelationshipKind::MemberOf, *Method, Record);
}
// Record properties and that the properties are members of the container.
for (const auto &Property : Record.Properties) {
auto PropertyPathComponentGuard = makePathComponentGuard(Property->Name);
auto ObjCProperty = serializeAPIRecord(*Property);
if (!ObjCProperty)
continue;
Symbols.emplace_back(std::move(*ObjCProperty));
serializeRelationship(RelationshipKind::MemberOf, *Property, Record);
}
serializeMembers(Record, Record.Ivars);
serializeMembers(Record, Record.Methods);
serializeMembers(Record, Record.Properties);
for (const auto &Protocol : Record.Protocols)
// Record that Record conforms to Protocol.
serializeRelationship(RelationshipKind::ConformsTo, Record, Protocol);
if (auto *ObjCInterface = dyn_cast<ObjCInterfaceRecord>(&Record))
if (auto *ObjCInterface = dyn_cast<ObjCInterfaceRecord>(&Record)) {
if (!ObjCInterface->SuperClass.empty())
// If Record is an Objective-C interface record and it has a super class,
// record that Record is inherited from SuperClass.
serializeRelationship(RelationshipKind::InheritsFrom, Record,
ObjCInterface->SuperClass);
// Members of categories extending an interface are serialized as members of
// the interface.
for (const auto *Category : ObjCInterface->Categories) {
serializeMembers(Record, Category->Ivars);
serializeMembers(Record, Category->Methods);
serializeMembers(Record, Category->Properties);
// Surface the protocols of the the category to the interface.
for (const auto &Protocol : Category->Protocols)
serializeRelationship(RelationshipKind::ConformsTo, Record, Protocol);
}
}
}
void SymbolGraphSerializer::serializeMacroDefinitionRecord(

View File

@ -0,0 +1,311 @@
// 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 -x objective-c-header -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
@protocol Protocol;
@interface Interface
@end
@interface Interface (Category) <Protocol>
@property int Property;
- (void)InstanceMethod;
+ (void)ClassMethod;
@end
//--- 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"
}
},
"relationships": [
{
"kind": "memberOf",
"source": "c:objc(cs)Interface(im)InstanceMethod",
"target": "c:objc(cs)Interface"
},
{
"kind": "memberOf",
"source": "c:objc(cs)Interface(cm)ClassMethod",
"target": "c:objc(cs)Interface"
},
{
"kind": "memberOf",
"source": "c:objc(cs)Interface(py)Property",
"target": "c:objc(cs)Interface"
},
{
"kind": "conformsTo",
"source": "c:objc(cs)Interface",
"target": "c:objc(pl)Protocol"
}
],
"symbols": [
{
"accessLevel": "public",
"declarationFragments": [
{
"kind": "keyword",
"spelling": "@interface"
},
{
"kind": "text",
"spelling": " "
},
{
"kind": "identifier",
"spelling": "Interface"
}
],
"identifier": {
"interfaceLanguage": "objective-c",
"precise": "c:objc(cs)Interface"
},
"kind": {
"displayName": "Class",
"identifier": "objective-c.class"
},
"location": {
"position": {
"character": 12,
"line": 3
},
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "identifier",
"spelling": "Interface"
}
],
"title": "Interface"
},
"pathComponents": [
"Interface"
]
},
{
"accessLevel": "public",
"declarationFragments": [
{
"kind": "text",
"spelling": "- ("
},
{
"kind": "typeIdentifier",
"preciseIdentifier": "c:v",
"spelling": "void"
},
{
"kind": "text",
"spelling": ")"
},
{
"kind": "identifier",
"spelling": "InstanceMethod"
}
],
"identifier": {
"interfaceLanguage": "objective-c",
"precise": "c:objc(cs)Interface(im)InstanceMethod"
},
"kind": {
"displayName": "Instance Method",
"identifier": "objective-c.method"
},
"location": {
"position": {
"character": 1,
"line": 8
},
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "text",
"spelling": "- "
},
{
"kind": "identifier",
"spelling": "InstanceMethod"
}
],
"title": "InstanceMethod"
},
"pathComponents": [
"Interface",
"InstanceMethod"
]
},
{
"accessLevel": "public",
"declarationFragments": [
{
"kind": "text",
"spelling": "+ ("
},
{
"kind": "typeIdentifier",
"preciseIdentifier": "c:v",
"spelling": "void"
},
{
"kind": "text",
"spelling": ")"
},
{
"kind": "identifier",
"spelling": "ClassMethod"
}
],
"identifier": {
"interfaceLanguage": "objective-c",
"precise": "c:objc(cs)Interface(cm)ClassMethod"
},
"kind": {
"displayName": "Type Method",
"identifier": "objective-c.type.method"
},
"location": {
"position": {
"character": 1,
"line": 9
},
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "text",
"spelling": "+ "
},
{
"kind": "identifier",
"spelling": "ClassMethod"
}
],
"title": "ClassMethod"
},
"pathComponents": [
"Interface",
"ClassMethod"
]
},
{
"accessLevel": "public",
"declarationFragments": [
{
"kind": "keyword",
"spelling": "@property"
},
{
"kind": "text",
"spelling": " ("
},
{
"kind": "keyword",
"spelling": "atomic"
},
{
"kind": "text",
"spelling": ", "
},
{
"kind": "keyword",
"spelling": "assign"
},
{
"kind": "text",
"spelling": ", "
},
{
"kind": "keyword",
"spelling": "unsafe_unretained"
},
{
"kind": "text",
"spelling": ", "
},
{
"kind": "keyword",
"spelling": "readwrite"
},
{
"kind": "text",
"spelling": ")"
},
{
"kind": "typeIdentifier",
"preciseIdentifier": "c:I",
"spelling": "int"
},
{
"kind": "identifier",
"spelling": "Property"
}
],
"identifier": {
"interfaceLanguage": "objective-c",
"precise": "c:objc(cs)Interface(py)Property"
},
"kind": {
"displayName": "Instance Property",
"identifier": "objective-c.property"
},
"location": {
"position": {
"character": 15,
"line": 7
},
"uri": "file://INPUT_DIR/input.h"
},
"names": {
"subHeading": [
{
"kind": "identifier",
"spelling": "Property"
}
],
"title": "Property"
},
"pathComponents": [
"Interface",
"Property"
]
}
]
}