独自Inspectorを作ってみる

ちょっと思うところあって,プラグイン開発に手を出してみた。作ったのはCode InspectionのカスタムInspector。要するにFindBugsのDetectorみたいなモンですな。あえて,IDEAのInspectorにしたのは,リアルタイムに検査してくれるから。


手習いとして作ったInspectorは,こんなチェックをするInspector。

  • ArrayListとそのサブクラスがターゲット
  • 引数が空だと文句を言う
  • 初期容量がゼロでも文句を言う


実際に動かした結果はこんな感じ。


正直,チェック内容の細かいところはどうでもよくて,以下のポイントがクリアできるかどうかを確認したかったのだ。いや,そんなのはできると思うんで,どうゆうコードを書けば実現できるのかを確認したかったってのが正しい。

  • コンストラクタを呼び出す部分だけをチェック対象にできるかどうか
  • チェック対象を,Javaのオブジェクト階層で絞ることができるか
    • 特定のクラスだけじゃなく,そのサブクラスも対象にできるかどうか


以下,作ったプラグインの自分メモを残す。

IDEA SDKとか,IDEAプラグインの概要みたいなもんは割愛。興味ある人は,ここら辺でも読んでくれい。
Information for Plugin Developers


DevKitのサンプルに「comparingReferences」というおあつらえ向きのサンプルがあったので,これを元に作成。プロジェクトの全体像は,以下の通り。
#はじめに断っておくが,xmlとかhtmlはUTF-8で記述してる。


プラグインのメタ情報をもつplugin.xmlの中身はこう。

<!DOCTYPE idea-plugin PUBLIC "Plugin/DTD" "http://plugins.intellij.net/plugin.dtd">
<idea-plugin>
  <name>ArrayListのチェックをするインスペクタ</name>
  <description>ArrayListの引数が空,初期サイズが0なのをチェックします</description>
  <version>1.0</version>
  <vendor>mars</vendor>
  <idea-version since-build="3000"/>
  <is-internal/>

  <extensions defaultExtensionNs="com.intellij">
    <inspectionToolProvider implementation="com.example.inspection.ArrayListCheckProvider" />
  </extensions>
</idea-plugin>

登録するコンポーネントは,Plugin Componentsに含まれるモンじゃないし,登録方法(extentionsタグ)もドキュメントに載ってる方法じゃないんだけど,サンプルがこうなってんだからよしとしよう。
#あとでInspectionGadgetsのplugin.xmlみたら,こっちはapplication-levelで登録してあったけどね。


次に拡張ポイントに指定してあるArrayListCheckProviderについて。

package com.example.inspection;

import com.intellij.codeInspection.InspectionToolProvider;

public class ArrayListCheckProvider implements InspectionToolProvider {
  public Class[] getInspectionClasses() {
    return new Class[] { ArrayListCheckInspection.class };
  }
}

これは見ての通り。InspectionToolProviderを実装して,追加したいInspectorクラスを返すだけ。Inspectorクラスは複数返せるので,何種類かInspectorを作っても平気なのがわかる。


そんでもって,Inspector本体のArrayListCheckInspectionの説明になるわけだが,全部載っけると長いので,部分的にかいつまんで載せていく。
まずは,導入部。

public class ArrayListCheckInspection extends LocalInspectionTool {
  @Nls @NotNull
  public String getGroupDisplayName() {
    return GroupNames.BUGS_GROUP_NAME;
  }

  @Nls @NotNull
  public String getDisplayName() {
    return "ArrayListのチェックをします.";
  }

  @NonNls @NotNull
  public String getShortName() {
    return "ArrayListCheckInspector";
  }

  public boolean isEnabledByDefault() {
    return true;
  }

