📖

JavaParserによるコード構文解析とシグネチャの言語化を考える

2024/12/03に公開

本記事は、GMOメディア株式会社 Advent Calendar 2024の2日目の記事です。

初めに

今回は、JavaParserを使用してメソッド単位でLLMの学習に必要な情報を抽出することを目的としています。
大規模なJavaコードベースを扱う際、全体像を把握するのは容易ではありません。JavaParserを活用し、コード構造を視覚化することで、LLMによる解説を通じてより深い洞察を得る方法を模索してみました。また、コード理解はプロジェクト成功に不可欠な要素であるため、実用的な活用方法を探求することが今回の取り組みのきっかけとなりました。
(LLMは今回触れません)
https://github.com/tomoya-k31/javaparser-sample

JavaParserとは?

JavaParserは、Javaのソースコードを構文解析するためのライブラリです。解析されたコードは抽象構文木(AST)として表現されます。利用者はVisitorパターンを用いて、このAST内の各ノードにアクセスすることができます。JavaParserは、Eclipse JDTと異なり、純粋にJavaの構文解析を目的としており、軽量で比較的に導入が容易なライブラリです。

個人的に、Visitorパターンはコードを読むのが大変なので好きではありません。

今回の環境とセットアップ

  • JDK Amazon Corretto 21.0.5
  • javaparser 3.26.2
  • Gradle 8.10

Gradle依存関係の追加

implementation 'com.github.javaparser:javaparser-core:3.26.2'
implementation 'com.github.javaparser:javaparser-core-serialization:3.26.2'
implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.26.2'

既存のGradleプロジェクトにタスクを登録

既存プロジェクトのbuild.gradleに以下追記して、「saveResolvedJars」というタスクを登録します。バージョン衝突が解決済みのライブラリ一覧をテキストで生成することができます。

tasks.register('saveResolvedJars') {
    def outputPath = project.hasProperty("outputFile") ? project.property('outputFile') : "${layout.buildDirectory.get()}/reports/resolved-jars.txt"
    doLast {
        def outputFile = file(outputPath)
        outputFile.parentFile.mkdirs()
        configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.each {
            outputFile.append("Library: ${it.moduleVersion.id}, Path: ${it.file.absolutePath}\n")
        }
        println "Resolved JAR paths saved to ${outputFile.absolutePath}"
    }
}
# 実行
./gradlew saveResolvedJars -PoutputFile=/tmp/resolved-jars.txt

# 確認(サンプルです)
head -n 3 /tmp/resolved-jars.txt
Library: org.apache.commons:commons-lang3:3.17.0, Path: /Users/tomoya-k31/.gradle/caches/modules-2/files-2.1/org.apache.commons/commons-lang3/3.17.0/b17d2136f0460dcc0d2016ceefca8723bdf4ee70/commons-lang3-3.17.0.jar
Library: jakarta.json:jakarta.json-api:2.1.3, Path: /Users/tomoya-k31/.gradle/caches/modules-2/files-2.1/jakarta.json/jakarta.json-api/2.1.3/4febd83e1d9d1561d078af460ecd19532383735c/jakarta.json-api-2.1.3.jar
Library: com.google.guava:guava:33.3.0-jre, Path: /Users/tomoya-k31/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/33.3.0-jre/13b4d0924e6023eda04b4c7aa83b32dfedf735a7/guava-33.3.0-jre.jar

※ 指定しているライブラリから推移的依存関係にあるライブラリも全て出力されるため、プロジェクト内から直接使用していないライブラリも出力されます。

JavaParserの基本的な使い方

VoidVisitorAdapter<T>を使用して、対象のJavaコードを再帰的に解析を行います。

実装サンプルの場合は、XxxNodeクラスを生成して抽出した結果を保持して、走破後にJSONファイルで結果を出力しています。

import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;

public class JavaAstVisitor extends VoidVisitorAdapter<XxxNode> {
    @Override
    public void visit(ClassExpr n, XxxNode arg) {
        super.visit(n, arg);
    }

    @Override
    public void visit(ClassOrInterfaceType n, XxxNode arg) {
        super.visit(n, arg);
    }

    @Override
    public void visit(ClassOrInterfaceDeclaration cd, XxxNode arg) {
        // コンストラクト一覧出力
        cd.getConstructors().forEach(constructor -> {
            System.out.println("Constructor: " + constructor.getDeclarationAsString());
        });
        super.visit(cd, arg);
    }
    
    @Override
    public void visit(MethodDeclaration md, XxxNode arg) {
        System.out.println(" - Method:" + md.getDeclarationAsString());
        super.visit(md, arg);
    }
}

import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;

import picocli.CommandLine;

@CommandLine.Command(name = "sample")
public class App implements Runnable {

    private static final JavaAstVisitor VISITOR = new JavaAstVisitor();
    
    @Override
    public void run() {
        CombinedTypeSolver combinedSolver = new CombinedTypeSolver();
        combinedSolver.add(new ReflectionTypeSolver());
        combinedSolver.add(new JarTypeSolver("libs/common.jar"));
        
        final ParserConfiguration parserConfiguration = new ParserConfiguration();
        parserConfiguration.setSymbolResolver(new JavaSymbolSolver(combinedSolver));
        StaticJavaParser.setConfiguration(parserConfiguration);
       
        try {
            CompilationUnit cu = StaticJavaParser.parse(Paths.get("src/main/java/sample/App.java"));
            XxxNode node = new XxxNode(cu)
            cu.accept(VISITOR, node);
        } catch (IOException e) {
            throw new UncheckedIOException("Failed to parse " + path + ": " + e.getMessage(), e);
        }
    }
    
