@completionHandlerAsync isn't supposed to suggest deprecation, just that the referenced function should have preference in an async context. It's more similar to @masters3d use case, but as mentioned, not a "always use instead of " but rather "when in an async context, use instead of ".
Regarding using @available/a more general purpose attribute: I was originally against this since it would be awkward to add the completion handler index, but after thinking about it a little more, I'm now not sure we actually need it. In general if the async function parameters match the callback function parameters (minus the completion handler), it's already easy to infer the completion handler index. If the parameters don't match up then having the index probably isn't going to be all that useful anyway - refactorings wouldn't be able to replace the call since they can't match up the parameters and compiler warnings don't need it regardless.
Downside of using @available is that I don't think it entirely makes sense on the callback function, ie. @available usually says something about the attributed function, but in this case it would be saying "there is this other function available that could be used instead". Though if we don't need the index, it could make sense to place the attribute on the async function instead.
The compiler/SourceKit could detect that process(data:) is an async version of process(data:completion:) by applying translation heuristics similar to ClangImporter's (possibly with a little extra logic to support a single Result parameter), and then tailor the diagnostics and fix-its it creates to match.
Since this would just be a change to the diagnostics used for existing syntax, it would not require an evolution proposal and it would be backwards-compatible with earlier compilers, which would simply output generic deprecation diagnostics.
If people strongly want a softer option that doesn't emit deprecation warnings unless the call is in an async context, we could co-opt this syntax to do that:
@available(*, renamed: "process(data:)") // No `deprecated` means suggest replacement only in async contexts
func process(data: Data, completion: (ProcessedData) -> Void) { ... }
func process(data: Data) async -> ProcessedData { ... }
This syntax is accepted by current compilers but doesn't seem to do anything.
It feels odd to me to use renamed - it implies that the functions have the same semantics, which isn't really true here. I can understand the appeal in that there wouldn't have to be any evolution proposal though.
renamed currently isn't typechecked at all either. We could allow non-quoted and typecheck that, but that would - if I understand correctly - still require a proposal.
I was going to write that maybe it doesn't matter if it's a completion handler or not, ie. we'd want to warn if we know there's an async alternative to a function being used in an async context. But I imagine it would be more common to want to use the non-async version (of a non-completion handler) in an async context - so limiting it to completion handlers only seems like a good idea.
And in that case I'm not a huge fan of heuristics - the idea of the initial pitch was to avoid that, since we'd always have the possibility the heuristic being off (resulting in either erroneous or missing diagnostics/similar for the refactorings).
This reminds me though - another aspect to this is that even for completion handlers, it may be that we'd like a way to silence the warning. Wrapping it in parentheses/a no-op function perhaps?
To add to the above - I probably sound more against the idea than I intend to be. I do think it's a decent idea in order to avoid the evolution proposal (and not adding another fairly-limited attribute to the language). Just wanted to make sure we're aware of the trade-offs.
I think finding ways to circumvent the Swift Evolution process while designing new features, when all user-facing features are supposed to be proposed and reviewed whether they introduce new syntax or not, shouldn't be a goal!
Agree that "renamed" doesn't totally fit the bill here. @available(/* ... */, async: "foo") seems like a pretty intuitive spelling to me.
I've been thinking about this a fair bit over the last couple weeks and am definitely leaning towards Becca's solution.
A large part of the reason for this pitch initially was for the completionHandlerIndex argument that would be useful for eg. refactorings. This didn't make a lot of sense to put on @available. But through the discussion in this thread I now realise that this isn't actually required - in the cases that a call could be refactored, it's simple enough to determine the index by looking at the parameters of the matched functions.
So from there, @available looks very reasonable solution.
The next question is whether we should introduce a new parameter to @available purely for this purpose. If renamed: required deprecated, I think a new parameter would be necessary (since it isn't necessarily deprecated). As Becca mentioned though, that's not the case.
Another argument for a separate parameter would be that it could be more specific in what it allows as its argument. But I don't think adding another inconsistent function reference is all that helpful. A better idea there is probably what Jordan mentioned, ie. typecheck the renamed: argument and deprecate allowing a reference to anything but a Swift declaration in scope.
So with all that said, I don't think anything new needs to be added for this purpose. Thanks for the input everyone, much appreciated!