loading...
Cover image for The hidden Kotlin gem you didn't think you'll love: Deprecations with ReplaceWith

The hidden Kotlin gem you didn't think you'll love: Deprecations with ReplaceWith

mreichelt profile image Marc Reichelt ・5 min read

This is one of the features in Kotlin that is enabled by IntelliJ IDEA and the Kotlin language working together like a charm, in order to give you a pleasant developer experience. If you look at the replaceWith documentation, you'd probably move on and never give it a second look. Maybe you'll click through to the documentation of the Deprecated annotation. At least the ReplaceWith docs say what this enables. But because there are no examples, you might just close your browser tab and code on.

But wait! There is so much to love about this. Trust me.

Deprecation of subSequence in Kotlin's stdlib

Let's start with an example. In Kotlin 1.3.72, there still is an extension function String.subSequence that takes a start and an end index. But the Kotlin creators wanted it to be more explicit, so they added a new variant that takes startIndex and endIndex as parameters instead. So they deprecated the old one with the @Deprecated annotation - you can see how they did this here. So if you use the old variant, you'll see it striked through like this:

subSequence is striked through

But because they added a replaceWith parameter on that deprecation, IntelliJ will offer you to fix this automatically if you hit Alt + Enter on it - and you can also decide to automatically fix this in your entire project!

IntelliJ offers to automatically replace the old variant

Live in action it looks like this:

IntelliJ offers to automatically replace the old variant

And all the person deprecating the old variant needed to do was to add this one liner before the old variant, worrying only about the code itself, and by doing this enabling millions of developers to be able to change their code in seconds:

@Deprecated("Use parameters named startIndex and endIndex.", ReplaceWith("subSequence(startIndex = start, endIndex = end)"))

Wouldn't it be nice if you could do the same? Yes, you can!

Replace all the things

Let's use a simple example to start. Say, you have a codebase where someonce decided to put the word do in front of lots of methods, like doLogout(). But it's an API, so you can't just get rid of these - you'll have to guide developers to use the new variant logout() instead. This is how:

@Deprecated(
    message = "Use simpler logout() instead",
    replaceWith = ReplaceWith("logout()")
)
fun doLogout() {
    logout()
}

And automatically, doLogout() will become doLogout() for those who use it. And by pressing Alt + Enter and auto-applying the fix, developers now can update this to logout() in the entire project.

Ok, but what about this example? Here, we have the parameters in a peculiar order and a peculiar naming, and we want to be able to clean that up, without breaking the project for anyone:

fun doLogin(pwd: String, usr : String) {
    login(usr, pwd)
}

Well actually, you can use the names pwd and usr in your ReplaceWith string, and IntelliJ will automatically insert the expressions used for pwd and usr into the replaced code!

@Deprecated(
    message = "Use simpler login instead with correct parameter order",
    replaceWith = ReplaceWith("login(usr, pwd)")
)
fun doLogin(pwd: String, usr : String) {
    login(usr, pwd)
}

fun login(username : String, password: String) {
}

I didn't see the implementation, but if I would take a good guess this has to work by the IDE taking the old Abstract Syntax Tree (AST), building the new AST of the ReplaceWith string, and copying over the expressions to the right placeholder positions. If anyone finds where this logic is implemented, please send me the GitHub link - I'd love to include it in this blog post! 🌟

Because it's so nice, here's how this looks if you apply it in the IDE:

doLogin being automatically replaced by IntelliJ IDEA

It can do even more: add more code, and imports

Let's say we have the doLogin method from before, but instead of using two parameters, we want to insert a new Credentials data class that sits in the auth package:

package auth

data class Credentials(
    val username: String,
    val password: String
)

This is how we do it:

@Deprecated(
    message = "Use simpler login with Credentials",
    replaceWith = ReplaceWith(
        expression = "login(Credentials(usr, pwd))",
        imports = ["auth.Credentials"]
    )
)
fun doLogin(pwd: String, usr: String) {
}

When a developer applies the auto-fix with Alt + Enter, this will automatically add an import as well as fixing the code in-place! This time, let's fix multiple occurrences at the same time, because this looks so cool. Imagine having hundreds of these in your code base, in dozens of files - all fixed automatically! πŸŽ‰

Replacing multiple occurrences at once, while adding an import

Deprecated can do more

That's it for now about ReplaceWith! One more thing about @Deprecated annotation: you can add a DeprecationLevel to it as well. The default is WARNING. But if you want to up the game and you want all developers to finally move over to the new API, you can force them by making this an ERROR. These have to be fixed first, otherwise their code won't compile. And finally, once all developers have moved over, you can even set this to HIDDEN - so no one will even see the old method anymore. This is useful if you still want to be binary-compatible. Here is how you can use all these:

@Deprecated(message = "", level = DeprecationLevel.WARNING)
fun warning() {
}

@Deprecated(message = "", level = DeprecationLevel.ERROR)
fun error() {
}

@Deprecated(message = "", level = DeprecationLevel.HIDDEN)
fun hidden() {
}

And here is how they will look when used:

Effect of all deprecation levels as shown in the IDE

I hope this will be a nice addition to your Kotlin toolbox!

Recap

So to recap, using @Deprecated with ReplaceWith lets you:

  • deprecate old code, and automatically suggest the desired change
  • this desired change will be available to all developers working on that code, and they can apply your code change with Alt + Enter instantly
  • it can even re-order the old parameter values if you need them to, or let you add entire new code constructs and imports
  • add deprecation levels to decide how fast users of the old code need to fix this
  • be more productive and have more fun!

Edit: Russell Wolf added a comment on Twitter that you can use this feature also for discovering APIs, which I think is really cool:

Have an awesome Kotlin! πŸŽ‰

If you liked this post, please give it a ❀️, click the +FOLLOW button below and follow me on Twitter! This will help me stay motivated to create many more of these posts! 😊

Cover photo by Malcolm Lightbody on Unsplash.

Posted on May 29 by:

mreichelt profile

Marc Reichelt

@mreichelt

Developer. Loves Linux, Android & open source. Making the world better, one step at a time!

Discussion

markdown guide
 

Since Java can use Kotlin, how can I make it work for Java too? For example, I have a few helper functions for Java, as this one:

@Deprecated(message = "", replaceWith = ReplaceWith("collection.isNullOrEmpty()"))
@JvmStatic
fun isEmpty(collection: Collection<*>?): Boolean = collection.isNullOrEmpty()

Can Java use the functions from Kotlin, that should be suggested here?