[TBDGen] Add support for Objective-C Categories.

Emit Objective-C Categories for extensions that have the @objc attribute
directly (or indirectly via one of its methods, subscripts, etc) attached.

Also associate and emit all methods for that category into the API JSON file.

This fixes rdar://94734748.
This commit is contained in:
Juergen Ributzka
2022-06-23 09:19:05 -07:00
parent fb5ef6893d
commit a9e02e91cb
8 changed files with 247 additions and 46 deletions

View File

@@ -42,12 +42,22 @@ ObjCInterfaceRecord *API::addObjCClass(StringRef name, APILinkage linkage,
return interface; return interface;
} }
void API::addObjCMethod(ObjCInterfaceRecord *cls, StringRef name, APILoc loc, ObjCCategoryRecord *API::addObjCCategory(StringRef name, APILinkage linkage,
APILoc loc, APIAccess access,
APIAvailability availability,
StringRef interface) {
auto *category = new (allocator)
ObjCCategoryRecord(name, linkage, loc, access, availability, interface);
categories.push_back(category);
return category;
}
void API::addObjCMethod(ObjCContainerRecord *record, StringRef name, APILoc loc,
APIAccess access, bool isInstanceMethod, APIAccess access, bool isInstanceMethod,
bool isOptional, APIAvailability availability) { bool isOptional, APIAvailability availability) {
auto method = new (allocator) ObjCMethodRecord( auto method = new (allocator) ObjCMethodRecord(
name, loc, access, isInstanceMethod, isOptional, availability); name, loc, access, isInstanceMethod, isOptional, availability);
cls->methods.push_back(method); record->methods.push_back(method);
} }
static void serialize(llvm::json::OStream &OS, APIAccess access) { static void serialize(llvm::json::OStream &OS, APIAccess access) {
@@ -151,6 +161,30 @@ static void serialize(llvm::json::OStream &OS,
}); });
} }
static void serialize(llvm::json::OStream &OS,
const ObjCCategoryRecord &record) {
OS.object([&]() {
OS.attribute("name", record.name);
serialize(OS, record.access);
serialize(OS, record.loc);
serialize(OS, record.linkage);
serialize(OS, record.availability);
OS.attribute("interface", record.interface);
OS.attributeArray("instanceMethods", [&]() {
for (auto &method : record.methods) {
if (method->isInstanceMethod)
serialize(OS, *method);
}
});
OS.attributeArray("classMethods", [&]() {
for (auto &method : record.methods) {
if (!method->isInstanceMethod)
serialize(OS, *method);
}
});
});
}
void API::writeAPIJSONFile(llvm::raw_ostream &os, bool PrettyPrint) { void API::writeAPIJSONFile(llvm::raw_ostream &os, bool PrettyPrint) {
unsigned indentSize = PrettyPrint ? 2 : 0; unsigned indentSize = PrettyPrint ? 2 : 0;
llvm::json::OStream JSON(os, indentSize); llvm::json::OStream JSON(os, indentSize);
@@ -167,6 +201,11 @@ void API::writeAPIJSONFile(llvm::raw_ostream &os, bool PrettyPrint) {
for (const auto *i : interfaces) for (const auto *i : interfaces)
serialize(JSON, *i); serialize(JSON, *i);
}); });
JSON.attributeArray("categories", [&]() {
llvm::sort(categories, sortAPIRecords);
for (const auto *c : categories)
serialize(JSON, *c);
});
JSON.attribute("version", "1.0"); JSON.attribute("version", "1.0");
}); });
} }

View File

