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

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.

4 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).

1 Like

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.

7 Likes

Coming back to this due to recent happenings, hopefully not too late to do so.

@Jumhyn Would you mind sharing the specific objections to the idea or solution brought forth by this pitch? I think it would be much more beneficial to everybody to advance the proposed solution into a state which is considered acceptable by the Language Workgroup

2 Likes

Not too late at all! In the grand scheme of Swift evolution, a couple of months is not a long delay. :smile:

Sure. As I said, some of the specific objections have already been discussed in this this thread, but IIRC it was the consensus of the workgroup that it is not desirable, both from a user's standpoint and a library author's standpoint, to allow users to wholesale silence all deprecation warnings for large swaths of code.

I don't think our discussion delved too deep into the why, but my personal opinions here are: doing so could create a situation where a user ignores deprecation warnings they had no intention of ignoring simply because they used the declaration inside a scope that was already silencing the warnings. It also potentially prevents library authors from tailoring their deprecation warnings to warn about 'more severe' or 'less severe' deprecations in order to influence client action—if clients have silenced all deprecation warnings from a particular library (or all warnings in a particular function) then the author can no longer be confident that users adopting a library version that adds a deprecation will actually see that deprecation and have a chance to act on it before an (eventual) obsoletion/removal.

3 Likes

I agree with this point, perhaps I should change the implementation to be per statement rather than a scope, does Swift have anything that could be applied to a statement? I think the previously mentioned #deprecated(Symbol) is not suitable as it could be hard to read with the # symbol, I did see something like ignoreDeprecation let variable = whatever but could that also be applied to stuff like subclassing or similar cases?

As mentioned by @saagarjha in an outside conversation, I think @deprecated would be best, for example: let x = @deprecated(DeprecatedFunction()), this ensures it’s used individually rather than a possibly massive scope, what do you think?

One problem with a statement based approach like this is that it doesn't cover many scopes in which deprecated declarations can be referenced. I think that a solution to this problem ought to be able to cover the following test cases:

func f(_ x: SomeDeprecatedType) { }
func g<T>(_ t: T) where T: SomeDeprecatedProtocol { }
class Derived: SomeDeprecatedClass { }

struct S {
  var y: SomeDeprecatedType
}

It seems to me that an attribute is necessary for these cases unless we want to invent a lot of novel syntax. Perhaps requiring that you specify a specific deprecated declaration in the attribute would help address some of the concerns that folks have with the attribute approach being too coarse grained:

@ignoreDeprecations(SomeDeprecatedType)
func f(_ x: SomeDeprecatedType) { }

@ignoreDeprecations(SomeType.foo)
func g() {
  let a = SomeType.foo
  // ...
}
1 Like

I actually really, really like this type of approach presented, however, how would it be covered for functions? This is definitely an extreme edge case but say there are 2 deprecated functions, same name but different return types, one returns Int and another returns String, how would that be handled?

This is a good catch, and it tends to come up whenever we need to refer to declarations in an attribute context. One possibility would be to borrow type coercion syntax for disambiguation:

func foo() { }
@available(*, deprecated)
func foo() -> Int { }

@ignoreDeprecations(foo as () -> Int)
func g() {
  let a: Int = foo()
}

The other flavor of this problem (not solved by my proposal above) is disambiguating declarations that are members of extensions from different modules. FWIW, I think we could live with not solving that problem immediately and look at solutions to that problem holistically since it occurs in many contexts already.

2 Likes

As a final concern, what about stuff like this?

override func viewDidLoad() {
  let volumeView = MPVolumeView()
  volumeView.showsRouteButton = false // want to silence the warning here
}

volumeView is declared locally in that context, how would we catch that?

If showsRouteButton is the deprecated declaration then the attribute in this case would be @ignoreDeprecations(MPVolumeView.showsRouteButton).

1 Like