UAST(统一抽象语法树)概念解释

  • Post author:
  • Post category:其他


UAST – Unified Abstract Syntax Tree

官方doc:

https://plugins.jetbrains.com/docs/intellij/uast.html, 最后修改时间:2022 年 7 月 1 日

Unified AST

Unified Abstract Syntax Tree

统一抽象语法树

UAST(Unified Abstract Syntax Tree)是不同JVM语言的 PSI上的一个抽象层。它提供了一个统一的 API,用于处理 类和方法声明、文字值和控制流运算符等 公共语言元素。

动机

不同的 JVM 语言有自己的 PSI (Program Structure Interface,程序结构接口),但许多 IDE(Integrated Development Environment,综合开发环境)功能,如检查、gutter 标记、引用注入和许多其他功能对于所有这些语言都以相同的方式工作。

使用 UAST 允许通过单个实现提供可跨所有受支持的 JVM 语言工作的功能。

Youtube演讲🗣 Writing IntelliJ Plugins for Kotlin (KotlinConf 2018 – Writing IntelliJ Plugins for Kotlin by Alec Strong & Egor Andreevici)提供了在实际场景中使用 UAST 的全面概述。

什么时候应该使用 UAST?

对于插件,这应该以相同的方式适用于所有 JVM 语言。

一些已知的例子是:

Spring 框架

Android Studio 开发者工具

插件开发套件

支持哪些语言?

Java:完全支持

Kotlin:全力支持

Scala:测试版,但完全支持

Groovy:仅声明,不支持方法体

如果要修改 PSI 怎么做?

UAST 是一个只读 API。有实验性的 UastCodeGenerationPlugin和JvmElementActionsFactory 两个类,但目前不建议外部使用。

与 UAST 合作/如何使用 UAST

UAST 的基本元素是UElement. 所有常见的基本子接口都位于uast模块的 declarations(声明)和 expressions(表达式)目录中。

所有这些子接口都提供了获取有关 公共语法元素 信息的方法:UClass 代表类声明、UIfExpression 代表条件表达式等等。

PSI 到 UAST 转换

要获得给定PsiElement的一种受支持语言的 UAST,请使用UastFacade类或UastContextKt.toUElement()方法(对应 Kotlin 是org.jetbrains.uast.toUElement)。

要转换PsiElement为特定的UElement,请使用以下方法之一:

对于简单的转换:

UastContextKt.toUElement(element, UCallExpression.class);

转换为不同的给定选项之一:

UastFacade.INSTANCE.convertElementWithParent(element, new Class[]{UInjectionHost.class, UReferenceExpression.class});

在某些情况下,PsiElement可能代表几个UElements。例如,当 Kotlin 中主构造函数的参数同时是UField和UParameter。当需要所有选项时,请使用:

UastFacade.INSTANCE.convertToAlternatives(element, new Class[]{UField.class, UParameter.class});

最好直接转换为特定类型的UElement,而不是在没有类型的情况下转换然后强制转换为特定类型:

因为性能:带类型进行 toUElement() 转换会快速失败

因为在某些情况下可能会得到不同的结果:使用类型的转换更可预测

UAST 到 PSI 转换

有时需要从UElement获取得到底层语言的源代码。为此,可使用 UElement#sourcePsi 属性返回PsiElement原始语言的对应项。

这sourcePsi是一个“物理” PsiElement,它主要用于获取原始文件中的文本范围(例如,用于突出显示)。避免将其转换sourcePsi为特定的类,因为这意味着从 UAST 抽象回退到特定于语言的 PSI。有些UElement是“虚拟的”,因此没有sourcePsi. 对于某些UElement,sourcePsi可能与从中获得的元素不同UElement。

此外,还有一个UElement#javaPsi属性返回 “Java-like” PsiElement。这是一种“假的/模拟的”PsiElement,让不同的 JVM 语言模拟 Java 语言,以保持与 Java-API 的兼容性。比如调用MethodReferencesSearch.search(PsiMethod)时,只有Java原生提供PsiMethod;而其他 JVM 语言通过UMethod#javaPsi提供一个“假的”PsiMethod。

请注意,这UElement#javaPsi仅适用于 Java。因此UElement#sourcePsi应该用于获取文本范围或用于检查警告/排水沟(gutter)标记放置位置的锚元素。

简而言之:

sourcePsi:

是物理的:代表原始语言的源码中一个真实存在的PsiElement;

可用于突出显示、PSI 修改、创建智能指针等;

除非绝对需要(例如,处理特定语言的情况),否则不应强制转换

javaPsi:

应仅用作 JVM 可见声明的表示形式:PsiClass, PsiMethod,PsiField用于获取它们的名称、类型、参数等,或将它们传递给接受 Java-PSI 声明的方法

不保证是物理的:不能存在于来源中

不可修改:调用修改方法可能会抛出非 Java 语言的异常

注意:两者sourcePsi和javaPsi都可以转换回UElement.

UAST 访问者