@@ -152,6 +152,16 @@ struct ObjCInterfaceRecord : ObjCContainerRecord {
superClassName(superClassName.data(), superClassName.size()) {} superClassName(superClassName.data(), superClassName.size()) {}
}; };
struct ObjCCategoryRecord : ObjCContainerRecord {
std::string interface;
ObjCCategoryRecord(StringRef name, APILinkage linkage, APILoc loc,
APIAccess access, APIAvailability availability,
StringRef interface)
: ObjCContainerRecord(name, linkage, loc, access, availability),
interface(interface.data(), interface.size()) {}
};
class API { class API {
public: public:
API(const llvm::Triple &triple) : target(triple) {} API(const llvm::Triple &triple) : target(triple) {}
@@ -167,11 +177,16 @@ public:
APIAvailability availability, APIAvailability availability,
StringRef superClassName); StringRef superClassName);
void addObjCMethod(ObjCInterfaceRecord *cls, StringRef name, APILoc loc, ObjCCategoryRecord *addObjCCategory(StringRef name, APILinkage linkage,
APILoc loc, APIAccess access,
APIAvailability availability,
StringRef interface);
void addObjCMethod(ObjCContainerRecord *record, StringRef name, APILoc loc,
APIAccess access, bool isInstanceMethod, bool isOptional, APIAccess access, bool isInstanceMethod, bool isOptional,
APIAvailability availability); APIAvailability availability);
void writeAPIJSONFile(llvm::raw_ostream &os, bool PrettyPrint = false); void writeAPIJSONFile(raw_ostream &os, bool PrettyPrint = false);
private: private:
const llvm::Triple target; const llvm::Triple target;
@@ -179,6 +194,7 @@ private:
llvm::BumpPtrAllocator allocator; llvm::BumpPtrAllocator allocator;
std::vector<GlobalRecord*> globals; std::vector<GlobalRecord*> globals;
std::vector<ObjCInterfaceRecord*> interfaces; std::vector<ObjCInterfaceRecord*> interfaces;
std::vector<ObjCCategoryRecord *> categories;
}; };
} // end namespace apigen } // end namespace apigen

View File

