Quantcast
Viewing all articles
Browse latest Browse all 7771

SpotBugsで独自のバグ検出ルールを実装する - 桜技録

独自ルールでバグを検出するSpotBugsプラグインの作り方を調べたメモ。

使ったソースコードはこちら。

github.com

プラグインを実装するのに必要なものは次の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.xmlmessages.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メソッドが実行されるようになる。

二番目のミュータブルかの判断だが、これをちゃんとやろうとすると難しい。ここでは簡単のために次のいずれかを満たしたときにミュータブルなクラスと看做すことにする。

  1. privateかつ非 finalインスタンスフィールドがある
  2. インスタンスフィールドに値を代入している(イニシャライザ、コンストラクタを除く)
  3. インスタンスフィールドに対して 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(...);
  }
}

stackOpcodeStackDetectorで定義されているフィールドで、オペランドスタックを表す。

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));
    }
  }
}

以上。

*1:Detectorと同じ役割を持ったDetector2というインターフェースもある。こちらを使った例はGitHubリポジトリを参照のこと。

*2:正確には不十分。これではXML定義やカスタムのアノテーションに対応できない。


Viewing all articles
Browse latest Browse all 7771

Trending Articles