独自ルールでバグを検出するSpotBugsプラグインの作り方を調べたメモ。
使ったソースコードはこちら。
プラグインを実装するのに必要なものは次の3つ。
最小構成での実装例
手始めにクラス名に Hoge
が含まれていないかを検証するシンプルなプラグインを実装してみる。
まずは依存モジュールにSpotBugsを追加。実行時はSpotBugs本体が存在している筈なので provided
スコープで追加する。
<dependency><groupId>com.github.spotbugs</groupId><artifactId>spotbugs</artifactId><version>4.8.4</version><scope>provided</scope></dependency>
次に Detector
インターフェースを実装する *1。このインタフェースは検出したいバグをクラスファイルの中から探し出して報告する役割を持っている。
package com.sciencesakura.example; import edu.umd.cs.findbugs.BugInstance; import edu.umd.cs.findbugs.BugReporter; import edu.umd.cs.findbugs.Detector; import edu.umd.cs.findbugs.ba.ClassContext; /** * {@code Detector} 実装例: クラス名に `Hoge` を含むクラスを検出する. */publicclass DetectHogeClass implements Detector { privatefinal BugReporter reporter; /** *① {@code BugReporter} 型の引数を受け取るコンストラクタを実装しないといけない. */public DetectHogeClass(BugReporter reporter) { this.reporter = reporter; } /** *②クラスファイルを解析するメソッド. */@Overridepublicvoid visitClassContext(ClassContext classContext) { var className = classContext.getClassDescriptor().getSimpleName(); if (className.contains("Hoge")) { // ③バグを報告する reporter.reportBug(new BugInstance(this, "X_DISALLOW_HOGE", NORMAL_PRIORITY) .addClass(classContext.getClassDescriptor())); } } /** *④すべてのクラスファイルの解析が完了したあとに呼ばれるメソッド. */@Overridepublicvoid report() { } }
① BugReporter
型の引数をひとつ受け取る public
なコンストラクタが必要。
② 検査対象のクラスファイル各々について visitClassContext(ClassContext)
が呼ばれる。引数として対象のクラスファイルを表す構造化されたオブジェクトが受け取れる。
③ 報告したいバグを検知した場合はバグを表す BugInstance
のインスタンスを作成し、①で受け取った BugReporter
で報告する。 BugInstance
に指定している X_DISALLOW_HOGE
という文字列はバグパターンを示す文字列で、後述する findbugs.xml
にて定義する。
④ すべてのクラスファイルの解析が終わると report()
が呼ばれる。
残る findbugs.xml
と messages.xml
を用意する。
<?xml version="1.0" encoding="UTF-8"?><!-- SpotBugsはpluginid属性でプラグインを識別するので他のプラグインと重複しないよう注意 --><FindbugsPlugin xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/spotbugs/spotbugs/4.8.4/spotbugs/etc/findbugsplugin.xsd"pluginid="com.sciencesakura.example.minimum"version="1.0.0"provider="sciencesakura.com"><!-- Detectorの定義. reports属性でこのDetectorが検出しうるBugPatternを紐付ける. カンマ区切りで複数指定可 --><Detector class="com.sciencesakura.example.DetectHogeClass"reports="X_DISALLOW_HOGE"/><!-- BugPatternの定義. abbrevはBugCode, categoryはBugCategoryに相当する. --><BugPattern type="X_DISALLOW_HOGE"abbrev="X"category="EXAMPLES"/></FindbugsPlugin>
<?xml version="1.0" encoding="UTF-8"?><MessageCollection xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/spotbugs/spotbugs/4.8.4/spotbugs/etc/messagecollection.xsd"><Plugin><ShortDescription>Example plugin</ShortDescription><Details>SpotBugsで独自バグパターンを作ってみたサンプル</Details></Plugin><BugCategory category="EXAMPLE"><Description>Example</Description><Abbreviation>X</Abbreviation><Details>The example category</Details></BugCategory><Detector class="com.sciencesakura.example.DetectHogeClass"><Details>クラス名に `Hoge` を含むクラスを検出する</Details></Detector><BugPattern type="X_DISALLOW_HOGE"><ShortDescription>Disallow `Hoge`</ShortDescription><LongDescription>Disallow `Hoge`</LongDescription><Details>識別子に単語 `Hoge` の使用を禁止します</Details></BugPattern><BugCode abbrev="X">Example</BugCode></MessageCollection>
特に説明は不要かと思う。例示のために <BugCategory>
で独自のカテゴリ EXAMPLE
を定義しているが、 SpotBugsが標準で提供しているカテゴリを使っても良い。例えばセキュリティに関するバグパターンを集めたプラグインfind-sec-bugsでは標準の SECURITY
カテゴリを使っている。
messages.xml
は多言語対応しており messages_ja.xml
のように名前にISO 639-1言語コードを付けたファイルを追加しておくと、ロケールに応じてSpotBugsがロードするXMLファイルが切り替わる。
プラグインのテスト
必要なものが揃ったのでテストしてみる。
JUnitとテストハーネスを依存モジュールに追加する。
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.10.2</version><scope>test</scope></dependency><dependency><groupId>com.github.spotbugs</groupId><artifactId>test-harness-jupiter</artifactId><version>4.8.4</version><scope>test</scope></dependency>
JUnitの拡張機能を使った SpotBugsExtension
が提供されており、任意のクラスファイルをターゲットにして検査を実行するテストが簡単に書ける。
package com.sciencesakura.example; import static org.junit.jupiter.api.Assertions.assertEquals; import edu.umd.cs.findbugs.BugInstance; import edu.umd.cs.findbugs.IFindBugsEngine; import edu.umd.cs.findbugs.test.SpotBugsExtension; import edu.umd.cs.findbugs.test.SpotBugsRunner; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Path; import java.util.function.Consumer; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(SpotBugsExtension.class) class DetectHogeClassTest { Consumer<IFindBugsEngine> customization = engine -> { try { // SpotBugs標準のバグは検知しないようフィルタを指定 engine.addFilter("target/test-classes/include.xml", true); } catch (IOException e) { thrownew UncheckedIOException(e); } }; @Test@DisplayName("NGケース") void ng(SpotBugsRunner runner) { var classFile = Path.of("target/test-classes/com/sciencesakura/example/test/Hoge.class"); var bugs = runner.performAnalysis(customization, classFile).getCollection(); bugs.forEach(b -> { System.err.println(b.getBugPattern().getType() + ' ' + b.getMessage() + ' ' + b.getPrimarySourceLineAnnotation()); }); assertEquals(1, bugs.size()); var bug = bugs.toArray(new BugInstance[0])[0]; assertEquals("X_DISALLOW_HOGE", bug.getBugPattern().getType()); assertEquals("com.sciencesakura.example.test.Hoge", bug.getPrimaryClass().getClassName()); } @Test@DisplayName("OKケース") void ok(SpotBugsRunner runner) { var classFile = Path.of("target/test-classes/com/sciencesakura/example/test/Fuga.class"); var bugs = runner.performAnalysis(customization, classFile).getCollection(); bugs.forEach(b -> { System.err.println(b.getBugPattern().getType() + ' ' + b.getMessage() + ' ' + b.getPrimarySourceLineAnnotation()); }); assertEquals(0, bugs.size()); } }
プラグインのビルドと利用
プラグインはJARファイルとしてビルドし配布する。実用を考えるならMaven Central等のリポジトリにアップロードするのが良い。
SpotBugsへのプラグインの追加方法はツールにより異なる。 spotbugs-maven-pluginの場合は pom.xml
にて設定できる。
<plugin><groupId>com.github.spotbugs</groupId><artifactId>spotbugs-maven-plugin</artifactId><version>4.8.4</version><configuration><!-- プラグインを指定する --><plugins><plugin><groupId>com.example</groupId><artifactId>your-spotbugs-plugin</artifactId><version>1.0.0</version></plugin></plugins></configuration></plugin>
VisitorパターンによるDetector実装例
先程の例はクラス名をチェックするだけの簡単な例だったが、実用的なDetectorを作るにはフィールドやメソッド、そしてJVM命令のオペコードやオペランドを解析する必要がしばしばある。 ClassContext
を自前で展開するのも手だが、実はこれらフィールド、メソッド、JVM命令といったクラスファイルに含まれる各エントリをVisitorパターンを使って巡回できるDetector実装が標準で提供されている。
下記はその BytecodeScanningDetector
を継承し、代表的なvisitメソッドでログを出力する例である。
package com.sciencesakura.example; import edu.umd.cs.findbugs.BugReporter; import edu.umd.cs.findbugs.BytecodeScanningDetector; import edu.umd.cs.findbugs.NonReportingDetector; import java.util.Map; import org.apache.bcel.Const; import org.apache.bcel.classfile.ElementValue; import org.apache.bcel.classfile.Field; import org.apache.bcel.classfile.JavaClass; import org.apache.bcel.classfile.Method; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * {@code Detector} 実装例: デバッグ用の情報を出力する. */publicclass DebugVisitor extends BytecodeScanningDetector implements NonReportingDetector { privatestaticfinal Logger log = LoggerFactory.getLogger(DebugVisitor.class); public DebugVisitor(BugReporter reporter) { } /** *①クラスファイルの解析スタート. */@Overridepublicvoid visit(JavaClass obj) { log.info("① visit({}クラス)", getDottedClassName()); } /** *②解析対象のクラスファイルか判定. */@Overridepublicboolean shouldVisit(JavaClass obj) { log.info("② shouldVisit({}クラス)", getDottedClassName()); returntrue; } /** *③フィールドの解析. */@Overridepublicvoid visit(Field obj) { log.info(" ③ visit({}フィールド)", getFieldName()); } /** *④メソッドの解析. */@Overridepublicvoid visit(Method obj) { log.info(" ④ visit({}メソッド)", getMethodName()); } /** *⑤オペコードの解析. */@Overridepublicvoid sawOpcode(int seen) { log.info(" ⑤ sawOpcode({})", Const.getOpcodeName(seen)); } /** *⑥アノテーション(クラス、フィールド、メソッド)の解析. */@Overridepublicvoid visitAnnotation(String annotationClass, Map<String, ElementValue> map, boolean runtimeVisible) { if (visitingField() || visitingMethod()) { // フィールドまたはメソッドのアノテーション log.info(" ⑥ visitAnnotation({})", annotationClass); } else { // クラスのアノテーション log.info(" ⑥ visitAnnotation({})", annotationClass); } } /** *⑦アノテーション(パラメータ)の解析. */@Overridepublicvoid visitParameterAnnotation(int p, String annotationClass, Map<String, ElementValue> map, boolean runtimeVisible) { log.info(" ⑦ visitParameterAnnotation({}, {})", p, annotationClass); } /** *⑧クラスファイルの解析終了. */@Overridepublicvoid visitAfter(JavaClass obj) { log.info("⑧ visitAfter({}クラス)", getDottedClassName()); } }
実際にクラスファイルを解析してみる。解析対象のクラスのソースコードとログ出力の結果を以下に示す。
package com.sciencesakura.example.test; import javax.annotation.Nonnull; import javax.annotation.concurrent.Immutable; @Immutablepublicfinalclass Fraction { privatefinalint numerator; privatefinalint denominator; public Fraction(int numerator, int denominator) { if (denominator == 0) { thrownew IllegalArgumentException("denominator must not be zero"); } this.numerator = numerator; this.denominator = denominator; } @Nonnullpublic Fraction plus(@Nonnull Fraction other) { returnnew Fraction( numerator * other.denominator + other.numerator * denominator, denominator * other.denominator ); } }
① visit(com.sciencesakura.example.Fractionクラス) ② shouldVisit(com.sciencesakura.example.Fractionクラス) ③ visit(numeratorフィールド) ③ visit(denominatorフィールド) ④ visit(<init>メソッド) ⑤ sawOpcode(aload_0) ⑤ sawOpcode(invokespecial) ⑤ sawOpcode(iload_2) ⑤ sawOpcode(ifne) ⑤ sawOpcode(new) ⑤ sawOpcode(dup) ⑤ sawOpcode(ldc) ⑤ sawOpcode(invokespecial) ⑤ sawOpcode(athrow) ⑤ sawOpcode(aload_0) ⑤ sawOpcode(iload_1) ⑤ sawOpcode(putfield) ⑤ sawOpcode(aload_0) ⑤ sawOpcode(iload_2) ⑤ sawOpcode(putfield) ⑤ sawOpcode(return) ④ visit(plusメソッド) ⑤ sawOpcode(new) ⑤ sawOpcode(dup) ⑤ sawOpcode(aload_0) ⑤ sawOpcode(getfield) ⑤ sawOpcode(aload_1) ⑤ sawOpcode(getfield) ⑤ sawOpcode(imul) ⑤ sawOpcode(aload_1) ⑤ sawOpcode(getfield) ⑤ sawOpcode(aload_0) ⑤ sawOpcode(getfield) ⑤ sawOpcode(imul) ⑤ sawOpcode(iadd) ⑤ sawOpcode(aload_0) ⑤ sawOpcode(getfield) ⑤ sawOpcode(aload_1) ⑤ sawOpcode(getfield) ⑤ sawOpcode(imul) ⑤ sawOpcode(invokespecial) ⑤ sawOpcode(areturn) ⑥ visitAnnotation(javax.annotation.Nonnull) ⑦ visitParameterAnnotation(0, javax.annotation.Nonnull) ⑥ visitAnnotation(javax.annotation.concurrent.Immutable) ⑧ visitAfter(com.sciencesakura.example.Fractionクラス)
比較のために解析対象のクラスファイルを javap
コマンドでデコンパイルした結果の抜粋を以下に示す。
public final class com.sciencesakura.example.test.Fraction { public com.sciencesakura.example.test.Fraction(int, int); descriptor: (II)V flags: (0x0001) ACC_PUBLIC Code: stack=3, locals=3, args_size=3 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: iload_2 5: ifne 18 8: new #7 // class java/lang/IllegalArgumentException 11: dup 12: ldc #9 // String denominator must not be zero 14: invokespecial #11 // Method java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V 17: athrow 18: aload_0 19: iload_1 20: putfield #14 // Field numerator:I 23: aload_0 24: iload_2 25: putfield #20 // Field denominator:I 28: return public com.sciencesakura.example.test.Fraction plus(com.sciencesakura.example.test.Fraction); descriptor: (Lcom/sciencesakura/example/Fraction;)Lcom/sciencesakura/example/Fraction; flags: (0x0001) ACC_PUBLIC Code: stack=5, locals=2, args_size=2 0: new #15 // class com/sciencesakura/example/Fraction 3: dup 4: aload_0 5: getfield #14 // Field numerator:I 8: aload_1 9: getfield #20 // Field denominator:I 12: imul 13: aload_1 14: getfield #14 // Field numerator:I 17: aload_0 18: getfield #20 // Field denominator:I 21: imul 22: iadd 23: aload_0 24: getfield #20 // Field denominator:I 27: aload_1 28: getfield #20 // Field denominator:I 31: imul 32: invokespecial #23 // Method "<init>":(II)V 35: areturn RuntimeVisibleAnnotations: 0: #36() javax.annotation.Nonnull RuntimeVisibleParameterAnnotations: parameter 0: 0: #36() javax.annotation.Nonnull } SourceFile: "Fraction.java" RuntimeInvisibleAnnotations: 0: #41() javax.annotation.concurrent.Immutable
見ての通りクラスファイルの構造に従った順番で巡回していることが分かる。 visitAnnotation
, visitParameterAnnotation
を訪れるのがアノテート対象のクラスやメソッドを訪れた後なのは不便だが、クラスファイルの構造がそうなっているので仕方がない。
サンプルコードでは省略したがConstant Poolの各エントリや、アノテーション以外のAttributesのためのvisitメソッドも用意されている。どんなvisitメソッドがあるかは BetterVisitor
を見ると良い。
実用的なDetectorの実装例
ログを吐くだけではつまらないのでもうちょっと実用的なDetectorを実装してみる。
ミュータブルなSpringの管理Beanを検出する
SpringのDIコンテナが管理するBeanはデフォルトでシングルトンとなる。そのためステートフルな管理Beanはあたかもグローバル変数かのように機能する。こういった管理Beanの存在はバグの温床となりやすく、また発生した障害は再現が難しいこともある。
上記の問題を予防するためにミュータブルな管理Beanを検出するDetectorを実装してみる。
実装に必要なのは次の2点:
- クラスファイルが管理Beanかどうかを判定すること
- クラスファイルがミュータブルかどうかを判定すること
一番目はクラスに @Component
系のアノテーションが付いているかでどうかで判定できる *2。
privatestaticfinal List<String> SPRING_BEAN_ANNOTATIONS = List.of( "org/springframework/stereotype/Component", "org/springframework/stereotype/Controller", "org/springframework/stereotype/Repository", "org/springframework/stereotype/Service", "org/springframework/web/bind/annotation/ControllerAdvice", "org/springframework/web/bind/annotation/ExceptionHandler", "org/springframework/web/bind/annotation/RestController", "org/springframework/web/bind/annotation/RestControllerAdvice" ); publicstaticboolean isBean(JavaClass javaClass) { return Stream.of(javaClass.getAnnotationEntries()) .map(a -> ClassName.fromFieldSignature(a.getAnnotationType())) .filter(Objects::nonNull) .anyMatch(SPRING_BEAN_ANNOTATIONS::contains); }
判定結果を shouldVisit(JavaClass)
からreturnしてやれば、管理Beanのときにだけ後続のvisitメソッドが実行されるようになる。
二番目のミュータブルかの判断だが、これをちゃんとやろうとすると難しい。ここでは簡単のために次のいずれかを満たしたときにミュータブルなクラスと看做すことにする。
- 非
private
かつ非final
なインスタンスフィールドがある - インスタンスフィールドに値を代入している(イニシャライザ、コンストラクタを除く)
- インスタンスフィールドに対して
add
,put
,set
で始まるメソッドを実行している(〃)
ではひとつずつ実装していく。
1. 非privateかつ非finalなインスタンスフィールドを検出する
これは簡単。
@Overridepublicvoid visit(Field obj) { if (obj.isStatic() || obj.isFinal() || obj.isPrivate()) { return; } reporter.reportBug(...); }
2.インスタンスフィールドに値を代入しているコードを検出する
ここからはJVMの命令を読む必要がある。
インスタンスフィールドへの代入は putfield
命令で行われる。 putfield
命令はオペコードに続いて代入先のフィールドを示す CONSTANT_Fieldref_info
のインデックスをオペランドに取る命令フォーマットとなっており、 CONSTANT_Fieldref_info
の中には更にフィールドが定義されているクラスを示す CONSTANT_Class_info
のインデックスが含まれている。要件を満たすにはこの CONSTANT_Class_info
が指しているクラスが現在訪問中のクラスと同じクラスかを見ればよい。
現在の命令のオペランドが参照しているConstant Poolのエントリの情報を取得できる便利メソッドが用意されている。 クラスの情報は getClassConstantOperand()
で取得できるのでこれを使ってチェックする。
// sawOpcode(int)から呼び出すprivatevoid detectSettingField() { // フィールドが自クラス・親クラスのものでないなら無視var targetClassName = getClassConstantOperand(); if (!getClassName().equals(targetClassName) && !getSuperclassName().equals(targetClassName)) { return; } reporter.reportBug(...); }
3. インスタンスフィールドに対してadd, put, setで始まるメソッドを実行しているコードを検出する
インスタンスメソッドの呼び出しは invokevirtual
命令、インタフェースのメソッドの呼び出しは invokeinterface
命令で行われる。
これら命令も putfield
と似たように呼び出し先のメソッドを示す CONSTANT_Methodref_info
のインデックスをオペランドに取っており、メソッドの名称は getNameConstantOperand()
で取得できる。
但し今回の判定のためにはメソッド名称のチェックだけでは不十分で、メソッドを実行するオブジェクト( hoge.fuga()
の hoge
のところ)が現在訪問中のクラスのインスタンスフィールドであるかどうかも確認しなくてはならない。これをチェックするには invoke
命令が実行されるときのオペランドスタックの中身を知っている必要がある。
こんなときに役に立つのが BytecodeScanningDetector
を継承した OpcodeStackDetector
である。 OpcodeStackDetector
を継承すると現在のオペランドスタックが分かるようになる。
もうコードを載せてしまおう。
// sawOpcode(int)から呼び出すprivatevoid detectMutateField() { // メソッドを実行するオブジェクトの参照をスタックから取得var objectRef = stack.getStackItem(getNumberArguments(getSigConstantOperand())); // 自クラス・親クラスのインスタンスフィールドでないなら無視var targetField = objectRef.getXField(); if (targetField == null || targetField.isStatic()) { return; } var targetClassName = targetField.getClassName(); if (!getDottedClassName().equals(targetClassName) && !getDottedSuperclassName().equals(targetClassName)) { return; } // メソッド名がadd/put/setで始まるか検出var methodName = getNameConstantOperand(); if (methodName != null&& methodName.matches("^(add|put|set)([A-Z]\\w*)?$")) { reporter.reportBug(...); } }
stack
が OpcodeStackDetector
で定義されているフィールドで、オペランドスタックを表す。
invokevirtual
, invokeinterface
命令を使う際はまずメソッドを実行したいオブジェクト参照をオペランドスタックにpushして、続けてメソッドに渡す引数をその数の分だけpushする。つまりオブジェクト参照はオペランドスタックの先頭から引数の数だけ下にズラしたところにある。 stack.getStackItem(getNumberArguments(getSigConstantOperand()))
としているのはそのためである。
完成形
以上をまとめ、イニシャライザとコンストラクタの場合は処理対象外にする処理も入れると次のようなDetectorができあがる。
package com.sciencesakura.example; import edu.umd.cs.findbugs.BugInstance; import edu.umd.cs.findbugs.BugReporter; import edu.umd.cs.findbugs.bcel.OpcodeStackDetector; import org.apache.bcel.Const; import org.apache.bcel.classfile.Field; import org.apache.bcel.classfile.JavaClass; import org.apache.bcel.classfile.Method; /** * {@code Detector} 実装例: ミュータブルなSpring Beanを検出する. */publicclass DetectMutableSpringBean extends OpcodeStackDetector { privatefinal BugReporter reporter; /** * {@code <init>} , {@code <clinit>} を訪問中かどうか. */privateboolean visitingInit; public DetectMutableSpringBean(BugReporter reporter) { this.reporter = reporter; } /** * Spring Beanであるクラスのみ訪問する. */@Overridepublicboolean shouldVisit(JavaClass obj) { return SpringBeans.isBean(obj); } /** *非privateかつ非finalなインスタンスフィールドを検出する. */@Overridepublicvoid visit(Field obj) { if (obj.isStatic() || obj.isFinal() || obj.isPrivate()) { return; } reporter.reportBug(new BugInstance(this, "X_MUTABLE_SPRING_BEAN", NORMAL_PRIORITY) .addClass(this) .addField(this)); } @Overridepublicvoid visit(Method obj) { var methodName = obj.getName(); visitingInit = Const.CONSTRUCTOR_NAME.equals(methodName) || Const.STATIC_INITIALIZER_NAME.equals(methodName); } @Overridepublicboolean beforeOpcode(int seen) { // <init> , <clinit> なら無視returnsuper.beforeOpcode(seen) && !visitingInit; } @Overridepublicvoid sawOpcode(int seen) { switch (seen) { // インスタンスフィールドへの代入case Const.PUTFIELD -> detectSettingField(); // インスタンスメソッド, インターフェースメソッドの呼び出し case Const.INVOKEVIRTUAL, Const.INVOKEINTERFACE -> detectMutateField(); default -> { } } } /** * インスタンスフィールドに代入を実行しているか検出する. */ private void detectSettingField() { // フィールドが自クラス・親クラスのものでないなら無視 var targetClassName = getClassConstantOperand(); if (!getClassName().equals(targetClassName) && !getSuperclassName().equals(targetClassName)) { return; } reporter.reportBug(new BugInstance(this, "X_MUTABLE_SPRING_BEAN", NORMAL_PRIORITY) .addClassAndMethod(this) .addReferencedField(this) .addSourceLine(this)); } /** * インスタンスフィールドに対して {@code add} , {@code put} , {@code set} で始まるインスタンスメソッドを実行しているか検出する. */ private void detectMutateField() { // メソッドを実行するオブジェクトの参照をスタックから取得 var objectRef = stack.getStackItem(getNumberArguments(getSigConstantOperand())); // 自クラス・親クラスのインスタンスフィールドでないなら無視 var targetField = objectRef.getXField(); if (targetField == null || targetField.isStatic()) { return; } var targetClassName = targetField.getClassName(); if (!getDottedClassName().equals(targetClassName) && !getDottedSuperclassName().equals(targetClassName)) { return; } // メソッド名がadd/put/setで始まるか検出 var methodName = getNameConstantOperand(); if (methodName != null && methodName.matches("^(add|put|set)([A-Z]\\w*)?$")) { reporter.reportBug(new BugInstance(this, "X_MUTABLE_SPRING_BEAN", NORMAL_PRIORITY) .addClassAndMethod(this) .addField(targetField) .addSourceLine(this)); } } }
以上。