12.2.8 自定义编译器插件

原文链接: https://kotlinlang.org/docs/custom-compiler-plugins.html

12.2.8 自定义编译器插件

警告: Kotlin 编译器插件 API 不稳定,并且每个版本都会引入破坏性变更。

在创建自己的自定义编译器插件之前,请先查看可用编译器插件列表,看看是否已有适合你用例的插件。

你还可以确认是否能用 Kotlin Symbol Processing (KSP) API 或 Android lint 这类外部 linter 达到目的。

如果你仍然找不到需要的功能,可以创建自定义编译器插件。请注意,Kotlin 编译器插件 API 是不稳定的。你需要投入大量持续的维护工作,因为每个新的编译器版本都会引入破坏性变更。

Kotlin 编译器与编译器插件

Kotlin 编译器各阶段

Kotlin 编译器会:

  • 解析源代码并把它转换为结构化的语法树。
  • 通过确定代码的含义、解析名称、检查类型并执行可见性规则来分析和解析代码。
  • 生成中间表示(IR),这是一种充当源代码与机器码之间桥梁的数据结构。
  • 逐步把 IR 降低(lowering)为更简单的形式。
  • 把降低后的 IR 转换为目标特定的输出,例如 JVM 字节码、JavaScript 或原生机器码。

插件可以通过前端 API 影响编译器的初始阶段,改变编译器解析代码的方式。例如,插件可以添加注解,或引入没有函数体的新方法,或修改可见性修饰符。这些变更在 IDE 中可见。

插件也可以通过后端 API 影响后续阶段,修改声明的行为。这些变更体现在编译完成后生成的二进制文件中。

在实践中,编译器插件会影响到从分析与解析到代码生成的各个阶段,覆盖前端和后端。例如,前端部分生成声明,后端部分为这些声明添加函数体。

带插件的 Kotlin 编译器各阶段

Kotlin 序列化插件就是一个很好的例子。该插件的前端部分添加伴生对象和序列化函数,以及用于防止名称冲突的检查。后端部分则通过 KSerializer 对象实现所需的序列化行为。

Kotlin 编译器插件模板

要开始编写自定义编译器插件,你可以使用 Kotlin 编译器插件模板。然后注册来自前端和后端插件 API 的扩展点。

注意: 目前,你只能使用 Gradle 开发自定义编译器插件。

前端插件 API

前端插件 API 也称为前端中间表示(FIR),它有以下专门用于自定义解析的扩展点:

| 扩展名 | 说明 |

| FirAdditionalCheckersExtension | 添加自定义编译器检查器。 | | FirDeclarationGenerationExtension | 生成新的声明。 | | FirExtensionSessionComponent | 在 FirSession 中注册自定义组件,供插件其他部分使用。 | | FirFunctionTypeKindExtension | 定义新的函数类型族。 | | FirMetadataSerializerPlugin | 读写声明元数据中的信息。 | | FirStatusTransformerExtension | 修改声明的状态属性,例如可见性或模态。 | | FirSupertypeGenerationExtension | 为已有类添加新的超类型。 | | FirTypeAttributeExtension | 根据类型注解为某些类型添加特殊属性。 |

IDE 集成

解析变更会影响 IDE 的行为,例如代码高亮和提示,因此让你的插件与 IDE 兼容非常重要。每个版本的 IntelliJ IDEA 和 Android Studio 都包含一个开发版的 Kotlin 编译器。这个版本是 IDE 专用的,与已发布的 Kotlin 编译器不二进制兼容。因此,当你升级 IDE 时,也需要更新编译器插件以保持可用。这也是社区插件默认不会被加载的原因。

为确保你的自定义编译器插件在不同 IDE 版本上都能工作,请针对每个 IDE 版本进行测试,并修复发现的问题。

如果有 Kotlin 编译器插件的开发工具包(devkit),支持多个 IDE 版本可能会更容易。如果你对这个特性感兴趣,请在我们的问题跟踪器中提供反馈。

后端插件 API

警告: 后端插件开发很难做到正确而不影响 IDE 或调试器性能,因此请谨慎、保守地进行改动。

后端插件 API 也称为 IR,只有一个扩展点:IrGenerationExtension。使用这个扩展点并重写 generate() 函数,为前端已生成的声明添加函数体,或修改已有的声明函数体。

通过这个扩展点所做的更改不会被编译器检查。你必须确保你的更改不会破坏编译器在该阶段的预期。例如,你可能无意中引入无效类型、错误的函数引用,或超出正确作用域的引用。

探索后端插件代码

你可以研究 Kotlin 序列化插件的代码,看看后端插件编译代码在实践中是什么样子。例如,SerializableCompanionIrGenerator.kt 会为关键的序列化成员补上缺失的函数体。其中一个例子是 generateChildSerializersGetter() 函数,它收集一组 KSerializer 表达式并以数组形式返回。

检查后端插件代码中的问题

你可以用三种方式检查后端插件代码中的问题:

  1. 校验 IR

构建 IR 树并启用 Xverify-ir 编译器选项。该选项会影响编译速度,因此只在测试期间使用。

  1. 转储并比较 IR 输出

使用 -Xphases-to-dump-before=ExternalPackageParentPatcherLowering 编译器选项在 IR 降低编译阶段之后创建转储文件。对于 JVM 后端,使用 -Xdump-directory=<your-file-directory> 编译器选项配置转储目录。手动编写期望的代码,生成另一个转储文件,然后比较两者以查看是否存在差异。

  1. 调试编译器代码

在 convertToIr.kt 文件中,于 convertToIrAndActualize() 函数中设置断点,并以调试模式运行编译器,从而在编译期间获取更详细的信息。

测试你的插件

实现插件之后,请对它进行充分测试。Kotlin 编译器插件模板已经配置好使用 Kotlin 编译器测试框架。你可以在以下目录中添加测试:

  • compiler-plugin/testData
  • compiler-plugin/testData/box 用于代码生成测试
  • compiler-plugin/testData/diagnostics 用于诊断测试

测试运行时,框架会:

  1. 解析测试源文件。例如 anotherBoxTest.kt
  2. 为每个文件构建 FIR 和 IR。
  3. 把它们写成文本转储文件。例如 anotherBoxTest.fir.txt 和 anotherBoxTest.fir.ir.txt。
  4. 如果此前已创建过这些文件,则把它们与此前的文件进行比较。

你可以用这些文件检查生成的差异中是否有非预期的改动。如果没有问题,新的转储文件就成为你最新的黄金文件:一个经过批准且可信的基准,你可以用它来比较将来的改动。

获取帮助

如果你在开发自定义编译器插件时遇到问题,请在 Kotlin Slack 的 #compiler 频道中求助。我们无法保证给出解决方案,但会尽力提供帮助。