Blog Post

Uncovering SourceKit Limitations While Updating Our API Documentation

Illustration: Uncovering SourceKit Limitations While Updating Our API Documentation

We recently underwent an effort to improve our PSPDFKit for iOS API documentation — which is generated by an open source tool — so as to provide a more Swift-first experience.

As part of it, we wanted to audit and modernize all our documentation beforehand to make sure all of our public APIs, which still use Objective-C in a lot of places, were correctly translated to Swift. This is because our goal was to have all our symbols and documentation comments visible for both Swift and Objective-C — including all the custom refinements and renames we do specifically to provide a first-class Swift API.

However, while doing this, we identified a few issues in the generation of Swift API, especially when generated from Objective-C code, that had an impact on the resulting documentation. Some APIs were simply not shown to be available in Swift documentation at all, while some symbols used the wrong name for types. This resulted in invalid Swift APIs, as those types would have a different name in Swift, which was usually changed from the Objective-C type name via the NS_SWIFT_NAME macro.

So now, let’s look at the actual issues we faced and how they impacted our Swift API documentation.

Forward Declaration

The first issue we came across was for Objective-C headers that used forward declarations. For example, when forward declaring a protocol in a header file and using this protocol as a parameter, return, or property type, the resulting API didn’t show up in the Swift documentation, and it was seemingly only available in Objective-C. However, using the API from Swift in the actual compiled framework worked without any problems. This meant the issue must have been somewhere in the API documentation generation workflow.

Additionally, there are different variations of this issue happening, depending on which types you’re dealing with and how you use them. For example, this issue affects not only protocol forward declarations, where the API that uses them is completely missing, but also class forward declarations. If you add a forward declaration for an Objective-C class that uses the same name in Objective-C and Swift, it creates the correct output, and the API using the class is shown correctly in both languages.

However, if the class uses a custom Swift name via the NS_SWIFT_NAME macro, the issue mentioned above occurs again, although in a more subtle way. If the forward declaration was made with the Objective-C name, which is the supported way, the generated Swift API shows up in Swift code as the incorrect original Objective-C name of the symbol, which isn’t a valid API.

A header file that exhibits this issue looks like the following:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@protocol PSPDFClassDelegate;

@interface PSPDFClass: NSObject

- (nullable id<PSPDFClassDelegate>)delegate;

@end

NS_ASSUME_NONNULL_END

When generating the API documentation for this class, the delegate symbol is missing in Swift. Note that if we import the file containing the PSPDFClassDelegate protocol instead of forward declaring it — for example, via #import "PSPDFClassDelegate.h — the delegate API correctly shows up in the generated Swift API.

Also, using delegate from actual Swift code works without any issues, and therefore isn’t a problem for the actual framework we provide.

Missing Nested Types

When declaring a nested Swift type name for Objective-C APIs, such as NS_SWIFT_NAME(Annotation.Tool), in some cases, the entire type is missing from the generated Swift API, and it only shows up as being available through Objective-C. This is the case when the top-level nested type of the symbol name (as in the aforementioned case of Annotation) isn’t available in the context of the header file of the nested type.

There are two approaches to fix this issue. Forward declaring the Objective-C type name makes the type show up, but only if the name isn’t adapted for Swift. The second option is to import the header containing the type. This is the preferred option, since it also works when renaming the type for Swift.

Curiously, forward declaring the renamed Swift type works. However, this is a code smell; it’s using a Swift type name in an Objective-C context, so it’s not something we recommend using in production. Additionally, it might only be useful for post-processing code for a tool so as to generate the correct API documentation output.

One example producing no output in a Swift API is the following:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

NS_SWIFT_NAME(Annotation.Tool)
@interface PSPDFAnnotationTool: NSObject

- (NSString *)toolType;

@end

NS_ASSUME_NONNULL_END

The entire Annotation.Tool class shows up as not having a Swift API at all. As with the issue above, there are workarounds to get a Swift API to show up again: You can either import the header declaring the Annotation type, or you forward declare Annotation.

Diving Into Source Code Analysis

We had to dive into the source code to figure out the origin of these issues, as we wanted to fix them before migrating our documentation to be Swift-first.

We currently use the tool jazzy to generate our API documentation. The first step was looking at the underlying code analysis tool used by jazzy, SourceKitten, and examining its parsed output to further understand the origin of our problems.

SourceKitten is actually an open source Swift layer around Apple’s source code analysis product, SourceKit, which is also used in Xcode to provide syntax highlighting and code completion, along with generating Swift interfaces from Objective-C headers.

Looking at the output of SourceKitten, we could see that the issue was also exhibited there, which told us the issues went deeper. Since SourceKitten heavily relies on SourceKit, the next step was trying to figure out if it also showed these same problems.

Inspecting the Generated Interface

To verify this, we created an Objective-C header with the above-mentioned code, and we looked at the Generated Interface in Xcode. This feature can be found when viewing an Objective-C header file and clicking the Navigate to Related Items button. Look for the four squares located in the top-left corner of the Xcode editor view, and select the Generated Interface > Header.h (Swift X Interface) option. This shows the Swift file that will be generated from your Objective-C code — including all the available APIs.

When we did this, voilà, the issue was also present there. APIs using any of the two above-mentioned issues related to forward declarations and nested types also either were not shown in the Generated Interface, or were using the wrong symbol names.

In the image below, you can see the Swift APIs are missing.

Swift API Missing

And when using one of the workarounds mentioned, like importing the header, the APIs show up again.

Swift API Available

Since SourceKit seemingly only ever parses a single file to provide the generated Swift interface, it doesn’t resolve the forward declarations, and the APIs turn out incorrect compared to what the actual API looks like when compiling code. That’s one of the reasons why, in some cases, the Generated Interface stays completely empty as well, making it look like there are no Swift APIs at all.

Information

All the above-mentioned issues don’t have any effect on the actual compiled product. All of the APIs, even if they aren’t shown in the Generated Interface, are still available and working. This only affects SourceKit and all the tools using it, as well as the Generated Interface in Xcode.

Once we figured this out, we could report an issue to Apple, but at the time of writing this post, we haven’t yet received an answer.

Alternative API Documentation Tools

Since the root cause of issues is located deep in the default development tool and not in the open source tool we’re using, there’s unfortunately no way we can fix those issues ourselves. As such, we’re looking into using different documentation tools, as we can’t know when, or even if, SourceKit might resolve these problems.

The most obvious choice would be using DocC as the API documentation tool. It was introduced at WWDC 2021, and because it added support to Objective-C in Xcode 14, it’d be the perfect candidate to switch to.

Both the issues mentioned above aren’t present in DocC, as it doesn’t use SourceKit under the hood, but rather a custom symbol graph generation for building the available API. DocC also provides the ability to directly ship a documentation catalog to SDK users to view the documentation directly in Xcode, which is another benefit. However, we need to evaluate DocC further before we can make a decision to switch to it at some point in the future.

Conclusion

Throughout all of this, I learned that even though it might look like an issue is present in a tool, it doesn’t always mean that it’s actually the tool’s fault, and it can be worth it to spend some time understanding the internals of a project and investigating more deeply.

Author
Stefan Kieleithner iOS Engineer

Stefan began his journey into iOS development in 2013 and has been passionate about it ever since. In his free time, he enjoys playing board and video games, spending time with his cats, and gardening on his balcony.

Related products
Share post
Free trial Ready to get started?
Free trial