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

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

I don't mean this as a mindless careless rant, quite the opposite; please don't take it that way.

I would just like to voice my frustration with the idea that developers are children who have to pedantically spell out each deprecated API we want to use before we use it in order to tell the compiler we're "extra sure" we want to ignore their deprecation warnings. This feels very much like the infantizing hand-holding many of us are used to from Apple software. It almost feels abusive, like I'm being told I can't be trusted to know what my project's own needs are.

There is a clear demand here for a feature that every other established ecosystem has in some form, and the unique response from the swift community has been "developers don't know what's good for them, they shouldn't be able to ignore warnings in an entire scope of their choosing willy-nilly." To this I say: why shouldn't we? Especially when Apple is so eager to deprecate perfectly fine APIs lately. (See: file URL inits)

Real world examples and use cases for silencing deprecation warnings have been given all over this thread. Swift itself even uses such flags in the CXX portion of the codebase. And yet the community does not hold Swift itself to the same high standard they hold Swift users to.

I understand Swift has a reputation to uphold. Carefully designed and well thought out APIs that are difficult to misuse, etc. But this API is different; it isn't one that plays a role in code quality. Instead, it has a larger impact on project management. I don't think it's our place to place this kind of burden on software development.

In my opinion

We should make it as easy as possible for developers to manage project warnings the way they see fit, even if they want to silence warnings for the entire project. (I.E. maybe this is my pet project just for me and idgaf.)

At the very least, I believe we need to give developers a way to silence all warnings within a given scope, without re-writing the symbol names in the attribute. I don't see anything wrong with giving developers the option to be more granular as suggested, but I think that kind of pedantic granularity should absolutely be optional.

Ideally, there should be a way to silence warnings at every level, at least starting with multi-line scope (like if #available), then to function scope, type scope, import scope, and finally target scope. Wouldn't be the end of the world if we only end up with the tightest scoped option—though I think something like line-scope is far tedious and pedantic—but I would love to see more than one of these included, each one accepting no flags or options by default, but optionally accepting any of the ideas in this thread (an "until" modifier, a list of declarations, etc)


I personally would not use this API if I had to spell out each depreciated API I wanted to ignore. I wouldn't use it out of spite. I don't have the patience to write boilerplate like that, and I would be insulted if I were forced to. :man_shrugging:t2:

6 Likes

Clang does not, to my knowledge, offer anything other than wholesale silencing of warnings for a particular span of code (or via a compiler flag), so to me this doesn't seem to be anything other than a signal of what has already been acknowledged—that there are legitimate use cases for silencing deprecation warnings.

Let me try to phrase my feelings in a way that doesn't come across as though I'm trying to place a burden on software development, because I really, honestly, don't see it that way.

There's an inherent interplay here between library authors (i.e., those that are marking things as deprecated) and end users (i.e., those that are potentially silencing the deprecation warnings). Obviously there's overlap between these two groups (since library authors are end users of other libraries) but I'm mostly thinking about the 'deprecator-deprecatee' relationship here.

When an author marks something as deprecated rather than removing it outright, it's because they want to provide a 'soft' signal to their users that they should stop relying on that functionality, and either remove it from their codebase or replace it with an equivalent. When users update to the version of the library with the deprecation they will get a warning, see what the alternative is, and either update their usage as appropriate or decide that they are not concerned about the warning at the moment. In either case, though, the library author has confidence that those who are using the library version with the deprecation have made a deliberate choice about how to address it. In a later update, the library author can with increased confidence remove the deprecated API without worrying that their users will be caught off-guard.

If Swift introduced the ability to silence deprecation warnings on arbitrary spans of code, this story becomes more complex for library authors. They can no longer necessarily be confident that users of the library version with the deprecation will see the deprecation warning, and so cannot safely remove the deprecated API without worrying that they will be unexpectedly breaking clients.

One may feel that these clients 'opted in' to being unexpectedly broken by removal of deprecated APIs, but with coarse-grained silencing that doesn't seem obvious to me. If a user silenced deprecation warnings in a function due to library A having deprecation warnings but being unmaintained, that doesn't necessarily help the author of library B who is actively maintaining their library and trying to responsibly remove outdated APIs, and leaves them at increased risk of breaking clients even when they are trying to do the right thing. This places a burden on library authors because it restricts their ability to evolve their APIs while trying to avoid breaking clients.

To a lesser extent, it also potentially places a burden on users who inadvertently silence more than they want to, and end up missing important declaration warnings. Yes a more fine-grained silencing mechanism, comes at the cost of requiring more boilerplate for users who really, really don't care at all about any deprecation warnings and would just want to silence everything, but I think there's a tradeoff here that isn't a clear-cut 'burden' in one direction or the other.

I read this as a rejection of the philosophy that Swift has generally used for all warnings, which is to enable them unconditionally on the 'problematic' code but provide ways for users to silence them by clarifying intent in source rather than just giving a compiler directive to say "don't warn me about this."

Deprecation warnings are 'special' today because there is no way to silence them by clarifying intent, and I think plenty of folks have successfully argued in this thread that that isn't really justifiable. What I have not seen a convincing argument for is why deprecation warnings are so special that the language must have blanket silencing for them at every level. Clang and GCC allow silencing deprecation warnings on any span of code, yes, but deprecation warnings are not special in that regard. That is simply the silencing mechanism that Clang and GCC have chosen.

I'm open to being convinced on this point, but I'd likely still lean towards a conservative approach to language evolution. Allowing silencing deprecation warnings at import or scope level would be difficult to 'take back' later if we decided it was a mistake, so I prefer a solution which starts with a (perhaps) overly-boilerplate-y silencing mechanism and then evaluate if the need for further work is still there.

I will note that a line-level option which doesn't accept a specific declaration is not incompatible with the broad guidance that the workgroup has provided here, which was specifically targeted at concerns around coarse-grained silencing of all deprecation warnings. I have a personal soft-spot for requiring users to specify the declaration that they wish to 'undeprecate,' but an expression-level feature at least drastically reduces the concerns outlined above. (Of course, single expressions can grow quite large and include arbitrary code in closures, so I'd maybe encourage carving out some exceptions to how 'deep' the directive would apply if we were to pursue that direction.)

