跳到主要内容

JEP 413:Java API 文档中的代码片段

概括

引入@snippetJavaDoc 的标准 Doclet 标签,以简化 API 文档中示例源代码的包含。

目标

  • 通过提供对源代码片段的 API 访问,促进源代码片段的验证。尽管正确性最终是作者的责任,但增强的支持javadoc和相关工具可以使其更容易实现。

  • 启用现代样式,例如语法突出显示以及名称与声明的自动链接。

  • 为创建和编辑片段提供更好的 IDE 支持。

非目标

  • 工具本身的目标不是javadoc能够验证、编译或运行任何源代码片段。该任务留给外部工具。

  • 提供测试来验证现有 JDK API 文档中的代码片段并不是我们的目标,尽管我们期望并行的工作能够做到这一点。

  • 目前支持交互式代码示例并不是我们的目标。尽管我们不排除将来提供此类支持,但任何此类支持都将需要外部基础设施,这超出了本提案的范围。

成功指标

  • 展示用新标签的基本实例替换<pre>{@code ...}</pre>关键 JDK 模块中的大部分(如果不是全部)块的能力,也许可以使用自动转换实用程序。 (查看和提交这些更改以及手动编辑选定的示例以使用标签的更高级功能超出了范围。)

动机

API 文档的作者经常在文档注释中包含源代码片段。尽管{@code ...}可以单独用于小代码片段,但重要的片段通常包含在具有以下复合模式的文档注释中:

<pre>{@code
lines of source code
}</pre>

当该javadoc工具在此文档注释上运行时,标准 doclet 将呈现精确反映标签正文的 HTML {@code ...},包括缩进,并且无需验证代码。例如,java.util.Stream 的源代码包含显示流的使用的文档注释。

这种方法存在多种缺点。

  • 工具无法可靠地检测代码片段,以检查其有效性。此外,这些片段通常不完整,带有占位符注释和省略号供读者填写空白。由于无法检查每个片段,很容易出现错误,并且在实践中经常出现。

  • 使用这种模式的片段不能合理地用语法突出显示来呈现,这是当今文档中代码片段的常见期望。没有正式指示片段中的内容类型,如果要验证片段或使用语法突出显示显示片段,则需要正式指示。

  • 使用此模式的片段无法在 IDE 中编辑,除非注释中是纯文本。此外,并非所有代码结构都可以包含在注释中。例如,/* ... */不能包括传统的注释,因为片段作为一个整体呈现在Java注释中,并且*/在这样的注释中不能表示序列。这也意味着字符序列*/不能在片段内部使用,这对于全局模式和正则表达式可能有用。

  • 使用此模式的片段不能包含 HTML 标记,这可能需要突出显示部分文本。

  • 使用此模式的片段不能包含文档注释标签,这可能需要将名称链接到 API 中其他位置的定义。

  • 使用此模式的片段受到有关缩进的不灵活规则的约束。这些是在删除任何前导空格和星号之后相对于注释行的开头定义的。

解决所有这些问题的更好方法是提供一个带有元数据的新标签,允许作者隐式或显式指定内容的类型,以便可以以适当的方式验证和呈现内容。允许将片段放置在单独的文件中也很有用,这些文件可以由作者的首选编辑器直接操作。

描述

标签@snippet

我们引入了一个新的内联标签 ,{@snippet ...}来声明要出现在生成的文档中的代码片段。它可用于声明_内联片段_(其中代码片段包含在标签本身内)和_外部片段_(其中代码片段从单独的源文件读取)。

有关代码片段的其他详细信息可以以_名称__值_对的形式作为_属性_给出,放置在初始标签名称之后。属性名称始终是一个简单的标识符。属性值可以用单引号或双引号字符括起来;不支持转义字符。属性与标签名称以及彼此之间通过空格字符(例如空格和换行符)分隔。=

片段可以指定一个id属性,该属性可用于识别 API 和生成的 HTML 中的片段,并且可用于创建指向片段的链接。在生成的 HTML 中,id 将被放置在生成的代表片段的最外层元素上。

代码片段通常是 ​​Java 源代码,但它们也可能是属性文件的片段、其他语言的源代码或纯文本。片段可以指定lang属性,该属性标识片段中的内容类型。对于内联代码片段,默认值为java。对于外部代码片段,默认值源自包含代码片段内容的文件名的扩展名。

在代码片段中,可以将标记标签放置在行注释中,以识别文本中的区域并指示如何呈现文本。 (我们将在下面看到标记标签的示例,例如@highlight@replace。)

内嵌片段

内联代码片段包含标签本身内代码片段的内容。

以下是内联代码片段的示例:

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

