乐于分享
好东西不私藏

从0到1:JetBrains 插件开发系列(五)PSI 与代码分析:理解 IntelliJ 的“大脑”

从0到1:JetBrains 插件开发系列(五)PSI 与代码分析:理解 IntelliJ 的“大脑”

从0到1:JetBrains 插件开发系列(五)

PSI 与代码分析:理解 IntelliJ 的“大脑”

作者:于天惠


如果你把 IntelliJ IDEA 比作一个“智能程序员”,那么它的大脑就是 PSI(Program Structure Interface)

正是 PSI 让 IDE 能:

  • • 精准高亮语法错误
  • • 安全地重命名变量(即使跨文件)
  • • 自动生成 getter/setter
  • • 实时检测未使用的导入

作为插件开发者,掌握 PSI 就等于掌握了修改和理解代码的能力。本文将带你揭开 PSI 的神秘面纱,学会如何遍历、分析甚至安全地修改代码结构。


一、什么是 PSI?为什么它如此重要?

PSI 是 IntelliJ Platform 对源代码的结构化抽象。它不是原始文本,而是一棵带语义信息的树(AST + Symbol Table)

与传统 AST 的区别

特性
传统 AST
IntelliJ PSI
仅包含语法结构
包含符号解析(如变量定义位置)
支持增量更新(只重解析变更部分)
与编辑器双向绑定(修改 PSI = 修改文本)

💡 简单说:PSI = AST + 语义 + 可编辑性


二、PSI 树的基本结构

以 Java 代码为例:

public class User {    private String name;    public String getName() { return name; }}

其 PSI 树大致如下(简化):

PsiJavaFile└── PsiClass ("User")    ├── PsiField ("name", type=String)    └── PsiMethod ("getName")        └── PsiReturnStatement            └── PsiReferenceExpression ("name") → 指向上面的 PsiField

关键特点:

  • • 每个节点都是 PsiElement 的子类
  • • 节点包含文本范围(start/end offset),可映射回编辑器位置
  • • 引用(Reference) 节点能解析到定义处(实现“跳转到声明”)

三、获取 PSI:从上下文出发

在 Action 或 Inspection 中,通常通过 AnActionEvent 或 ProblemDescriptor 获取当前 PSI 文件:

// 在 Action 中val psiFile = e.getData(CommonDataKeys.PSI_FILE) as? PsiJavaFile ?: return// 在 Inspection 中overridefun visitElement(element: PsiElement) {    if (element is PsiMethod) {        // 处理方法    }}

⚠️ 注意:PSI 类型是语言相关的(PsiJavaFileKtFilePyFile),需先判断语言。


四、遍历 PSI:Visitor 模式是标准方式

IntelliJ 推荐使用 Visitor 模式 遍历 PSI 树,而非递归手动遍历。

示例:统计 Java 文件中的方法数量

