Skip to content

Instantly share code, notes, and snippets.

@allevato
Last active August 22, 2017 15:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save allevato/76f098a761147f3be5017d10a6f4ecbb to your computer and use it in GitHub Desktop.
Save allevato/76f098a761147f3be5017d10a6f4ecbb to your computer and use it in GitHub Desktop.

Accessibility-based @available diagnostics

Introduction

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

Motivation

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.

An example

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.

Workarounds and their problems

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.

Proposed solution

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 for fileprivate 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.

Detailed design

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.

Source compatibility

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.

Effect on ABI stability

The format of the @available attribute encoded in the .swiftmodule will change because the accessibility ordinal will be added to the serialized attribute.

Effect on API resilience

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.

Alternatives considered

Suppressing same-file deprecation warnings only

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.

Suppressing same-module deprecation warnings only

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.

Explicit suppression of deprecation warnings at usage sites

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, many unavailable APIs in the Swift standard library have been stubbed out to simply call Builtin.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 the unavailable 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.


Rationale

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment