NullPointerExceptionを防ぐためInferをM1 Macで試してみた

これは [Java Advent Calendar 2023 シリーズ1](https://qiita.com/advent-calendar/2023/java) の22日目の記事です。

# 背景

Javaで開発をしていると、Null Pointer Exception(以下NPE)を発生させてしまうことがある。

`@Nullable`アノテーションなどを設定すれば、[Checkstyle](https://checkstyle.sourceforge.io/)等のツールでnullに対するチェックを行うことができるが、`@Nullable`アノテーションを導入していないコードベースには適用できない。

アノテーションを導入していなくてもNPEの発生を解析できるツールを調査したところ、Facebook社が公開している静的解析ツール[Infer](https://fbinfer.com/)を見つけた。


これを手元の環境であるM1 Macで動作させようと思ったが、Getting Started通りに実施しても利用できなかった。

そこで、本記事ではInferを利用したい方向けに、Infer　v1.0.0をM1 Macにて利用できるようにする方法を紹介する。

# Inferとは

InferはOCamlで書かれた静的解析ツールである。Java/C/C++/Objective-Cについて、解析が行える。

サンプルによると、以下のコードでnull dereferenceのエラーを、アノテーションなしに捕捉できる。

```Java
// Hello.java
class Hello {
  int test() {
    String s = null;
    return s.length();
  }
}
```

```
Hello.java:5: error: NULL_DEREFERENCE
  object s last assigned on line 4 could be null and is dereferenced at line 5
```


# Java用InferをDockerで利用できるようにする

InferはOCamlで書かれている。自前でOCamlのビルド環境を整え、ソースからビルドするのは、私にOcamlの経験がないため難しい。そこで、用意されているDockerfileを用いてDockerイメージからInferを利用することにした。

なお、利用するJavaのバージョンは17を想定する。

まずは、リポジトリを取得する。

```bash
git clone --depth 1 https://github.com/facebook/infer/
cd docker/master-java
# Java17でコンパイルさせるため、Dockerfileを修正する
vim Dockerfile
```

以下のようにDockerfileを修正する。

```Dockerfile
FROM debian:bullseye-slim AS compilator

LABEL maintainer "Infer team"

# mkdir the man/man1 directory due to Debian bug #863199
RUN apt-get update && \
    mkdir -p /usr/share/man/man1 && \
    apt-get install --yes --no-install-recommends \
      autoconf \
      automake \
      bzip2 \
      cmake \
      curl \
      g++ \
      gcc \
      git \
      libc6-dev \
      libgmp-dev \
      libmpfr-dev \
      libsqlite3-dev \
      sqlite3 \
      make \
      opam \
      openjdk-17-jdk-headless \ # openjdk-17とする
      patch \
      patchelf \
      pkg-config \
      python3 \
      python3-distutils \
      unzip \
      xz-utils \
      zlib1g-dev && \
    rm -rf /var/lib/apt/lists/*

# Disable sandboxing
# Without this opam fails to compile OCaml for some reason. We don't need sandboxing inside a Docker container anyway.
RUN opam init --reinit --bare --disable-sandboxing --yes --auto-setup

# Download the latest Infer from git
# 2023/12/22現在、最新版はv1.1.0である。これはビルドできるが動作させることはできなかったため、1.0.0を利用する。
RUN cd / && \
    git clone --branch v1.0.0 --depth 1 https://github.com/facebook/infer/

# Build opam deps first, then infer. This way if any step fails we
# don't lose the significant amount of work done in the previous
# steps.
RUN cd /infer && ./build-infer.sh java --only-setup-opam
RUN cd /infer && ./build-infer.sh java

# Generate a release
RUN cd /infer && \
    make install-with-libs \
    BUILD_MODE=opt \
    PATCHELF=patchelf \
    DESTDIR="/infer-release" \
    libdir_relative_to_bindir="../lib"

FROM debian:bullseye-slim AS executor

# ここでは、Inferを実行する環境用のstageであるので、
# Javaのバージョンと、ビルドツールのMavenをインストールする。
RUN apt-get update && apt-get install --yes --no-install-recommends openjdk-17-jdk-headless maven


# Get the infer release
COPY --from=compilator /infer-release/usr/local /infer

# Install infer
ENV PATH /infer/bin:${PATH}

# if called with /infer-host mounted then copy infer there
RUN if test -d /infer-host; then \
      cp -av /infer/. /infer-host; \
    fi
```

Dockerイメージをビルドする。

```bash
docker build -t infer .
```

# Inferの利用: 単一ファイルのコンパイルの場合

`javac` でコンパイルする際にInferを利用する方法を説明する。

ビルドしたDockerイメージを起動する。

```bash
echo $PWD
# path/to/infer/docker/master-java

# Inferが用意しているサンプルコードの入ったフォルダexamplesを、 /infer-examplesとしてマウントする
docker run -it -v $PWD/../../examples:/infer-examples infer /bin/bash
```

起動したDockerコンテナ内で以下のコマンドを実行する。
```bash
# Docker上で複数プロセスを動かせないので-j 1というオプションをつけることで、1プロセスで処理させる
# refer: https://github.com/facebook/infer/issues/1763
infer run -j 1 -- javac Hello.java
```

上記コマンドを実行すると、以下のような出力が得られる。
```
Capturing in javac mode...
Found 1 source file to analyze in /infer-examples/infer-out
1/1 [############################################################] 100% 4.716ms

Hello.java:11: error: Null Dereference
  object `s` last assigned on line 10 could be null and is dereferenced at line 11.
   9.     int test() {
  10.       String s = null;
  11. >     return s.length();
  12.     }
  13.   }


Found 1 issue
          Issue Type(ISSUED_TYPE_ID): #
  Null Dereference(NULL_DEREFERENCE): 1
```

これにより、コードのNull Dereferenceを検出できた。


# Inferの利用: mavenを利用するプロジェクトの場合

以下では、より実践的に、mavenを利用するプロジェクトにおいてInferを利用する方法を説明する。

基本的な動作は同じであり、以下で動く。

```bash
infer capture -- mvn clean compile
infer analyze -j 1
```

注意点として、clean compileとしたほうが良い。

Inferの動作の仕組みは、以下に詳しい。
https://fbinfer.com/docs/infer-workflow


# IDE, CIへの統合

静的解析ツールを利用する場合、IDEやCIへの統合が可能であるかは検討するポイントであると思う。

## IDE(VSCode)について
IDEについて、VSCodeにInferを利用できる拡張機能として以下がある。
https://github.com/MagpieBridge/InferIDE

ただし、2023/12/18 現在、動作させるにはJava8が入っている必要がある。
また、この拡張はInferのDocker Imageを利用するが、そのDocker Imageが参照しているInferのバージョンが古く、最終更新が4年前であった。

## CI(Github Actions)について

CIについて、[Github Actions Marketplaceで検索](https://github.com/marketplace?category=&type=actions&verification=&query=Infer)したが、既存のActionでInferが利用できるものはなかった。

CIでは、Dockerコンテナが利用できれば、既存のActionがなくても少しスクリプトを書けば利用はできる。
しかし、Dockerhubに公式のイメージはなく、自前でイメージをメンテナンスする必要があるなど、一定工数が必要であるように思われた。

# 試した感想

手元のSpring BootのプロジェクトにInferを適用してみたが、正直期待したような、nullになりえる箇所をすべて解析できるわけではなかった。
個人的には、他のlintツールと組み合わせて、それで拾えないものについて利用するのがよいように感じた。


# その他
## 注意: 2023/12/22時点ではbrewでインストールができない

公式ドキュメントの[Getting Started](https://fbinfer.com/docs/getting-started)には、Macの場合Homebrewでインストールできるとある。
しかし、試してみると以下のようなメッセージが出てインストールができない。

```bash
$ brew install infer
...
Error: infer has been disabled because it does not build!
```

GithubのrepoのIssuesからエラーメッセージで検索すると、Homebrew formulaはdisabledになり、特に動きがないとある。
[[brew] Error: infer has been disabled because it does not build! · Issue #1768 · facebook/infer](https://github.com/facebook/infer/issues/1768)