class MethodCounter : JavaRecursiveElementVisitor() {    var methodCount = 0    overridefun visitMethod(method: PsiMethod) {        methodCount++        super.visitMethod(method) // 继续遍历子节点    }}// 使用val counter = MethodCounter()psiFile.accept(counter)println("Methods: ${counter.methodCount}")

常用 Visitor 基类:

语言
Visitor 基类
Java
JavaRecursiveElementVisitor
Kotlin
KtTreeVisitorVoid
通用
PsiRecursiveElementVisitor

✅ 优势:自动处理语言细节,避免空指针,性能优化。


五、实战案例:检测未加注释的 public 方法

我们来实现一个简单的 代码检查器(Inspection):如果 public 方法没有文档注释(Javadoc),就标黄并提供快速修复。

步骤 1:创建 Inspection 类

class PublicMethodWithoutCommentInspection : LocalInspectionTool() {    overridefun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {        return object : JavaElementVisitor() {            overridefun visitMethod(method: PsiMethod) {                // 1. 检查是否为 public                if (!method.hasModifierProperty(PsiModifier.PUBLIC)) return                // 2. 检查是否有文档注释                if (method.docComment == null) {                    // 3. 报告问题(高亮方法名)                    holder.registerProblem(                        method.nameIdentifier ?: method,                        "Public method lacks documentation",                        AddJavadocQuickFix()                    )                }            }        }    }}

步骤 2:实现 QuickFix(快速修复)

class AddJavadocQuickFix : LocalQuickFix {    overridefun getFamilyName() = "Add Javadoc"    overridefun applyFix(project: Project, descriptor: ProblemDescriptor) {        val method = descriptor.psiElement.parent as? PsiMethod ?: return        // 🔒 必须在 Write Action 中修改 PSI!        WriteCommandAction.runWriteCommandAction(project) {            val factory = JavaPsiFacade.getElementFactory(project)            val docComment = factory.createDocCommentFromText("/**\n * TODO\n */")            method.addBefore(docComment, method.firstChild)        }    }}

步骤 3:在 plugin.xml 中注册

<extensions defaultExtensionNs="com.intellij">    <localInspection language="JAVA"                     displayName="Public Method Without Comment"                     groupName="MyPlugin"                     enabledByDefault="true"                     level="WARNING"                     implementationClass="inspections.PublicMethodWithoutCommentInspection"/></extensions>

✅ 效果:打开 Java 文件,所有无注释的 public 方法下方出现黄色波浪线,按 Alt+Enter 可一键添加 Javadoc。


六、关键原则:安全修改 PSI

PSI 是 IDE 的核心数据结构,任何修改都必须遵守线程与事务规则

1. 必须在 Write Action 中修改

所有 PSI 修改操作(增删改节点)必须包裹在 WriteCommandAction 中:

WriteCommandAction.runWriteCommandAction(project) {    // 修改 PSI}

2. 不要缓存 PSI 元素

PSI 树会因用户编辑而重建,缓存的 PsiElement 可能失效。应缓存 VirtualFile + offset,需要时重新解析。

3. 避免在 Read Action 中触发 Write

这会导致死锁。可通过 ApplicationManager.getApplication().runWriteAction 显式切换。


七、PSI 与文本的同步机制

当你修改 PSI 时,IDE 会自动同步到编辑器文本,反之亦然。这种双向绑定由 Document ↔ PSI 同步器维护。

但要注意:

  • • 直接修改 Document(文本)不会立即更新 PSI(需等待解析)
  • • 直接修改 PSI 会立即更新 Document 和 UI

📌 最佳实践:始终通过 PSI 修改代码,而非直接操作文本。


八、小挑战:动手写一个 Inspection

请实现以下功能:

  1. 1. 检测 Java 中所有 private 字段未使用 final 修饰 的情况
  2. 2. 高亮字段名,并提供 QuickFix:添加 final 关键字
  3. 3. 注册为默认启用的 WARNING 级别检查

提示:

  • • 使用 PsiField.hasModifierProperty(PsiModifier.FINAL)
  • • 使用 field.setModifierProperty(PsiModifier.FINAL, true) 添加修饰符

完成后,你将掌握完整的 Inspection 开发流程!


九、下一步预告

PSI 让我们能“读懂”代码,但真正的工程级插件还需要:

  • • 持久化用户配置
  • • 构建专业 UI 对话框
  • • 管理复杂状态

在下一篇中,我们将聚焦 UI 与交互设计,学习如何打造符合 JetBrains 设计规范的对话框与设置面板。

标题预告:《对话框与 UI 构建:打造专业交互体验》


结语:代码不仅是文本,更是结构

很多开发者习惯把代码当作纯文本处理,用正则表达式“暴力”替换。但这在复杂场景下极易出错。

而 PSI 提供了一种安全、精准、语义化的操作方式。一旦你习惯用 PSI 思考,就会发现:代码是一种可编程的数据结构

而这,正是现代 IDE 智能化的根基。

下期见!


本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 从0到1:JetBrains 插件开发系列(五)PSI 与代码分析:理解 IntelliJ 的“大脑”

评论 抢沙发

8 + 5 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