[Pitch] Attribute to silence Deprecation Warnings for a specified scope

Deprecation warnings can be really annoying, and this would be better than what we have now — but I don't think this is the best long term solution. Even if you could limit the attribute to certain libraries, that wouldn't be the type of granularity I want.
When I compile a piece of code for the first time, I want to see all deprecation warnings, and I wouldn't want to hide them from team members by annotating problematic code.
After all, it's not just a problem of that code, but also of some dependency; so you can't silence it safely.

Therefor, I would definitely prefer a compiler flag — or even take away deprecation handling from the compiler, and make it a responsibility of the linter.

A compiler flag is a good idea too, after all, we could provide both, why not a scope based solution and a project solution? I’ll look into this after I finish implementing import support for the current implementation I have. Thank you!

Actually a great idea, I’ll think more about it

This goes to what I’m asking here:

In what circumstances would such an attribute be more ergonomic/useful around a use site rather than an import site?

And if use site annotations are the preferred design, when is a scope-by-scope setting the way to go, versus a line-by-line or file-by-file annotation?

Is there a role for an import-site annotation in addition to use-site annotations? Is there a role for a module-by-module flag if there are use site annotations?

My personal bias is that, in the WebKit example, supporting only an import-site annotation appears to be the simplest for both the user and the language. Users of the deprecated feature will be calling those APIs in multiple scopes, and it would not be ergonomic to have scope-by-scope use-site annotations, nor would they want to ignore unrelated deprecation warnings across so much of their code. I’m not sure why this is considered to be overcomplicating?

Take for example: you’re importing FooKit, FooKit is used extensively around the project however you may want to, for example, subclass your class from one of the FooKit classes but that class has no alternative unlike other deprecated symbols in FooKit, so you may want to attach the attribute to the subclass only. If you want an import based solution then fine, I’m writing one in the linked pr which should start working with a couple of changes. I just think that a scope based solution and an import based solution can work together, it doesn’t just have to be one

Sure, two different ways of silencing deprecation warnings could work together, but does it hold its own weight—that is, is the second solution best for some set of usages, such that we would want it in addition to the first?

In this example, it seems you’d want to silence only FooKit.DeprecatedClass-related deprecation warnings in the file or files where you implement the subclass, which would be done with an attribute on import class FooKit.DeprecatedClass. To my mind, it would be less desirable to silence all deprecation warnings for anything implemented in the scope of your custom subclass (you’d probably want to be notified if you unintentionally called a deprecated UIKit method, for instance), which would be the function of attaching such an attribute to the subclass.

Is there another scenario you have in mind which benefits from use-site silencing of all deprecation warnings?

The import method seems to work well for modules and classes, but if instead you have a bunch of related methods or free functions that get deprecated it doesn't seem that straightforward, other than applying it to the whole module.

If the worry is that more deprecation warnings could appear in the future but would be silenced in those scopes, maybe we could have this:

@ignoreDeprecationWarnings(upTo: macOS 11)

Here, only deprecations done in macOS 11 and earlier would be silenced. If something else gets deprecated in macOS 12, the warning won't be silenced and you'll get notified.


Combined with my previous suggestion you could have:

@ignoreDeprecationWarnings(upTo: macOS 11, until: macOS 13)

It would silence deprecations made in macOS 11 and earlier, and they would reappear for macOS 13.

Big picture, I'd love to see the issue of 'necessary' uses of deprecated APIs solved, so I'm a big fan of the explorations in this thread.