This seems... extreme. The situation today requires extensive boilerplate to work around (I've seen codebases that write Objective-C versions of types, methods, and functions for the sole purpose of silencing the deprecation warning from Objective-C and then round-tripping right back to Swift), so even a solution that required you to spell out those APIs entirely on the Swift side of things would to my eye be a marked improvement. Perhaps you're already even more insulted by the current state of affairs, though. :smile:

9 Likes

If the deprecated declarations are explicitly spelled out I don't really see a need to remove the import support for this attribute, personally.

Speaking for myself personally, I continue to feel that the correct level of granularity for “undeprecation” should be at the import site (or, equivalently, at some sort of redeclaration site).

This would achieve the goal of making sure only warnings about specific APIs are silenced without requiring undue repetition multiple times within the same file, and without making the spelling out of the declaration feel like make-work handholding since the undeprecation would naturally attach to it.

I am open to being convinced that the use site (whether line by line, or scope by scope) is superior in some ways, and/or further that it would be superior to allow silencing of all warnings on a line or in a scope. However, to comport with the language’s overall philosophy that @Jumhyn spells out above, the “maximum” scope for an across-the-board deprecation warning silencing then would necessarily have to be limited, perhaps to only a line at a time.

Whichever way the proposal authors feel—use site versus declaration site—the proposal should make a convincing argument one way or another (or for including both). However, there should be a detailed exploration with sufficient discussion of the pros and cons of each approach which ultimately arrives at a conclusion, a reasoned stand to justify the design proposed. The proposal should not be a “kitchen sink” of all possible suggestions nor, on the other hand, simply repeat back what the workgroup feels.

Let me emphasize here that what follows is, besides being @Jumhyn’s perspective and very well written, more or less in line with what I understand the language workgroup’s guidance would be based on our discussions on the topic.

3 Likes

I'd like to add that there are important use cases that this alone doesn't handle well—using deprecated declarations in your own module. SwiftProtobuf is the prime example where we'd like to place deprecation attributes on properties that are marked deprecated in the .proto file so that external users can be warned about them, but we also need to access those properties internally during serialization, and those accesses also produce warnings.

But that's also why per-API granularity controls on their own don't work either; we'd have to generate an @ignoresDeprecation around every serialization function for every property, which doesn't scale, or we'd have to create deprecated public wrappers around not-deprecated private properties, which also doesn't scale.

So I think we need a combination of approaches here:

  • Something that gives API users the ability to say "I know what I'm doing, let me use this thing", which is the primary thing being discussed above
  • Something that gives API authors the ability to continue using their own APIs without undue burden and without introducing a dialect (e.g., they should not have to pass a compiler flag like -ignore-my-own-deprecations)

I'd personally be fine with automatically excluding deprecation warnings for references to symbols in the same module because I don't need my hand held for my own code, but some folks in the past have even argued that that's too broad because they have huge modules with contributions from many people. Ignoring deprecations in the same file would work for my use case, but I don't know if losing the module granularity burdens any others. I think that we should ultimately just make that decision though, because I'm not sure we can satisfy everyone without making the controls much finer and complicated than they need to be.

2 Likes

Why doesn’t this scale? Isn’t the Swift code generated from the .proto file anyway?

Just because we can generate a lot of code doesn't mean we should. Every bit of code we generate is going to have some effect on compilation performance and perhaps runtime performance. There will be additional metadata generated so there's a binary size cost, and the compiler will have to do more work during compilation. The wrapper properties may introduce their own runtime performance issues; if we try to work around that with @inlinable and friends, then we're giving the compiler even more work to do during the optimization passes to consolidate those things—and that's relying on the optimizer to do the right thing in all cases.

None of that is a reasonable price to pay when the problem we want to solve is "let me internally reference this thing that I want to disrecommend for external users."

1 Like

When you control the deprecation attribute, I feel like the best solution is to allow the author to specify that it should only apply to external clients.

3 Likes

We’ve had discussions along these lines here before—that is, annotating an API as deprecated for public use only. This is something that people have reached for naturally.

It can be thought of as orthogonal to the silencing of warnings, since the feature goes to improving the expressivity of availability annotations (i.e., this API actually isn’t deprecated for me; Swift just doesn’t have a spelling for that) rather than acknowledging a true deprecation warning (i.e., I am actually invoking an API that the author of that API would rather I didn’t, I just don’t have a good alternative or can’t get around to fixing this).

Another relevant or at least somewhat related discussion would be the formalization of the @_spi annotation.

1 Like

Maybe something like this?

This is similar to how swiftlint is doing it.

1 Like

My bias is towards a solution that enables suppression per-scope (or per-expression) because on the projects I've worked on in the past when a new deprecation cropped up, the decision of what to do about it generally needed to evaluated case-by-case at each of the use sites. In some rare cases we would want to simply ignore the deprecation altogether everywhere, but it was more common for the course of action to be dependent on the code using the deprecated API. In a sense, the deprecation diagnostics formed a todo list of situations to consider.

Here are some examples of the potential divergents courses of action that depend on the use site:

  • Adopt the replacement API right away because it is straightforward to do so.
  • Ignore the deprecation at this use site because the refactoring needed or qualification required is too much to deal with in this moment. Record a bug to follow up on this use specifically.
  • Ignore the deprecation here at this use site because the use of the now deprecated API is load-bearing in this one spot. At other use sites, though, I want to refactor to adopt the new thing or to funnel through a layer of indirection to help reduce my dependence on the deprecated API.

I think this tool needs to be flexible enough to cover many different scenarios and a per-scope solution that references a specific deprecation accomplishes that. I worry that a design that prioritizes simplicity in some kinds of deprecation scenarios (e.g. own module deprecations or deprecations that will never be addressed) will still be lacking for many developers. A flexible design that has broad applicability while not precluding us from making future ergonomic enhancements after we get some experience is what I'd like to see.

9 Likes

@Jumhyn @xwu @tshortli A couple of updates:

  • I am planning not to use the previously mentioned @ignoreDeprecations(instance: Type.instanceSymbol) (and the same for static:) syntax, but rather, use #selector way of referring to symbols (ie, @ignoreDeprecations(MPVolumeView.showsRouteButton, NSObject.className)

  • Personal thoughts which I've not shared yet: I do think, for the sake of flexibility, there should be an all-catch * input that can be put in if the user really, really wants to ignore every single deprecation, though im not sure if this is a completely good idea.

Please let me know what you all think about both ideas ^

1 Like

Would it be possible to have this toggle-able by a flag so when one have some spare time to fix warning they can easily see them all ?

Also one thing I tend to do is to add a date in the warning to remember me to act on it after a certain date is passed maybe this could be explored

Edit: another thought maybe add some kind a category like log levels to be able to quickly see sort warnings

That syntax seems pretty reasonable to me, I can’t think of any dire need to distinguish between static and instance symbols in this position since all we care about is uniquely referring to a particular declaration.

I remain pretty skeptical that providing a way to silence all deprecation warnings in a scope is a good idea, but if we were to provide such functionality using * doesn’t seem unreasonable. My only thought is whether it might be weird to use * here to mean something different than it does in @available attributes, given that this attribute will in some ways serve as a counterpart @available.

2 Likes