在 UAST 中,没有统一的方法来获取的子级UElement(尽管可以通过 UElement#uastParent 获取其父级)。因此,将 UAST 作为树遍历的唯一方法是将 UastVisitor 传递给 UElement.accept()方法。

注意:在 UAST 的访问者 中有一个约定,如果visit*()返回true,则不会将访问者传递给孩子节点。否则,UastVisitor将继续深一层遍历。

UastVisitor可以通过 UastVisitorAdapter或UastHintedVisitorAdapter 转换为PsiElementVisitor。更推荐使用UastHintedVisitorAdapter ,因为它能提供更好的性能和更可预测的结果。

作为一般规则,建议不要使用UastVisitor:如果您不需要处理许多不同类型的UElement,并且元素的结构不是重点考虑因素,那么最好使用 PsiElementVisitor 来遍历 PSI 树,并通过UastContext.toUElement()将每个 PsiElement 转换为对应的UAST。

UAST 性能提示

UAST 不是零成本抽象:对于某些语言,某些方法可能会出乎意料地代价昂贵,因此在优化时要小心,因为它可能会产生相反的效果。

在某些情况下,转换为UElement也可能需要对某些语言进行解析,同样,可能会出乎意料地昂贵。仅在必要时才应执行转换为 UAST。例如,将整个PsiFile文件转换为UFile,然后单独遍历它以收集UMethod声明是低效的。相反,可以遍历PsiFile中每个遇到的匹配元素并将其转换为显式UMethod。

当您将访问者传递给UElement.accept(),或获取UElement#uastParent时,UAST 不会做额外操作。

当对于性能优化有严格要求时,请考虑使用UastLanguagePlugin.getPossiblePsiSourceTypes() 在将 PsiElement 转换为 UAST 之前提前过滤 PsiElement。

UAST注意事项

ULiteralExpression 不应用于字符串

ULiteralExpression表示文字值,如数字、布尔值和字符串。尽管字符串值也是文字,但 ULiteralExpression 使用起来并不是很方便。例如,它不处理 Kotlin 的字符串插值。处理字符串文字时,当要评估字符串的值或执行语言注入时,请改用 UInjectionHost。

sourcePsi 和 javaPsi,psi 和 UElement 作为 PSI

由于历史原因,UElement 和 PsiElement 两者之间的关系是复杂的。一些UElement实现PsiElement接口;例如,UMethod实现了PsiMethod。但强烈不鼓励将UElement 作为 PsiElement来使用,Plugin DevKit 提供了相应的检查(Plugin DevKit | Code | UElement as PsiElement usage)。此“implements(实现)”被认为已弃用,将来可能会被删除。

此外,还有UElement#psi 属性;它返回与javaPsi或sourcePsi相同的元素。由于很难猜测会返回什么,因此它也已被弃用。

因此,sourcePsi和javaPsi将作为从UElement获取PsiElement的唯一方式。 请参阅相应部分。

我应该使用 UMethod 还是 PsiMethod、UClass 还是 PsiClass ?

UAST 通过UMethod、UField、UClass等 提供了一种统一的表示 JVM 兼容声明的方式。但与此同时,所有 JVM 语言插件都实现了PsiMethod,PsiClass等以兼容 Java,可以通过UElement#javaPsi属性获得。

所以问题变为:“我应该用什么来表示我的代码中的 Java 声明?”。答案是:我们鼓励使用PsiMethod,PsiClass作为 Java 声明的通用接口(因为他们不受限于 JVM 语言),并且不鼓励在 API 中公开 UAST 接口。

注意:对于方法体,没有这样的替代方案,因此不鼓励公开UExpression,请考虑改为暴露原始元素 PsiElement。

UAST/PSI 树结构不匹配

UAST 是不同语言的 PSI 之上的抽象级别,并尝试构建统一的树(请参阅检查 UAST 树)。这导致 UAST 和原始语言之间的树结构可能产生严重分歧,因此不能保证保留祖先-后代关系。

例如,

generateSequence(uElement, UElement::uastParent).mapNotNull { it.sourcePsi }

generateSequence(uElement.sourcePsi) { it.parent }

二者的结果可能不同,包括元素的数量,和它们的顺序。

在插件中使用 UAST

要注册适用于 UAST 的扩展,请在plugin.xml中指定language=“UAST”。

检查 UAST 树

要检查 UAST 树,请调用内部操作 工具 | 内部行动 | UAST | 转储 UAST 树(按每个 PsiElement)。

检查

把 AbstractBaseUastLocalInspectionTool 用作基类。如果检查仅针对默认类型的子集(UFile、UClass、UField和UMethod),请在重载构造函数中指定UElements 作为提示以提高性能。

线标记

使用UastUtils.getUParentForIdentifier()或UAnnotationUtils.getIdentifierAnnotationOwner()进行注释以获得合适的“标识符”元素(有关详细信息,请参阅行标记提供程序)。



版权声明:本文为TTKatrina原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。