从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 的区别
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
💡 简单说: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 类型是语言相关的(
PsiJavaFile、KtFile、PyFile),需先判断语言。
四、遍历 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 基类:
|
|
|
|---|---|
|
|
JavaRecursiveElementVisitor |
|
|
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. 检测 Java 中所有 private 字段未使用 final修饰 的情况 -
2. 高亮字段名,并提供 QuickFix:添加 final关键字 -
3. 注册为默认启用的 WARNING 级别检查
提示:
-
• 使用 PsiField.hasModifierProperty(PsiModifier.FINAL) -
• 使用 field.setModifierProperty(PsiModifier.FINAL, true)添加修饰符
完成后,你将掌握完整的 Inspection 开发流程!
九、下一步预告
PSI 让我们能“读懂”代码,但真正的工程级插件还需要:
-
• 持久化用户配置 -
• 构建专业 UI 对话框 -
• 管理复杂状态
在下一篇中,我们将聚焦 UI 与交互设计,学习如何打造符合 JetBrains 设计规范的对话框与设置面板。
标题预告:《对话框与 UI 构建:打造专业交互体验》
结语:代码不仅是文本,更是结构
很多开发者习惯把代码当作纯文本处理,用正则表达式“暴力”替换。但这在复杂场景下极易出错。
而 PSI 提供了一种安全、精准、语义化的操作方式。一旦你习惯用 PSI 思考,就会发现:代码是一种可编程的数据结构。
而这,正是现代 IDE 智能化的根基。
下期见!

夜雨聆风
