静的解析ツールSpoonをDocker上で動かしてみる

## はじめに

仕事でJavaのソースの困ったことがあり、使ってないクラスやメソッドを整理するため、色々調べていたら静的解析ツールのSpoonがあったので試してみました。

ローカル環境は汚したくないので基本Dockerを使って検証しています。

## Spoonとは

Spoonは、Javaソースコードを解析・変換するためのオープンソースライブラリです。

### 主な特徴

- AST（抽象構文木）の構築: ソースコードを解析し、クラス、メソッド、フィールドなどの情報をツリー構造として取得
- コード変換: 解析したモデルを元に、リファクタリングやコード生成などの処理が可能
- 詳細な情報取得: クラスの継承関係、メソッドが参照するオブジェクトやメソッドなど、コード内部の関係性を把握できる
- ソースコードの変換（メソッドの追加や削除など）
    - 変換時に既存のソースコードの構成（インデントや変数名など）を維持して出力が可能（詳しくは`SniperJavaPrettyPrinter`を調べてもらえればと）

## 準備するファイルと構成

実行するだけの最低限のシンプル構成として以下の構成でファイルを準備します。

```
/spoon-tool-demo
  ├── src
  │   └── main
  │       └── java
  │             │
  │           (package com.example.tools) # 省略
  │             │
  │             └── SpoonToolDemo.java
  │       
  ├── Dockerfile
  └── pom.xml
```

## 用意するファイルの中身

### Dockerfile

```docker
# ----------------------------------------------
# Stage 1: Spoonツールのjarファイルをビルド
# ----------------------------------------------
FROM maven:3.8.5-openjdk-17-slim AS builder
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN mvn clean package

# ----------------------------------------------
# Stage 2: Spoonツールを実行
# ----------------------------------------------
FROM openjdk:17-slim

# 作業ディレクトリ
WORKDIR /app

# 環境変数定義 --------------------------------------------------------------

# 調査対象ソースコードディレクトリパス（相対or絶対） ※Dockerとボリュームマウントする必要あり
ENV SOURCE_DIR=/app/xxxx-project/src/main/java

# 調査対象パッケージ
ENV TARGET_PACKAGE=com.example.xxxx

# Stage 1でビルドしたJarファイルをコピー
COPY --from=builder /build/target/static-analysis-tool-1.0.0.jar app.jar

# jar実行
ENTRYPOINT ["java", "-jar", "app.jar"]
```

### pom.xml

```xml
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example.tools</groupId>
  <artifactId>spoon-tool</artifactId>
  <version>1.0.0</version>
  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <!-- Spoon依存関係 -->
    <dependency>
      <groupId>fr.inria.gforge.spoon</groupId>
      <artifactId>spoon-core</artifactId>
      <version>11.2.0</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <!-- コンパイラプラグイン -->
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.10.1</version>
        <configuration>
          <source>17</source>
          <target>17</target>
        </configuration>
      </plugin>
      <!-- Maven Shade Pluginによる実行可能jarの生成 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.2.4</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <!-- 署名ファイルを除外するフィルター -->
              <filters>
                <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                    <exclude>META-INF/*.SF</exclude>
                    <exclude>META-INF/*.DSA</exclude>
                    <exclude>META-INF/*.RSA</exclude>
                  </excludes>
                </filter>
              </filters>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>com.example.tools.SpoonToolDemo</mainClass>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>
```

### SpoonToolDemo.java

```java
package com.example.tools;

import spoon.Launcher;
import spoon.reflect.CtModel;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtType;
import spoon.reflect.visitor.filter.TypeFilter;
import spoon.support.sniper.SniperJavaPrettyPrinter;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;

public class SpoonToolDemo {

    // ソースディレクトリ（ "～/main/java"のパス ）
    private static String SOURCE_DIR = System.getenv("SOURCE_DIR");

    /**
     * Tool Main
     */
    public static void main(String[] args) {

        try {
            // Spoon の初期化
            Launcher launcher = new Launcher();
            launcher.addInputResource(SOURCE_DIR);
            launcher.getEnvironment().setPrettyPrinterCreator(
                    () -> new SniperJavaPrettyPrinter(launcher.getEnvironment()));
            launcher.getEnvironment().setNoClasspath(true);

            // モデル生成
            launcher.buildModel();
            CtModel model = launcher.getModel();
            
            // Spoonで構築したAST（ソースコードのモデル）内に存在するすべての型を取得
            List<CtType<?>> allTypes = model.getElements(new TypeFilter<CtType<?>>(CtType.class));

            // 全ての型を出力
            List<CtType<?>> allTargetTypes = new ArrayList<>();
            for (CtType<?> type : allTypes) {
                // パッケージなどで絞り込む場合はここに処理追加
                System.out.println(type.getQualifiedName());
            }

            // 全てのメソッドを出力
            List<CtMethod<?>> allTargetMethods = new ArrayList<>();
            for (CtType<?> type : allTargetTypes) {
                Set<CtMethod<?>> methods = type.getMethods();
                for (CtMethod<?> method : methods) {
                    System.out.println(method.getDeclaringType().getQualifiedName() + "#" + method.getSignature());
                }
            }

            System.out.println("すべての型およびメソッド一覧の取得終了： 型" + allTargetTypes.size() + "件, メソッド：" + allTargetMethods.size() + "件");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

## ツールの実行

Windows環境で実行するためには、以下のコマンドを実行します。
（Docker Desktop for Windowsがインストールされている前提です）

```bash
# ビルド
docker build -t spoon-tool-demo .

# 実行　※Windows環境でのパス指定方法※
docker run --rm -v ${PWD}\..\xxxx-project:/app/xxxx-project spoon-tool-demo
```

## 最後に

Docker上でSpoonを動かす環境が準備できたので、このソースを改修してコントローラクラスから参照されているメソッドやサービスクラスなど、参照している情報を解析することができます。

## 参考リンク

https://spoon.gforge.inria.fr/