萌え萌えIrenka
運命だったのか魔が差したのか、Irenkaのドキュメントを読んでしまって。惚れた。やばい、これは使わなければ。と、すぐに地豆に適用してしまったw
Irenkaとは何か。
誤解を恐れずに言えば、高機能Checkstyleみたいなものです。ただ、出来ることは何かを見つけて指摘するだけじゃない。コンパイルのタイミングで、コードを書き換えちゃうことだってできるのだ。
とあるコーディング規約(Irenkaでは「掟」と呼ぶのか?w)があって、それに違反した箇所を見つけて指摘するのがCheckstyle。Checkstyleは、独自のルールを組む事が出来ない訳ではないのだが、俺はよく知らない。
Irenkaは、掟に背いた部分を「指摘する」のはもちろん、「勝手に直す」こともできる無茶苦茶な奴なんだ。しかも掟は自分で書ける。(というか、標準では提供されていないっぽいので自分で書くしかないw)
コンパイルのタイミングでコンパイラに「介入」を行い、問題点の指摘やコードの修正など、様々な動作が出来、それを自分で書くことができるのがIrenka。
その動作を、Irenkaでは「Hack」と呼ぶ。
今日は萌えに萌えたので、エントリー長いゼ。
Irenkaの導入
細かい説明はドキュメントに譲るとして、ザックリと導入方法を。
- 更新サイトからIrenkaをインストール
- IrenkaのHackを記述するプロジェクト(A)と、Hack対象のプロジェクト(B)を生成する。
- 両者のプロジェクトのプロパティ>Java Compiler>Irenka で Irenka Builderを有効にする。
- AにHackを書く(後述)。
- BのビルドパスにAを追加する。
- Bのプロジェクトのプロパティ>Java Compiler>Irenka で「追加」を選択し、今作ったHackを追加する。
以上でProject BにHackが適用される。
「指摘する」Hackの例
今日初めてドキュメントを読み、いきなりHackを書いてみた。というか誰かが書いたHackをあまり見つける事が出来なかった(そんなにマジメに探してないけどw)ので、自分で書くしか無い訳だがw
まず、書いた中で一番簡単な例。
/** * 変数名の命名規則をチェックするIrenka Hack。 * @author daisuke */ public class VariableNamingRuleChecker { /** * 配列に関する命名規則チェック。 * @when variable.type <= {@link Object[]} */ public void foundArray(CtVariable<?> variable, Messager messager) { String name = variable.getSimpleName(); if (name.endsWith("s") == false) { messager.warn(variable, "配列の識別子は、複数形でなければなりません。"); } } }
このHackを適用すると、配列型の変数名がsで終わっていなければProblemに警告が表示される、という感じ。
クラスがHackと見なされる条件は以下の通り(全て満たすこと)。
Irenka Studio Users' Guide (PDF)
- ビルドパス上のファイル内で定義されており、同ファイルは拡張子が".java"である
- publicであり、かつabstractでないトップレベルのクラスとして宣言されている
- 同クラス内で引数を持たないpublicコンストラクタを宣言する
- 同クラス内で1つ以上のHack Actionと解釈可能なメソッドを宣言する
Hack Actionというのは、Hackクラス内のメソッド。メソッドがHackActionと見なされる条件は以下の通り(全て満たすこと)。
Irenka Studio Users' Guide (PDF)
- Javaドキュメンテーションコメントが直接付与されている
- Javaドキュメンテーションコメント内にIrenka Search Queryを記述する"@when"タグブロックを有する
- publicであり、かつstaticでないメソッドとして宣言されている
- 1つ以上の引数を宣言し、いずれも次の型のうちいずれかを有する
- org.ashikunep.irenka.dom.CtElement 型、またはそのサブタイプ
- org.ashikunep.irenka.toolkit.Tool 型のサブタイプ
- org.ashikunep.irenka.event.CtEvent 型、またはそのサブタイプ
Irenka Search Queryとは。ザックリ言えば「何を探すか」を記述するためのJava言語仕様に酷似した問い合わせ言語。先ほどの例で言う「variable.type <= {@link Object}」だ。これは「変数の型が、Objectのサブタイプである」という条件を表す。
コンパイル時に、この条件を満たす変数が見つかり次第、このHackActionが呼び出される訳だ。そして、その対象となった変数にまつわる情報が、引数(CtVariable)として渡される。後は中で好きな事を書けばいい。
ここで、もう一つHackActionを見てみる。
/** * {@link Map}に関する命名規則チェック。 * @when */ public void foundMap(Messager messager, CtVariable<? extends Map<?, ?>> variable) { String name = variable.getSimpleName(); if (name.endsWith("s") == false && name.endsWith("Map") == false) { messager.warn(variable, "Map型の識別子は、複数形もしくはMapで終わらなければなりません。"); } }
あー、今度は@whenがからっぽだー。だけど、CtVariableのGenericsにMapのサブクラスを指定している。こうすることにより、Map型の変数を探すことができる。「@when variable.type <= {@link Map}」と同等かな。
さて、次のHackActionは…。やり方がわからんかった部分もあるw
/** * {@link Collection}に関する命名規則チェック。 * @when */ public void foundCollection(CtVariable<? extends Collection<?>> variable, DeclarationFactory decFactory, Messager messager) { String name = variable.getSimpleName(); if (name.endsWith("s")) { return; } if (decFactory.typeOf(List.class).isAssignableFrom(variable.getType()) && name.endsWith("List")) { return; } if (decFactory.typeOf(Set.class).isAssignableFrom(variable.getType()) && name.endsWith("Set")) { return; } messager.warn(variable, "Collection型の識別子は、複数形もしくはList,Setで終わらなければなりません。"); }
Collection型で探して、Listならsuffix=List, Setならsuffix=Setを許したかったのだが、特定のクラスのCtTypeインスタンスをどうやって手に入れていいか分からなかった。あう。す。
その他、変数名だけではなく、クラス名などにも応用できる。
/** * ユーティリティクラスに関するチェックを行うIrenka Hack。 * @author daisuke */ public class UtilClassChecker { /** * Utils拒否。 * @when klass.simpleName =~ "^.*Utils$" */ public void foundUtilsClass(CtClass<?> klass, Messager messager) { messager.warn(klass, "ユーティリティクラスのsuffixは、UtilsではなくUtilであるべきです。"); } }
さらに、名前の警告だけではなく、Utilクラスは「privateの引数なしコンストラクタを一つだけ持つべき」という掟も、以下のように。
/** * 引数無しprivateコンストラクタチェック。 * @when klass.simpleName =~ "^.*Util$" */ public void foundUtilClass(CtClass<?> klass, Messager messager) { int constructorSize = klass.getDeclaredConstructors().size(); if (constructorSize > 1) { messager.warn(klass, "ユーティリティクラスが複数のコンストラクタを持っています。"); } else { CtConstructor<?> ctConstructor = klass.getDeclaredConstructors().get(0); int constructorParameterSize = ctConstructor.getParameters().size(); if (constructorParameterSize != 0) { messager.warn(ctConstructor, "ユーティリティクラスは引数無しのコンストラクタを持つべきです。"); } else if (ctConstructor.getVisibility() != VisibilityKind.PRIVATE) { messager.warn(ctConstructor, "ユーティリティクラスはprivateコンストラクタを持つべきです。"); } } }
あと更に、「コンストラクタの中身はカラッポじゃなきゃいけない」という掟も作りたかったのだが、作り方が分からなかった><
「勝手に直す」Hackの例
最後に。「勝手に直す」Hackも書いてみたので紹介する。ちょっとデカくなってしまうので細かい解説はしない。しかも、まだ今日Irenkaを学び始めたばかりなので甘い所も多いと思う。実現したくても方法が分からない事もあり。まだ不完全ですが晒します。詳しい方、どうか色々御指南頂ければ幸いです。
このHackは「accessor(setter/getter)メソッドに、勝手にJavadocを付ける」というものです。accessorには、対応する操作対象フィールドがあるハズで、そのフィールドについているJavadocコメントを取得し、そこからaccessor用のJavadocコメントを生成、それをメソッドに付加する、ということをやってます。
import java.util.List; import org.ashikunep.irenka.dom.CtClass; import org.ashikunep.irenka.dom.CtField; import org.ashikunep.irenka.dom.CtMethod; import org.ashikunep.irenka.dom.CtType; import org.ashikunep.irenka.dom.doc.CtDocBlock; import org.ashikunep.irenka.toolkit.DocFactory; /** * 自動でJavadocコメントを付加するIrenka Hack。 * @author daisuke */ public class AutoJavadocDescriber { /** * getterのJavadocコメントを設定する。 * @when * method.simpleName =~ "^get[A-Z][a-zA-Z0-9]*$" * void != method.returnType // 戻り値がvoidではない * () = method.parameters // 引数がない */ public void foundGetter(CtMethod<?> method, DocFactory docFactory) { String fieldDescription = getTargetFieldDescription(method); if (fieldDescription == null) { return; } setSummaryBlock(method, docFactory, fieldDescription + "を取得する。"); setBlock(method, docFactory, "return", fieldDescription); } /** * setterのJavadocコメントを設定する。 * @when * method.simpleName =~ "^set[A-Z][a-zA-Z0-9]*$" * void = method.returnType // 戻り値がvoidである * // 1 = method.parameters.size // TODO 引数が1つである、を表現したいが方法不明 */ public void foundSetter(CtMethod<?> method, DocFactory docFactory) { String fieldDescription = getTargetFieldDescription(method); if (fieldDescription == null) { return; } setSummaryBlock(method, docFactory, fieldDescription + "を設定する。"); setBlock(method, docFactory, "param", getFieldName(method) + " " + fieldDescription); } /** * 一文字目を小文字に変える。 * @param str 処理対象文字列 * @return 処理結果 */ private String decapital(String str) { final char[] ch = str.toCharArray(); ch[0] = Character.toLowerCase(ch[0]); return new String(ch); } /** * accessorメソッドから、その操作対象フィールドの名称を取得する。 * @param method accessorメソッド * @return フィールド名 */ private String getFieldName(CtMethod<?> method) { return decapital(method.getSimpleName().substring(3)); } /** * フィールドを示す説明句を取得する。 * @param method * @return フィールドを示す説明句 */ private String getTargetFieldDescription(CtMethod<?> method) { String fieldName = getFieldName(method); CtClass<?> klass = (CtClass<?>) method.getParent(); CtField<?> field = klass.getField(fieldName); if (field == null) { return null; } String fieldJavadocString = null; List<CtDocBlock> fieldJavadocBlocks = field.getJavadoc().getBlocks(); if (fieldJavadocBlocks.size() != 0) { fieldJavadocString = fieldJavadocBlocks.get(0).getFragments().get(0).toString(); } if (fieldJavadocString == null || fieldJavadocString.length() == 0) { fieldJavadocString = "{@link #" + field.getSimpleName() + "}"; } return fieldJavadocString; } /** * 特定のタグのブロックをセットする。 * @param method * @param docFactory * @param targetTag 対象となるタグ * @param javadocString */ private void setBlock(CtMethod<?> method, DocFactory docFactory, String targetTag, String javadocString) { CtDocBlock newDocBlock = docFactory.newDocBlock(targetTag); newDocBlock.getFragments().add(docFactory.newDocText(javadocString)); List<CtDocBlock> methodJavadocBlocks = method.getJavadoc().getBlocks(); for (CtDocBlock methodJavadocBlock : methodJavadocBlocks) { String tag = methodJavadocBlock.getTag(); if (("@" + targetTag).equals(tag) && methodJavadocBlock.exists()) { // 対象タグ(@returnや@param)が見つかったら、新しいタグに置き換える // methodJavadocBlock.substitute(newDocBlock); // FIXME methodJavadocBlockをnewDocBlockと置換したいのだが、これじゃダメらしい。 return; } } // 対象タグがなかったら、単純に追加する。 methodJavadocBlocks.add(newDocBlock); } /** * 概要ブロック(1つ目のブロック)をセットする。 * @param method * @param docFactory * @param javadocString */ private void setSummaryBlock(CtMethod<?> method, DocFactory docFactory, String javadocString) { CtDocBlock summaryDocBlock = docFactory.newDocBlock(null); summaryDocBlock.getFragments().add(docFactory.newDocText(javadocString)); List<CtDocBlock> methodJavadocBlocks = method.getJavadoc().getBlocks(); if (methodJavadocBlocks.isEmpty() || methodJavadocBlocks.get(0).getTag() != null) { methodJavadocBlocks.add(0, summaryDocBlock); } else { // FIXME 1行目のみを置換したいんだけどな…。改行では区切れないのだろうか。 methodJavadocBlocks.set(0, summaryDocBlock); } } }