これは Java Advent Calendar 2023 シリーズ1 の22日目の記事です。
背景
Javaで開発をしていると、Null Pointer Exception(以下NPE)を発生させてしまうことがある。
@Nullable
アノテーションなどを設定すれば、Checkstyle等のツールでnullに対するチェックを行うことができるが、@Nullable
アノテーションを導入していないコードベースには適用できない。
アノテーションを導入していなくてもNPEの発生を解析できるツールを調査したところ、Facebook社が公開している静的解析ツールInferを見つけた。
これを手元の環境であるM1 Macで動作させようと思ったが、Getting Started通りに実施しても利用できなかった。
そこで、本記事ではInferを利用したい方向けに、Infer v1.0.0をM1 Macにて利用できるようにする方法を紹介する。
Inferとは
InferはOCamlで書かれた静的解析ツールである。Java/C/C++/Objective-Cについて、解析が行える。
サンプルによると、以下のコードでnull dereferenceのエラーを、アノテーションなしに捕捉できる。
// 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を想定する。
まずは、リポジトリを取得する。
git clone --depth 1 https://github.com/facebook/infer/
cd docker/master-java
# Java17でコンパイルさせるため、Dockerfileを修正する
vim 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イメージをビルドする。
docker build -t infer .
Inferの利用: 単一ファイルのコンパイルの場合
javac
でコンパイルする際にInferを利用する方法を説明する。
ビルドしたDockerイメージを起動する。
echo $PWD
# path/to/infer/docker/master-java
# Inferが用意しているサンプルコードの入ったフォルダexamplesを、 /infer-examplesとしてマウントする
docker run -it -v $PWD/../../examples:/infer-examples infer /bin/bash
起動したDockerコンテナ内で以下のコマンドを実行する。
# 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を利用する方法を説明する。
基本的な動作は同じであり、以下で動く。
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で検索したが、既存のActionでInferが利用できるものはなかった。
CIでは、Dockerコンテナが利用できれば、既存のActionがなくても少しスクリプトを書けば利用はできる。
しかし、Dockerhubに公式のイメージはなく、自前でイメージをメンテナンスする必要があるなど、一定工数が必要であるように思われた。
試した感想
手元のSpring BootのプロジェクトにInferを適用してみたが、正直期待したような、nullになりえる箇所をすべて解析できるわけではなかった。
個人的には、他のlintツールと組み合わせて、それで拾えないものについて利用するのがよいように感じた。
その他
注意: 2023/12/22時点ではbrewでインストールができない
公式ドキュメントのGetting Startedには、Macの場合Homebrewでインストールできるとある。
しかし、試してみると以下のようなメッセージが出てインストールができない。
$ 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