3 スニペットのプログラマーズ・ガイド

JEP 413では、APIドキュメント内のコード例のサポートを改善するJavaDoc機能をJDK 18以降に追加します。このガイドでは、一連の単純な例を使用して、この機能の使用方法について説明します。

はじめに

APIドキュメントの作成者は、短い例や1行の例では{@code ...}、長い例の場合は<pre>{@code ...}</pre>のような構造体を使用して、ドキュメント・コメントにソース・コードのフラグメントを含めることがよくあります。{@snippet ...}タグはこれらの手法にかわるもので、より使いやすく、より強力で柔軟性に優れています。

次の例に示すように、ドキュメントのコメントでは、空白文字とアスタリスクを行の先頭に付けるのが一般的です。

/**
 * The main program.
 *
 * The code calls the following statement:
 * <pre>{@code
 *   System.out.println("Hello, World!");
 * }</pre>
 */
public static void main(String... args) {
   ...
}

後続の例では、スニペット・タグおよび関連ファイルが、枠線付きのインデントされたブロック内に表示されています。簡潔かつ明確にするために、スニペット・タグは、囲んでいるコメントの字体装飾なしで表示されています。(実際の用途でこのような装飾を使用することは必須でも誤りでもありません。)枠線のないブロックは、標準ドックレットによって生成された対応する出力を表示するために使用されています。すべてのスニペットの出力には、左上隅に「クリップボードにコピー」ボタンがあります。

インライン・スニペット

最も単純な形式では、{@snippet ...}を使用して、ソース・コードやその他の形式の構造化テキストなどのテキストの断片を囲むことができます。

{@snippet :
   public static void main(String... args) {
       System.out.println("Hello, World!");
   }
}

これは、生成された出力に次のように表示されます。

   public static void main(String... args) {
       System.out.println("Hello, World!");
   }

いくつかの固有の制限は別にして、スニペットの内容に制限はありません。制限は、ドキュメント・コメント内にスニペットを埋め込んだ結果です。インライン・スニペットの制限は次のとおりです。

  • 囲んでいるコメントが終了するため、コンテンツに文字ペア*/を含めることはできません
  • Unicodeエスケープ・シーケンス(\uNNNN)は、ソース・コードの解析中に解釈されるため、文字の存在と同等のUnicodeエスケープ・シーケンスは区別できません
  • @snippetタグの右中カッコを決定できるように、中カッコ文字({})は"バランス"がとれている必要があります。これは適切にネストされた左中カッコ文字と右中カッコ文字が同じ数だけあることを意味します。

インデント

インライン・スニペットの内容は、最初のコロン(:)の後ろの改行と最後の右中カッコ(})の間にあるテキストです。偶発的な空白は、String.stripIndentの場合と同じ方法でコンテンツから削除されます。つまり、最後の右括弧のインデントを調整することで、生成される出力のインデント量を制御できます。

この例では、スニペット・タグは前の例と同じですが、生成される出力のインデントを排除するために、最後の右中カッコのインデントが増加しています。

{@snippet :
   public static void main(String... args) {
       System.out.println("Hello, World!");
   }
}

これは、生成された出力に次のように表示されます。

public static void main(String... args) {
    System.out.println("Hello, World!");
}

属性

