【Modern Java】Java18で追加された Code Snippets in Java API Documentation (JEP 413)

f:id:Naotsugu:20200724174249p:plain

blog1.mammb.com

JEP 413: Code Snippets in Java API Documentation

Java API ドキュメントにコード断片を書く場合、以前は以下のように <pre>{@code ... }</pre> として記載することが一般的でした。

例えば Stream の JavaDoc は以下のようになっています。

/**
 * A sequence of elements supporting sequential and parallel aggregate
 * operations.  The following example illustrates an aggregate operation using
 * {@link Stream} and {@link IntStream}:
 *
 * <pre>{@code
 *     int sum = widgets.stream()
 *                      .filter(w -> w.getColor() == RED)
 *                      .mapToInt(w -> w.getWeight())
 *                      .sum();
 * }</pre>
 * ...
 */

Java 18 では、専用の {@snippet ...} タグにてコード断片を記載できるようになりました。

/**
 * {@snippet :
 * int sum = widgets.stream()
 *                  .filter(w -> w.getColor() == RED)
 *                  .mapToInt(w -> w.getWeight())
 *                  .sum();
 *
 * }
 */

出力は以下のようになります。

f:id:Naotsugu:20220408225035p:plain


インラインスニペット

{@snippet : から } の中にコード断片を記載します。

/**
 * The following code shows how to use {@code Optional.isPresent}:
 * {@snippet :
 * if (v.isPresent()) {
 *     System.out.println("v: " + v.get());
 * }
 * }
 */

{@snippet ...} タグでは、<> などの文字をエスケープする必要はなく、&HTMLエンティティを使用する必要はありません。

ただし、/* ... */ のコメントは使用できません。

インデントは {@snippet ...} タグの閉じタグ } の位置が起点となります。

package-info.java の JavaDoc にコードスニペットを書くこともできます。

Java コードだけでなく、例えば、プロパティ値を以下のように記載することもできます。

/**
 * {@snippet lang="properties" :
 *    house.number=42
 *    house.street=Main St.
 *    house.town=AnyTown, USA
 * }
 */


ハイライト

@snippet タグの属性として @highlight を指定することで、コードのハイライトを指定できます。

/**
 * A simple program.
 * {@snippet :
 * class HelloWorld {
 *     public static void main(String... args) {
 *         System.out.println("Hello World!");  // @highlight substring="println"
 *     }
 * }
 * }
 */

f:id:Naotsugu:20220408232035p:plain

正規表現のバウンダリ・マッチャー \b を使ってい以下のようにハイライトを指定することもできます。

/**
 * {@snippet :
 *   public static void main(String... args) {
 *       for (var arg : args) {             // @highlight region regex = "\barg\b"
 *           if (!arg.isBlank()) {
 *               System.out.println(arg);
 *           }
 *       }                                  // @end
 *   }
 *   }
 */

f:id:Naotsugu:20220408232156p:plain


リンク

@link にてコードスニペットにリンクを作成できます。

/**
 * {@snippet :
 *  public static void main(String... args) {
 *    System.out.println("Hello, World!");  // @link substring="System.out" target="System#out"
 *  }
 * }
 */

以下のように System.out の API ドキュメントにリンクを貼ることができます。

f:id:Naotsugu:20220409203410p:plain


テキストの置き換え

@replace で API ドキュメントに出力する内容を置き換えることができます。

/**
 * A simple program.
 * {@snippet :
 * class HelloWorld {
 *   public static void main(String... args) {
 *     System.out.println("Hello World!");  // @replace regex='".*"' replacement="..."
 *   }
 * }
 * }
 */

f:id:Naotsugu:20220409203903p:plain


外部スニペット(External snippets)

コードスニペットを外部ファイルに配備して参照することができます。 外部スニペットとすることで、/* ... */ のコメントも利用することができます。

外部スニペットは、snippet-files サブディレクトリに配備します。

例えば、Main.java から参照する外部スニペットファイル ShowOptional.java は以下のように配備します。

src
└── foo
    ├── Main.java
    ├── doc-files
    │   └── icon.png
    └── snippet-files
        └── ShowOptional.java

ShowOptional.java は以下のように記載し、

package foo;

import java.util.Optional;

public class ShowOptional {
    void show(Optional<String> v) {
        // @start region="example"
        if (v.isPresent()) {
            System.out.println("v: " + v.get());
        }
        // @end
    }
}

Main.java からは以下のように参照できます。

/**
 * The following code shows how to use {@code Optional.isPresent}:
 * {@snippet class="ShowOptional" region="example"}
 */
public class Main {
}

上記のような外部スニペットには、ドキュメント対象のファイルを検索できるよう --source-path の指定が必要です(後述します)。

f:id:Naotsugu:20220409204954p:plain