  @NotNull
  public HighlightDisplayLevel getDefaultLevel() {
    return HighlightDisplayLevel.ERROR;
  }

LocalInspectionToolを継承するのがInspectorの証なんだろな。getGroupDisplayName()/getDisplayName()/getShortName()でInspectorの属するグループと自身の名前を返す。
getShortName()が,InspectorのIDになるらしく,inspectionDescriptionsパッケージに,getShortName()と同じ名前のHTMLコンテンツを配置しておくと,それがInspectorの説明文になるらしい。

=== inspectionDescriptions/ArrayListCheckInspector.html ===
<html>
<body>
もっかして,ここに説明書くと,Errorsの設定画面に表示される?
</body>
</html>

あと残りのisEnabledByDefault()/getDefaultLevel()は,そのInspectorが有効かどうかと警告レベルのデフォルトを返す。これらの設定結果は,Settings→ErrosのInspection設定画面で確認することができる。


余談だが,Inspection設定画面のOptionsに,なにかオプションを設定したい場合は,createOptionsPanel()をオーバライドすればいい(らしい)。参考までに,実装例を載せておく。

public JComponent createOptionsPanel() {
  JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
  final JTextField checkedClasses = new JTextField(CHECKED_CLASSES);
  checkedClasses.getDocument().addDocumentListener(new DocumentAdapter() {
    public void textChanged(DocumentEvent event) {
      CHECKED_CLASSES = checkedClasses.getText();
    }
  });

  panel.add(checkedClasses);
  return panel;
}


Inspectionの初期設定まわりはこれでおしまい。つづけて,Inspectionの実行部分を紹介。Inspectionのエントリポイントは,checkMethod()/checkClass()の2つ(どっちが,いつ動くかはよくわかってない。少なくともテストしている限りでは,checkMethod()しか動いてなかった気がする)。

public ProblemDescriptor[] checkMethod(PsiMethod method, InspectionManager manager, boolean isOnTheFly) {
  return analyzeCode(method.getBody(), manager);
}

public ProblemDescriptor[] checkClass(PsiClass aClass, InspectionManager manager, boolean isOnTheFly) {
  ArrayList<ProblemDescriptor> problemList = null;
  PsiClassInitializer[] initializers = aClass.getInitializers();
  for (PsiClassInitializer initializer : initializers) {
    ProblemDescriptor[] problemDescriptors = analyzeCode(initializer, manager);
    if (problemDescriptors != null) {
      if (problemList == null) problemList = new ArrayList<ProblemDescriptor>();
      problemList.addAll(Arrays.asList(problemDescriptors));
    }
  }
  return problemList == null
         ? null
         : problemList.toArray(new ProblemDescriptor[problemList.size()]);
}

正直,この部分はサンプルコード丸パクリなんで「まあ,こんなもんだろ」程度でしか理解してない。とは言え,要するにこのエントリポイントを起点に,コードの構造をなめていくってのは理解できる(ドックレットみたいなもんでしょ)。


IDEAの場合,PSI(Program Structure Interface)と呼ばれる構造木を持つので,これを旨いこと使って目的の場所を探すことになる。で,このPSIを自由自在に操れるようになるのが,この手のプラグインを書くキモになるんだが,いきなり自由自在ってワケにはいかない。
そんなときに役立ったのが,これ。→PsiViewerプラグイン


名前見りゃ機能の想像が付くと思うが,今使っているIDEAのポイントをPSIの視点で表示するプラグイン。大変重宝する(つか,これないとムリ)。
試しに,エディタ中にこんなコード,

private void hoge() {
  List list = new ArrayList();
}

を書いて,PsiViewerで覗くと,こんな構造になってるって教えてくれる。


と,お膳立てが済んだところで,Inspectorの心臓部のコードを紹介。

private ProblemDescriptor[] analyzeCode(PsiElement where, final InspectionManager manager) {
  if (where == null) return null;

  final ArrayList problemList = new ArrayList();
  where.accept(new PsiRecursiveElementVisitor() {
    public void visitNewExpression(PsiNewExpression expression) {
      PsiType type = expression.getType();
      if (isCheckedType(type)) {
        PsiExpressionList arguments = expression.getArgumentList();
        PsiExpression[] argExpressions = arguments.getExpressions();
        if (argExpressions.length == 0) {
          ProblemDescriptor problemDescriptor = manager.createProblemDescriptor(
                                expression,
                                "引数がゼロ個はダメです.",
                                LocalQuickFix.EMPTY_ARRAY,
                                ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
          problemList.add(problemDescriptor);
        } else {
          PsiExpression arg0 = argExpressions[0];
          if ("0".equals(arg0.getText())) {
            ProblemDescriptor problemDescriptor = manager.createProblemDescriptor(
                                arg0,
                                "初期値:0はダメです.",
                                LocalQuickFix.EMPTY_ARRAY,
                                ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
            problemList.add(problemDescriptor);
          }
        }
      }
    }
  });

  return problemList.size() == 0
         ? null
         : (ProblemDescriptor[]) problemList.toArray(new ProblemDescriptor[problemList.size()]);
}

ここのポイントは2つ。1つは,PsiElement.accept(PsiElementVisitor visitor)ってやつ。見ての通り,Visitorパターンの適用例で,何種類かVisitorがある。これを使って,対象となるPsiElementの中身を走査していくワケだ。今回,処理したいポイントが,PsiNewExpressionなのは,PsiViewerで確認済みなので,そこだけオーバライドする。


もう1つは,問題を見つけたら,そこを指し示すProblemDescriptorを返すこと。ProblemDescriptorに問題があるPsiElementを指定することで,先のスクリーンショットのようにエディタ上でハイライト表示させることができる。
ついでいうと,ProblemDescriptorにLocalQuickFixを与えることで,修正用の電球アイコンも作れるようだ。
#問題判定ロジックそのものは,そんなに厳密じゃないので,テキトウにみてくれ。


最後が,チェック対象を絞り込む型判定部分。

private boolean isCheckedType(PsiType type) {
  if (!(type instanceof PsiClassType)) return false;
  if ("java.util.ArrayList".equals(type.getCanonicalText())) {
    return true;
  }
  PsiType[] supperTypes = type.getSuperTypes();
  for (int i = 0; i < supperTypes.length; i++) {
    String text = supperTypes[i].getCanonicalText();
    if ("java.util.ArrayList".equals(text)) {
      return true;
    }
  }
  return false;
}

この型判定のやり方がわかったおかげて,ホントにやりたかったことの現実味が増したと言えよう。
以上,おしまい。


ps.
PSIの使い方でちょっと悩みはしたもののInspectorの仕組み自体は,そんな難しくなかった。チェックする内容にもよるが,ちょっとしたもんだったら,この独自Inspectorは使える。
#そんなん作っても喜ぶのは自分だけってのは,とりあえず忘れとこう。:-P


今度はQuickFixに挑戦だ。:-D