生成的文档中包含的代码片段的内容是冒号 ( ) 后面的换行符:和右大括号 ( }) 之间的文本。 (我们不希望 API 文档中经常出现两个右大括号的视觉歧义;例如,它只出现在 JDK 文档注释中的一小部分源代码片段中。)

不需要对 HTML 实体转义<>、等字符&,也不需要转义文档注释标记。

使用String::stripIndent从内容中去除前导空格。这解决了<pre>{@code ...}</pre>块的一个恼人的缺点,即要显示的文本总是在任何前导空格和星号字符之后立即开始。在代码片段中,生成的输出中的缩进是相对于源文件中右大括号位置的缩进。这类似于文本块中的缩进相对于结束位置的方式"""

内联片段的内容有两个限制:

  1. 内联代码片段不能使用/* ... */注释,因为这*/会终止封闭的文档注释。此限制适用于文档注释中的所有内容;它不是特定于@snippet标签的。

  2. 内联代码片段的内容只能包含平衡的大括号字符对。整个内联标记由与左大括号匹配的第一个右大括号终止。此限制适用于所有内联标签;它不是特定于@snippet标签的。

尽管有这些限制,当示例代码很短、不需要 IDE 中的语言级编辑支持并且不需要与文档中其他地方的其他代码片段共享时,内联代码片段还是很方便的。

外部片段

外部代码片段是指包含代码片段内容的单独文件。

在外部代码片段中,冒号、换行符和后续内容可以省略。

这是与之前相同的示例,作为外部片段:

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

其中包含以下内容ShowOptional.java的文件:

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

标签中的属性{@snippet ...}标识文件以及要显示的文件区域的名称。和@start标签@end定义ShowOptional.java区域的边界。在这种情况下,该区域的内容与前面示例中的内容相同。 (下面提供了有关@start和标签的更多信息。)@end

与内联片段不同,外部片段对其内容没有限制。特别是,它们可能包含/* ... */评论。

外部代码的位置可以使用该class属性通过类名指定,也可以使用该属性通过短相对文件路径指定file。在任何一种情况下,文件都可以放置在包层次结构中,该snippet-files层次结构以包含带有标记的源代码的目录的子目录为根{@snippet ...}。或者,可以将文件放置在由--snippet-path工具选项指定的辅助搜索路径上javadoc。子目录的使用与目前辅助文档文件子目录snippet-files的使用类似。doc-files

外部代码片段的文件可能包含多个区域,在不同的代码片段标签中引用,出现在文档的不同部分。

外部代码片段很有用,因为它们允许将示例代码编写在单独的文件中,这些文件可以在 IDE 中直接编辑,并且可以在多个相关代码片段之间共享。目录中的文件可以在同一包中的片段之间共享,并且与其他包中的目录snippet-files中的片段隔离。snippet-files辅助搜索路径上的文件存在于单个共享名称空间中,并且可以从文档中的任何位置引用。

混合片段

混合代码片段既是内部代码片段又是外部代码片段。它在标签本身中包含片段的内容,以方便任何人阅读所记录的类的源代码,并且它还引用包含片段内容的单独文件。

如果将混合代码片段作为内联代码片段处理的结果与将其作为外部代码片段处理的结果不匹配,则会发生错误。

标记标签

标记定义片段内容内的区域。它们还控制内容的呈现,例如突出显示部分文本、修改文本或链接到文档中的其他位置。它们可用于内部、外部和混合片段。

标记以@name_开头,后跟任何必需的参数。它们被放置在//注释中(或其他语言或格式中的等效内容),以免过度干扰源代码的主体,而且还因为/* ... */注释不能在内联片段中使用。此类注释称为_标记注释

可以将多个标记标签放置在同一个标​​记注释中。标记标签适用于包含注释的源行,除非注释以冒号 ( :) 终止,在这种情况下,标记标签仅适用于下一行。如果标记注释特别长,或者如果片段内容的语法格式不允许注释与非注释源出现在同一行,则后一种语法可能很有用。标记注释不会出现在生成的输出中。

因为其他一些系统使用类似于标记注释的元注释,所以@以无法识别的名称开头的注释将被忽略。如果名称被识别,但标记注释中有后续错误,则报告错误。相对于从代码片段生成的输出,在这种情况下生成的输出是未定义的。

地区

区域是可选命名的行范围,用于标识要由片段显示的文本。它们还定义操作的范围,例如突出显示或修改文本。

区域的开始由以下任一标记

  • @start region=姓名, 或
  • 指定或name_的_@highlight, ,@replace或标签。如果匹配标签不需要该名称,您可以省略该名称。@link``region``region=``@end

区域的末尾由@end@end region=_name_标记。如果给出了名称,则标签结束以该名称开始的区域。如果未给出名称,则标记将结束尚未具有匹配@end标记的最近启动的区域。

@start对于不同的匹配和标签对创建的区域没有限制@end。区域甚至可以重叠,尽管我们预计这种用法不会很常见。

突出显示

要突出显示一行或一系列行中的内容,请使用@highlight后跟参数来指定要考虑的文本范围、该范围内要突出显示的文本以及突出显示的类型。

如果指定了regionregion=名称,则范围是该区域,直到相应的@end标记。否则,范围只是当前行。

要突出显示范围内文字字符串的每个实例,请使用 string 指定字符串,substring=_其中_string_可以_是用单引号或双引号括起来的标识符或文本。要突出显示范围内与正则表达式匹配的每个文本实例,请使用regex=string。如果未指定这两个属性,则突出显示整个范围。

可以使用参数指定突出显示的类型type。有效的类型名称为bolditalichighlighted。类型的名称将转换为 CSS 类名称,其属性可以在系统样式表中定义或在用户定义的样式表中覆盖。

例如,以下是如何使用标签@highlight来强调特定方法名称的使用:

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

这将在生成的文档中显示为:

一个简单的程序。

类 HelloWorld {
公共静态无效主(字符串...参数){
系统.out. println ("你好世界!");
}
}

以下是如何突出显示一系列行中所有提及变量的方法。我们使用匿名区域来设置操作的范围,并使用正则表达式边界匹配器 ( \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
* }
* }
*/

这将在生成的文档中显示为:

公共静态无效主(字符串...参数){
for (var arg : args) {
if (! arg .isBlank()) {
System.out.println( arg );
}
}
}

修改显示的文本

将代码片段的内容编写为可由外部工具访问和验证的代码通常很方便,但以无法编译的形式显示它。例如,可能需要包含import用于说明目的的语句以及使用导入类型的代码。或者,可能需要显示带有省略号或某种其他标记的代码,以指示应在该点插入附加代码。这可以通过用一些替换文本替换片段的部分内容来完成。

要将某些文本替换为替换文本,请使用@replace后跟参数来指定要考虑的文本范围、该范围内要替换的文本以及替换文本。

如果指定了regionregion=名称,则范围是该区域,直到相应的@end标记。否则,范围只是当前行。

要替换范围内文字字符串的每个实例,请使用 string 指定substring=字符串,其中_string_可以是用单引号或双引号括起来的标识符或文本。要替换范围内与正则表达式匹配的每个文本实例,请使用regex=string。如果这两个属性均未指定,则整个范围将被替换。

使用参数指定替换文本replacement。如果使用正则表达式来指定要替换的文本,则可以使用$_数字_或$_名称_来替换正则表达式中找到的组,如String::replaceAll所定义。

例如,以下是如何用println省略号替换调用的参数:

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

这将在生成的文档中显示为:

一个简单的程序。

类 HelloWorld {
公共静态无效主(字符串...参数){
System.out.println( ... );
}
}

要删除文本,请使用@replace空替换字符串。要插入文本,请使用@replace替换一些放置在应插入替换文本的位置的无操作文本。无操作文本可能是“//”标记,或空语句 ( ;)。

链接文本

要将文本链接到 API 中其他位置的声明,请使用@link后跟参数来指定要考虑的文本范围、要链接的该范围内的文本以及链接的目标。

如果指定了regionregion=名称,则范围是该区域,直到相应的@end标记。否则,范围只是当前行。

要链接范围内文字字符串的每个实例,请使用 string 指定字符串,substring=_其中_string_可以_是用单引号或双引号括起来的标识符或文本。要链接范围内与正则表达式匹配的每个文本实例,请使用regex=string。如果这两个属性均未指定,则链接整个范围。

使用参数指定目标target。其值的形式与标准内联{@link ...}标记所使用的形式相同。

例如,以下是如何将文本链接System.out到其声明:

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

这将在生成的文档中显示为:

一个简单的程序。

类 HelloWorld {
公共静态无效主(字符串...参数){
System.out.println ( ... );
}
}

(链接的完整目标将取决于生成文档时可用的其他信息。)

其他类型的文件

前面几节中的示例显示了 Java 源代码片段,但也支持其他类型的文件,例如属性文件。与 Java 源代码完全相同,属性文件格式的代码片段可以在内联代码片段中使用,并且可以使用属性在外部代码片段中指定属性文件file

以下是包含文件全部内容的外部代码片段.properties

/**
* Here are the configuration properties:
* {@snippet file="config.properties"}
*/

在属性文件中,标记注释使用此类文件的标准注释语法,即以井号 ( #) 字符开头的行。因为某些标记标签default的默认作用域是当前行,并且由于属性文件不允许注释与非注释内容放在同一行,所以可能需要使用以 , 结尾的标记注释:形式以便标记注释被视为适用于下一行。

下面是定义一些属性的代码片段,突出显示了第二个属性的值:

/**
* Here are some example properties:
* {@snippet lang=properties :
* local.timezone=PST
* # @highlight regex="[0-9]+" :
* local.zip=94123
* local.area-code=415
* }
*/

如果行尾注释合法的话,效果就好像标记注释被放置在下一行的末尾:

/**
* Here are some example properties:
* {@snippet lang=properties :
* local.timezone=PST
* local.zip=94123 # @highlight regex="[0-9]+"
* local.area-code=415
* }
*/

片段标签参考

属性是为片段标签和标记标签提供参数的名称-值对。值可以是标识符或用单引号或双引号括起来的字符串。不支持字符串中的转义序列。对于某些属性,该值是可选的并且可以省略。

标签的属性{@snippet}

  • class— 包含片段内容的类
  • file— 包含片段内容的文件
  • id— 代码片段的标识符,用于识别生成的文档中的代码片段
  • lang— 代码片段的语言或格式
  • region— 要显示的内容中的区域名称

标记标签,出现在标记注释中:

  • start— 标记区域的开始

    • region— 地区名称
  • end— 标记区域的结束

    • region——地区名称;对于匿名区域可以省略
  • highlight— 突出显示一行或区域内的文本

    • substring— 要突出显示的文字文本
    • regex— 要突出显示的文本的正则表达式
    • region— 定义要在其中查找要突出显示的文本的范围的区域
    • type— 突出显示的类型,例如bolditalichighlighted
  • replace— 替换行或区域内的文本

    • substring— 要替换的文字文本
    • regex— 要替换的文本的正则表达式
    • region— 定义要在其中查找要突出显示的文本的范围的区域
    • replacement— 替换文本
  • link— 行或区域内的链接文本

    • substring— 要替换的文字文本
    • regex— 要替换的文本的正则表达式
    • region— 定义要在其中查找要突出显示的文本的范围的区域
    • target— 链接的目标,以适合 {@link ...} 标记的形式之一表示
    • type— 链接类型:(link默认)或之一linkplain

验证片段

能够以编程方式验证片段的内容非常重要,因为否则内容只是太多文本,因此容易出现拼写错误和其他人为错误。即使代码片段中的代码最初是有效的,但随着代码片段中使用的编程语言和 API 随着时间的推移而发展,它也可能变得无效。

我们将扩展编译器树 API以支持该@snippet标签。这将允许外部工具扫描文档注释中的片段标签,以验证其内容。

通过提供这样的 API,我们不会将验证的概念限制为javadoc工具内可用的支持。相反,目标是支持使用现有的测试基础设施来测试片段的内容。

使用外部代码片段文件的一个显着优点是,我们期望此类文件在某些​​合适的编译上下文中是可编译的。库的测试基础设施将负责定位这些文件并验证它们是否可以编译(也许可以使用 Java 编译器 API)。相同的基础设施也可能运行生成的类文件。

对于内联代码片段,特别是那些不是完整编译单元的代码片段,将由测试基础设施将代码片段包装在完整编译单元中,以便可以编译并可能运行。

为了验证JDK API文档中snippet标签的使用,我们希望在jtreg测试框架中提供支持。

生成的 HTML

生成的用于呈现代码片段的 HTML 故意未指定,只是生成的元素始终是块元素,例如元素div。因此,文档注释中的片段标签应始终在可接受流内容的上下文中使用,而不是在仅允许短语内容的上下文中使用,例如spanor a(即锚点)元素。

为每个片段生成的 HTML 将声明一个id属性,以便该片段可以成为文档中其他位置的链接的目标。idHTML 中的属性值将是id代码片段标记中声明的属性值(如果有),否则将使用默认值。

备择方案

  • 各种第三方 JavaScript 解决方案提供语法突出显示。然而,JDK API文档中经常包含涉及新语言特性的示例,而这些解决方案可能无法及时支持这些示例。此外,此类解决方案通常基于正则表达式,这可能非常脆弱,并且无法利用生成文档时可用的其他知识。

  • 我们考虑使用块注释来指定片段内容中的标记。但是,标记的块注释在视觉上会干扰源代码,并且只能在外部代码片段中使用。

  • 我们考虑使用文本块来包含内联片段的内容。但是,这与接受文本内容的现有内联标签不一致,并且为了遵循文本块的完整规范,它将引入有关转义序列的附加规则。它还会使使用文本块作为内联代码片段中的实际内容变得更加困难。

测试

我们将使用标准的功能测试基础设施来测试此功能javadoc,包括jtreg测试和相关工具来检查生成的文档的正确性。我们还将<pre>{@code ...}</pre>现有文档中现有的简单块转换为简单的片段。