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

Hello Swift Forums! I'm here to to discuss a new idea: An attribute to silence deprecation warnings in a specified scope, I have made a working implementation here, and I'd like to know the thoughts of the community.

Some example usage of the implementation I've made:

@ignoreDeprecationWarnings
class someHelper {
    // deprecated symbols can now be used in this scope without any warnings, ie:
    let config = deprecatedType() // no warning
}

The attribute can be applied to any declaration that @available can be applied to

The main motivation for this is, in a lot of projects, particularly large ones, developers may be forced to use deprecated APIs with no replacement, some vendors, including Apple sometimes, will deprecate APIs with either no replacements at all or no documentation as to what newer APIs should be used, and working in a project with a bunch of warnings that are basically unsolvable isn’t particularly nice and may lead developers to silencing all warnings in their project instead (see the -suppress-warnings compiler option), which I believe is a worse solution than just silencing deprecation warnings.

I also believe this to be a core feature of any language, Clang, GCC, and other compilers all have options to silence deprecation warnings, in fact, Swift itself even uses this in it's own code, the reality is dead simple: sometimes developers are just forced to work with deprecated APIs.

Open to any thoughts about this, thanks!

24 Likes

I'd welcome such an ability to suppress warnings (not just deprecated) or promote them to errors. bike shedding:

@warning(deprecated=ignore, unusedVariables=error)
struct X / func x() / etc here

#warning(deprecated=ignore, unusedVariables=error)
statements here
#end

Plus the corresponding compiler options to change warnings per file and per target/project.

1 Like

I’ve thought of this before however this wouldn’t work with Swift right now, main reason is that this would require knowing the diagnostic warning / error ID, which unlike Clang, Swift doesn’t show the diagnostic ID, which means someone would have to browse the Swift source code in order to find the specific diagnostic ID to know how to input them to the attribute or pound expression in order to silence them, and diagnostic IDs are too ugly to consider showing them when diagnosing one may say

Those error codes can be not shown in the main Xcode UI, only in the build log, works fine in C++:

Swift diagnostic IDs are named differently though, in snake case, and outputting the diagnostic ID could be a whole other proposal, but if that’s done, then using an attribute to silence any warning specified should be a whole lot easier, though adding such an attribute may not be practical right now

I haven’t made my mind up about this feature yet, but I think there needs to be a stronger motivation.

The claim that some vendors deprecate certain symbols without any replacements is definitely a statement that should be justified with examples. Deprecating APIs isn’t done so arbitrarily and replacements are usually offered and documented. If the issue is that deprecating APIs is often done without offering much guidance to the user, the argument could easily shift to how tooling could guide users through migrations.

I think warnings becoming too many to be actually useful is the strongest point of this proposal. As suggested by others, silencing deprecation warnings could fall under a broader feature that offers more granularity over controlling warnings in general. But even at its current scope, the proposed feature deals with the consequences of bad deprecation strategies. To tackle the underlying problem of too many deprecation warnings, in old projects for instance, current migration features could be improved. If migrations to new APIs are not able to express the evolution of a feature, new language features could be warranted. If, on the other hand, this is a problem with vendors not being able to fit a more complex evolution of a feature (e.g. it’s not just changing a method, but changing the overall API design), we could design a feature where migration articles can be embedded directly into documentation and linked to by migration warnings.

2 Likes

While I agree we should tackle the underlying problem of irresponsibly deprecating symbols without any alternatives or references as to what new symbols to use, just as you said, this type of attribute would fall more under the user control of warnings, and since deprecation warnings in particular are one of the most common & yet unsolvable sometimes, I believe an attribute such as this is just going to benefit users, maybe in the future if diagnostic IDs ever become shown to users like Clang shows them, then I believe this attribute should be removed and instead replaced with an attribute to manage warnings overall (as @tera mentioned)

It’s also not impossible to tackle the underlying problem and introduce a feature such as this, I believe they can both coexist.

1 Like

