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

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

Agree with @Jumhyn’s view overall.

I differ on the issue of import-level deprecations because the use cases presented above relate to entire libraries being deprecated without equivalent replacement.

Individual deprecated functions, etc., can be feasibly dealt with by re-declaration or in other ways (even re-implementation sometimes), but an acknowledgment per use site or these workarounds you mention aren’t really an answer to the problem where, say, all of WebView is deprecated.

The user intention that we want to make possible to express here is, “Yes—I know WebView is deprecated but I want to use it anyway,” which is not best served by re-declaring the entire API surface of WebView or annotating every call to a WebView API.

On the other hand, I am not sure I understand what problem is uniquely addressed by a project-wide compiler flag to silence all deprecation warnings—this would mean that a user wants to use any and all deprecated APIs from any modules and in any place without thinking about it, and I’m doubtful this is desirable to enable.

This goes to my concern above about scope-level deprecations too. The core of where I agree with @Jumhyn lies (to my understanding) in that we both think it’s important that the user to be able to express an intention of the form “I know ‘X’ is deprecated but I want to use it here” for some specific “X” and for some granularity of “here.”

We may differ somewhat on the level of specificity we are comfortable with, but we’re both wary of features that enable silencing of deprecation warnings from who-knows-where, as it’s not really an expression acknowledging awareness of what warnings are actually being silenced.

3 Likes

@Jumhyn @xwu Good points, I think Jumhyn's point that users should declare a file with definitions explicitly using the attribute to ignore the warnings rather than having a compiler flag is good, I just wanted to ask though, why do you guys think it'd be a good idea for @testable to imply @ignoreDeprecationWarnings? I think that may be best under a compiler flag, ie -testable-import-imply-ignore-deprecations

Because @testable gives access to internal declarations not publicly available to users, so if we adhere to the notion that deprecations are an API-level concern (i.e., they’re about what public interface users can access and where) and on that basis decide to allow projects to use internal self-deprecations without warning, then the same flexibility should apply to testable imports because that is in the nature of what @testable means (access to internals).

Concerns over users who perhaps may not want to ignore deprecation warnings in contexts where the module is imported using @testable, though?

Not from me; there may be issues I haven’t thought of, but when you import a module using @testable, you lose the ability to distinguish between what’s publicly available and what’s internal (and therefore the ability to test that—this is relevant when you want to test that users writing the obvious thing end up calling the intended overload among many, for instance), so I don’t see why losing warnings about availability due to deprecation is an issue.

1 Like

Speaking of tests, doesn't this work already to silence deprecation warnings?

class TestDeprecation: XCTestCase {

    @available(*, deprecated)
    func testExample() throws {
		deprecatedFunctionCall()
    }

}

Also, I think it's convenient that tests have to be marked as deprecated. It helps the reader notice this is testing deprecated stuff. It also helps the API author take notice of the impact of deprecations, such as tests representing use cases that can't be rewritten using a non-deprecated replacement.

3 Likes

Perhaps the per-declaration-use undeprecation could be tweaked to be a bit more expansive. E.g., if a user undeprecates UIWebView by doing:

typealias MyWebView = WebView

that would have the effect of undeprecating all symbols within MyWebView that are deprecated at a level at or below the level of WebView. I'm not sure if there's an implementation burden here (at the time deprecation warnings are emitted, would the compiler be able to 'know' that a symbol was found via MyWebView instead of WebView?), but I think I'd feel better about this approach than allowing users to too easily undeprecate an entire import.

It there isn't an implementation burden here and we go through with automatically silencing self-deprecation warnings, then I'd imagine the above could just be reduced to:

@available(*, deprecated)
typealias WebView = UIKit.WebView

requiring no new innovations in the language.

2 Likes

I'm strongly opposed to this idea. All of the reasoning about making explicit decisions when referencing deprecated external symbols applies just as equally to internal symbols. You want to minimize existing usage and discourage introducing new usage.

This happens for API/binary compatibility reasons as @millenomi already mentioned—the goal is to freeze some structure of your code such that it doesn't break clients, but that doesn't mean that new code should ever be on that path. It's also common to go through heavy iterations on pre-1.0 libraries that deprecate large portions of the API surface at a time. After a round or two of this it extremely easy to lose track of what the current correct interface is.

It's also a useful tool in projects that don't have any API at all (i.e. applications): many projects of a sufficient age want to undergo structural changes (for example, migrating to new versions platform API, or better representations of business requirements) that are best represented as deprecating existing internal types in favor of new ones.

5 Likes

Proposal opened here.

2 Likes

Yeah, as nifty as it would be to accomplish without changing the surface level syntax, I'm inclined to agree with @frozendevil: I don't think it's tenable to just stop warning about internal uses of deprecated symbols. At the very least I think this behavior would need to be enabled by a compiler flag, but then users would have to choose between using other libraries' deprecated symbols and being able to use deprecations for themselves internally.

If we want to reuse the deprecation-silencing behavior of @available(*, deprecated) for this purpose, I think it would be better to take an approach such as visibility modifiers for declarations, as @allevato mentions above. So the undeprecation of WebView would look something like:

@available(*, deprecated(public))
typealias WebView = UIKit.WebView

which would mean "this symbol is deprecated at the public visibility level, but not at the internal level."

Although, as I write it out, I'm not sure if this solution feels quite right, either. Ostensibly, the local redeclaration of WebView is only internal anyway, so it seems like a bit of an abuse to say "this internal symbol is deprecated for public uses." It also seems like this would just decay to the scope-level silencing feature, since presumably:

@available(*, deprecated(public))
internal func noDeprecationWarningsHere() {
  // use deprecated symbols to your hearts content
}

would exhibit the same deprecation-silencing behavior.

ETA: I suppose this is also (for me) an argument against a compiler flag for disabling internal deprecation warnings, since that would also turn @available(*, deprecated) into a scope-level deprecation silencing mechanism.

So, all that's to say, as a proponent of silencing at the granularity of declaration use, I think reusing @available doesn't work quite well enough for me. I think this feature is deserving of a new annotation which directly expresses "I know this particular symbol is deprecated but I'm going to use it anyway".

1 Like

The Language Workgroup discussed this pitch briefly and felt that it was unlikely to be accepted if brought to review in its current form. There was general agreement that it's probably appropriate to solve this problem with a source-level feature (to avoid the 'dialectization' issue raised by, e.g., compiler flags) and that deprecation warnings are a bit of an exception amongst Swift's warnings today in that there is no way to silence them when the user really does want to. However, there were also concerns about the potential downsides of a solution which allows for overly coarse-grained silencing of such warnings, some of which have been raised in this thread.

The Workgroup feels that this pitch would benefit from more discussion refocused on solutions which enable more narrowly tailored silencing mechanisms, similar to how other warnings are silenced.

5 Likes