checkstyle用の独自チェックを実装してみた
Javaの静的検査ツールcheckstyle用に、
独自のチェック処理を実装してみたので、メモを残しておきます。
checkstyle
http://checkstyle.sourceforge.net/
独自チェックの実装方法は、以下の説明の通りです。
この説明を参考に実装してみます。
Writing Checks | checkstyle
http://checkstyle.sourceforge.net/writingchecks.html
このエントリでは実装するチェックは、
@JavaBeanアノテーションのあるクラス名は「〜Bean」で無ければならない。
というチェック実装してみます。
独自チェックの実装
それでは、チェック処理を実装します。
checkstyleへの依存関係の定義
まずは、以下のようにbuild.gradleにchesktyleの依存関係を指定します。
build.gradle
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
// https://mvnrepository.com/artifact/com.puppycrawl.tools/checkstyle
compile group: 'com.puppycrawl.tools', name: 'checkstyle', version: '8.11'
}
チェック処理の実装
チェック処理は「com.puppycrawl.tools.checkstyle.api.AbstractCheck」を継承したクラスに実装します。
src/main/java/com/github/takemikami/checkstyle/customcheck/checks/ClassNameByAnnotationCheck.java
package com.github.takemikami.checkstyle.customcheck.checks;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import static com.puppycrawl.tools.checkstyle.checks.naming.AbstractNameCheck.MSG_INVALID_PATTERN;
public class ClassNameByAnnotationCheck extends AbstractCheck {
// チェックのパラメータ設定
String annotation;
Pattern format;
public void setAnnotation(String annotation) { this.annotation = annotation; }
public void setFormat(Pattern format) { this.format = format; }
// 対象とするTokenTypeの指定
@Override
public int[] getDefaultTokens() { return this.getRequiredTokens(); }
@Override
public int[] getAcceptableTokens() { return this.getRequiredTokens(); }
@Override
public int[] getRequiredTokens() {
return new int[]{TokenTypes.CLASS_DEF};
}
// チェック処理の実装
@Override
public void visitToken(DetailAST ast) {
// ASTからクラスのアノテーションを取り出す
List<String> annotations = new LinkedList<String>();
DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
if (modifiers == null) return;
DetailAST child = modifiers.getFirstChild();
while (child != null) {
if(child.getType() == TokenTypes.ANNOTATION) {
DetailAST ident = child.findFirstToken(TokenTypes.IDENT);
if (ident != null && ident.getText() != null) {
annotations.add(ident.getText());
}
}
child = child.getNextSibling();
}
// 対象となるアノテーションがあるかチェック
if(!annotations.contains(this.annotation)) return;
// クラス名がformatにマッチしない場合はエラー
DetailAST crrIdent = ast.findFirstToken(TokenTypes.IDENT);
if (!this.format.matcher(crrIdent.getText()).find()) {
log(crrIdent,
MSG_INVALID_PATTERN,
crrIdent.getText(),
format.pattern());
}
}
}
チェックのパラメータ設定部分は、
以下のような、checkstyle.xmlに対応して。
format, annotationのプロパティを、受け取れるようにしています。
checkstyle.xml
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="TreeWalker">
<module name="com.github.takemikami.checkstyle.customcheck.checks.ClassNameByAnnotationCheck">
<property name="format" value="^.*Bean$"/>
<property name="annotation" value="JavaBean"/>
<message key="name.invalidPattern"
value="Javabean class ''{0}'' must match pattern ''{1}''."/>
</module>
</module>
</module>
対象とするTokenTypeの指定部分は、
「TokenTypes.CLASS_DEF」として、クラス定義部分のチェックを行うことを指定しています。
チェック処理の実装部分では、
受け取ったAST(抽象構文木)の内容を確認して、問題がある場合に「log」でエラーを出力します。
名称チェックの処理は、
例えばMemberNameCheckなどに実装されているのでこちらのコードも参考にしてください。
https://github.com/checkstyle/checkstyle/blob/master/src/main/java/com/puppycrawl/tools/checkstyle/checks/naming/MemberNameCheck.java
ASTの理解
チェック処理ではASTオブジェクトにアクセスしていくことになります。
checkstyleでは、コマンドを実行してjavaコードのASTを表示することが出来るので、
これを利用してASTの構造を理解していきます。
以下のようにダウンロード・実行します。
$ wget https://github.com/checkstyle/checkstyle/releases/download/checkstyle-8.11/checkstyle-8.11-all.jar
$ java -jar ./checkstyle-8.11-all.jar -t src/main/java/com/github/takemikami/checkstyle/customcheck/checks/ClassNameByAnnotationCheck.java
先ほどのJavaコードのASTは以下のようになります。
PACKAGE_DEF -> package [1:0]
|--ANNOTATIONS -> ANNOTATIONS [1:52]
|--DOT -> . [1:52]
| |--DOT -> . [1:40]
| | |--DOT -> . [1:29]
| | | |--DOT -> . [1:18]
| | | | |--DOT -> . [1:11]
| | | | | |--IDENT -> com [1:8]
| | | | | `--IDENT -> github [1:12]
| | | | `--IDENT -> takemikami [1:19]
| | | `--IDENT -> checkstyle [1:30]
| | `--IDENT -> customcheck [1:41]
| `--IDENT -> checks [1:53]
`--SEMI -> ; [1:59]
IMPORT -> import [3:0]
|--DOT -> . [3:42]
| |--DOT -> . [3:38]
| | |--DOT -> . [3:27]
| | | |--DOT -> . [3:21]
| | | | |--DOT -> . [3:10]
| | | | | |--IDENT -> com [3:7]
| | | | | `--IDENT -> puppycrawl [3:11]
| | | | `--IDENT -> tools [3:22]
| | | `--IDENT -> checkstyle [3:28]
| | `--IDENT -> api [3:39]
| `--IDENT -> AbstractCheck [3:43]
`--SEMI -> ; [3:56]
※途中省略※
CLASS_DEF -> CLASS_DEF [13:0]
|--MODIFIERS -> MODIFIERS [13:0]
| `--LITERAL_PUBLIC -> public [13:0]
|--LITERAL_CLASS -> class [13:7]
|--IDENT -> ClassNameByAnnotationCheck [13:13]
|--EXTENDS_CLAUSE -> extends [13:40]
| `--IDENT -> AbstractCheck [13:48]
`--OBJBLOCK -> OBJBLOCK [13:62]
|--LCURLY -> { [13:62]
|--VARIABLE_DEF -> VARIABLE_DEF [16:4]
| |--MODIFIERS -> MODIFIERS [16:4]
| |--TYPE -> TYPE [16:4]
| | `--IDENT -> String [16:4]
| |--IDENT -> annotation [16:11]
| `--SEMI -> ; [16:21]
|--VARIABLE_DEF -> VARIABLE_DEF [17:4]
| |--MODIFIERS -> MODIFIERS [17:4]
| |--TYPE -> TYPE [17:4]
| | `--IDENT -> Pattern [17:4]
| |--IDENT -> format [17:12]
| `--SEMI -> ; [17:18]
|--METHOD_DEF -> METHOD_DEF [18:4]
| |--MODIFIERS -> MODIFIERS [18:4]
| | `--LITERAL_PUBLIC -> public [18:4]
| |--TYPE -> TYPE [18:11]
| | `--LITERAL_VOID -> void [18:11]
| |--IDENT -> setAnnotation [18:16]
| |--LPAREN -> ( [18:29]
| |--PARAMETERS -> PARAMETERS [18:30]
| | `--PARAMETER_DEF -> PARAMETER_DEF [18:30]
| | |--MODIFIERS -> MODIFIERS [18:30]
| | |--TYPE -> TYPE [18:30]
| | | `--IDENT -> String [18:30]
| | `--IDENT -> annotation [18:37]
| |--RPAREN -> ) [18:47]
| `--SLIST -> { [18:49]
| |--EXPR -> EXPR [18:67]
| | `--ASSIGN -> = [18:67]
| | |--DOT -> . [18:55]
| | | |--LITERAL_THIS -> this [18:51]
| | | `--IDENT -> annotation [18:56]
| | `--IDENT -> annotation [18:69]
| |--SEMI -> ; [18:79]
| `--RCURLY -> } [18:81]
※以降省略※
CLASS_DEFのあたりをみると、
ASTからクラス名・スコープ・継承元クラス名などにアクセス出来ることが確認できます。
チェック処理のビルド
ここまでで独自のチェックが実装できたので、ビルドしてjarを作っておきます。
$ gradle jar
build/libs以下にjarが生成されます。
独自チェックの適用方法
それでは、実装したチェックを実際のコードに適用します。
gradleの設定
まずは、以下のようにbuild.gradleにchesktyleの設定と独自チェックjarのclasspathを指定します。
plugins {
id 'java'
id 'checkstyle'
}
repositories {
mavenCentral()
}
dependencies {
checkstyle group: 'com.puppycrawl.tools', name: 'checkstyle', version: '8.11'
checkstyle files('※独自チェックjarのパス※')
}
checkstyle {
toolVersion '8.11'
}
checkstyle.xmlの定義
以下のように独自チェックのcheckstyle.xml定義を追加します。
config/checkstyle/checkstyle.xml
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="TreeWalker">
<module name="com.github.takemikami.checkstyle.customcheck.checks.ClassNameByAnnotationCheck">
<property name="format" value="^.*Bean$"/>
<property name="annotation" value="JavaBean"/>
<message key="name.invalidPattern"
value="Javabean class ''{0}'' must match pattern ''{1}''."/>
</module>
</module>
</module>
対象コードとチェックの実施
以下のように、チェックに引っかかるコードを作成します。
src/main/java/sample/Target.java
package sample;
import java.beans.JavaBean;
@JavaBean
public class Target {
}
以下のようにチェックを実行すると、問題を指摘して失敗します。
$ gradle check
※途中省略※
[ant:checkstyle] [ERROR] ※プロジェクトのパス※src/main/java/sample/Target.java:6:14: Javabean class 'Target' must match pattern '^.*Bean$'. [ClassNameByAnnotation]
※途中省略※
以上のような流れで、
checkstyleに独自のチェックを追加することが出来ました。
会社やプロジェクトによって、独自のルールを設けていることも多いですが。
機械的に検査出来るルールは、人の目によるレビューではなく、
checkstyleで自動チェックさせるとストレスが少なくなります。
特にプロジェクト固有のルールなどは、
例えば、業務や部署名などをPrefixにつけるといったような、
単純だが、世間一般のチェックツールには存在しないものケースも多いので。
独自チェックの実装方法を知っておくと、活用範囲を広げられるでしょう。
自分で使いそうなチェック処理はここに書いていこうとしています。
https://github.com/takemikami/checkstyle-custom-checks