Kotlin Analysis API
上述代码在遍历properties,即一个KaPropertySymbol列表,对应示例类中的str和index,每个KaPropertySymbol成员的returnType就是KaType对象,可以获取到具体的类型,然后判定类型是否是某一个特殊种类。下面示例对是遍历属性的注解,对于每个注解,通过arguments获取注解具体的值,arguments是一个KaAnnotationValue类型的
0
1
前言
让开发更高效:插件与注解技术
在项目开发中,IDE插件和注解处理器已经成为提升开发效率的重要工具。它们通过自动化重复任务、减少样板代码、增强代码检查等方式,显著改善开发体验。
IDE插件开发的意义
IDE插件不仅仅是简单的工具扩展,它们通过多种方式改变开发工作流:
-
提升开发效率:自动化代码生成、模板插入和常用操作
-
增强代码质量:静态分析、代码检查和规范性验证
-
简化复杂任务:将多步操作整合为单个命令
-
统一团队规范:强制执行编码标准和最佳实践
常见开源项目案例
Room是Google官方推荐的持久化库,其注解处理器在编译时验证SQL查询的正确性。Room插件在编译阶段检查SQL语法,确保类型匹配,避免了运行时的数据库错误。
Kotlin的发展
在编程语言不断演进的浪潮中,Kotlin凭借其优雅的设计和强大的实用性,在短短十年间完成了从实验室项目到主流开发语言的华丽转身。无论是移动开发、服务端编程还是跨平台应用,Kotlin都已成为现代开发者不可或缺的工具。
未来发展重点
-
Kotlin 2.0时代:聚焦性能提升和语言简化
-
编译器改进:更快的编译速度和更好的优化
-
多平台增强:进一步完善各平台的支持和工具链
-
WebAssembly支持:探索在前端和边缘计算的更多可能
本文内容参考JetBrains官方文档,并加入了一些个人理解与实际开发中遇到的问题说明。对于有兴趣深入探索的开发者,可以结合JetBrains官方文档和实际项目需求,从小型分析工具做起,逐步掌握这套强大API的精髓。
02
Kotlin Analysis API的简介
Kotlin Analysis API是用于理解和分析Kotlin代码语义(如符号、类型)的一套底层库,它为IntelliJ IDEA等IDE的智能功能和代码分析工具提供了核心支持。它像一个功能强大的“代码显微镜”,能让其他工具深入理解代码结构。
在现代软件开发中,对代码的静态分析和理解变得日益重要。无论是构建IDE功能、代码质量工具,还是实现高级重构功能,都需要深入到代码结构的本质层面。Kotlin Analysis API正是为此而生的一套强大工具集,它为开发者提供了直接访问Kotlin编译器内部表示的能力,开启了代码智能分析的新可能。
Analysis API又称为“K2”,它常用于开发IDE插件。其与基于PSI(程序结构接口)的K1有较大区别。
核心组件:分析API的四大支柱
该API围绕四大核心概念构建,共同支撑起对代码的精准分析。
| 核心组件 | 核心接口/类 | 主要职责 |
|---|---|---|
| 会话管理 | KaSession |
管理分析会话的生命周期、上下文和资源,是API的主要入口。 |
| 符号系统 | KaSymbol
及其子类 |
代表代码中的声明(如类、函数、属性),是访问代码元素的抽象模型。 |
| 作用域管理 | KaScope |
管理符号在不同上下文(如文件、函数体)中的可见性范围。 |
| 类型系统 | KaType
及其子类 |
表示和操作Kotlin的各种类型(如类类型、函数类型),支持智能转换等特性。 |
使用Analysis API的意义
Kotlin Analysis API 允许开发者在编译器前端阶段访问Kotlin代码的语义信息。与传统的方式不同,它不需要实际执行代码,而是在编译过程的早期阶段提供对代码结构的深度洞察。
这套API的核心价值在于:它将编译器内部复杂的符号解析、类型推断和绑定过程暴露为简洁、稳定的接口,让工具开发者能够专注于构建功能,而非处理编译器的内部复杂性。
Kotlin 编译器对外暴露了一个 Kotlin 抽象语法树(AST),它构建在 PSI API 之上,而 PSI 是 IntelliJ IDEA 的一部分。PSI早先是用于对类进行分析,如一个类内部存在什么成员变量,存在什么方法,但它并不完善。
对于某些语言(例如 Java),PSI 既充当语法树,又是语义信息的来源;但在 Kotlin 中,这两个概念被明确地分离开来。
Kotlin PSI 并没有对“语句(statement)”和“表达式(expression)”进行严格区分。不过,存在一个名为 KtStatementExpression 的标记接口,用于标注具有语句特性的结构。
Analysis API 构建在 Kotlin PSI 之上,主要以一组扩展函数和扩展属性的形式实现,用来提供对语义信息的访问。
统一 IDE / Compiler / Plugin 的语义模型
K2 的 FIR(Frontend IR)是:
• 编译器用
• IDE 用
• 插件用
一次建模,所有地方复用。你看到的类型 = 编译器看到的类型
增量 + 按需分析
• 分析 必要的最小语义片段
• 自动缓存
• 自动失效
• 线程安全
而不是像 K1 的PSI那样:“我只是想知道一个变量的类型,你给我分析整个世界?”
高层、稳定、可组合的 API
Analysis API 是 “意图级 API”,不是“数据结构级 API”。换句话说,PSI 负责“长什么样”,Analysis 负责“它是什么意思”
| 维度 | PSI | Analysis API |
|---|---|---|
|
插入 / 删除代码 |
✅ |
❌ |
|
查找字段/方法 |
⚠️(不准) |
✅ |
|
获取类型 |
❌ |
✅ |
|
跨模块 |
❌ |
✅ |
|
IDE/Compiler 一致性 |
❌ |
✅ |
|
性能 |
⚠️ |
✅ |
解决PSI存在的问题
举一些简单的例子说明一下,Analysis API相比PSI结构分析的优势所在
函数返回值问题
定义一个方法,返回类型为List,声明一个对象x获取方法的返回值
fun foo(): List<String>
val x = foo()
此时,仅使用PSI接口,无法获取x的类型,会得到null,而实际上x应当是List且泛型为String。
同名方法问题
对于以下方法定义
fun foo(x: Int) {}
fun foo(x: String) {}
PSI接口只能获取方法名为foo,至于究竟是哪一个foo,无法判定
隐藏对象类型的声明
var str = ""
var i = 0
var z = 0L
//PSI只能判断显式地声明的变量类型,如 var str : String = ""
对于未显式地声明的变量,PSI接口无法获取到具体的类型,Analysis API则可以
03
Kotlin Analysis API主要组成部分
在介绍具体的内容之前,我们先讲清楚一个概念,KaSession
KaSession
KaSession 是与 Analysis API 交互的入口点。它提供了分析 Kotlin 代码所需的各种组件和工具。
每个 KaSession 都与特定模块相关联,并从该模块的视角提供分析结果。换句话说,KaSession 只能看到所属模块中的声明,以及其所有依赖(包括直接依赖和传递依赖)中的声明。
要获取 KaSession,请使用 analyze {} 函数,并传入一个 KtModule 或该模块中的某个 KtElement。
@RequiresReadLock
private fun analysisKtFile(ktClass: KtClass) {
analyze(ktClass) {
}
}
KaSession 在 lambda 块内可作为扩展接收器使用。该会话仅在此块内有效,不应在其外部存储或访问。
analyze {} 调用仅可在读操作内部使用。你可以使用 @RequiresReadLock 注解来指定该方法必须在读操作中被调用。
什么是读操作和写操作?
正在编辑的代码文件是一个复杂的数据库(称为 PSI(程序结构接口) 树),它存储了所有代码的结构信息(类、函数、变量、引用等)。
• 读操作:任何 查询、分析、读取 这个数据库状态的操作。
例如:查找一个符号的所有引用、获取一个表达式的类型、检查代码中是否有未使用的变量、代码高亮、跳转到定义。
特点:不改变数据库本身。可以多个“读者”同时进行。
• 写操作:任何 修改 这个数据库状态的操作。
例如:重命名一个变量、移动一个函数、添加一个导入语句、格式化代码。
特点:会改变数据库。为了保证数据一致性,写操作必须独占访问,期间不能有读操作进行。
为什么 analyze {} 必须在读操作内部使用?
analyze {} 函数的目的是对代码进行静态分析,例如解析类型、解析引用、检查可见性等。这本质上是一个纯粹的读操作。
• 稳定性保证:要求它在读操作中执行,确保了在分析过程中,底层的PSI树(代码结构)不会被其他线程(尤其是UI线程中触发的编辑操作)突然修改。
• 线程安全:它强制调用者(你)进入一个受保护的上下文。在这个上下文中,框架保证了你看到的代码状态是一致的、稳定的。如果你尝试在不持有读锁的情况下调用它,框架会抛出异常来提醒你。
注意线程问题
如果将上述代码编译为插件,并在IDE中运行,会导致错误:
org.jetbrains.kotlin.analysis.api.impl.base.sessions.ProhibitedAnalysisException: Analysis is not allowed: Called in the EDT thread.
EDT即事件分发线程。
在IntelliJ平台插件开发中,EDT线程是处理所有用户界面事件的主线程。根据IntelliJ平台的最佳实践:
任何可能阻塞或耗时的操作(如代码分析、文件IO等)都不应在EDT线程执行。违反这一原则会导致界面冻结或崩溃
平台提供了多种机制(如ReadAction、后台任务等)来处理非UI线程操作
如果你认为你的处理逻辑较简单,并不会使得EDT线程阻塞而产生未响应问题,可以强制analyze {} 代码块执行在EDT线程中:
@OptIn(KaAllowAnalysisOnEdt::class)
private fun analysisKtFile(ktClass: KtClass) {
allowAnalysisOnEdt {
analyze(ktClass) {
}
}
}
KaLifetimeOwner
KaSession 以及从其中获取的大多数实体(如符号、类型等)仅在同一读操作和创建它们的 analyze 代码块内有效。所有此类实体都拥有 KaLifetimeOwner 这一超类型。
关键在于避免将 KaLifetimeOwner 对象缓存任意长时间,包括将其存储在长生命周期类的属性中或静态上下文中。这样做极有可能导致严重的内存泄漏,因为这些实体持有整个底层解析会话。应始终在 analyze 代码块内部获取和使用它们,或将其作为参数传递给需要它们的函数。
如果需要将部分解析逻辑提取到单独的函数中,建议将 KaSession 作为扩展参数或普通参数传递,而不是将其保存在某些共享的上下文类中。
fun perform(element: KtElement) {
analyze(element) {
if (check(element)) {
modify(element)
}
}
}
// 使用扩展方法
fun KaSession.check(element: KtElement) { ... }
fun modify(element: KtElement) { ... }
在**analyze {}**代码块内,KaSession也可以写作useSiteSession变量
fun perform(element: KtElement) {
analyze(element) {
check(useSiteSession, element)
}
}
// 作为参数传递
fun check(useSiteSession: KaSession, element: KtElement) { ... }
Symbols
符号(Symbol) 是 Analysis API 中的核心概念,Symbol用于表示 Kotlin 中可见的各种声明。符号不仅会为源代码文件中的声明创建,也会为来自 Kotlin 和 Java 库的声明,以及由编译器插件和编译器本身生成的声明创建。
KaSymbol 提供有关符号可见性的信息(例如 public、private 等),即使这些信息在 PSI 中并未被显式声明。
abstract class Base {
protected abstract fun check(value: String): Boolean
}
class Impl : Base() {
override fun check(value: String) = value.isNotEmpty()
}
例如,尽管 Impl.check() 并没有写出 protected 修饰符,编译器仍然知道它具有该可见性。
通过提供对这类语义信息的访问,Analysis API 中的符号使得对 Kotlin 代码的分析更加强大且全面。
Symbols的种类
KaSymbol 是整个符号层级结构的根类型。下面是其中的一部分子类型:
• KaFileSymbol:表示一个 Kotlin 文件。
• KaPackageSymbol:表示一个包含声明和子包的包。
• KaDeclarationSymbol:表示 Kotlin 文件中的一个声明。
• KaClassifierSymbol:表示类、类型别名或类型参数。
• KaCallableSymbol:表示函数或变量。
• KaScriptSymbol:表示脚本声明(隐式地嵌套在脚本文件中)。
……
示例为获取一个KaClassSymbol,以对类进行解析:
private fun analysisKtFile(ktClass: KtClass) {
analyze(ktClass) {
val symbol = ktClass.classSymbol ?: return@analyze
}
}
详细使用会在下文中结合实例进行演示
KaSymbolPointer
KaSymbolPointer 是一种用于在不同分析会话之间引用 KaSymbol 的机制。由于符号只在创建它们的特定分析会话中有效,符号指针提供了一种持久化这些引用并在之后恢复对应符号的方法。
在KaSession内容中,我们提到过,该会话仅在此块内有效,不应在其外部存储或访问。
如果你需要将 KaSymbol 传递到另一个分析会话中,请使用 KaSymbolPointers。与符号不同,指针不会捕获分析会话,因此可以自由传递或缓存。
可以通过**createPointer()**获取到KaSymbolPointer
val symbol: KaFunctionSymbol = ...
val pointer = symbol.createPointer()
要从 KaSymbolPointer 恢复一个符号,需要在 analyze 代码块中使用 restoreSymbol() 扩展函数:
analyze(someKtElement) {
val restoredSymbol = functionPointer.restoreSymbol()
if (restoredSymbol != null) {
// 可以使用恢复的symbol
}
}
使用场景
符号指针对于需要在不同分析会话中保持对符号的引用至关重要。常见的应用场景包括:
• 缓存分析结果:存储符号指针,便于后续检索与分析。
• 跨模块分析:在同一项目的不同模块间引用符号。
• 长时间运行的操作:在后台任务或多个用户交互过程中持续保持对符号的引用。
通过使用符号指针,可以确保代码维持有效的符号引用,避免因状态失效引发潜在问题,同时防止内存泄漏。
Scopes
Scope即作用域,它充当声明(declarations)的容器,包含函数、属性、类和类型别名。当编译器解析名称引用时,会通过一系列作用域查找对应的声明。作用域的考虑顺序和类型取决于引用的上下文环境。
在 Analysis API 中,作用域通过 KaScope 接口可见。要获取作用域,请使用类或脚本中定义的作用域获取工具。
这些作用于往往指的是:方法、属性、嵌套类等
我们举个简单的例子来理解Scopes的作用。
当我们通过如下示例代码获取到一个KtClass对象的Symbol后:
analyze(ktClass) {
val symbol = ktClass.classSymbol ?: return false
}
代码中的Symbol其实是一个KaClassSymbol对象,那么它的Scopes是什么呢?有如下的对象:
• memberScope
• declaredMemberScope
• combinedMemberScope
• delegatedMemberScope
• combinedDeclaredMemberScope
• staticMemberScope
• staticDeclaredMemberScope
其中memberScope和declaredMemberScope是当前类中的方法、成员变量等的集合,区别在于declaredMemberScope仅限于类本身内部声明的部分,不包含继承自父类或接口的部分,而memberScope则包含继承的部分。
下面其它的Scope则是对他们更细致的划分,比如static,delegated(指通过by委托生成的成员)
通过这种分类我们可以获取到自己需要的内容。下面示例代码获取到类中声明的对象的集合。每一个KaPropertySymbol对应一个对象。
val properties = symbol
.declaredMemberScope.declarations
.filterIsInstance<KaPropertySymbol>()
class TestKotlin {
var str = ""
var index = 0
}
如果symbol指的是示例中的TestKotlin,那么通过以上方式获取到的properties即为str和index两个变量对应的KaPropertySymbol
KaTypes
KaTypes是代码中获取到的所有类型的父类
KaTypes有多种子类型,用于表示 Kotlin 中的不同种类类型:
• KaClassType:表示 Kotlin 的类类型,包括类、接口、对象和枚举类。
• KaFunctionType:表示 Kotlin 的函数类型,包括常规函数、挂起函数以及带有接收者的函数。
• KaTypeParameterType:表示类型参数类型,例如声明 class Box<T>(val element: T) 中的 T。
• KaDefinitelyNotNullType:表示在程序中特定点已知不可为空的类型。
• KaCapturedType:表示在类型推断过程中被捕获的类型,通常出现在处理泛型和型变时。
• KaFlexibleType:表示具有灵活边界的类型,通常用于平台类型或可空性不确定的类型。
• KaIntersectionType:表示由多个类型交集形成的类型。
• KaDynamicType:表示 Kotlin 中的动态类型,用于与动态类型语言或平台进行互操作。
• KaErrorType:表示某种未解析的类型。
KaTypes一些常用的方法:
fun KaType.isSubtypeOf(supertype: KaType, errorTypePolicy: KaSubtypingErrorTypePolicy = KaSubtypingErrorTypePolicy.STRICT): Boolean
//检查给定的KaType对象是否是supertype的子类
fun KaType.isClassType(classId: ClassId): Boolean
//检查给定的KaType对象是否是某个类,即classId相同
val KaType.isPrimitive: Boolean
//给定的KaType是否是以下类型之一: Byte, Short, Int, Long, Float, Double, Char, Boolean(也可能是它们的空值)
val KaType.isAnyType
val KaType.isNothingType
val KaType.isUnitType
val KaType.isByteType
val KaType.isUByteType
val KaType.isShortType
val KaType.isUShortType
val KaType.isIntType
val KaType.isUIntType
val KaType.isLongType
val KaType.isULongType
val KaType.isFloatType
val KaType.isDoubleType
val KaType.isCharType
val KaType.isBooleanType
val KaType.isCharSequenceType
val KaType.isStringType
//给定的KaType是否是某个Kotlin类型之一或是它们的空值
获取与使用KaType
我们沿用上面写到的示例:
在获取到KaPropertySymbol后,我们就可以获取到每个对象的类型是什么
// 示例类
class TestKotlin {
var str = ""
var index = 0
}
analyze(ktClass) {
val symbol = ktClass.classSymbol ?: return false
val properties = symbol
.declaredMemberScope.declarations
.filterIsInstance<KaPropertySymbol>()
properties.forEach { it ->
if (it.returnType.isIntType || it.returnType.isUIntType){
//做一些处理
}
}
}
上述代码在遍历properties,即一个KaPropertySymbol列表,对应示例类中的str和index,每个KaPropertySymbol成员的returnType就是KaType对象,可以获取到具体的类型,然后判定类型是否是某一个特殊种类。由此可以知道str为String类型,而index则为Int。
复杂类型判断
上面的示例比较简单,因为示例中只使用了String和Int这样的基本类型,如果现在是一个MutableList,且泛型为String,或者泛型是一个自定义对象,那么应该如何判断呢?
这里先引入另一个概念:ClassId
ClassId
classId即一个类的完整类名,如"android.view.View"
可以通过asFqNameString获取到这个值并作判断
classId?.asFqNameString() == "android.view.View"
也可以将一个字符串转换为ClassId对象
ClassId.fromString("android/view/View")
有了这个概念,就可以对复杂类型的KaType进行判断了,下面示例将判断是否为List,且对List的泛型进行判断:
通过扩展方法来实现这个功能,首先通过isKotlinList先来判断是否为List,如果确定为List类型,则通过typeArguments取到其泛型对象的类型,再判断这个类型是否为"kotlin.String"。当然String也可以根据需求灵活替换成其他类。
fun KaType.isKotlinList(): Boolean {
if (this !is KaClassType) return false
val fqName = classId.asFqNameString()
return fqName == "kotlin.collections.List" ||
fqName == "kotlin.collections.MutableList" ||
fqName == "java.util.ArrayList"
}
fun KaType.listElementType(): KaType? {
if (this !is KaClassType) return null
if (!isKotlinList()) return null
return typeArguments.firstOrNull()?.type
}
fun KaType.isStringList(): Boolean {
val elementType = listElementType() as? KaClassType ?: return false
return elementType.classId.asFqNameString() == "kotlin.String"
}
有时候我们需要知道一个对象是不是某个类型,不光只针对对象本身,而是包含了对其父类的判断。
比如某个类A的对象a,它本身的直接父类是B类,而B又继承自Serializable,此时我需要知道对象a是一个Serializable对象,可以通过下面的示例进行判断,即获取KaType的Symbol,然后通过superTypes递归进行判断:
fun KaType.isSerializable(): Boolean {
val classType = this as? KaClassType ?: return false
val symbol = classType.symbol as? KaClassSymbol ?: return false
return symbol.superTypes.any {
it is KaClassType && it.classId.asFqNameString() == "java.io.Serializable" || it.isSerializable()
}
}
04
Annotations
注解在Kotlin中扮演着至关重要的角色,它们提供元数据并影响代码行为。Analysis API 允许访问和分析应用于声明及类型的注解。
主要成员:
val classId: ClassId?
注解类的限定名称,如果注解调用未解析则为 null。
val hasArguments: Boolean
如果该注解调用包含一个或多个参数,则为 true。
val arguments: List<KaNamedAnnotationValue>
传递给注解构造函数的参数列表。
KaAnnotated 声明的唯一属性是annotations,其类型为 KaAnnotationList。KaAnnotationList 本身实现了 List接口,因此你可以直接对注解进行迭代遍历。
fun KaSession.processAnnotations(symbol: KaDeclarationSymbol) {
for (anno in symbol.annotations) {
// 处理
}
}
KaAnnotationList 不仅仅是一个列表。它还能高效地检查是否存在带有特定 ClassId 的注解。
fun KaSession.hasDeprecatedAnnotation(symbol: KaDeclarationSymbol): Boolean {
val classId = ClassId.fromString("kotlin/Deprecated")
return classId in symbol.annotations
}
也可以批量获取到所有注解的类型。
fun KaSession.collectAnnotations(types: List<KaType>): Set<ClassId> {
return types.flatMapTo(HashSet()) { it.annotations.classIds }
}
获取注解的值
仅获取到注解并不足够,注解往往有声明值
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.BINARY)
annotation class TestField(
val required: Boolean = false
)
使用注解
class TestKotlin {
@TestField(required = true)
var str = ""
@TestField(required = false)
var index = 0
}
当我们进行插件开发时候,往往需要获取注解的参数值。后续会通过判断注解赋值进行生成代码,如Room会通过注解的SQL语句来生成逻辑。
下面示例对是遍历属性的注解,对于每个注解,通过arguments获取注解具体的值,arguments是一个KaAnnotationValue类型的列表,KaAnnotationValue可能涉及多种类型的值,需要针对不同类型进行处理。
val properties = symbol.declaredMemberScope
.declarations
.filterIsInstance<KaPropertySymbol>()
properties.forEach { it ->
val ann = it.findMyAnnotation()
val builder = StringBuilder()
ann?.arguments?.forEach { arg ->
builder.append(" ${arg.name} ${arg.expression.printValue()} ")
}
}
fun KaPropertySymbol.findMyAnnotation(): KaAnnotation? {
return backingFieldSymbol?.annotations?.find {
it.classId?.asFqNameString() == "com.test.TestField"
}
}
fun KaAnnotationValue.printValue(): String =
when (this) {
is KaAnnotationValue.ConstantValue ->
value.toString()
is KaAnnotationValue.ArrayValue ->
values.joinToString(prefix = "[", postfix = "]") { it.printValue() }
else ->
toString()
}
06
结语
Kotlin Analysis API 不仅是一个技术工具,更是连接代码表面语法和深层语义的桥梁。它降低了构建高质量开发工具的门槛,让更多开发者能够利用编译器的强大能力。无论是构建团队内部工具,还是开发公共的代码质量平台,Analysis API都提供了坚实的基础。随着Kotlin生态的不断发展,这套API必将在提升开发体验和代码质量方面发挥越来越重要的作用。
更多推荐


所有评论(0)