- Proposal: SE-0000
- Author(s): Tony Allevato
- Status: Awaiting review
- Review manager: TBD
- Pull request: apple/swift#NNNNN
We propose adding accessibility specifiers to platform-agnostic
@available
attributes to improve API evolution and eliminate incorrect
compiler diagnostics for library authors.
Swift-evolution thread: TBD
Swift's @available
attribute supports API versioning based on Apple
platforms (iOS, introduced: 4.0, deprecated: 10.0
) or the Swift language
version (swift, unavailable: 3.1
). Third-party developers can also use
the platform-agnostic form of this attribute to control the deprecation and
availability of their own APIs, allowing them to evolve cleanly over time
and provide clients with valuable diagnostics. For example:
@available(*, deprecated, message: "Use MyBetterClass instead")
public class MyClass { ... }
public class MyBetterClass { ... }
For third-party code, however, the @available
attribute currently offers
a poor experience; when a declaration is marked as deprecated or
unavailable, all references to that declaration generate warnings (for
deprecated
) or errors (for unavailable
), including those in the
declaring library itself. Furthermore, there is no way to suppress those
diagnostics in parts of the code base where they are not relevant.
The key observation is this: As APIs evolve, declarations may become deprecated or unavailable for external clients but still need to be referenced privately in order for the library to function correctly.
One motivating example comes from the Swift protocol buffers project. Protocol buffers are a data serialization format that supports the generation of native code in several languages, including Swift, for working with data. One particular feature of protocol buffers is the ability to mark a type or field as deprecated in its descriptor, and languages that support deprecation annotations are encouraged to use them in their generated code so that client code can benefit from the extra information.
Consider the following generated struct from a protocol buffer that represents a "person" record with a name and phone number. (Many details have been removed or renamed to focus on the parts relevant to this discussion.)
public struct Person {
public var name: String
public var phoneNumber: String
public mutating func decode(from decoder: Decoder) throws {
try decoder.decodeString(into: &name)
try decoder.decodeString(into: &phoneNumber)
}
}
Later, we decide that we need to support storing multiple phone numbers for a
Person
, so we mark the old field as deprecated and add a new array field.
There are two reasons that we must still allow references to the deprecated
field:
- Source compatibility: Clients of the generated code should still be able to access the field so that legacy code continues to compile. However, they should receive a warning from the compiler that its use is deprecated.
- Data integrity: Internally, serialization code must still access the deprecated field in order to prevent data loss for users of that field. The library author should not receive a deprecation warning for this usage.
The new version of the struct will look like this:
public struct Person {
public var name: String
@available(*, deprecated, message: "Use phoneNumbers instead")
public var phoneNumber: String
public var phoneNumbers: [String]
public mutating func decode(from decoder: Decoder) throws {
try decoder.decodeString(into: &name)
try decoder.decodeString(into: &phoneNumber) // warning emitted here
try decoder.decodeRepeatedString(into: &phoneNumbers)
}
}
It is not currently possible to achieve the second bullet above in Swift. A deprecation warning for this necessary internal usage of the field should not be considered simply an annoyance—such a diagnostic is incorrect. The type as implemented above cannot function correctly without that reference, and to the library author the warning is misleading noise in their build.
This problem would be worse if we wished to mark an API as unavailable
instead of deprecated
, because the library itself would not compile.
Furthermore, a library with numerous deprecation warnings that cannot be suppressed might appear to clients or contributors to be poorly maintained, through no fault of the developer. This negative perception can be harmful to the adoption of a library.
In some cases, such as the protocol buffer example given above, a workaround would be to shadow the property with a private equivalent and wrap it with a public computed property. The public wrapper would be deprecated but the private one would not be, allowing the internal implementations to access the private data without emitting diagnostics. This, however, is boilerplate code that increases the maintenance cost of the code base. Such computed property wrappers can also have negative performance implications, and while those performance issues should also eventually be resolved in the language, that still does not address the long-term maintenance issues.
In other cases, this kind of shadowing trick is not feasible. For example, the
deprecation of a public protocol could be handled by restricting its accessibility,
mangling the protocol's name and then creating a public typealias
, but this
would require changes throughout the library's code base to replace references to the
protocol in inheritance hierarchies, constraint lists, and so forth—and the alias
itself would still emit a warning. This would also have a negative impact on other
diagnostics because the original name of the protocol, not the alias, would be used
in some messages emitted by the compiler.
Thanks to Xiaodi Wu
for suggesting the approach used here. We propose allowing an optional
accessibility modifier to be associated with platform-and-version-agnostic
uses of @available
:
@available(*, deprecated [private | fileprivate | internal | public])
@available(*, unavailable [private | fileprivate | internal | public])
The accessibility modifier indicates the narrowest access level at which a reference to a deprecated/unavailable declaration causes a warning/error to be emitted. In other words,
private
: The declaration is deprecated/unavailable for private usage and above. That is, diagnostics are never suppressed.fileprivate
: The declaration is deprecated/unavailable forfileprivate
usage and above. Diagnostics are suppressed for references in the declaring scope or in extensions to that scope that are in the same file.internal
: The declaration is deprecated/unavailable for internal usage and above. Diagnostics are suppressed for references in the declaring file.public
: The declaration is deprecated/unavailable for public usage. Diagnostics are suppressed for references in the declaring module.
open
is not permitted here, as it does not describe a strictly different
visibility than public
.
The accessibility modifier can be omitted, as before. If it is, then the
default behavior is to treat it as if it were private
. This preserves
Swift's current behavior.
We do not add accessibility support to introduced
or obsolete
because
they require a version number corresponding to a platform or Swift release
and are thus less useful for annotating third-party APIs.
The advantages of this approach compared to other approaches are discussed in the Alternatives Considered section below.
The parser will be extended to allow an optional accessibility modifier
(public
, internal
, fileprivate
, private
) after the deprecated
or
unavailable
token in an @available
attribute.
An Accessibility
-typed field will be added to AvailableAttr
, which will
be populated for platform-agnostic deprecated
and unavailable
attributes.
This field will be serialized into the .swiftmodule
file along with the
other fields of the attribute.
The type checker's deprecated
and unavailable
diagnostics would be
extended to return early if the reference to a deprecated/unavailable
declaration required the given accessibility or higher, suppressing the
diagnostic.
This is not a source-breaking change and there is no functional change for
existing code. The substitution of private
by default when the accessibility
modifier is omitted preserves existing behavior.
The format of the @available
attribute encoded in the .swiftmodule
will
change because the accessibility ordinal will be added to the serialized
attribute.
The declaration of deprecated and/or unavailable APIs would essentially be the same as it is today with respect to the boundary between a module and its clients; this feature allows finer control of those diagnostics within a module.
An early draft of this proposal focused on a single, simpler use case—suppressing deprecation warnings for references to a deprecated declaration in the same file. This would be sufficient for the protocol buffer example above, but participants on swift-evolution pointed out other valid use cases that would not be satisfied by that rule:
-
Zach Waldowski brings up the fact that
@available
attributes that emit deprecation warnings for all references can be a useful refactoring tool:I will note that I use deprecations extremely frequently while refactoring, and [suppressing deprecation warnings in the same file] would defeat that use case. As I have a few larger projects with long compile times, losing that functionality would be disappointing.
-
Anders Ha points out a scenario where same-file suppression is not enough and same-module suppression is needed:
The problem happened on deprecated public protocols in ReactiveSwift. Since conformance cannot be stripped until it is inaccessible, we have a bunch of deprecation warnings on conformance clauses, scattered in the codebase. Having this on a file basis is not enough in our cases, while for sure not all deprecations need such functionality.
There was also some discussion about whether deprecation warnings were useful at all inside the same module because the module boundary is the primary API boundary in Swift. This, would not satisfy the protocol buffer use case, where the user is often generating code that ends up in the same module as the rest of their application logic. Same-module deprecation warnings can also be useful for teams working on large modules, where one team member wants to use deprecation warnings as a way of identifying obsolete internal APIs and encouraging other team members to migrate away from them.
Another approach that was brought up in a
feature request was to allow a
"deprecated block" that would suppress all deprecation warnings inside it.
This would be similar to the approach taken by other languages (such as Java's
@SuppressWarnings("deprecation")
annotation). However, this has several
drawbacks:
- It places undue burden on API authors who still need to reference deprecated or unavailable APIs internally, because now they must annotate every usage site in addition to the declaration itself.
- Suppressing deprecation warnings is likely harmless, but suppressing
errors for
unavailable
declarations would be quite harmful and incorrect. For example, manyunavailable
APIs in the Swift standard library have been stubbed out to simply callBuiltin.unreachable()
. Users should not be allowed to call these under any circumstances, so a block-based suppression strategy would not be able to help in theunavailable
case. - Deprecation warnings are a way for API authors to communicate to their users that a declaration will be removed in the future and that they should migrate away from it. Selectively suppressing those warnings hides vital information; arguably, users should be forced to face those warnings in their builds. The right way for a client to achieve a warning-free build is to remove the deprecated reference, not to hide the warnings.
It should be noted, though, that this proposal in no way precludes such a feature from being added in the future.
On [Date], the core team decided to (TBD) this proposal. When the core team makes a decision regarding this proposal, their rationale for the decision will be written here.