    public static void main(String[] args) {
        System.exit(new CommandLine(new App()).execute(args));
    }
}

TypeSolver の実装クラスの比較

今回はメソッド内から参照している別クラス(標準ライブラリ・外部ライブラリ含む)を型情報として取得したかったため、TypeSolverを定義して利用しています。型の解決方法によって使用用途が異なります。

※ 解決できないクラス情報を参照しようとすると UnsolvedSymbolExceptionになります

特徴 ReflectionTypeSolver JavaParserTypeSolver JarTypeSolver MemoryTypeSolver
対象 JVMクラスパス上のクラス ソースコード上の型 JARファイル内のクラス メモリ上で明示的に登録された型
型解決方式 リフレクション ソースコード解析 JARファイルを解析して型情報を抽出 手動で登録
適用範囲 実行環境で利用可能なクラス プロジェクトのソースコード内のクラス 外部ライブラリや依存関係のクラスを解決 動的に登録された型に限定
典型的な用途 標準ライブラリ、依存ライブラリ ソースコード全体の解析 JARファイルから型情報を抽出して解決する場合 テストや限定的な解析
依存関係 JVMが持つクラス情報 プロジェクト内のソースコード JARファイルの読み込みと解析 開発者が手動で登録

詳細な説明

  • ReflectionTypeSolver:
    • 使用場面: JVMクラスパス上の型を解決したいとき。
    • メリット: 実行時のクラスパスにある型を簡単に解決できる。
    • デメリット: クラスパスに含まれていない型は解決できない。
  • JavaParserTypeSolver:
    • 使用場面: プロジェクトのソースコード内で型を解決したいとき。
    • メリット: ソースコード内のクラスを解析して型解決が可能。
    • デメリット: 大規模なコードベースの場合、パフォーマンスが低下する可能性がある。
  • JarTypeSolver:
    • 使用場面: 外部ライブラリや依存関係に含まれる型を解決したいとき。
    • メリット: JARファイルを解析して型情報を解決でき、外部依存関係を効率よく解決できる。
    • デメリット: JARファイルの解析が必要なため、パフォーマンスへの影響を考慮する必要がある。
  • MemoryTypeSolver:
    • 使用場面: 小規模なプロジェクトやテスト環境で型情報を手動で登録したいとき。
    • メリット: 自由に型を登録でき、簡単なケースで有効。
    • デメリット: 手動で型を登録する必要があるため、手間がかかる。

適用シナリオ

  • プロジェクト全体の型解析: JavaParserTypeSolverReflectionTypeSolver を組み合わせて使用。
  • 外部ライブラリの型解決: JarTypeSolver を使用してJARファイル内の型を解決。
  • テストや特定の型のみを解決: MemoryTypeSolver を使用してカスタムの型を登録。(今回は未使用)

クラス情報を任意のJSONフォーマットに変換(仮)

# ルート構造
- name: string - クラスのフルパス(例: "sample/App.java")
- classes: array - クラスおよび内部クラスの情報を格納する配列
- imports: array - インポートされているパッケージやクラスのリスト
- package: string - パッケージ名

## classes/inner_classes フィールドの構造
- interface: boolean - クラスがインターフェースであるかを示します。
- abstract: boolean - クラスが抽象クラスであるかを示します。
- class: string - クラスの名前。
- inner_classes: array - 内部クラスを含む配列。
- methods: array - クラス内のメソッドを表す配列。

## methods フィールドの構造
- name: string - メソッドの名前(例: "run( )")。
- body: string - メソッドの本体(Javaコードの文字列)。
- modifiers: string - アクセス修飾子(例: "public")。
- references: array - メソッド内で参照されるクラスや変数のリスト。
- signature: string - メソッドの完全なシグネチャ(例: "sample.App.run()")。

シグネチャの言語化(プロンプト)

## Instruction
Explain the purpose and functionality of a given Java method in natural language based on the provided details. The explanation should be clear and concise, targeting developers and stakeholders.

## Role
You are an expert software documentation writer specializing in Java and Spring Framework. Write explanations in a professional and structured manner.

## Input Data
- **Language for explanation:** `{Language}`
- **Framework:** `{Framework}`
- **Class file path:** `{Class file path}`
- **Class name:** `{Class name}`
- **Code inside the method:**
  {Code inside the method}
- **Referenced classes and signatures from this method:
  {Referenced class}.{Called method}: {Explanation of the called signature}

## Output Format
- Overview of the method:
  Provide a brief description of what the method does.
- Purpose:
  Explain the reason or context for using this method.
- Key processing steps:
  Describe the primary logic or flow within the method.
- Interaction with other components:
  Explain how the method interacts with other classes or methods.

## Conditions
- The explanation must be in the specified language ({Language}).
- Avoid excessive technical jargon unless necessary.
- Provide a well-structured explanation for readability.

実行結果の例

[WIP]

まとめ

以上、JavaParserの紹介でした。

今後は、これをGitHub Actionsと連携させ、更新されたファイルのみを処理して更新できる仕組みを構築し、コードレビューやコード検索に活用する予定です。現在計画しているのは、シグネチャ単位でLLMに言語化させ、それを学習させたり、RAGでインデックス化することで、レビューの質を向上できるのではないかという仮説のもと、実験の準備を進めています。

関連リンク

GMOメディアテックブログ

Discussion