I think another important thing to keep in mind here when comparing potential solutions (if it hasn't already been mentioned up-thread somewhere), is that there is legitimate value in deprecation warnings. Even more precisely, I see immense value in warnings about newly deprecated declarations when libraries are updated. It may very well be the case that at the time of importing FooKit, I feel comfortable about ignoring all the deprecated APIs, but I don't know if we should provide a way for users to easily express "I will never care about any deprecated APIs in the future".

ETA: Another way of phrasing what I'm trying to get at is that while a blanket "ignore all deprecation warnings" compiler flag, import/scope annotation, etc., might seem to be to the benefit of users, it might actually do more harm than good, and I think that it is strictly worse for library developers who have an interest in making sure that their users are alerted in a timely manner about new deprecations

The story of the danger I'm imagining, with both a scope-by-scope solution and an import-by-import solution looks like the following. I'm writing an app, using FooKit 1.0, which has various APIs, some deprecated from the 0.x days:

// FooKit
@available(*, deprecated, message: "No replacement, sorry!")
func myGreatFunc() {}

func myOtherFunc() {}

As an application developer, I really want to use myGreatFunc(), and I know it won't be going away any time soon, so I want to disable the deprecation warning (either so I can compile with warnings-as-errors, or keep warnings spam low, whatever). With coarser solutions, that might look like this

// MyApp

// import-based
@noDeprecationWarnings
import FooKit

// scope based
@noDeprecationWarnings
func useFooKit() {
  myGreatFunc()
  myOtherFunc()
}

One day, I decide to update to FooKit 2.0:

// FooKit
@available(*, deprecated, message: "No replacement, sorry!")
func myGreatFunc() {}

@available(*, deprecated, message: "This is inefficient, use myFixedOtherFunc!")
func myOtherFunc() {}

func myFixedOtherFunc() {}

Now, in the MyApp target, I would really have liked to get the deprecation warning for myOtherFunc(), but I won't! Sure, with the scope-based solution I could have preemptively wrapped the call to myGreatFunc() in a separate scope, something like:

@noDeprecationWarnings
do {
  myGreatFunc()
}

but I suspect most people wouldn't do that by default.

This pulls me toward a solution closer to "line-by-line", but I might even prefer something more granular. Since deprecations are applied on a declaration-by-declaration basis, they should be disabled on a declaration-by-declaration basis. The solution in the FooKit example above would look something like:

func useFooKit() {
  #deprecated(myGreatFunc)()
  myOtherFunc()
}

This would apply anywhere you refer to a (deprecated) declaration:

class MySubclass: #deprecated(OldSuperclass) {
  func myMethod() {
    #deprecated(self.oldMethod)()
  }
}

In cases where you're using the deprecated declaration liberally, you would of course be free to redeclare the declaration to 'undeprecate' it:

typealias OldSuperclass = #deprecated(FooKit.OldSuperclass)

extension OldSuperclass {
  func oldMethodFixed() {
    #deprecated(self.oldMethod)()
  }
}

This would fix all the cases where I've wanted to disable deprecation warnings, and I can't think of a situation where it wouldn't work (though please poke holes if you see any :slightly_smiling_face:). I also really like that it mirrors the granularity at which deprecations are applied. IMO, the burden of local redeclaration for common uses of deprecated is not too high.

ETA:
For an example of why I think even line-by-line is too coarse, consider (using syntax akin to try/await):

// FooKit

@available(*, deprecated, message: "No replacement, sorry!")
func myGreatFunc(_: Int) {}

@available(*, deprecated, message: "This is inefficient, use getAnIntFast!")
func getAnInt() -> Int {}

func getAnIntFast() -> Int {}

// MyApp
func useFooKit() {
  // This ignores for myGreatFunc, but also getAnInt which we _do_ want a warning for!
  ignoreDeprecations myGreatFunc(getAnInt())
}
7 Likes
For greatest flexibility we can support silencing on all scopes.
// import scope
@warning (deprecated=ignore)
import FooKit

// type scope
@warning (deprecated=ignore)
struct Foo {
}

// func / method scope
@warning (deprecated=ignore)
func foo() {
}

// statement scope
@warning (deprecated=ignore)
if bar {
}

// block of statements scope
@warning (deprecated=ignore)
do {
	let x = y()
	let x = t()
}

// individual statement / declaration scope
@warning (deprecated=ignore)
let x = y

Import scope is a bit too coarse for me (I may want to see the warnings in one file but not in another file, but AFAIK I can have a single import statement to import "FooKit" - in other words I am not forced to "import FooKit" in every file where I use it).

In Swift, you are required to import FooKit in every file where you use it.

I remember seeing counterexamples (and that was unexpected). Next time I see it - I will update this message.

