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

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

This seems good, however what about static and non static contexts? Ie

class XClass {
  static let whatever = 3
  let whatever = 3
}
1 Like

@tshortli Looks like there is a perfect solution to what I mentioned above, for example:

class XClass {
   @available(*, deprecated)
   static let something = someFunction()

   @available(*, deprecated)
   let something = someOtherFunction()
}

now, in order to use either, the code should look something like this:

// to use the static property without a warning:
@ignoresDeprecation(static: XClass.something)
func doSomething() { /* do stuff */ }

// to use the instance property without a warning:
@ignoresDeprecation(instance: XClass.something)
func doSomething() { /* do something */ }

This obviously wouldn't apply to any global symbols or types, ie

class SubclassOne: @ignoresDeprecation(DeprecatedClass)

What do you think?

1 Like

Something like @ignoresDeprecation(static: ...) does seem like a viable approach to disambiguation in this case. I would like to think more about whether there's a way to perform the disambiguation inline in the qualified name syntax since it's likely that we'd want to use the syntax in more attributes in the future and it would be a bit unfortunate to need a label in each context.

Just to clarify, this doesn't seem to me like a syntax that would be accepted under the proposal we're discussing now. The class declaration is the entity that would have the attribute applied:

@ignoresDeprecation(DeprecatedClass)
class SubclassOne: DeprecatedClass { ... }

That said, if we wanted to explore the statement based approach more, this does demonstrate a possible solution to the problem I originally posed, which was what to do with all the potential references to deprecated declarations in non-statement contexts. It occurs to me that maybe all those references are to types, in which case you could propose a new grammar for type identifiers that allows you to add attributes to them at the use site:

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

struct S {
  var y: @ignoreDeprecation(SomeDeprecatedType)
}

This removes the attractive nuisance of overly broad suppression. However, it also leads to repetition in contexts where the same type is used repeatedly, so I don't know that I prefer it.

I think I'd much prefer the first syntax proposed by you, as the repetition could lead to hard-to-read code (as you said), with that being said, it looks like we've settled potential (known) concerns for now!

@xwu, @Jumhyn, @hamishknight Thoughts? TLDR on the redesign of the attribute: the attribute will now take in parameters, being the symbols to silence the deprecation warnings of, rather than blocking ALL deprecation warnings in scope, example:

class AudioPlayerController: UIViewController {
    // ...
    @ignoresDeprecation(instance: MPVolumeView.showsRouteButton)
    override func viewDidLoad() {
        let volumeView = MPVolumeView()
        volumeView.showsRouteButton = false
        view.addSubview(volumeView)
        // rest of code
    }
    // ...
}

or:

@ignoresDeprecation(deprecatedGlobalFunction(), DeprecatedStruct)
func deleteItem() {
   // use deprecatedGlobalFunction and / or DeprecatedStruct here without any warnings
   let conf = DeprecatedStruct(something: 3)
   deprecatedGlobalFunc()
}
2 Likes

This feels like a better balance to me.

2 Likes

Any concerns or suggestions to make this model better? I’m also considering removing import support from the attribute due to this new model