10
3

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

Last updated at Posted at 2023-12-21

これは 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

10
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
3