Yes that would be a solution to the warning part of the problem. But I alluded to some deprecation-specific solutions in my previous post, although I was quite vague. Sometimes, for example, I have found that when updating code in my library renaming my declaration is not always enough (e.g. a.f(arg) -> a.g(arg)), but it could be that the pattern of the arguments and the underlying value is inverted (arg.f(a)), or maybe a method changes to a property. These can’t be fully expressed today (especially the pattern inversion), but could perhaps, through the use of an automated tool, remove a large part of deprecated code without silencing the warnings. It’s also important to note that silencing deprecation warnings isn’t like other types of warnings, because deprecated code is not code you might not have intended but choose to maintain; it’s code that after some time will stop working, where you won’t have the option of silencing the corresponding error. Thus, if adopted by large projects, it could go from no warnings to errors without any input from the user.

I’m not sure exactly what problems you and other folks have faced problems with deprecation have dealt with, so it’d be great if you all could share examples, so that we can have a more focused discussion.

1 Like

Most of the contents of OSAtomic.h has been deprecated without a replacement for years. We have had the swift-atomics package to tide us over for the last 2 years, but it’s still not official API.

There are other examples, I’m sure.

7 Likes

A deprecation with no alternative at all seems rare (even in Guillaume's example, swift-atomics is the designated alternative even if it is not in the standard library). However, I still think this is an important problem to solve regardless of whether there are many examples of deprecation without a migration path.

Many individuals and teams strive to address all warnings in their codebase as a matter of principle. The fewer long standing warnings there are, the easier it is to notice the introduction of new ones and address them quickly. It's a common enough desire that many compilers including the Swift compiler have an option to upgrade warnings into errors to enforce the policy directly. However, deprecations make using -warnings-as-errors pretty difficult in my experience because they have unique characteristics:

  • They may appear in existing code as the result of adopting a new SDK, rather than as the result of adopting a new compiler or language mode. Adopting new SDKs can be a frequent occurrence for some teams.
  • The work required to address them can vary quite a bit. Sometimes, it's just a rename that can be addressed trivially with a fixit. Other times, an entire framework might be deprecated in favor of a new one with APIs that are structured much differently and it will take feature-level work to adopt the new framework and qualify that adoption.
  • The benefit to responding to a deprecation also varies quite a bit. Sometimes, the library author is pushing you to a new API because there are flaws in the deprecated one. Other times the deprecated API is entirely functional but won't be maintained going forward. Different motivations for deprecation create different levels of urgency for the client.

I think it is entirely reasonable to give programmers a tool to suppress specific deprecation warnings after they've seen them. Many times, the typical course of action would be to log an issue in the project's issue tracker acknowledging the deprecation and schedule the work to address it at some future point in time. Other times, the suppression might be permanent. As long as you've had the opportunity to acknowledge the deprecation in an auditable way, it ought to be up to you when and how you address it without resigning yourself to long periods where your build log is full of diagnostics that you must ignore.

I think this is worth talking about, especially when considering the design of this feature since it's an obvious generalization. However, it's also a more controversial idea that is likely to get a lot of pushback (I think there are a number of long threads on this topic I'll try to find). I personally am really interested in having the discussion about a feature like this that is limited to deprecations because I think that the case for suppressing them is much stronger than the case for all diagnostics.

5 Likes

It is also not just a matter of having a replacement API: sometimes the new API is buggy and does not work as well as the deprecated one. Third-parties sometimes need to wait several iterations of the OS (which in the case of Apple means several years) before being able to use the new API. Ideally we would not need to silence deprecation warnings, but the reality we live in is different.

8 Likes

Sometimes Swift warnings are just suggesting a “better practice”. While I totally appreciate the suggestion, sometimes it could be inapplicable. I met one when I try to restrict the associatedtype of an underlying protocol within SPI:

public protocol P1 {
    associatedtype T
}

public protocol P2: P1 {}

@_spi(Private)
public struct S {}

@_spi(Private)
public extension P2 {
    // warning: Typealias overriding associated type 'T' from protocol 'P1'
    // is better expressed as same-type constraint on the protocol
    typealias T = S
}

The suggestion doesn’t work because the protocol declaration is public, but the desired type is SPI, so we have no way to express it in the body.

2 Likes

I agree that deprecation warnings are special (although I’d like to put concurrency modernization warnings in the same bucket).

In practical terms, on our large codebase with many developers, dropping an OS version can result in several person-months of work to deal with depreciations, especially when handling them “properly” requires architectural changes. Turning off errors-as-warnings for even a few weeks means that other kinds of warnings means that other, more urgent kinds of warnings inevitably sneak in.

(Now, you may have opinions about this that fall in “don’t do that, then” territory, but that’s about project management rather than engineering, so I won’t engage on that.)

In situations like this, Swift’s tooling feels restrictive and very immature. It’s at a point where we’d probably benefit from parsing warnings and elevating most of them to errors in a compiler wrapper or CI tool, which feels like a silly thing to need to do.

7 Likes

That a warning isn't correct when composed with an unofficial feature goes to the point that it's unsupported. It's worth contributing an improvement or bug report, but an inapplicable warning is pretty benign in the scheme of all things that can go wrong when using unsupported features, and I wouldn't give it much weight when motivating the design of the language.

I really like this categorization! While there are philosophical reasons for Swift not having a general feature to silence arbitrary warnings, it's pretty apparent that modernization warnings are different in kind. We've always had special handling for this (e.g., availability annotations and conditionals, special compiler modes for staging in the Law of Exclusivity or concurrency features).

In a sense, then, I see the point here being that the features we currently have for modernization purposes are piecemeal and incomplete, and it'd be nice to explore the design space for building out from there. For instance, could some sort of annotation be used on the import statement for deprecated libraries, so the user can signal to the compiler (and readers of the code) awareness of the deprecation and intentionality in choosing to use deprecated APIs for lack of a replacement?

6 Likes

For this, I could extend the Implementation of the attribute I’ve written to include import statements, what do you think?

Possibly. My point is that I think there's room to explore the design space here.

If the predominant scenarios where one encounters the need to silence deprecation warnings is on a library-by-library basis, then possibly only an attribute for imports is sufficient (in the style of @preconcurrency import).

If it's much more the norm that there's just one or two deprecated APIs that users find themselves unable to avoid, then perhaps something decorating the use site is the more narrowly targeted solution that is best motivated. Or maybe people tend to group such usages together such that a file-by-file toggle to silence the warning is best.

It's not clear to me that the best solution is to silence all deprecation warnings (as opposed to specific ones, ones from specific libraries, ones with particular Swift version cutoffs, or in the other direction, all modernization warnings), and for the scope of whole types or functions, simply because @available works in that way. Perhaps it is, but I think this bears some more clarification and experimentation.

1 Like

Counterexample: the transition from UIWebView (iOS) and WebView (macOS) to WKWebView.

  • UIWebView is part of UIKit. UIKit is huge and silencing the UIWebView deprecation doesn't necessarily mean developers want to silence all other UIKit deprecations
  • On macOS, WebView is part of WebKit, the same framework the replacement API WKWebView is defined in. In this case, silencing deprecation warnings from WebKit would likely work better for many (most?) developers.

(Background info: Making the switch has been difficult for many developers because of the reasons mentioned by @tclementdev above: the replacement API was entirely different and AFAIK (at least initially) didn't have all features of the deprecated APIs. Case in point: this recent Twitter thread from @danielpunkass.)

3 Likes

Using @_deprecationAttr as a stand-in name, would it be sufficient, then, to write:

// iOS
import UIKit
@_deprecationAttr import class UIKit.UIWebView

// macOS
@_deprecationAttr import WebKit
1 Like

Is this not overcomplicating it? The solution and implementation I wrote are the best in my eyes, just attach @ignoreDeprecationWarnings to whatever scope you’re about to use those symbols in (subclass, function, etc)

3 Likes

I think the scoped attribute is a good proposal.

The improvement I can think of would be a way to declare I might want to revisit this use of something deprecated later. For instance, something like this:

@ignoreDeprecationWarnings(until: macOS 13)

which would mean deprecation warnings are going to appear again when compiled in a future macOS SDK. Of course you'd be speculating about the number of the next OS version, but at least at some point you'll be reminded to reconsider the use of the deprecated API.

I don't really know how valuable this would be. Thoughts?