region を指定しない場合は、外部ファイルの内容が全てそのままスニペットとしてAPIドキュメントに含まれます。

class 属性で指定できる他、file属性で file="ShowOptional.java" のように指定することもできます。

また、Java コードだけではなく、プロパティファイルなども扱えます。

/**
 * Here are the configuration properties:
 * {@snippet file="config.properties" region="house"}
*/
...
# @start region=house
house.number=42
house.street=Main St.
house.town=AnyTown, USA
# @end region=house
...


source-path オプションの設定

前述の外部スニペットを使用するには、javadoc コマンドに--source-path( または -sourcepath) を指定する必要があります。

Gradle 7.4 では Java18 がサポートされていないため、 -sourcepath を以下のように指定する必要があります。

build.gradle.kts

tasks.withType<Javadoc> {
    options {
        this as StandardJavadocDocletOptions
        addStringOption("sourcepath", sourceSets.main.get().java.srcDirs.joinToString(File.pathSeparator))
    }
}

build.gradle の場合は以下のようになるでしょう。

tasks.withType<Javadoc> {
    options.addStringOption('sourcepath', sourceSets.main.allJava.getSourceDirectories().getAsPath())
}


外部スニペットのスニペットパス指定

javadoc コマンドに --snippet-path で特定のディレクトリを指定することで、特定のディレクトリに外部スニペットファイルを配備することができます。

テストコードをAPI ドキュメントにスニペットとして記載するのが良いプラクティスになるでしょう。

以下のようなディレクトリ構成の Gradle プロジェクトの場合、

src
├─  main
│      └──  java
│            └── foo
│                 └── Main.java
└──  test
       └──  java
             └── foo
                  └── MainTest.java

build.gradle.kts に以下のように -sourcepath--snippet-path を指定します。

tasks.withType<Javadoc> { //configureEach is not needed
    options {
        this as StandardJavadocDocletOptions // unsafe cast
        addStringOption("sourcepath", sourceSets.main.get().java.srcDirs.joinToString(File.pathSeparator))
        addStringOption("-snippet-path", sourceSets.test.get().java.srcDirs.joinToString(File.pathSeparator))
    }
}


Main.java では以下のように外部スニペットを指定します。

/**
 * The following code shows how to use {@code Optional.isPresent}:
 * {@snippet file="test/MainTest.java" region="example"}
 */
public class Main {
}

MainTest.java に以下のようにスニペットを用意した場合、

class MainTest {
    void show(Optional<String> v) {
        // @start region="example"
        if (v.isPresent()) {
            System.out.println("v: " + v.get());
        }
        // @end
    }
}

以下のような API ドキュメントが得られます。

f:id:Naotsugu:20220409220114p:plain


ハイブリッドスニペット

インラインスニペットと外部スニペットを合わせて以下のように記載することができます。

/**
 * {@snippet class=ExternalSnippets region=join2 :
 * // join a series of strings
 * var delimiter = ... ;
 * var result = String.join(delimiter, args);
 * }
 */

ハイブリッドスニペットをインライン・スニペットとして処理した結果と、外部スニペットとして処理した結果が一致しない場合は、エラーとなります。

クラスのソースコードを読む人の便宜のために、タグ自体の中にスニペットの内容を含み、またスニペットの内容を含む別のファイルを参照したものです。

スニペットの内容が開発を続けていく中で修正漏れとなることを防ぐ用途で利用することができます。


snippet タグの属性

属性 説明
class スニペットのコンテンツを含むクラスを指定
file スニペットのコンテンツを含むファイルを指定
lang スニペットの言語または形式を指定
region スニペットのリージョンを指定
id スニペットを識別するためのスニペットの識別子


マークアップタグ

  • @start : リージョンの始まり
    • region:リージョン名
  • @end:リージョンの終わり

    • region:リージョン名(省略可能)
  • @highlight:テキストの強調表示

    • substring:強調表示するテキスト
    • regex:強調表示するテキストの正規表現
    • region:強調表示するテキストを検索する領域
    • type:強調表示のタイプ(bold, italic, highlighted)
  • @replace:テキスト置換

    • substring:置換するテキスト
    • regex:置換するテキストの正規表現
    • region:置換するテキストを検索する領域
    • replacement—置換テキスト
  • @link:テキストのリンクを作成

    • substring:リンクに置き換えられるテキスト
    • regex:リンクに置換するテキストの正規表現
    • region:リンクに置換するテキストを検索する領域
    • target:リンクのターゲット
    • type:リンクのタイプ(link がデフォルト)または linkplain


まとめ

Java18で追加された Code Snippets in Java API Documentation について見てみました。

現在は周辺ツールの対応がありませんが、タグをサポートするコンパイラツリーAPIを拡張も導入されているため、将来は外部ツールによるコードスニペットの検証などが行えるようになるものと思われます。