@@ -747,6 +747,15 @@ void TBDGenVisitor::visitAbstractFunctionDecl(AbstractFunctionDecl *AFD) {
if (AFD->hasAsync()) { if (AFD->hasAsync()) {
addAsyncFunctionPointerSymbol(SILDeclRef(AFD)); addAsyncFunctionPointerSymbol(SILDeclRef(AFD));
} }
// Skip non objc compatible methods or non-public methods.
if (isa<DestructorDecl>(AFD) || !AFD->isObjC() ||
AFD->getFormalAccess() != AccessLevel::Public)
return;
if (auto *CD = dyn_cast<ClassDecl>(AFD->getDeclContext()))
recorder.addObjCMethod(CD, SILDeclRef(AFD));
else if (auto *ED = dyn_cast<ExtensionDecl>(AFD->getDeclContext()))
recorder.addObjCMethod(ED, SILDeclRef(AFD));
} }
void TBDGenVisitor::visitFuncDecl(FuncDecl *FD) { void TBDGenVisitor::visitFuncDecl(FuncDecl *FD) {
@@ -956,30 +965,9 @@ void TBDGenVisitor::visitClassDecl(ClassDecl *CD) {
} }
TBD.addMethodDescriptor(method); TBD.addMethodDescriptor(method);
if (auto methodOrCtorOrDtor = method.getDecl()) {
// Skip non objc compatible methods or non-public methods.
if (!methodOrCtorOrDtor->isObjC() ||
methodOrCtorOrDtor->getFormalAccess() != AccessLevel::Public)
return;
// only handle FuncDecl here. Initializers are handled in
// visitConstructorDecl.
if (isa<FuncDecl>(methodOrCtorOrDtor))
recorder.addObjCMethod(CD, method);
}
} }
void addMethodOverride(SILDeclRef baseRef, SILDeclRef derivedRef) { void addMethodOverride(SILDeclRef baseRef, SILDeclRef derivedRef) {}
if (auto methodOrCtorOrDtor = derivedRef.getDecl()) {
if (!methodOrCtorOrDtor->isObjC() ||
methodOrCtorOrDtor->getFormalAccess() != AccessLevel::Public)
return;
if (isa<FuncDecl>(methodOrCtorOrDtor))
recorder.addObjCMethod(CD, derivedRef);
}
}
void addPlaceholder(MissingMemberDecl *) {} void addPlaceholder(MissingMemberDecl *) {}
@@ -1001,10 +989,6 @@ void TBDGenVisitor::visitConstructorDecl(ConstructorDecl *CD) {
addAsyncFunctionPointerSymbol( addAsyncFunctionPointerSymbol(
SILDeclRef(CD, SILDeclRef::Kind::Initializer)); SILDeclRef(CD, SILDeclRef::Kind::Initializer));
} }
if (auto parentClass = CD->getParent()->getSelfClassDecl()) {
if (parentClass->isObjC() || CD->isObjC())
recorder.addObjCMethod(parentClass, SILDeclRef(CD));
}
} }
visitAbstractFunctionDecl(CD); visitAbstractFunctionDecl(CD);
@@ -1397,8 +1381,11 @@ public:
addOrGetObjCInterface(decl); addOrGetObjCInterface(decl);
} }
void addObjCMethod(const ClassDecl *cls, void addObjCCategory(const ExtensionDecl *decl) override {
SILDeclRef method) override { addOrGetObjCCategory(decl);
}
void addObjCMethod(const GenericContext *ctx, SILDeclRef method) override {
SmallString<128> buffer; SmallString<128> buffer;
StringRef name = getSelectorName(method, buffer); StringRef name = getSelectorName(method, buffer);
apigen::APIAvailability availability; apigen::APIAvailability availability;
@@ -1413,12 +1400,23 @@ public:
access = apigen::APIAccess::Private; access = apigen::APIAccess::Private;
} }
auto *clsRecord = addOrGetObjCInterface(cls); apigen::ObjCContainerRecord *record = nullptr;
api.addObjCMethod(clsRecord, name, moduleLoc, access, isInstanceMethod, if (auto *cls = dyn_cast<ClassDecl>(ctx))
record = addOrGetObjCInterface(cls);
else if (auto *ext = dyn_cast<ExtensionDecl>(ctx))
record = addOrGetObjCCategory(ext);
if (record)
api.addObjCMethod(record, name, moduleLoc, access, isInstanceMethod,
false, availability); false, availability);
} }
private: private:
/// Follow the naming schema that IRGen uses for Categories (see
/// ClassDataBuilder).
using CategoryNameKey = std::pair<const ClassDecl *, const ModuleDecl *>;
llvm::DenseMap<CategoryNameKey, unsigned> CategoryCounts;
apigen::APIAvailability getAvailability(const Decl *decl) { apigen::APIAvailability getAvailability(const Decl *decl) {
bool unavailable = false; bool unavailable = false;
std::string introduced, obsoleted; std::string introduced, obsoleted;
@@ -1475,11 +1473,46 @@ private:
return cls; return cls;
} }
void buildCategoryName(const ExtensionDecl *ext, const ClassDecl *cls,
SmallVectorImpl<char> &s) {
llvm::raw_svector_ostream os(s);
ModuleDecl *module = ext->getParentModule();
os << module->getName();
unsigned categoryCount = CategoryCounts[{cls, module}]++;
if (categoryCount > 0)
os << categoryCount;
}
apigen::ObjCCategoryRecord *addOrGetObjCCategory(const ExtensionDecl *decl) {
auto entry = categoryMap.find(decl);
if (entry != categoryMap.end())
return entry->second;
SmallString<128> interfaceBuffer;
SmallString<128> nameBuffer;
ClassDecl *cls = decl->getSelfClassDecl();
auto interface = cls->getObjCRuntimeName(interfaceBuffer);
buildCategoryName(decl, cls, nameBuffer);
apigen::APIAvailability availability = getAvailability(decl);
apigen::APIAccess access =
decl->isSPI() ? apigen::APIAccess::Private : apigen::APIAccess::Public;
apigen::APILinkage linkage =
decl->getMaxAccessLevel() == AccessLevel::Public
? apigen::APILinkage::Exported
: apigen::APILinkage::Internal;
auto category = api.addObjCCategory(nameBuffer, linkage, moduleLoc, access,
availability, interface);
categoryMap.try_emplace(decl, category);
return category;
}
apigen::API &api; apigen::API &api;
ModuleDecl *module; ModuleDecl *module;
apigen::APILoc moduleLoc; apigen::APILoc moduleLoc;
llvm::DenseMap<const ClassDecl*, apigen::ObjCInterfaceRecord*> classMap; llvm::DenseMap<const ClassDecl*, apigen::ObjCInterfaceRecord*> classMap;
llvm::DenseMap<const ExtensionDecl *, apigen::ObjCCategoryRecord *>
categoryMap;
}; };
apigen::API APIGenRequest::evaluate(Evaluator &evaluator, apigen::API APIGenRequest::evaluate(Evaluator &evaluator,

View File

@@ -68,7 +68,8 @@ public:
virtual void addSymbol(StringRef name, llvm::MachO::SymbolKind kind, virtual void addSymbol(StringRef name, llvm::MachO::SymbolKind kind,
SymbolSource source) {} SymbolSource source) {}
virtual void addObjCInterface(const ClassDecl *decl) {} virtual void addObjCInterface(const ClassDecl *decl) {}
virtual void addObjCMethod(const ClassDecl *cls, SILDeclRef method) {} virtual void addObjCCategory(const ExtensionDecl *decl) {}
virtual void addObjCMethod(const GenericContext *ctx, SILDeclRef method) {}
}; };
class SimpleAPIRecorder final : public APIRecorder { class SimpleAPIRecorder final : public APIRecorder {

View File

@@ -364,12 +364,12 @@ public func myFunction2() {}
// CHECK-NEXT: "super": "NSObject", // CHECK-NEXT: "super": "NSObject",
// CHECK-NEXT: "instanceMethods": [ // CHECK-NEXT: "instanceMethods": [
// CHECK-NEXT: { // CHECK-NEXT: {
// CHECK-NEXT: "name": "init", // CHECK-NEXT: "name": "method1",
// CHECK-NEXT: "access": "public", // CHECK-NEXT: "access": "public",
// CHECK-NEXT: "file": "/@input/MyModule.swiftinterface" // CHECK-NEXT: "file": "/@input/MyModule.swiftinterface"
// CHECK-NEXT: }, // CHECK-NEXT: },
// CHECK-NEXT: { // CHECK-NEXT: {
// CHECK-NEXT: "name": "method1", // CHECK-NEXT: "name": "init",
// CHECK-NEXT: "access": "public", // CHECK-NEXT: "access": "public",
// CHECK-NEXT: "file": "/@input/MyModule.swiftinterface" // CHECK-NEXT: "file": "/@input/MyModule.swiftinterface"
// CHECK-NEXT: } // CHECK-NEXT: }
@@ -416,12 +416,12 @@ public func myFunction2() {}
// CHECK-NEXT: "super": "_TtC8MyModule4Test", // CHECK-NEXT: "super": "_TtC8MyModule4Test",
// CHECK-NEXT: "instanceMethods": [ // CHECK-NEXT: "instanceMethods": [
// CHECK-NEXT: { // CHECK-NEXT: {
// CHECK-NEXT: "name": "init", // CHECK-NEXT: "name": "method1",
// CHECK-NEXT: "access": "public", // CHECK-NEXT: "access": "public",
// CHECK-NEXT: "file": "/@input/MyModule.swiftinterface" // CHECK-NEXT: "file": "/@input/MyModule.swiftinterface"
// CHECK-NEXT: }, // CHECK-NEXT: },
// CHECK-NEXT: { // CHECK-NEXT: {
// CHECK-NEXT: "name": "method1", // CHECK-NEXT: "name": "init",
// CHECK-NEXT: "access": "public", // CHECK-NEXT: "access": "public",
// CHECK-NEXT: "file": "/@input/MyModule.swiftinterface" // CHECK-NEXT: "file": "/@input/MyModule.swiftinterface"
// CHECK-NEXT: } // CHECK-NEXT: }
@@ -429,4 +429,5 @@ public func myFunction2() {}
// CHECK-NEXT: "classMethods": [] // CHECK-NEXT: "classMethods": []
// CHECK-NEXT: } // CHECK-NEXT: }
// CHECK-NEXT: ], // CHECK-NEXT: ],
// CHECK-NEXT: "categories": [],
// CHECK-NEXT: "version": "1.0" // CHECK-NEXT: "version": "1.0"

View File

@@ -0,0 +1,108 @@
// REQUIRES: objc_interop, OS=macosx
// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend(mock-sdk: %clang-importer-sdk-nosource -I %t) %s -typecheck -emit-module-interface-path %t/MyModule.swiftinterface -enable-library-evolution -module-name MyModule -swift-version 5
// RUN: %target-swift-api-extract -o - -pretty-print %t/MyModule.swiftinterface -module-name MyModule -module-cache-path %t | %FileCheck %s
import Foundation
// This should create an ObjC Category and a method with custom name.
extension NSDictionary {
@objc
public subscript(key: Any) -> Any? {
@objc(__custom_name:)
get { return nil }
}
}
// This shouldn't create an interface.
public class A {}
// This shouldn't create a category.
extension A {
public func run() {}
}
// This creates an interface.
public class B: NSObject {}
// This creates a category.
@objc
extension B {
public func run() {}
}
// This shouldn't create a category.
extension B {
public func noop() {}
}
// This creates a category with index 1.
@objc
extension B {
public func fun() {}
}
// CHECK: "interfaces": [
// CHECK-NEXT: {
// CHECK-NEXT: "name": "_TtC8MyModule1B",
// CHECK-NEXT: "access": "public",
// CHECK-NEXT: "file": "/@input/MyModule.swiftinterface",
// CHECK-NEXT: "linkage": "exported",
// CHECK-NEXT: "super": "NSObject",
// CHECK-NEXT: "instanceMethods": [
// CHECK-NEXT: {
// CHECK-NEXT: "name": "init",
// CHECK-NEXT: "access": "public",
// CHECK-NEXT: "file": "/@input/MyModule.swiftinterface"
// CHECK-NEXT: }
// CHECK-NEXT: ],
// CHECK-NEXT: "classMethods": []
// CHECK-NEXT: }
// CHECK-NEXT: ],
// CHECK-NEXT: "categories": [
// CHECK-NEXT: {
// CHECK-NEXT: "name": "MyModule",
// CHECK-NEXT: "access": "public",
// CHECK-NEXT: "file": "/@input/MyModule.swiftinterface",
// CHECK-NEXT: "linkage": "exported",
// CHECK-NEXT: "interface": "NSDictionary",
// CHECK-NEXT: "instanceMethods": [
// CHECK-NEXT: {
// CHECK-NEXT: "name": "__custom_name:",
// CHECK-NEXT: "access": "public",
// CHECK-NEXT: "file": "/@input/MyModule.swiftinterface"
// CHECK-NEXT: }
// CHECK-NEXT: ],
// CHECK-NEXT: "classMethods": []
// CHECK-NEXT: },
// CHECK-NEXT: {
// CHECK-NEXT: "name": "MyModule",
// CHECK-NEXT: "access": "public",
// CHECK-NEXT: "file": "/@input/MyModule.swiftinterface",
// CHECK-NEXT: "linkage": "exported",
// CHECK-NEXT: "interface": "_TtC8MyModule1B",
// CHECK-NEXT: "instanceMethods": [
// CHECK-NEXT: {
// CHECK-NEXT: "name": "run",
// CHECK-NEXT: "access": "public",
// CHECK-NEXT: "file": "/@input/MyModule.swiftinterface"
// CHECK-NEXT: }
// CHECK-NEXT: ],
// CHECK-NEXT: "classMethods": []
// CHECK-NEXT: },
// CHECK-NEXT: {
// CHECK-NEXT: "name": "MyModule1",
// CHECK-NEXT: "access": "public",
// CHECK-NEXT: "file": "/@input/MyModule.swiftinterface",
// CHECK-NEXT: "linkage": "exported",
// CHECK-NEXT: "interface": "_TtC8MyModule1B",
// CHECK-NEXT: "instanceMethods": [
// CHECK-NEXT: {
// CHECK-NEXT: "name": "fun",
// CHECK-NEXT: "access": "public",
// CHECK-NEXT: "file": "/@input/MyModule.swiftinterface"
// CHECK-NEXT: }
// CHECK-NEXT: ],
// CHECK-NEXT: "classMethods": []
// CHECK-NEXT: }
// CHECK-NEXT: ],

View File

@@ -92,6 +92,7 @@ public class MyClass2 : NSObject {
// CHECK-NEXT: "classMethods": [] // CHECK-NEXT: "classMethods": []
// CHECK-NEXT: } // CHECK-NEXT: }
// CHECK-NEXT: ], // CHECK-NEXT: ],
// CHECK-NEXT: "categories": [],
// CHECK-NEXT: "version": "1.0" // CHECK-NEXT: "version": "1.0"
// CHECK-NEXT: } // CHECK-NEXT: }
@@ -264,12 +265,12 @@ public class MyClass2 : NSObject {
// CHECK-SPI-NEXT: "super": "NSObject", // CHECK-SPI-NEXT: "super": "NSObject",
// CHECK-SPI-NEXT: "instanceMethods": [ // CHECK-SPI-NEXT: "instanceMethods": [
// CHECK-SPI-NEXT: { // CHECK-SPI-NEXT: {
// CHECK-SPI-NEXT: "name": "init", // CHECK-SPI-NEXT: "name": "spiMethod",
// CHECK-SPI-NEXT: "access": "private", // CHECK-SPI-NEXT: "access": "private",
// CHECK-SPI-NEXT: "file": "/@input/MyModule.swiftmodule" // CHECK-SPI-NEXT: "file": "/@input/MyModule.swiftmodule"
// CHECK-SPI-NEXT: }, // CHECK-SPI-NEXT: },
// CHECK-SPI-NEXT: { // CHECK-SPI-NEXT: {
// CHECK-SPI-NEXT: "name": "spiMethod", // CHECK-SPI-NEXT: "name": "init",
// CHECK-SPI-NEXT: "access": "private", // CHECK-SPI-NEXT: "access": "private",
// CHECK-SPI-NEXT: "file": "/@input/MyModule.swiftmodule" // CHECK-SPI-NEXT: "file": "/@input/MyModule.swiftmodule"
// CHECK-SPI-NEXT: } // CHECK-SPI-NEXT: }
@@ -284,18 +285,19 @@ public class MyClass2 : NSObject {
// CHECK-SPI-NEXT: "super": "NSObject", // CHECK-SPI-NEXT: "super": "NSObject",
// CHECK-SPI-NEXT: "instanceMethods": [ // CHECK-SPI-NEXT: "instanceMethods": [
// CHECK-SPI-NEXT: { // CHECK-SPI-NEXT: {
// CHECK-SPI-NEXT: "name": "init", // CHECK-SPI-NEXT: "name": "spiMethod",
// CHECK-SPI-NEXT: "access": "public", // CHECK-SPI-NEXT: "access": "private",
// CHECK-SPI-NEXT: "file": "/@input/MyModule.swiftmodule" // CHECK-SPI-NEXT: "file": "/@input/MyModule.swiftmodule"
// CHECK-SPI-NEXT: }, // CHECK-SPI-NEXT: },
// CHECK-SPI-NEXT: { // CHECK-SPI-NEXT: {
// CHECK-SPI-NEXT: "name": "spiMethod", // CHECK-SPI-NEXT: "name": "init",
// CHECK-SPI-NEXT: "access": "private", // CHECK-SPI-NEXT: "access": "public",
// CHECK-SPI-NEXT: "file": "/@input/MyModule.swiftmodule" // CHECK-SPI-NEXT: "file": "/@input/MyModule.swiftmodule"
// CHECK-SPI-NEXT: } // CHECK-SPI-NEXT: }
// CHECK-SPI-NEXT: ], // CHECK-SPI-NEXT: ],
// CHECK-SPI-NEXT: "classMethods": [] // CHECK-SPI-NEXT: "classMethods": []
// CHECK-SPI-NEXT: } // CHECK-SPI-NEXT: }
// CHECK-SPI-NEXT: ], // CHECK-SPI-NEXT: ],
// CHECK-SPI-NEXT: "categories": [],
// CHECK-SPI-NEXT: "version": "1.0" // CHECK-SPI-NEXT: "version": "1.0"
// CHECK-SPI-NEXT: } // CHECK-SPI-NEXT: }

View File

@@ -48,5 +48,6 @@ public struct TestStruct {
// CHECK-NEXT: } // CHECK-NEXT: }
// CHECK-NEXT: ], // CHECK-NEXT: ],
// CHECK-NEXT: "interfaces": [], // CHECK-NEXT: "interfaces": [],
// CHECK-NEXT: "categories": [],
// CHECK-NEXT: "version": "1.0" // CHECK-NEXT: "version": "1.0"
// CHECK-NEXT: } // CHECK-NEXT: }