统一抽象语法树
统一抽象语法树
UAST - Unified Abstract Syntax Tree
UAST (Unified Abstract Syntax Tree) 是不同编程语言的PSI的一个抽象层。它为class声明,method声明, 字符串,运算符等操作提供了统一API。
动机-为什么要使用AST
不同的编程语言有不同的PSI,但是像代码检查,边侧栏标记,引用注入等功能是相同的。
UAST 使用统一的实现方式来支持所有适用的编程语言。
Writing IntelliJ Plugins for Kotlin 介绍了UAST在实际场景中的应用
什么时候使用UAST?
如果插件需要为所有的JVM编程语言提供统一的操作方式时,可以使用UAST。
示例:
支持哪些编程语言
Java: 全部支持
Kotlin: 全部支持
Scala: 测试版本, 但全部支持
Groovy: 只支持声明, 不支持方法体
能通过AST修改PSI吗?
UAST只支持读取,暂不支持修改。有2个实验性质的类UastCodeGenerationPlugin 和 JvmElementActionsFactory ,但是现在不推荐使用它们。
使用UAST
UAST的基类是 UElement ,所有通用的操作类都位于 declarations 和 expressions 包下,通过这些类可以获取常用语法元素的信息,例如:UClass 可以获取类的声明,UIfExpression 可以获取条件表达式,等等。
PSI转换为UAST
如果编程语言支持UAST,使用 UastFacade 或UastContextKt.toUElement() 可以把PSI 转换为UAST。
PsiElement 转换时想要指定UElement类型,可以使用下面的方法:
- 简单的转换
- 转换为选项中的某一种
- 某些情况下,PsiElement 可能代表多个 UElement,例如:在 Kotlin 中一个主构造器的参数同时包括 UParameter 和 UField ,这时转换需要指定所有类型:
提示
转换的时候最好指定转换类型,而不是转换完后再强制转换,原因如下:
- 性能原因:toUElement() 指定转换类型的时候,如果类型不匹配,会fail-fast
- 因为在某些情况下可以转换为多种不同的类型,所以指定转换类型能直接得到想要的类型
UAST转换为PSI
UElement 的 sourcePsi 属性值就是原来的 PsiElement 。
sourcePsi 是一个”物理的“ PsiElement ,它通常用来获取在源码中的文本范围(例如:高度某一个范围内的文本)。需要避免把sourcePsi 转换为某一个类,因为这意味着从 UAST 抽象回退到编程语言的 PSI。。
有的 UElement 是 "virtual" 的,它们没有 sourcePsi ,它们的 sourcePsi 可能不等于获取UElement的元素。
有的 UElement 的javaPsi属性会返回一个 "Java-like" PsiElement ,这是其它的编程语言为兼容Java而虚拟的一个 "Java-like" PsiElement。例如:调用 MethodReferencesSearch.search(PsiMethod) 时,只有Java才会返回一个 PsiMethod ,其它的JVM编程语言会通过UMethod#javaPsi返回一个 "fake" PsiMethod
只有在Java中 UElement 的 javaPsi 属性才是实际存在的PsiElement。 所以 UElement#sourcePsi 可以用来获取文本范围或锚点,对获取到的内容进行拼写检查或显示警告标记
总结如下:
sourcePsi:
是实际存在的: 一个在源码中真实存在的PsiElement
可以用来高亮显示,修改PSI, 创建 smart-pointers 等.
除非必需,否则不要转换为PsiElement (例如:不同的编程语言需要不同的处理)
javaPsi:
只能表示JVM可见的声明: 可用于获取PsiClass、PsiMethod、PsiField 的 名称、类型、参数等, 或将它们当参数传递给 Java-PSI;
不保证是物理存在的: 在源码中可能不存在
不能被修改: 不是Java时,调用修改方法会抛出异常
注意: sourcePsi 和 javaPsi 可以被转换回 UElement.
UAST访问器
在UAST中,没有统一的方式可以获取UElement的子元素(但是可以使用 UElement#uastParent 获取它的父元素)。所以,将 UAST 用树的结构进行遍历的唯一方法是将 UastVisitor 传递给 UElement.accept() 方法。
注意: UAST-visitors 中有一个约定,如果 visit*() 返回 true,则不会将 UastVisitor 传递给子元素, 返回false, UastVisitor 可以继续遍历.
可以使用 UastVisitorAdapter 或 UastHintedVisitorAdapter 将 UastVisitor 转换为 PsiElementVisitor ,其中UastHintedVisitorAdapter 性能更好,准确性更高。
一般情况下,不推荐直接使用 UastVisitor:如果你不需要处理各种不同类型的 UElements,并且这些元素的结构不重要的话,最好直接遍历 PSI-tree (使用 PsiElementVisitor ),遍历的时候可以使用 UastContext.toUElement() 把 PsiElement 转换为它对应的 UAST 。
UAST性能提示
对一些编程语言来说,某些方法 性能开销很高,所以优化性能的可能会拖慢性能。
在某些情况下,PsiElement 转换为 UElement 可能需要解析编程语言,会进一步拖慢性能。所以只在必要的时候再转换为 UAST 。例如:把整个 PsiFile 转换为 UFile ,然后遍历UFile的元素从而收集UMethod,这样的效率很低。应该直接遍历 PsiFile,然后把它的元素明确地转换为 UMethod,以此来达到收集 UMethod的目的
通过把 UastVisitor 参数传递给 UElement.accept()遍历UAST树 和获取 UElement 的 uastParent属性 是 lazy 的。
对于真正困难的性能优化,可以在 PsiElements 转换为 UAST 之前,先使用 UastLanguagePlugin.getPossiblePsiSourceTypes() 过滤想处理的 PsiElements
UAST注意事项
字符串不能使用 ULiteralExpression
ULiteralExpression ULiteralExpression 表示字面量类型,如numbers, booleans 和 string。 但是ULiteralExpression 不能很好的处理 string 字面量。例如:它不能处理 Kotlin's string interpolations 。To process string literals when evaluating their value or to perform language injection(不理解什么意思) ,请使用 UInjectionHost
sourcePsi and javaPsi, psi and UElement as PSI
由于历史原因。UElement 和 PsiElement 的关系是很复杂的。一些 UElements 实现了 PsiElement 接口(例如:UMethod 实现了 PsiMethod 接口) 。可以使用Plugin DevKit (Plugin DevKit | Code | UElement as PsiElement usage) 检查代码,避免将UElement 用作 PsiElement 。后期会解除这些实现关系。
UElement 的 psi属性也是过期的,因为它很难确定是 javaPsi还是sourcePsi。因此 sourcePsi 和 javaPsi 属性是从 UElement 获取 PsiElement 的唯一方法。
能使用UMethod 或 PsiMethod, UClass 或 PsiClass 吗?
UAST 为了兼容JVM,提供了统一的方式,例如 UMethod, UField, UClass,等等。但同时所有的JVM语言插件为了兼容Java,也实现PsiMethod、PsiClass等接口,这些实现类可以使用 UElement的javaPsi属性来获取它们对应的PsiElement。
在所有JVM编程语言中,推荐使用 PsiMethod、PsiClass 作为 Java 声明的通用接口,不推荐在 API 中放开 UAST 接口 注意:对于方法体来说,没有这样的替代方案,因此不鼓励放开(例如:UExpression)。可以考虑使用 raw PsiElement 来访问方法体。
UAST/PSI Tree 结构不匹配
UAST是对不同的编程语言的PSI的一层抽象,抽象出一个统一的结构tree。由于兼容了不同的编程语言,所以UAST和原编程语言的结构会不一致,也就不能保证他们之间的父子关系。例如:
// 下面2个方法的结果在元素的数量 ,和元素的顺序上可能是不同的
generateSequence(uElement, UElement::uastParent).mapNotNull { it.sourcePsi }
generateSequence(uElement.sourcePsi) { it.parent }
在插件中使用UAST
To register extensions applicable to UAST, specify language="UAST" in plugin.xml.
Inspecting UAST Tree
To inspect UAST Tree, invoke internal action Tools | Internal Actions | UAST | Dump UAST Tree (By Each PsiElement).
Inspections
Use AbstractBaseUastLocalInspectionTool as base class and specify language="UAST" in registration. If inspection targets only a subset of default types (UFile, UClass, UField, and UMethod), specify UElements as hints in overloaded constructor to improve performance.
Line Marker
Use UastUtils.getUParentForIdentifier() or UAnnotationUtils.getIdentifierAnnotationOwner() for annotations to obtain suitable "identifier" element (see Line Marker Provider for details).