スニペットには、name=valueペアである属性を含めることができます。値は、一重引用符(')文字または二重引用符(")文字で囲むことができます。識別子や数値などの単純な値は引用符で囲む必要がありません。ノート: 属性値ではエスケープ・シーケンスはサポートされていません。

lang属性は、スニペット・テキストの言語を識別し、その言語でサポートされる可能性のある行コメントまたは行末コメントの種類を推測するために使用されます。標準ドックレットでは、javaおよびpropertiesがサポートされる値として認識されます。属性の値は、生成されたHTMLにも渡されます。この属性は、スニペット・テキストの分析に使用できる他のツールで使用できます。

{@snippet lang="java" :
   public static void main(String... args) {
       System.out.println("Hello, World!");
   }
}

多くの場合、スニペットにはJavaソース・コードが含まれますが、これに限定されません。スニペットには、"properties"ファイルに出現する可能性のあるリソースなど、他の形式の構造化テキストを含めることができます。

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

これは、生成された出力に次のように表示されます。

   house.number=42
   house.street=Main St.
   house.town=AnyTown, USA

id属性を使用すると、個別のスニペットに一意の名前を付ける識別子を指定できます。標準ドックレットでは、生成されたHTMLに渡す場合を除き、属性を使用しません。この属性は、スニペット・テキストの分析に使用できる他のツールで使用できます。

{@snippet id="example" :
   public static void main(String... args) {
       System.out.println("Hello, World!");
   }
}

マークアップ・コメント

スニペットには、生成された出力に表示される内容に影響を与えるために使用できるマークアップ・コメントを含めることができます。マークアップ・コメントは、スニペットの宣言言語での行末コメントであり、1つ以上のマークアップ・タグを含みます。マークアップ・タグは通常、@nameargumentsの形式です。ほとんどの引数はname=valueペアです。この場合、値はスニペット・タグattributesと同じ構文を持ちます。

強調表示

スニペットの行の一部またはすべてを強調表示するには、@highlightタグを使用します。強調表示するコンテンツは、substring引数を使用してリテラル文字列として指定することも、regex引数を使用して正規表現で指定することもできます。どちらも指定されていない場合は、行全体が強調表示されます。

次の例では、単純な正規表現を使用して、文字列リテラルの内容を強調表示するように指定します。

{@snippet :
   public static void main(String... args) {
       System.out.println("Hello, World!");      // @highlight regex='".*"'
   }
}

これは、生成された出力に次のように表示されます。

   public static void main(String... args) {
       System.out.println("Hello, World!");
   }

リンク

テキストをAPI宣言にリンクするには、@linkタグを使用します。リンクのターゲットは、ドキュメント・コメントの他の場所にある標準の{@link ...}タグに使用されるものと同じ構文およびメカニズムを使用します。特に、@linkタグで使用できる名前のセットは、ソース・コード内のそのポイントに表示される名前のセットであり、インポートされたタイプとメンバーを含みます。

次の例では、メソッド名printlnがプラットフォーム・ドキュメントの宣言にリンクされています。

PrintStreamの単純な使用は、ソース・ファイルの先頭にあるインポート宣言によって名前がインポートされることを意味します。かわりにクラスの完全修飾名を使用することは、同様に適切であるものの、より冗長です。

スニペットは、生成された出力に次のように表示されます。

   public static void main(String... args) {
       System.out.println("Hello, World!");
   }

テキストの変更

例を提示する際に、省略記号またはその他のトークンを使用して、その位置の特定の詳細が重要でないことを読み手に示すと便利な場合があります。ただし、そのようなトークンはスニペットの宣言言語では無効である可能性があります。この問題を解決するには、スニペットの本文で有効なプレースホルダ値を使用し、マーカー・コメントを使用して、生成された出力のプレースホルダ値を代替テキストで置換するように指定します。

次の例では、プレースホルダ値として空の文字列が使用され、それを省略記号で置き換える必要があることを@replaceタグで指定します。

{@snippet :
   public static void main(String... args) {
       var text = "";                           // @replace substring='""' replacement=" ... "
       System.out.println(text);
   }
}

生成された出力では、空の文字列リテラル""が3つのドット...に置き換えられていることがわかります。

   public static void main(String... args) {
       var text =  ... ;
       System.out.println(text);
   }

正規表現の使用

正規表現の使用は、行または領域内の文字列の特定のインスタンスを識別する必要がある場合に注意が必要です。この状況では、境界マッチャまたはゼロ幅のルックアヘッドまたはルックビハインドとともに正規表現を使用して、目的のインスタンスを選択できます。

次の例では、単語境界を使用して、行の前方にある別の文字列の部分文字列である文字列を分離します。

{@snippet :
    int x2 = x;      // @highlight regex='x\b'
    }

これは、生成された出力に次のように表示されます。

int x2 = x;

次の例では、ゼロ幅のルックアヘッドを使用して、文内のxの2番目のインスタンスを分離します。ルックアヘッドが「1つ以上のスペース」になるのを防ぐために、ルックアヘッドの+をエスケープする必要があります。

{@snippet :
    x = x + 1;      // @highlight regex='x(?= \+)'
    }

これは、生成された出力に次のように表示されます。

x = x + 1;

ゼロ幅のルックビハインドも使用できます。この場合、正規表現は(?!= )xになります。境界マッチャ、ルックアヘッドまたはルックビハインドのどれを使用するかの選択は、スタイルの問題にすぎません。

一般に、正規表現を使用する場合は、生成されるドキュメントを常にチェックして、正規表現が予期されるテキストと一致していること、および出力が意図したとおりであることを確認することをお薦めします。

リージョン

前述の例のマークアップ・コメントは、同じ行の前方のコンテンツにのみ影響しました。ただし、行の範囲、つまりリージョンのコンテンツに影響を与えると便利な場合があります。

リージョンは匿名または名前付きにできます。マークアップ・タグを匿名リージョンに適用するには、リージョンの先頭に配置し、@endタグを使用してリージョンの末尾をマークします。

次の例では、指定したリージョン内に出現するtextという単語がすべて強調表示され、リージョン内の一部のコンテンツが置換されます。

{@snippet :
   public static void main(String... args) {    // @highlight region substring="text" type=highlighted
       var text = "";                           // @replace substring='""' replacement=" ... "
       System.out.println(text);
   }                                            // @end
}

これは、生成された出力に次のように表示されます。

   public static void main(String... args) {
       var text =  ... ;
       System.out.println(text);
   }

リージョンの開始と終了の間の対応を明示的に指定する場合は、region属性で名前を付けることで、名前付きリージョンを使用できます。

次の例は、リージョンの名前(この場合はR1)が明示的に指定されている点を除き、前の例と同じです。この例は小さく単純であり、単独では名前付きリージョンの使用を保証しませんが、メカニズムの説明に役立ちます。

{@snippet :
   public static void main(String... args) {    // @highlight region=R1 substring="text" type=highlighted
       var text = "";                           // @replace substring='""' replacement=" ... "
       System.out.println(text);
   }                                            // @end region=R1
}

リージョンに名前を付けても生成される出力には影響せず、次のように表示されます。

   public static void main(String... args) {
       var text =  ... ;
       System.out.println(text);
   }

リージョンはネストできます。ネストされたリージョンに名前を付ける必要はありませんが、わかりやすくするために名前付きリージョンを使用することもできます。おそらく一般的ではありませんが、リージョンはネストする必要はなく、重複する可能性があります。重複するリージョンでは、名前付きリージョンを使用して、個々のリージョンの開始と終了の関係を確立する必要があります。

外部スニペット

インライン・スニペットを使用するのは常に便利ではなく、常に可能ともかぎりません。1つの例の異なる部分を表示したり、インライン・スニペットでは表現できない/* ... */コメントを含めたりするのが望ましい場合があります(このようなコメントはネストせず、囲まれているコメントは末尾の*/で終了するため)。文字シーケンス*/は、globパターンや正規表現などの文字列リテラルにも出現することがあり、従来のコメントに文字シーケンスを記述しようとする場合と同じ問題があります。これに対処するために、スニペット・タグが外部ファイルのコードを参照する外部スニペットを使用できます。

外部ファイルは、スニペット・タグを含むパッケージのsnippet-filesサブディレクトリ、またはjavadocの実行時に--snippet-pathオプションを使用して指定された完全に独立したディレクトリに配置できます。次の例は、ファイルをレイアウトできる2つの異なる方法を示しています。

最初の例は、srcという名前のディレクトリを示しています。これには、クラスp.Mainのソース、doc-filesサブディレクトリ内のイメージicon.pngsnippet-filesディレクトリ内の外部スニペットのファイルSnippets.javaが含まれます。doc-files/icon.pngの存在は、doc-filesディレクトリとsnippet-filesディレクトリの使用方法が似ていることを示しているだけです。標準ドックレットでこの例の外部スニペットを特定するために、追加のオプションは必要ありません。

  • src
    • p
      • Main.java
      • doc-files
        • icon.png
      • snippet-files
        • Snippets.java

ノート:

一部のビルド・システムでは、snippet-filesが有効なJava識別子ではなく、Javaパッケージ名の一部にできない場合でも、snippet-filesディレクトリ内のファイルを、包含しているパッケージ階層の一部として(間違って)処理する場合があります。このような場合、ローカルのsnippet-filesディレクトリは使用できません。

次の例では、前の例と同様に、ファイルSnippets.javaが別のソース階層に移動されます。javadocを実行する場合は、その階層のルートを--snippet-pathオプションで指定する必要があります。

  • src
    • p
      • Main.java
      • doc-files
        • icon.png
  • snippet-files
    • Snippets.java

基本的な外部スニペット

Javaソース・ファイルのclass属性を使用してクラス名を使用するか、file属性を使用してファイル名で、スニペットの外部ファイルを識別できます。

外部ソース・ファイル内のHelloWorldというクラスを参照する基本的な外部スニペットの簡単な例を次に示します。

{@snippet class=HelloWorld }

スニペット自体を含むクラスと同じパッケージ・ディレクトリをルートとするファイルsnippet-files/HelloWorld.javaの内容を次に示します。

public class HelloWorld {
    /**
     * The ubiquitous "Hello, World!" program.
     */
    public static void main(String... args) {
        System.out.println("Hello, World!");
    }
}

当然のことながら、生成される出力は外部ソース・ファイルに似ています。

public class HelloWorld {
    /**
     * The ubiquitous "Hello, World!" program.
     */
    public static void main(String... args) {
        System.out.println("Hello, World!");
    }
}

外部ファイルの一部の選択

外部ファイルの一部のみを含めるには、名前付きリージョンを定義して使用します。

@snippetタグのregion属性を使用して、含める外部ファイル内のリージョンに名前を付けます。

{@snippet class=ExternalSnippets region=main }

外部ソース・ファイルで、@startおよび@endタグを使用してリージョンを定義します。

...
/*                                // @start region=main
 * Prints "Hello, World!"
 */
System.out.println("Hello, World!");
// @end region=main
...

生成される出力は次のようになります。

/*
 * Prints "Hello, World!"
 */
System.out.println("Hello, World!");

外部ファイルには、異なるスニペットによって参照される複数のリージョンがある場合があります。次に、前の例と同じファイルにある可能性がある別のスニペットの例を示します。これはjoinという名前のリージョンを参照します。

{@snippet class=ExternalSnippets region=join }

次に、外部ソース・ファイル内のリージョンを示します。

...
// join a series of strings       // @start region=join
var result = String.join(" ", args);
// @end region=join
...

生成される出力は次のようになります。

// join a series of strings
var result = String.join(" ", args);

外部ソース・ファイル内でリージョンを混在させ、一致させることができます。一部のリージョンは、スニペット・タグによって参照されるファイルの部分を定義するために使用され、その他のリージョンは、表示されるテキストを強調表示または変更するためにマークアップ・タグとともに使用されます。

前の例のバリエーションを次に示します。表示されるリージョンには、表示されるテキストを変更するためのマークアップ・コメントが含まれています。

@snippetタグは基本的に前と同じです。

{@snippet class=ExternalSnippets region=join2 }

外部ファイルは、タグを組み合せて表示するリージョンをマークし、マークアップ・コメントを使用して表示されるテキストを変更します。

...
// join a series of strings       // @start region=join2
var delimiter = " " ;             // @replace substring='" "' replacement="..."
var result = String.join(delimiter, args);
// @end region=join2
...

生成される出力は次のようになります。

// join a series of strings
var delimiter = ... ;
var result = String.join(delimiter, args);

外部ファイルの種類

外部スニペットは、Javaソース・ファイルに限定されません。HTMLの<pre>要素で表示するのに適した任意の形式の構造化テキストを使用できます。Java以外のファイルを参照する場合、file属性を使用してファイルのパスを指定します。これは、ローカルのsnippet-filesディレクトリまたは--snippet-pathオプションで指定されたパスからの相対である必要があります。

次に、プロパティ・ファイル内のhouseというリージョンを参照する外部スニペットの例を示します。

{@snippet file=external-snippets.properties region=house }

次に、そのプロパティ・ファイルの関連部分を示します。

...
# @start region=house
house.number=42
house.street=Main St.
house.town=AnyTown, USA
# @end region=house
...

生成される出力は次のようになります。

house.number=42
house.street=Main St.
house.town=AnyTown, USA

行末コメントの制限

行末コメントはマークアップ・コメントに使用すると便利ですが、いくつかの制限があります。すべての言語が行末コメントをサポートしているわけではなく、このようなコメントを使用できる場所に制限がある場合があります。たとえば、プロパティ・ファイルは行コメントのみをサポートし、コメント文字は行の最初の空白以外の文字です。また、Javaソース・ファイルでも、テキスト・ブロック内で行末コメントを使用することはできません。

これらの制限を回避するには2つの方法があります。リージョンが1行のみの場合でも、適切なテキストをリージョンで囲み、そのリージョンのコンテンツにマークアップを適用できます。これは、Javaソース・コードのテキスト・ブロックのコンテンツにマークアップ・コメントを適用する方法です。また、この状況では、マークアップ・コメントの特別な構文があります。マークアップ・コメントがコロン(:)で終わる場合、次の行の行末コメントのように処理されます。

次の例では、プロパティ・ファイルで@highlightタグを使用して、次の行の一部のテキストを強調表示します。

{@snippet file=external-snippets.properties region=house2 }
...
# @start region=house2
house.number=42
# @highlight substring="Main St." :
house.street=Main St.
house.town=AnyTown, USA
# @end region=house2
...

生成される出力は次のようになります。

house.number=42
house.street=Main St.
house.town=AnyTown, USA

ハイブリッド・スニペット

外部スニペットは、テスト計画の一部としてコンパイルおよび実行が比較的容易であるため、使用すると便利です。インライン・スニペットは、少なくとも短い例で使用すると便利です。これにより、作成者や開発者が、囲んでいるコメントのコンテキストでスニペットのコンテンツを参照できるようになるためです。

ハイブリッド・スニペットは、利便性においてわずかなコストで、両者の長所を最大限に活用します。ハイブリッド・スニペットは、インライン・スニペットと外部スニペットの組合せです。インライン・スニペットとしては、他のインライン・スニペットと同様にインライン・コンテンツがありますが、外部スニペットとしては、外部ファイルおよび場合によってはそのファイル内のリージョンを指定する属性もあります。

2つのフォームが相互に同期しなくなる可能性を回避するために、標準ドックレットは、スニペット・タグをインライン・スニペットとして処理した結果が外部スニペットで処理した結果と同じであることを検証します。これはAPIの開発中にメンテナンス負荷を発生させる可能性があるため、最初にスニペットをインライン・スニペットまたは外部スニペットのいずれかとして開発し、その後の開発プロセスでスニペットのコードが安定したときにハイブリッド・スニペットに変換することをお薦めします。

次の例では、前述の例の2つ(1つはインライン・スニペット用、もう1つは外部スニペット用)を1つのハイブリッド・スニペットに結合しています。インライン・コンテンツは、外部スニペットのリージョンのコンテンツと厳密に同じではありません。外部スニペットはコンパイル可能なコードになるように@replaceタグを使用しますが、読みやすくするために、インライン・スニペットはかわりに...を直接表示します。

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

生成される出力は次のようになります。

// join a series of strings
var delimiter = ... ;
var result = String.join(delimiter, args);

スニペットのテスト

標準ドックレットは、スニペットをコンパイルまたはテストしません。かわりに、外部ツールおよびライブラリ・コードによるテスト機能をサポートします。

外部スニペットは、スニペットのコンテンツが外部ソース・ファイルに配置され、ソース・ファイルの種類に適した標準ツールを使用してコードをコンパイルして実行できるため、テストが最も簡単です。

インライン・スニペットは、まずスニペットを特定してから、その処理方法を決定する必要があるため、テストが難しくなります。

コンパイラAPIコンパイラ・ツリーAPIの組合せを使用してスニペットを特定し、ソース・ファイルを解析して構文ツリーを取得しできます。さらに、それらのツリーを宣言用にスキャンしてから、スニペットの関連ドキュメント・コメント・ツリーをスキャンできます。要素がソース・ファイルに宣言されていれば、DocTrees.getDocCommentTreeを使用して、elementのドキュメント・ツリー・コメントを特定することもできます。

スニペットを特定した後の処理は、スニペットの種類およびテスト目標によって異なります。langおよびidは、検出された各スニペットの種類および特定のインスタンスの識別に役立ちます。それがJavaソース・コードのスニペットであり、何らかのヒューリスティックがある場合は、javacを使用して解析することで、構文的に正しいコードであることを確認できます。場合によっては、コンパイル単位を形成するために必要に応じてラップします。スニペット・コードの解析以外のことも行うには、通常はより多くのコンテキストが必要になります。これは、スニペットのidから推測される場合があります。たとえば、スニペットをコンパイルして実行もできるテンプレートにスニペットをインジェクトできます。