There are cases where extension members and conformances can be visible in files other than the one that imports them, however these are bugs:

1 Like

Additionally, if you bridge in an Obj-C precompiled header, the modules visible in that header will become visible to Swift as well (at least, they did the last time I tried it).

Note that there are uses for silencing deprecation in a single module or in modules that are from the same source. A notable example is in swift-corelibs-foundation: we have several deprecated methods (eg: Scanner.scanString(_:into:)) that have modern replacements, but we need to keep around because our mandate is source compatibility. These methods have to be both called internally (eg. deprecated open methods someone may have overridden may need to be called to maintain execution invariants when the modern replacements are called on a subclass) and of course tested, opening a project to an avalanche of its own deprecation warnings that are valid but cannot and should not be addressed at its internal or unit testing call sites.

(SCF specifically has a ton of warnings and the fact we cannot mark some as “this code is forever correct, trust me” makes dealing with the real ones that much harder and building with -Werror impossible.)

6 Likes

This is the issue that swift-protobuf has as well. When a field is declared as deprecated in the original .proto file, we'd like to mark it as deprecated in Swift, but the code internal to the generated type also gets/sets the field during serialization, and that would produce "false" deprecation warnings.

Doing something like public/private wrapping of the field could avoid this, but it's overhead that shouldn't be necessary IMO, so right now we just don't propagate the deprecation information.

Previous discussions on this topic have led to some interesting ideas like visibility modifiers for deprecation (e.g., something can be declared as deprecated everywhere except the same file/the same module) and those would probably work for the situation @millenomi described, but those discussions didn't address having an escape hatch to suppress the warning if you need to use a deprecated declaration in a scope where it was deprecated.

2 Likes

I'll be clear also that sometimes similar challenges can cross module boundaries, which is common in testing (where the unit test bundle is a separate module), or with frameworks factored into multiple modules (Foundation vs FoundationNetworking, for example).

4 Likes

Turning this around a bit, are there very many examples where self-deprecation warnings are “true positives” rather than nuisances?

A priori, it seems a library that’s grown enough complexity to “vend” to itself a distinct internal pseudo-API of enough history and stability that it actively relies on internal deprecation warnings is likely to want the compile-time benefits of true modularization.

I guess what I’m wondering is, would it make more sense to commit more fully to a design where deprecation is an API-level concern and disable self-deprecation warnings (at least by default).

Would these use cases, in your experience, be ergonomically addressed by an attribute on the import statement that disables deprecation warnings on a file-by-file basis for that particular import? And, perhaps, if we do agree that deprecation is an API-level concern, having @testable imply @_no_deprecation_warnings_strawman_spelling?

2 Likes

Update: import support works with the implementation now as expected, so you can do

@ignoreDeprecationWarnings import deprecatedLib

func someFunc() {
   let s = someDeprecatedSymbol // no warnings because deprecatedLib was imported with `@ignoreDeprecatedWarnings`
}
1 Like

@xwu Thoughts on a compiler flag so this can be done project-wide?

I continue to think it will be an attractive nuisance to offer mechanisms for users to wholesale ignore deprecation warnings at the import or project level, with the possible exception of the ‘self-deprecation’ issues mentioned above—I’d be fine with offering a compiler flag to ignore internal deprecation warnings, and I think it’s probably a good idea to have @testable imply the suppression of deprecation warnings.

But outside of those narrow cases, I think it’s actively undesirable to allow users to easily silence such warnings without thinking about specific deprecations. It seems super easy to do this in a non-forward-looking manner, and then accidentally find yourself wishing you hadn’t applied the suppression so widely when an API is unexpectedly ripped out from under you. Each deprecation should require explicit acknowledgement from clients before they’re able to use it without warning. Otherwise, library developers can no longer rely on deprecation as a reliable way to soften the transition to new APIs.

For users who really want to silence every deprecation warning, I’m really fine with requiring them to define a MyApp+Undeprecations.swift file where they redeclare, typealias, and extend away each deprecation warning individually. That seems to me like a fair balance to strike.

10 Likes