桜技録

🐈🐈🐈🐈🐘

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` を含むクラスを検出する.
 */
public class DetectHogeClass implements Detector {

  private final BugReporter reporter;

  /**
   * ① {@code BugReporter} 型の引数を受け取るコンストラクタを実装しないといけない.
   */
  public DetectHogeClass(BugReporter reporter) {
    this.reporter = reporter;
  }

  /**
   * ②クラスファイルを解析するメソッド.
   */
  @Override
  public void 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()));
    }
  }

  /**
   * ④すべてのクラスファイルの解析が完了したあとに呼ばれるメソッド.
   */
  @Override
  public void 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) {
      throw new 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} 実装例: デバッグ用の情報を出力する.
 */
public class DebugVisitor extends BytecodeScanningDetector implements NonReportingDetector {

  private static final Logger log = LoggerFactory.getLogger(DebugVisitor.class);

  public DebugVisitor(BugReporter reporter) {
  }

  /**
   * ①クラスファイルの解析スタート.
   */
  @Override
  public void visit(JavaClass obj) {
    log.info("① visit({}クラス)", getDottedClassName());
  }

  /**
   * ②解析対象のクラスファイルか判定.
   */
  @Override
  public boolean shouldVisit(JavaClass obj) {
    log.info("② shouldVisit({}クラス)", getDottedClassName());
    return true;
  }

  /**
   * ③フィールドの解析.
   */
  @Override
  public void visit(Field obj) {
    log.info("  ③ visit({}フィールド)", getFieldName());
  }

  /**
   * ④メソッドの解析.
   */
  @Override
  public void visit(Method obj) {
    log.info("  ④ visit({}メソッド)", getMethodName());
  }

  /**
   * ⑤オペコードの解析.
   */
  @Override
  public void sawOpcode(int seen) {
    log.info("    ⑤ sawOpcode({})", Const.getOpcodeName(seen));
  }

  /**
   * ⑥アノテーション(クラス、フィールド、メソッド)の解析.
   */
  @Override
  public void visitAnnotation(String annotationClass, Map<String, ElementValue> map, boolean runtimeVisible) {
    if (visitingField() || visitingMethod()) {
      // フィールドまたはメソッドのアノテーション
      log.info("    ⑥ visitAnnotation({})", annotationClass);
    } else {
      // クラスのアノテーション
      log.info("  ⑥ visitAnnotation({})", annotationClass);
    }
  }

  /**
   * ⑦アノテーション(パラメータ)の解析.
   */
  @Override
  public void visitParameterAnnotation(int p, String annotationClass, Map<String, ElementValue> map,
    boolean runtimeVisible) {
    log.info("    ⑦ visitParameterAnnotation({}, {})", p, annotationClass);
  }

  /**
   * ⑧クラスファイルの解析終了.
   */
  @Override
  public void visitAfter(JavaClass obj) {
    log.info("⑧ visitAfter({}クラス)", getDottedClassName());
  }
}

実際にクラスファイルを解析してみる。解析対象のクラスのソースコードとログ出力の結果を以下に示す。

package com.sciencesakura.example.test;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;

@Immutable
public final class Fraction {

  private final int numerator;

  private final int denominator;

  public Fraction(int numerator, int denominator) {
    if (denominator == 0) {
      throw new IllegalArgumentException("denominator must not be zero");
    }
    this.numerator = numerator;
    this.denominator = denominator;
  }

  @Nonnull
  public Fraction plus(@Nonnull Fraction other) {
    return new 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

private static final 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"
);

public static boolean 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なインスタンスフィールドを検出する

これは簡単。

@Override
public void 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)から呼び出す
private void 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)から呼び出す
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(...);
  }
}

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を検出する.
 */
public class DetectMutableSpringBean extends OpcodeStackDetector {

  private final BugReporter reporter;

  /**
   * {@code <init>} , {@code <clinit>} を訪問中かどうか.
   */
  private boolean visitingInit;

  public DetectMutableSpringBean(BugReporter reporter) {
    this.reporter = reporter;
  }

  /**
   * Spring Beanであるクラスのみ訪問する.
   */
  @Override
  public boolean shouldVisit(JavaClass obj) {
    return SpringBeans.isBean(obj);
  }

  /**
   * 非privateかつ非finalなインスタンスフィールドを検出する.
   */
  @Override
  public void 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));
  }

  @Override
  public void visit(Method obj) {
    var methodName = obj.getName();
    visitingInit = Const.CONSTRUCTOR_NAME.equals(methodName) || Const.STATIC_INITIALIZER_NAME.equals(methodName);
  }

  @Override
  public boolean beforeOpcode(int seen) {
    // <init> , <clinit> なら無視
    return super.beforeOpcode(seen) && !visitingInit;
  }

  @Override
  public void 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定義やカスタムのアノテーションに対応できない。

bash-completionの補完定義はどこに置く?

bash-completion に補完定義を追加するとき、スクリプトファイルはどこにおくべきか。

自ユーザにのみ適用する場合

選択肢①: $XDG_DATA_HOME/bash-completion/completions/*

ディレクト$XDG_DATA_HOME/bash-completion/completions (デフォルトは ~/.local/share/bash-completion/completions )配下に補完したいコマンドと同じ名前のファイルを置いておけば勝手に読み込んでくれる。

なお completions の親ディレクトリは環境変数 BASH_COMPLETION_USER_DIR でカスタマイズ可能。

例) npm コマンドで補完が効くようにする

npm completion >$XDG_DATA_HOME/bash-completion/completions/npm

まずはコレを使うことを考える。

選択肢②: ~/.bash_completions

選択肢①が遅延ロードされるのに対して、こちらはbash-completionの読み込み時に一緒に読み込まれる。

ファイル名は環境変数 BASH_COMPLETION_USER_FILE でカスタマイズ可能。

システム全体に適用する場合

選択肢③: /usr/share/bash-completion/completions/*

ディレクト/usr/share/bash-completion/completions 配下に補完したいコマンドと同じ名前のファイルを置いておけば勝手に読み込んでくれる。

ディストリビューションによってディレクトリは異なる。

選択肢④: /etc/bash_completion.d/*

選択肢③が遅延ロードされるのに対して、こちらはbash-completionの読み込み時に一緒に読み込まれる。

ディストリビューションによってディレクトリは異なる。

ディレクトリは環境変数 $BASH_COMPLETION_COMPAT_DIR でカスタマイズ可能。

SpotBugsでGeneratedアノテーションがついたクラスやメソッドを除外できるようになる

アノテーション・プロセッサ等のツールで自動生成したコードは静的コード解析の対象から外したいもの。

従来SpotBugsの利用者はパッケージやクラスに関する 除外フィルタ の機能を使ってこれに対応してきた。

<!-- これまでの手法 -->
<FindBugsFilter>
  <Match>
    <Package name="com.example.hoge.gen"/>
  </Match>
  <Match>
    <Class name="~.*Entity"/>
  </Match>
</FindBugsFilter>

しかしこの手法では自動生成する成果物のパッケージやクラス名に一定の制約が生じてしまうのは避けられず、またプロジェクトごとに異なる成果物の名前に応じてフィルタを調整しなくてはいけなかった。

その状況が次のSpotBugs 4.8.0(仮)のリリースで変わる。

github.com

新しく AnnotationMatcher なるものを追加するPRがマージされており、「特定のパターンのアノテーションが付いたクラス、メソッド、フィールドを解析対象外にする」といったルールが設定できるようになった。

具体的にはこんな感じ。

<!-- これからの手法 -->
<FindBugsFilter>
  <Match>
    <Annotation name="~.*\.Generated"/>
  </Match>
</FindBugsFilter>

イマドキのコード生成ツールはFQCNはさまざまであるがなにかしらの Generated アノテーションを付けてコード出力するのが常識だと言っていい。

上記のフィルタならばそれら Generated アノテーション付きのコードを解析対象から除外しつつ前述の問題点はすべて解消できる。

新規プロジェクト用のテンプレートに是非反映させたくなるフィルタではないだろうか。

なお次回リリースについてはこちらで議論されている。特定の日付は出てないけれどリリース手順の確認などがなされておりリリースは近い予感。

github.com

配列ではなくオブジェクトからjq select

{
  "045eb00f-e662-4c06-b1ea-052eeefe6307": {
    "name": "hoge",
    "score": 1078
  },
  "85ee1327-aee9-4645-bf01-e035ddfe7ee6": {
    "name": "fuga",
    "score": 961
  },
  "727c03fa-08d2-4fa3-9143-c64f5fc3d404": {
    "name": "piyo",
    "score": 1609
  }
}

エントリのコレクションを配列ではなく上のようにIDをキーとしたオブジェクトで表現したJSONがある。

ここからある条件に合致したエントリ(オブジェクトのメンバ)をjqで抽出するにはどうするか?

to_entries

to_entries を使うとオブジェクトを配列に変換することができる。

jq to_entries
[
  {
    "key": "045eb00f-e662-4c06-b1ea-052eeefe6307",
    "value": {
      "name": "hoge",
      "score": 1078
    }
  },
  {
    "key": "85ee1327-aee9-4645-bf01-e035ddfe7ee6",
    "value": {
      "name": "fuga",
      "score": 961
    }
  },
  {
    "key": "727c03fa-08d2-4fa3-9143-c64f5fc3d404",
    "value": {
      "name": "piyo",
      "score": 1609
    }
  }
]

オブジェクトのメンバを表すオブジェクト {"key": "キー", "value": "値"} を要素とする配列が手に入る。

配列になってしまえば後は select 関数が使える。

# scoreが1000以上のエントリのみ抽出
jq 'to_entries | map(select(1000 <= .value.score))'
[
  {
    "key": "045eb00f-e662-4c06-b1ea-052eeefe6307",
    "value": {
      "name": "hoge",
      "score": 1078
    }
  },
  {
    "key": "727c03fa-08d2-4fa3-9143-c64f5fc3d404",
    "value": {
      "name": "piyo",
      "score": 1609
    }
  }
]

from_entries

from_entries で逆方向の変換もできる。

# scoreが1000以上のエントリのみ抽出
jq 'to_entries | map(select(1000 <= .value.score)) | from_entries'
{
  "045eb00f-e662-4c06-b1ea-052eeefe6307": {
    "name": "hoge",
    "score": 1078
  },
  "727c03fa-08d2-4fa3-9143-c64f5fc3d404": {
    "name": "piyo",
    "score": 1609
  }
}

macOSでCHMファイルを閲覧する

Windowsでのヘルプでお馴染みCHMMicrosoft Compiled HTML Help)をMacで読みたくなった。

以前は ichm なるツールがあったようだが、公式サイトのドメインが売りに出されており信頼できるバイナリの入手元が不明で、また自前でビルドしようにも手が掛かりそう(リポジトリにはXcode用のプロジェクト・フォルダが転がってるだけ)だったので他を探した。

github.com

こちらを導入してみる。

READMEにある通りSourceForgeにある同名プロジェクトは誰かが勝手に作ったものだそうなのでそちらからバイナリを落としたりしないこと(怖っ)。

Mac用のバイナリは提供されないので自前ビルドする。

必要なもの

ライブラリ

いずれもHomebrewで導入可。

brew install chmlib wxwidgets

今回は最新版であるCHMLIB 0.40とwxWidgets 3.1.5を利用した。

ツール

  • autoconf
  • automake

こちらもHomebrewで導入可。

brew install autoconf automake

ビルド

リポジトリをcloneしたディレクトリ直下で下記を行う。

  1. ./bootstrap を実行
  2. ./configure && make を実行

生成された実行可能ファイル src/xchmmac/Info.plistmac/xCHM.icns を次のように配置するとLaunchpadにxCHMが表示される。

/Application/xCHM.app/Contents
├── Info.plist
├── MacOS
│   └── xchm
└── Resources
    └── xCHM.icns

利用

起動するとウィンドウが立ち上がるのであとはメニュー等を見て操作する。

手元で試した限りではコンテンツが日本語のCHMはもちろん、ファイル名が日本語であっても問題なく閲覧できた。

ノート: 情報科学における論理④

小野寛晰『情報科学における論理』の読書ノート。なるべく練習問題も解いてく。(→前回)

第2章 述語論理(承前)

古典述語論理の形式体系LK

推論規則

古典命題論理での推論規則に加えて次の推論規則を持つ。なお  A は論理式、  \Gamma , \Delta は論理式の列、  t は任意の項、  z は対象変数を表す。

∀ Left (∀L)
 \cfrac { A [ t / x ] , \Gamma \vdash \Delta } { \forall x A , \Gamma \vdash \Delta }
∀ Right (∀R)
 \cfrac { \Gamma \vdash \Delta , A [ z / x ] } { \Gamma \vdash \Delta , \forall x A }
∃ Left (∃L)
 \cfrac { A [ z / x ] , \Gamma \vdash \Delta } { \exists x A , \Gamma \vdash \Delta }
∃ Right (∃R)
 \cfrac { \Gamma \vdash \Delta , A [ t / x ] } { \Gamma \vdash \Delta , \exists x A }

∀Rと∃Lを適用するには、  z が下式で自由な出現を持ってはならない。この条件を変数条件(eigenvariable condition)と呼び、また  z 固有変数(eigenvariable)と呼ぶ。

Cut除去定理

命題論理と同様、述語論理のLKに於いてもcut除去定理が成り立つ。

決定不能

命題論理と異なり、与えられた式が述語論理のLKで証明可能かどうかは決定不能

完全性定理

完全性と健全性

命題論理と同様、述語論理のLKも健全かつ完全。

形式理論

閉じた論理式の集合を形式理論(formal theory)または理論と呼ぶ。

理論  T に含まれる論理式からなる有限列  \Sigma を適当に選んで、式  \Sigma , \Gamma \vdash \Delta がLKで証明可能になるとき、  \Gamma \vdash \Delta  T で証明可能といい、  T \vdash ( \Gamma \vdash \Delta ) で表す。(この本が式の導出記号を  \vdash でなく  \rightarrow で記述しているのはこういうことかー。式を構成する記号か、公理から式を導出可能なこと表す記号か紛らわしいね)

 T \vdash ( \vdash ) のとき、つまり式  \Sigma \vdash が証明可能なとき  T 矛盾する(inconsistent)という。

モデル

言語  \mathscr{ L } 上の理論  T  \mathscr{ L } に対する構造  \mathfrak{ A } について、任意の  P \in T  \mathfrak{ A } \models P であるとき、  \mathfrak{ A }  T のモデルと呼ぶ。

二変数述語記号として  = が与えられているとき、次の論理式の集合を等号の公理と呼ぶ。以降同記号を含む言語上の理論には等号の公理が含まれるものとする。

  •  \forall x ( x = x )
  •  \forall x \forall y ( x = y \rightarrow y = x )
  •  \forall x \forall y \forall z ( ( x = y \land y = z ) \rightarrow x = z )
  •  \forall x_1 \cdots \forall x_m \forall y_1 \cdots \forall y_m ( ( x_1 = y_1 \land \cdots \land x_m = y_m ) \rightarrow f ( x_1 , \cdots , x_m ) = f ( y_1 , \cdots , y_m ) )  (  f  m 変数関数記号)
  •  \forall x_1 \cdots \forall x_m \forall y_1 \cdots \forall y_m ( ( x_1 = y_1 \land \cdots \land x_m = y_m ) \rightarrow ( P ( x_1 , \cdots , x_m ) \rightarrow P ( y_1 , \cdots , y_m ) ) )  (  P  m 変数熟語記号)

例として次の記号からなる言語  \mathscr{ L }_1 を考える。

  • 対象定数:  e
  • 一変数関数記号:  ^{-1}
  • 二変数関数記号:  \cdot
  • 二変数述語記号:  =

等号の公理に加えて次の論理式からなる  \mathscr{ L }_1 上の理論  T_1 があるとする。

  •  \forall x \forall y \forall z ( ( x \cdot y ) \cdot z = x \cdot ( y \cdot z ) )
  •  \forall x ( e \cdot x = x \land x \cdot e = x )
  •  \forall x ( x \cdot x^{ -1 } = e \land x^{ -1 } \cdot x = e )

これは明らかに群の定義である。ある群  G があり、その単位元や演算を  \mathscr{ L }_1 の各記号に対応させる写像  I を取れば、構造  ( G , I )  T_1 のモデルになる。また  T_1 のモデルはひとつの群を自然に定める。従ってしばしば群  G と構造  ( G , I ) を同一視する。

強い形の完全性
  1. 任意の理論  T がモデルを持つならば、  T は無矛盾である。
  2. 任意の理論  T が無矛盾ならば、  T はモデルを持つ。

2番を強い形の完全性定理という。

練習問題

問2.13

f:id:sciencesakura:20211204181436p:plain

問2.14

f:id:sciencesakura:20211204182302p:plain

問2.15

2-a)  \forall x B \equiv \forall y ( B [ y / x ] )

f:id:sciencesakura:20211204182642p:plain  f:id:sciencesakura:20211204182657p:plain

2-b)  \exists x B \equiv \exists y ( B [ y / x ] )

f:id:sciencesakura:20211204183439p:plain  f:id:sciencesakura:20211204183500p:plain

3-a)  A \land \forall x B \equiv \forall ( A \land B )

f:id:sciencesakura:20211204183939p:plain  f:id:sciencesakura:20211204183959p:plain

3-b)  A \lor \exists x B \equiv \exists x ( A \lor B )

f:id:sciencesakura:20211204184238p:plain  f:id:sciencesakura:20211204184255p:plain

4-a)  A \lor \forall x B \equiv \forall x ( A \lor B )

f:id:sciencesakura:20211204184710p:plain  f:id:sciencesakura:20211205031724p:plain

4-b)  A \land \exists x B \equiv \exists x ( A \land B )

f:id:sciencesakura:20211205031816p:plain  f:id:sciencesakura:20211205031846p:plain

5-a)  \forall x B \land \forall x C \equiv \forall x ( B \land C )

f:id:sciencesakura:20211205031951p:plain  f:id:sciencesakura:20211205032022p:plain

5-b)  \exists x B \lor \exists x C \equiv \exists x ( B \lor C )

f:id:sciencesakura:20211205032116p:plain  f:id:sciencesakura:20211205032149p:plain

6-a)  ( \forall x B \lor \forall x C ) \rightarrow \forall x ( B \lor C )

f:id:sciencesakura:20211205032257p:plain

6-b)  \exists x ( B \land C ) \rightarrow ( \exists x B \land \exists x C )

f:id:sciencesakura:20211205032329p:plain

10-a)  \lnot \forall x B \equiv \exists x \lnot B

f:id:sciencesakura:20211205032655p:plain  f:id:sciencesakura:20211205032740p:plain

10-b)  \lnot \exists x B \equiv \forall x \lnot B

f:id:sciencesakura:20211205032832p:plain  f:id:sciencesakura:20211205032858p:plain

11-a)  A \rightarrow \forall x B \equiv \forall x ( A \rightarrow B )

f:id:sciencesakura:20211205034117p:plain  f:id:sciencesakura:20211205034130p:plain

11-b)  A \rightarrow \exists x B \equiv \exists x ( A \rightarrow B )

f:id:sciencesakura:20211205035656p:plain  f:id:sciencesakura:20211205035732p:plain

12-a)  \forall x B \rightarrow A \equiv \exists x ( B \rightarrow A )

f:id:sciencesakura:20211205040801p:plain  f:id:sciencesakura:20211205041237p:plain

12-b)  \exists x B \rightarrow A \equiv \forall x ( B \rightarrow A )

f:id:sciencesakura:20211205042811p:plain  f:id:sciencesakura:20211205043425p:plain

13)  \exists x ( B \rightarrow C ) \equiv \forall x B \rightarrow \exists x C

f:id:sciencesakura:20211205044708p:plain  f:id:sciencesakura:20211205045725p:plain

14)  \forall x ( B \rightarrow C ) \rightarrow ( \forall x B \rightarrow \forall x C )

f:id:sciencesakura:20211205050415p:plain

15)  \forall x ( B \rightarrow C ) \rightarrow ( \exists x B \rightarrow \exists x C )

f:id:sciencesakura:20211205050857p:plain

Oracle 18c XE on CentOS 7.9 on VirtualBoxセットアップ

自分用の作業記録。

  • CentOS 7.9 (2009)
  • Oracle Database Express Edition Release 18.4.0.0.0

仮想マシン構成

  • メモリ: 4096 MB
  • ストレージ: 16 GB VDI (Dynamically allocated)
  • ネットワーク1: NAT
  • ネットワーク2: Host-only Adapter (DHCP disabled)

Oracle XEのインストールに際してインターネットからの依存パッケージのDLが発生する為NATでインターネット・アクセスを可能にする。

ホストマシンからDBへ接続が行えるようHost-only Adapterを追加している。なおホストマシンからは固定IPでアクセスしたいのでDHCPは使わない。

ゲストOSインストール

https://www.centos.org/centos-linux/ からDLしたインストール・メディア(DVDタイプ)をIDEバイスに追加して仮想マシンを起動。GUIインストーラが立ち上がる。

[INSTALLATION SOURCE] 、 [INSTALLATION DESTINATION] はデフォルトのまま。 [SOFTWARE SELECTION] には Minimal Install を指定した。

[NETWORK & HOSTNAME] を開いてアダプタが2つ認識されていることを確認。 enp0s3enp0s8 という名前が付いていた。

enp0s3の接続をONにするとHost-only Adapterのネットワーク・アドレスには乗らないIPアドレスが振られていたのでNATの方のアダプタだと判明。

もう一方のenp0s8は接続がONにできない。 [IPv4 Settings] を開き、 [Method] に Manual を指定、 [Additional static addresses] にHost-only Adapterに適合するゲートウェイサブネットマスクIPアドレスを追加する。これで接続をONにできる。

あとは流れに沿ってインストールを進める。

ホストマシンからの接続確認

インストール後の再起動が済んだらホストマシンからsshでリモート・ログインを試みる。インストーラでネットワーク設定を終わらせておいたので繋がると思ったが失敗。

VirtualBoxのウィンドウからログイン、 ip addr してみる。

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:4e:db:2d brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global noprefixroute dynamic enp0s3
       valid_lft 86388sec preferred_lft 86388sec
    inet6 fe80::a00:27ff:fe4e:db2d/64 scope link
       valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:11:f1:63 brd ff:ff:ff:ff:ff:ff

enp0s8にIPアドレスが振られていない。 nmtui でenp0s8の設定を確認すると何故か [Automatically connect] にチェックが付いていなかった。チェックを付けて systemctl restart NetworkManager する。

ip addr でenp0s8に固定IPが表示されること、ホストマシンからssh接続できることを確認する。

Oracle XEインストール

マニュアルに従って実施。

https://docs.oracle.com/en/database/oracle/oracle-database/18/xeinl/procedure-installing-oracle-database-xe.html

https://docs.oracle.com/en/database/oracle/oracle-database/18/xeinl/setting-oracle-database-xe-environment-variables.html

自動起動の設定

OSが立ち上がったらOracle XEも起動するようにする。

chkconfig --add oracle-xe-18c

ファイアウォールの設定

デフォルトでOracleのリスナが使う1521番ポートへ外部(ホストマシン)から接続ができるようにする。

firewall-cmd --permanent --add-port=1521/tcp
firewall-cmd --reload