深入探索 Kotlin 编译器:FIR 与 IR 插件测试
本文深入探讨 Kotlin 编译器插件的测试机制,核心围绕两种测试类型:用于验证编译时错误的 “诊断测试 (Diagnostic Test)”,和用于验证运行时行为的 “黑盒代码生成测试 (Box Test)”。我们将以官方的 Kotlin/compiler-plugin-template 项目为例,揭示测试指令(Test Directives)如何驱动复杂的测试流程,并阐明测试类的继承关系如何决定测试的行为与验证逻辑。
核心测试类型 在 Kotlin 编译器插件模板项目中,主要通过以下两种测试类型来确保插件的正确性:
诊断测试 (Diagnostic Test) 此测试的核心目标是验证编译器在特定阶段能否正确报告预期的编译错误或警告。它通过分析源码,在编译流程的某个指定阶段(如 FRONTEND)停下来,检查编译器输出的诊断信息是否与预期完全一致。这种测试不关心代码能否最终运行,只关心编译过程的正确性。
黑盒代码生成测试 (Box Test) 黑盒测试关注的是“输入”与“最终输出”的对应关系,而不关心编译器的内部实现细节。测试流程会将一段合法的 Kotlin 源码作为输入,完整地执行编译、代码生成和运行,最后验证程序的运行时输出是否符合预期。这种测试用于确保编译器插件在修改代码后,其运行时行为仍然正确。
项目结构解析 分析的源码基于官方的 Kotlin/compiler-plugin-template 。该模板项目为开发编译器插件提供了标准的结构和测试框架。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ├───.github ├───.gradle ├───.idea ├───.kotlin ├───build ├───compiler-plugin │ ├───src │ │ └───org/jetbrains/kotlin/compiler/plugin/template │ │ ├───fir │ │ └───ir │ ├───test-fixtures │ │ └───org/jetbrains/kotlin/compiler/plugin/template │ │ ├───runners │ │ └───services │ ├───test-gen │ │ └───org/jetbrains/kotlin/compiler/plugin/template │ │ └───runners │ └───testData │ ├───box │ └───diagnostics ├───gradle ├───gradle-plugin ├───kotlin-js-store └───plugin-annotations └───src
其中与测试直接相关的关键目录如下:
compiler-plugin/testData: 存放测试用例源文件(.kt),按测试类型分为 box 和 diagnostics。
compiler-plugin/test-fixtures: 包含测试辅助工具类,例如自定义的测试运行器基类和配置器。
compiler-plugin/test-gen: 存放由测试框架根据 testData 自动生成的测试类代码。
黑盒代码生成测试 (Box Test) 实现 黑盒测试验证从 FIR 到 IR 再到最终代码生成的完整流程。
测试基类 AbstractJvmBoxTest 所有 JVM 平台的黑盒测试都继承自 AbstractJvmBoxTest。此类负责配置测试环境、编译器指令和插件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package org.jetbrains.kotlin.compiler.plugin.template.runnersimport org.jetbrains.kotlin.compiler.plugin.template.services.ExtensionRegistrarConfiguratorimport org.jetbrains.kotlin.compiler.plugin.template.services.PluginAnnotationsProviderimport org.jetbrains.kotlin.test.FirParserimport org.jetbrains.kotlin.test.builders.TestConfigurationBuilderimport org.jetbrains.kotlin.test.directives.CodegenTestDirectivesimport org.jetbrains.kotlin.test.directives.FirDiagnosticsDirectivesimport org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectivesimport org.jetbrains.kotlin.test.runners.codegen.AbstractFirBlackBoxCodegenTestBaseimport org.jetbrains.kotlin.test.services.EnvironmentBasedStandardLibrariesPathProviderimport org.jetbrains.kotlin.test.services.KotlinStandardLibrariesPathProvideropen class AbstractJvmBoxTest : AbstractFirBlackBoxCodegenTestBase (FirParser.LightTree) { override fun createKotlinStandardLibrariesPathProvider () : KotlinStandardLibrariesPathProvider { return EnvironmentBasedStandardLibrariesPathProvider } override fun configure (builder: TestConfigurationBuilder ) { super .configure(builder) with(builder) { defaultDirectives { +CodegenTestDirectives.DUMP_IR +FirDiagnosticsDirectives.FIR_DUMP +JvmEnvironmentConfigurationDirectives.FULL_JDK +CodegenTestDirectives.IGNORE_DEXING } useConfigurators( ::PluginAnnotationsProvider, ::ExtensionRegistrarConfigurator ) } } }
自动生成的测试类 JvmBoxTestGenerated 是一个由测试框架自动生成的类,它会扫描 compiler-plugin/testData/box 目录下的所有 .kt 文件,并为每个文件生成一个对应的 @Test 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package org.jetbrains.kotlin.compiler.plugin.template.runners;import com.intellij.testFramework.TestDataPath;import org.jetbrains.kotlin.test.util.KtTestUtil;import org.jetbrains.kotlin.test.TargetBackend;import org.jetbrains.kotlin.test.TestMetadata;import org.junit.jupiter.api.Test;import java.io.File;import java.util.regex.Pattern;@SuppressWarnings("all" ) @TestMetadata("compiler-plugin/testData/box" ) @TestDataPath("$PROJECT_ROOT " ) public class JvmBoxTestGenerated extends AbstractJvmBoxTest { @Test @TestMetadata("simple.kt" ) public void testSimple() { runTest("compiler-plugin/testData/box/simple.kt" ); } }
诊断测试 (Diagnostic Test) 实现 诊断测试仅关心编译器的某个特定阶段是否按预期工作,通过 RUN_PIPELINE_TILL 指令来控制编译流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package foo.barimport org.jetbrains.kotlin.compiler.plugin.template.SomeAnnotationfun test () { val s = MyClass().foo() s.<!UNRESOLVED_REFERENCE!>inc<!>() }
编译器各阶段的顺序定义在 org.jetbrains.kotlin.test.services.TestPhase 中:
FRONTEND
FIR2IR
KLIB
BACKEND
FIR 与 IR 在插件开发中的应用 对编译器插件开发者而言,理解 FIR 和 IR 的角色至关重要。
FIR (Frontend Intermediate Representation) : 是一个更接近 Kotlin 源码的中间表示。主要用于语法分析、语义分析和诊断。详细信息请参考 FIR 官方文档 。
IR (Intermediate Representation) : 是一个更底层的、与具体平台无关的中间表示。它是连接前端和后端的桥梁,大多数代码生成和转换插件(如添加/修改函数、类)都在 IR 阶段进行。详细信息请参考 Compiler Plugins 官方文档 。
实际案例:Zipline IR 插件 Zipline 项目通过 IR 插件实现了跨平台(Kotlin/JS, Kotlin/JVM)的 RPC 调用。其核心原理是在编译期动态修改实现了 ZiplineService 接口的代码。
第一步:开发者编写的代码 开发者仅需定义一个简单的服务接口。
1 2 3 4 5 6 7 package com.exampleimport app.cash.zipline.ZiplineServiceinterface EchoService : ZiplineService { fun echo (request: String ) : String }
第二步:Zipline IR 插件在编译时的工作 在编译器的 IR 阶段,ZiplineIrGenerationExtension 插件被激活。它扫描所有实现了 ZiplineService 的接口,并执行以下内存中的 IR 变换:
创建伴生对象 :如果接口没有 companion object,则为其创建一个。
生成 Adapter 类 :在伴生对象内部,生成一个名为 Adapter 的嵌套类,该类实现了 ZiplineServiceAdapter,用于桥接 Zipline 的底层通信。
生成代理类 :在 Adapter 内部,再生成一个私有的代理类(如 GeneratedOutboundService),该类实现了原始的 EchoService 接口,并将所有方法调用转发给 Zipline 的 RPC callHandler。
第三步:编译器后端看到的最终 IR 结构 插件处理完成后,编译器后端看到的 EchoService 的 IR 结构在逻辑上等同于以下代码。这份代码并未出现在源文件中,而是由插件在内存中动态构建,并直接用于生成最终的目标平台代码(如 JVM 字节码)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.exampleimport app.cash.zipline.ZiplineServiceimport app.cash.zipline.internal .bridge.ZiplineServiceAdapterimport kotlinx.serialization.KSerializerinterface EchoService : ZiplineService { fun echo (request: String ) : String companion object { internal class Adapter (...) : ZiplineServiceAdapter<EchoService>, KSerializer<EchoService> { private class GeneratedOutboundService ( private val callHandler: OutboundCall.Handler ) : EchoService { override fun echo (request: String ) : String { return callHandler.call(this , 0 , request) as String } } } } }
总结 通过分析 Kotlin 编译器插件的测试框架,我们可以看到:
诊断测试 和 黑盒测试 是保障插件质量的两种核心手段,分别关注编译过程的正确性和最终代码的运行时行为。
测试指令 (如 RUN_PIPELINE_TILL)提供了精细控制编译流程的能力。
IR 变换 是编译器插件实现功能强大的代码织入与修改的关键,Zipline 等项目充分展示了其在实际应用中的巨大潜力。
参考链接