I just finished landing a set of changes which fix a hole in Swift's availability checking model. While this was mostly intended for use by Apple, I feel that it is also worth documenting and communicating to the community since people do ship third-party frameworks that make use of OS availability.
As most of you are aware, Swift's "availability" feature checks if referenced declarations are going to be available at runtime by comparing the OS version in an @available annotation with the "availability context" of the usage location, which is either the deployment target, an @available annotation on an outer declaration, or an if #available
block:
@available(macOS 100, *)
func newFeatureInMacOS100() {}
func usesNewFeature() {
newFeatureInMacOS100() // error!
}
@available(macOS 200, *)
func newerCodeOnMacOS200() {
newFeatureInMacOS100() // OK!
}
func conditionalCodeOnMacOS200() {
if #available(macOS 200, *) {
newFeatureInMacOS100() // OK!
}
}
While we have always been able to check the availability of referenced declarations, we didn't check the availability of protocol conformances. This came up in the following scenario. Suppose a framework defines a type and a protocol:
public struct Box< Contents > {
var contents: Contents
}
public protocol Boxable {
associatedtype Contents
var contents: Contents { get set }
}
Now a newer version of the framework might add a retroactive conformance of the type to a protocol:
extension Box : Boxable {}
Because we only checked availability of declarations and not conformances, it was possible to use this conformance outside of the right availability context:
func takesBoxable<T : Boxable>(_ box: T) {
print(box.contents)
}
func passBoxable<Contents>(_ box: Box<Contents>) {
takesBoxable(box)
}
The passBoxable()
function uses Box<Contents>
and Boxable
, two declarations which are unconditionally available. However, by passing the Box
to takesBoxable()
, it was referencing the protocol conformance of Box<Contents>
to Boxable
, which might not exist at runtime, since it was added after the fact.
The solution to this problem is to add an @available annotation on the extension defining the conformance:
@available(macOS 100, *) extension Box : Boxable {}
Now the compiler will complain:
box.swift:17:3: warning: conformance of 'Box<Contents>' to 'Boxable' is only available in macOS 100 or newer
takesBoxable(box)
^
box.swift:17:3: note: add 'if #available' version check
takesBoxable(box)
^
box.swift:16:6: note: add @available attribute to enclosing global function
func passBoxable<Contents>(_ box: Box<Contents>) {
^
As you can see above, this problem is currently diagnosed as a warning. I had to do this because some versions of Alamofire found in our source compatibility test suite violated the new restriction. The -enable-conformance-availability-errors
frontend flag, which is off by default, upgrades it to an error.
If you encounter the warning in your own code once you upgrade to a Swift compiler release that includes the new check, please treat it as an error; violating the rule can result in linker errors or runtime crashes if the conformance does not actually exist at runtime.
I would prefer to tighten up the rule and turn the warning into an error eventually; either unconditionally once we feel we can do so without causing any disruption to community projects, or perhaps conditionalize it on a new -swift-version
language mode, if we ever decide to introduce a new one.
One more thing: In addition to OS version availability, the conformance availability checking feature also supports unavailable conformances, which cannot be referenced anywhere:
@available(*, unavailable)
extension Box : Boxable {}
And deprecated conformances as well:
@available(*, deprecated)
extension Box : Boxable {}
These are diagnosed just like references to unavailable declarations and deprecated declarations, respectively.
Note that references to unavailable protocol conformances are always diagnosed as errors, even when the -enable-conformance-availability-errors
flag is set. This is because I did not find any example code that violates this rule in our source compatibility test suite. If this becomes a problem, please let us know.