IDEA插件开发-Find Usage功用增强

在平时的开发中,咱们会有以下需求:

Temp.java
AClass a = new AClass();
a.setName(b.getName());
...
Temp2.java
BClass a = new BClass();
b.setName(c.getName());
...
Temp3.java
CClass a = new CClass();
DClass a = getDClass();
c.setName(d.getName());   
....

业务场景

业务场景是这样的,咱们是一个对外交互的体系,每次交互需求调用多个上游体系获取不同的字段,将上游体系API返回的DTO经过Get,Set的办法的转换为自己体系的数据传输方针。

比方上图:假设咱们体系的AClass的name字段需求赋值,DClass是外部体系,代码中经过BClass,CClass,终究从d的getName办法取到值。

常常会有业务人员或者测验人员乃至包含开发人员会问询某个字段取自上游的哪个体系的哪个字段?

我往往只了解自己体系的字段,所以我在IDEA里经过AClass的name字段右键使用Find Usage功用,去看a.setName办法在哪里被赋值找到b.getName,然后在找b.setName在哪里被调用赋值,一层一层找,终究找到d.getName。然后给业务人员说:我找到了,是D体系的name字段!!

可是假如仅仅少量几个字段,开发人员尚可保护,但假如出行字段有上百种,每个字段都这样查会十分费时,合理的办法是保护一份文档,他人问的时分直接查就行。可是咱们都知道,人力保护文档终究随着代码变动,会导致文档与代码不一致。所以咱们开发了这款IDEA插件,该插件能够从代码层面进行剖析,将剖析结果生成一份精确的Excel文档,后续取值代码有修改的时分,从头生成文档即可。

规划思路

规划思路很简略,便是把人工操作的过程用IDEA插件代替:先找set办法,在看set办法内部的get办法来源,递归的寻觅,直到找到最后的字段没有set办法调用就停止。

如上文:先找a.name 的引证列表,然后遍历引证列表找到a.setName(b.getName());这条语句,再剖析出b.name的引证列表,找出b.setName(c.getName());以此类推,终究找到c.setName(d.getName()) ;语句,此时再剖析d.name,能够发现在项目里没有d.setName办法了,咱们能够认为d.name便是终究的方针

插件演示

先找一个类模拟我说的场景:

IDEA插件开发-Find Usage功能增强

这个插件有两个功用:

第一个功用,使用Find Usage窗口提示:在class的特点上按alt+enter(IDEA的Intention功用),点击find usage plus选项

IDEA插件开发-Find Usage功能增强

之后会弹出:

IDEA插件开发-Find Usage功能增强

第二个功用,生成Excel:在class的上按alt+enter,会以类名+日期的格式生成这个类下一切字段的追寻的Excel文件(生成在项目跟目录下),假如有多处赋值,就有多条记载。

IDEA插件开发-Find Usage功能增强

输出的内容包含字段名,终究找到类也便是上文说的DClass,调用链,和输出信息,假如是根本类型赋值的话会标明出来。

限制:也有一些小的限制,便是只能剖析GetSet办法的赋值,假如赋值当地大于5处也不会计算。

有需求的能够下载项目后自己编译插件,项目地址:gitee.com/kagami1/fin…

我在库房下也放了一个编译好的插件:gitee.com/kagami1/fin…

下面的文章开端解说项目。

知识点

规划这款插件不难,难的是IDEA插件API的使用,IDEA插件的资料比较少,许多用法我需求查IDEA插件论坛才干知道。所以写一篇文章记载一下。

怎样实现Intention功用

Intention功用,也便是alt+enter弹出的框。只需求承继com.intellij.codeInsight.intention.PsiElementBaseIntentionAction并在plugin.xml里装备一下就能够了。承继PsiElementBaseIntentionAction有两个办法需求留意isAvailable担任intention是否显示,invoke担任履行intention逻辑。

怎样获取引证列表

经过ReferencesSearch办法,第一个参数是一个PsiElement元素,意思是要找的引证,也便是上文中的AClass的name字段。

Query<PsiReference> search = ReferencesSearch.search(setterForField, GlobalSearchScope.projectScope(project), false);

关于PSI的知识需求看下官网:plugins.jetbrains.com/docs/intell… 我的了解是IDEA把一个文件(不仅仅java的)解析成一个一个PSI元素用于编辑器处理。

