In order to maximize the benefits brought by Kotlin in terms of productivity and safety, Meta engineers have been hard at work to translate their 10 million line Android codebase from Java into Kotlin. One year into this process, they have ported approximately half of their codebase and developed a specific tool, Kotlinator, to automate the process as much as possible.
Instead of translating only actively developed code, which could sound the most convenient approach, Meta engineers decided to go for a full migration to avoid the risk that any remaining Java code could be the source of nullability issues but also to remove the drawbacks of using two different toolchains in parallel and the performance hit of having to compile a mixed codebase.
From the beginning, it was clear to Meta engineers that the support provided by IntelliJ's J2K translation tool was not enough for such a large codebase and that they needed to automate the conversion process as much as possible. However, J2K provided the foundations for their conversion solution, Kotlinator.
The first step was transforming J2K into a headless tool that could be run on a remote machine. The headless J2K was implemented as an IntelliJ plugin extending the ApplicationStarter
class and calling directly into JavaToKotlinConverter
, as the IntelliJ conversion button does.
Before running the headless J2K, Kotlinator uses a pre- and post-conversion steps to make sure that converted code can build. These steps deal with nullability, apply some known J2K workarounds, and make the generated code more idiomatic.
Both phases contain dozens of steps that take in the file being translated, analyze it (and sometimes its dependencies and dependents, too), and perform a Java->Java or Kotlin->Kotlin transformation if needed.
Meta open sourced some of those conversions so that they could be directly used and also to provide examples of Kotlin AST manipulation through the Kotlin compiler API.
Most of the conversion steps are built using a metaprogramming tool leveraging JetBrains' Program Structure Interface (PSI) libraries, which can parse files and create syntactic and semantic code models without resorting to the compiler. This is important, say Meta engineers, because in many cases post-processed code would not compile at all, so an alternative method to transform it was required and PSI provided just that.
Any build errors resulting from these steps are handled by interpreting the compiler's error messages just as a human would do, explain Meta engineers, but in an automated way specified using some metaprogramming.
Instead of simply reducing developers' effort, these automated steps were also used to minimize the possibility of human error when converting code manually. In the process, Meta engineers collaborated with JetBrains to extend J2K to enable it to run hooks injected by client libraries directly in the IDE.
As Meta engineers explain, a large part of their effort to make Java code translatable to Kotlin is aimed at making it null-safe. This can be achieved simply using a static analyzer, such as Nullsafe or NullAway, to detect all suspect cases, but it is still not enough to eliminate the risk of null-pointer exceptions (NPEs).
NPEs, for example, can arise when some non null-safe dependency passes a null
value into a formally non-nullable parameter to a function. One approach taken by Kotlinator to reduce this risk is "erring on the side of more nullable", which means defaulting to considering parameters and return types as nullable when the code does not suggest they actually are non-nullable. Additionally, they built a Java compiler plugin that collects nullability data at runtime to identify parameters and return types that could be null
in spite of not being declared as nullable
.
As it is evident from Meta report, translating a large Java codebase into Kotlin is no trivial effort and requires some very advanced engineering. Meta's journey to make their codebase 100% Kotlin has not come to an end yet but it surely provides the opportunity for some deep understanding of the differences between the two languages and how to transform Kotlin code in a programmatic way. There is much more to this than can be covered here, so do not miss the original article if you are interested in the full detail.