怎样获取setName(b.getName())中的b相关的元素

这儿涉及到一个东西类的使用PsiTreeUtil.getParentOfType;

for (PsiReference psiReference : psiReferences) {
    System.out.println("开端处理引证:" + psiReference.getCanonicalText());
    PsiCall psiCall = PsiTreeUtil.getParentOfType(psiReference.getElement(), PsiCall.class);
    PsiExpressionList argumentList = psiCall.getArgumentList();
    if (argumentList == null || argumentList.getExpressions().length == 0) {
        System.out.println("set办法的入参为空,不计算这种情况 " + psiCall.getText());
        continue;
    }
    PsiExpression[] psiExpressions = argumentList.getExpressions();
    if (psiExpressions.length > 1) {
        System.out.println("不支持大于两个入参的Set办法" + psiCall.getText());
        continue;
    }
    PsiExpression psiExpression = psiExpressions[0];//b.getName()
    ....
}

IDEA中把java文件中的元素解析成一颗语法树,所以经过这颗树能够获取调用链中的各种信息。

比方这儿:我在AClass的name上进行PsiReference查找,这儿咱们找到了a.setName(b.getName());的引证。但仅凭psiReference是无法获取b的信息的。而这句话PsiTreeUtil.getParentOfType(psiReference.getElement(), PsiCall.class);的意思是在PSI树上找有没有PsiCall。而a.setName正是调用语句,所以能找到。之后就简略了,从call中获取argument就能够找到b.getName的信息了,再从b找到BClass。

PsiTreeUtil简略来说是一个操作PSI树的东西类咱们乃至能够从name一向往上找,能够找到name字段的类,归于哪个文件,归于哪个模块,哪个项目。

怎样将结果展现到Find Usage窗口中

这段代码找了良久总算找到能用的了

UsageViewManager usageViewManager = UsageViewManager.getInstance(project);
UsageViewPresentation usageViewPresentation = new UsageViewPresentation();
usageViewPresentation.setTargetsNodeText("涉及到的类");
usageViewPresentation.setCodeUsagesString("追寻到的引证");
usageViewPresentation.setTabText("Find Usage Plus");
UsageTarget[] usageTargets = {new PsiElement2UsageTargetAdapter(resultBeans.get(0).getField().getContainingClass())};
int count = (int) resultBeans.stream().filter(p -> p.getPsiElement() != null).count();
PsiElement[] primaryElements = new PsiElement[count];
UsageInfo[] usageInfo = new UsageInfo[count];
for (int i = 0; i < resultBeans.size(); i++) {
    ResultCollector.ResultBean resultBean = resultBeans.get(i);
    if (resultBean.getPsiElement() == null) {
        continue;
    }
    primaryElements[i] = resultBean.getPsiElement();
    usageInfo[i] = new UsageInfo(resultBean.getPsiElement());
}
if (primaryElements.length == 0) {
    UsageViewManager.getInstance(project).showUsages(UsageTarget.EMPTY_ARRAY, new Usage[]{}, usageViewPresentation);
} else {
    Usage[] convert = UsageInfoToUsageConverter.convert(primaryElements, usageInfo);
    usageViewManager.showUsages(usageTargets, convert, usageViewPresentation);
}

没看错,这么大一篇代码便是为了组装usageViewManager.showUsages的三个参数,第一个参数是指窗口中”涉及到的类”所指的当地,第二个参数是指窗口中”追寻到的引证”的当地,点击今后IDEA主动帮你相关到代码上。

其他细节

除了上面说的之外,剩余的就只有一些代码上的细节了。

  • 约定psiReferences大于5处的时分不计算
if (psiReferences.size() > 5) {
     System.out.println("set调用处大于5处,跳过");
     resultCollector.collect(psiField, ResultCollector.BIGGER_5, null);
     return;
 }
  • 不支持大于两个入参的Set办法
if (psiExpressions.length > 1) {
    System.out.println("不支持大于两个入参的Set办法" + psiCall.getText());
    continue;
}
  • 由于采用了递归的形式,遇到
a.setName(b.getName());
b.setName(a.getName());

的情况的时分会无限递归,所以需求用一个调集进行循环断定。