JEP 413: Code Snippets in Java API Documentation
Фрагменты кода в документации Java API
| Authors | Jonathan Gibbons, Pavel Rappo |
| Ответственный | Pavel Rappo |
| Тип | Feature |
| Область | JDK |
| Статус | Closed / Delivered |
| Выпуск | 18 |
| Компонент | tools / javadoc(tool) |
| Обсуждение | javadoc dash dev at openjdk dot java dot net |
| Рецензенты | Alex Buckley |
| Создан | 2018/04/13 10:54 |
| Обновлён | 2025/05/05 21:06 |
| Задача | 8201533 |
Аннотация
Ввести тег @snippet для Standard Doclet инструмента JavaDoc, чтобы упростить включение примеров исходного кода в документацию API.
Цели
-
Упростить проверку фрагментов исходного кода, предоставив доступ к этим фрагментам через API. Хотя за корректность в конечном счёте отвечает автор, улучшенная поддержка в
javadocи связанных инструментах может облегчить её достижение. -
Сделать возможным современное оформление, например подсветку синтаксиса, а также автоматическое связывание имён с объявлениями.
-
Обеспечить лучшую поддержку создания и редактирования сниппетов в IDE.
Что не является целью
-
Целью не является, чтобы сам инструмент
javadocмог проверять, компилировать или запускать фрагменты исходного кода. Эта задача оставлена внешним инструментам. -
Целью не является предоставление тестов для проверки фрагментов кода в существующей документации API JDK, хотя мы ожидаем, что это будет сделано в рамках параллельной работы.
-
Целью не является поддержка интерактивных примеров кода на данный момент. Хотя мы не исключаем такой поддержки в будущем, любая такая поддержка потребует внешней инфраструктуры, выходящей за рамки этого предложения.
Критерии успеха
- Продемонстрировать возможность заменить большинство, если не все, случаи использования блоков
<pre>{@code ...}</pre>в ключевых модулях JDK базовыми экземплярами нового тега, возможно, с помощью утилиты автоматического преобразования. (Рецензирование и фиксация этих изменений, а также ручное редактирование отдельных примеров для использования более продвинутых возможностей тега, выходят за рамки этой работы.)
Мотивация
Авторы документации API часто включают фрагменты исходного кода в документирующие комментарии. Хотя для небольших фрагментов кода можно использовать один только {@code ...}, нетривиальные фрагменты обычно включаются в документирующие комментарии с помощью такой составной конструкции:
<pre>{@code
lines of source code
}</pre>
Когда инструмент javadoc обрабатывает этот документирующий комментарий, стандартный доклет генерирует HTML, который в точности воспроизводит тело тега {@code ...}, включая отступы, и не проверяет код. Например, исходный код java.util.Stream содержит документирующий комментарий, который показывает использование потока.
У этого подхода есть ряд недостатков.
-
У инструментов нет способа надёжно обнаруживать фрагменты кода, чтобы проверять их корректность. Более того, фрагменты часто неполны: в них есть комментарии-заполнители и многоточия, чтобы читатель сам заполнил пробелы. Поскольку проверить каждый фрагмент невозможно, в них легко возникают ошибки, и на практике они встречаются часто.
-
Фрагменты, оформленные таким образом, нельзя разумным образом показать с подсветкой синтаксиса, которую в наши дни обычно ожидают от фрагментов кода в документации. Нет формального указания на вид содержимого фрагмента, а оно необходимо, если фрагмент нужно проверить или показать с подсветкой синтаксиса.
-
Фрагменты, оформленные таким образом, нельзя редактировать в IDE иначе как обычный текст внутри комментария. Кроме того, не все конструкции кода можно включить в комментарии. Например, нельзя включить традиционные комментарии
/* ... */, поскольку фрагмент целиком находится внутри Java-комментария, а последовательность*/не может быть представлена внутри такого комментария. Это также означает, что внутри фрагментов нельзя использовать последовательность символов*/, которая может быть полезна для glob-шаблонов и регулярных выражений. -
Фрагменты, оформленные таким образом, не могут содержать HTML-разметку, которая могла бы пригодиться для выделения частей текста.
-
Фрагменты, оформленные таким образом, не могут содержать теги документирующих комментариев, которые могли бы пригодиться для связывания имён с определениями в других местах API.
-
Фрагменты, оформленные таким образом, подчиняются негибким правилам в отношении отступов. Они определяются относительно начала строки комментария после удаления всех начальных пробельных символов и звёздочек.
Лучший способ решить все эти проблемы — предоставить новый тег с метаданными, которые позволяют автору неявно или явно указать вид содержимого, чтобы его можно было проверить и представить подходящим образом. Также было бы полезно разрешить размещать фрагменты в отдельных файлах, с которыми автор может работать напрямую в предпочитаемом редакторе.
Описание
Тег @snippet
Мы вводим новый встроенный тег {@snippet ...} для объявления фрагментов кода, которые должны появиться в сгенерированной документации. С его помощью можно объявлять как встроенные сниппеты, в которых фрагмент кода находится внутри самого тега, так и внешние сниппеты, в которых фрагмент кода считывается из отдельного исходного файла.
Дополнительные сведения о сниппете можно задать в виде атрибутов — пар имя=значение, размещаемых после начального имени тега. Имя атрибута всегда является простым идентификатором. Значение атрибута может быть заключено в одинарные или двойные кавычки; escape-символы не поддерживаются. Атрибуты отделяются от имени тега и друг от друга пробельными символами, например пробелом и переводом строки.
Сниппет может задавать атрибут 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>: отображаемый текст всегда начинается сразу после начальных пробелов и звёздочек. В сниппетах отступ в сгенерированном выводе отсчитывается относительно позиции закрывающей фигурной скобки в исходном файле. Это похоже на то, как отступ внутри Text Blocks (текстовые блоки) отсчитывается относительно позиции закрывающих """.
На содержимое встроенных сниппетов наложено два ограничения:
-
Встроенный сниппет не может использовать комментарии
/* ... */, потому что*/завершил бы объемлющий документирующий комментарий. Это ограничение относится ко всему содержимому документирующих комментариев и не является особенностью тега@snippet. -
Содержимое встроенного сниппета может содержать только сбалансированные пары фигурных скобок. Весь встроенный тег завершается первой правой скобкой, которая соответствует открывающей. Это ограничение относится ко всем встроенным тегам и не является особенностью тега
@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 других пакетов. Файлы в дополнительном пути поиска находятся в едином общем пространстве имён, и на них можно ссылаться из любого места документации.
Гибридные сниппеты
Гибридный сниппет является одновременно внутренним и внешним сниппетом. Он содержит содержимое сниппета внутри самого тега — для удобства тех, кто читает исходный код документируемого класса, — и при этом ссылается на отдельный файл, содержащий содержимое сниппета.
Если результат обработки гибридного сниппета как встроенного не совпадает с результатом его обработки как внешнего, это считается ошибкой.
Теги разметки задают области внутри содержимого сниппета. Они также управляют представлением содержимого, например выделяют части текста, изменяют текст или добавляют ссылки на другие места документации. Их можно использовать во внутренних, внешних и гибридных сниппетах.
Теги разметки начинаются с @имя, за которым следуют обязательные аргументы. Они размещаются в комментариях // (или их эквивалентах в других языках или форматах), чтобы не мешать без необходимости телу исходного кода, а также потому, что комментарии /* ... */ нельзя использовать во встроенных сниппетах. Такие комментарии называются комментариями разметки.
В одном комментарии разметки можно разместить несколько тегов разметки. Теги разметки применяются к строке исходного кода, содержащей комментарий, если только комментарий не заканчивается двоеточием (:). В этом случае теги разметки применяются только к следующей строке. Второй вариант синтаксиса может быть полезен, если комментарий разметки особенно длинный или если синтаксический формат содержимого сниппета не допускает комментариев в одной строке с исходным кодом, не являющимся комментарием. Комментарии разметки не попадают в генерируемый результат.
Поскольку некоторые другие системы используют метакомментарии, похожие на комментарии разметки, комментарии, которые начинаются с @ и за которыми следует нераспознанное имя, игнорируются. Если имя распознано, но далее в комментарии разметки есть ошибки, выдаётся сообщение об ошибке. В таких случаях результат, генерируемый из сниппета, не определён.
Области
Области — это диапазоны строк, которым можно дать имя и которые определяют текст, отображаемый сниппетом. Они также задают границы действия таких операций, как выделение или изменение текста.
Начало области отмечается одним из способов:
@start region=имя или- тегом
@highlight,@replaceили@link, в котором указанregionилиregion=имя. Имя можно опустить, если оно не нужно соответствующему тегу@end.
Конец области отмечается тегом @end или @end region=имя. Если имя указано, тег завершает область, начатую с этим именем. Если имя не указано, тег завершает последнюю начатую область, у которой ещё нет соответствующего тега @end.
Нет никаких ограничений на области, создаваемые разными парами соответствующих тегов @start и @end. Области могут даже перекрываться, хотя мы не ожидаем, что такое использование будет распространённым.
Выделение
Чтобы выделить содержимое в строке или в диапазоне строк, используйте @highlight с аргументами, которые задают границы рассматриваемого текста, текст внутри этих границ, который нужно выделить, и тип выделения.
Если указан region или region=имя, границами служит эта область до соответствующего тега @end. В противном случае границами служит только текущая строка.
Чтобы выделить каждое вхождение литеральной строки в заданных границах, укажите строку с помощью substring=строка, где строка может быть идентификатором или текстом в одинарных или двойных кавычках. Чтобы выделить каждое вхождение текста, соответствующего регулярному выражению, в заданных границах, используйте regex=строка. Если ни один из этих атрибутов не указан, выделяется всё содержимое в заданных границах.
Тип выделения можно указать параметром type. Допустимые имена типов: bold, italic и highlighted. Имя типа преобразуется в имя CSS-класса, свойства которого можно определить в системной таблице стилей или переопределить в пользовательской таблице стилей.
Например, вот как с помощью тега @highlight подчеркнуть использование определённого имени метода:
/**
* A simple program.
* {@snippet :
* class HelloWorld {
* public static void main(String... args) {
* System.out.println("Hello World!"); // @highlight substring="println"
* }
* }
* }
*/
В сгенерированной документации это будет выглядеть так:
Простая программа.class HelloWorld {
public static void main(String... args) {
System.out.println("Hello World!");
}
}
Вот как выделить все упоминания переменной в диапазоне строк. Мы используем анонимную область, чтобы задать границы операции, и граничные сопоставители регулярных выражений (\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
* }
* }
*/
В сгенерированной документации это будет выглядеть так:
public static void main(String... args) {
for (var arg : args) {
if (!arg.isBlank()) {
System.out.println(arg);
}
}
}
Изменение отображаемого текста
Часто удобно писать содержимое сниппета как код, к которому могут обращаться и который могут проверять внешние инструменты, но отображать его в форме, которая не компилируется. Например, может быть желательно включить инструкции import для наглядности вместе с кодом, использующим импортированные типы. Или может быть желательно отображать код с многоточием или другим маркером, указывающим, что в этом месте должен быть вставлен дополнительный код. Это можно сделать, заменив части содержимого сниппета некоторым текстом замены.
Чтобы заменить часть текста текстом замены, используйте @replace с аргументами, которые задают границы рассматриваемого текста, текст внутри этих границ, который нужно заменить, и текст замены.
Если указан region или region=имя, границами служит эта область до соответствующего тега @end. В противном случае границами служит только текущая строка.
Чтобы заменить каждое вхождение литеральной строки в заданных границах, укажите строку с помощью substring=строка, где строка может быть идентификатором или текстом в одинарных или двойных кавычках. Чтобы заменить каждое вхождение текста, соответствующего регулярному выражению, в заданных границах, используйте regex=строка. Если ни один из этих атрибутов не указан, заменяется всё содержимое в заданных границах.
Текст замены задаётся параметром replacement. Если заменяемый текст задан регулярным выражением, для подстановки групп, найденных регулярным выражением, можно использовать $номер или $имя, как определено в String::replaceAll.
Например, вот как заменить аргумент вызова println многоточием:
/**
* A simple program.
* {@snippet :
* class HelloWorld {
* public static void main(String... args) {
* System.out.println("Hello World!"); // @replace regex='".*"' replacement="..."
* }
* }
* }
*/
В сгенерированной документации это будет выглядеть так:
Простая программа.class HelloWorld {
public static void main(String... args) {
System.out.println(...);
}
}
Чтобы удалить текст, используйте @replace с пустой строкой замены. Чтобы вставить текст, используйте @replace для замены некоторого ничего не делающего текста, размещённого там, куда должен быть вставлен текст замены. Таким ничего не делающим текстом может быть маркер '//' или пустая инструкция (;).
Ссылки из текста
Чтобы связать текст ссылкой с объявлениями в другом месте API, используйте @link с аргументами, которые задают границы рассматриваемого текста, текст внутри этих границ, который нужно сделать ссылкой, и цель ссылки.
Если указан region или region=имя, границами служит эта область до соответствующего тега @end. В противном случае границами служит только текущая строка.
Чтобы сделать ссылкой каждое вхождение литеральной строки в заданных границах, укажите строку с помощью substring=строка, где строка может быть идентификатором или текстом в одинарных или двойных кавычках. Чтобы сделать ссылкой каждое вхождение текста, соответствующего регулярному выражению, в заданных границах, используйте regex=строка. Если ни один из этих атрибутов не указан, ссылкой становится всё содержимое в заданных границах.
Цель задаётся параметром 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"
* }
* }
* }
*/
В сгенерированной документации это будет выглядеть так:
Простая программа.class HelloWorld {
public static void main(String... args) {
System.out.println(...);
}
}
(Полная цель ссылки будет зависеть от другой информации, доступной при генерации документации.)
Другие типы файлов
В примерах из предыдущих разделов показаны фрагменты исходного кода на Java, но поддерживаются и другие типы файлов, например файлы свойств. Точно так же, как для исходного кода на Java, фрагменты кода в формате файлов свойств можно использовать во встроенных сниппетах, а файлы свойств можно указывать во внешних сниппетах с помощью атрибута file.
Вот внешний сниппет, который включает всё содержимое файла .properties:
/**
* Here are the configuration properties:
* {@snippet file="config.properties"}
*/
В файле свойств комментарии разметки используют стандартный для таких файлов синтаксис комментариев, т. е. строки, начинающиеся с символа решётки (#). Поскольку по умолчанию границами действия некоторых тегов разметки служит текущая строка, а файлы свойств не допускают комментариев в одной строке с содержимым, не являющимся комментарием, может потребоваться форма комментария разметки, оканчивающаяся на :, чтобы комментарий разметки применялся к следующей строке.
Вот сниппет, который определяет несколько свойств и выделяет значение второго свойства:
/**
* 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
Атрибуты — это пары «имя — значение», которые передают аргументы тегам сниппетов и тегам разметки. Значение может быть идентификатором или строкой в одинарных или двойных кавычках. Escape-последовательности в строках не поддерживаются. У некоторых атрибутов значение необязательно и может быть опущено.
Атрибуты тега {@snippet}:
class— класс, содержащий содержимое сниппетаfile— файл, содержащий содержимое сниппетаid— идентификатор сниппета для его обозначения в сгенерированной документацииlang— язык или формат сниппетаregion— имя области содержимого, которую нужно отобразить
Теги разметки, размещаемые в комментариях разметки:
-
start— отмечает начало областиregion— имя области
-
end— отмечает конец областиregion— имя области; для анонимной области может быть опущено
-
highlight— выделяет текст в строке или областиsubstring— литеральный текст, который нужно выделитьregex— регулярное выражение для текста, который нужно выделитьregion— область, задающая границы, в которых ищется текст для выделенияtype— тип выделения, напримерbold,italicилиhighlighted
-
replace— заменяет текст в строке или областиsubstring— литеральный текст, который нужно заменитьregex— регулярное выражение для текста, который нужно заменитьregion— область, задающая границы, в которых ищется текст для выделенияreplacement— текст замены
-
link— делает ссылкой текст в строке или областиsubstring— литеральный текст, который нужно заменитьregex— регулярное выражение для текста, который нужно заменитьregion— область, задающая границы, в которых ищется текст для выделенияtarget— цель ссылки в одной из форм, подходящих для тега {@link ...}type— тип ссылки:link(по умолчанию) илиlinkplain
Проверка сниппетов
Важно иметь возможность проверять содержимое сниппета программно, потому что иначе содержимое — это просто текст, а значит, в нём возможны опечатки и другие человеческие ошибки. Даже если код в сниппете изначально корректен, он может стать некорректным по мере развития языка программирования и API, используемых в сниппете.
Мы расширим Compiler Tree API, чтобы он поддерживал тег @snippet. Так внешние инструменты смогут искать теги snippet в документирующих комментариях, чтобы проверять их содержимое.
Предоставляя такой API, мы не ограничиваем понятие проверки той поддержкой, которая доступна в инструменте javadoc. Цель скорее в том, чтобы поддержать использование существующей тестовой инфраструктуры для тестирования содержимого сниппетов.
Существенное преимущество внешних файлов сниппетов в том, что мы ожидаем, что такие файлы будут компилируемыми в некотором подходящем контексте компиляции. Находить эти файлы и проверять, что они компилируются, например с помощью Java Compiler API, будет задачей тестовой инфраструктуры библиотеки. Та же инфраструктура может также запускать получившиеся class-файлы.
Для встроенных сниппетов, особенно тех, которые не являются полной единицей компиляции, оборачивать фрагмент кода в полную единицу компиляции, чтобы его можно было скомпилировать и, возможно, запустить, будет задачей тестовой инфраструктуры.
Для проверки использования тега snippet в документации API JDK мы надеемся обеспечить поддержку в тестовом фреймворке jtreg.
Генерируемый HTML
HTML, генерируемый для отображения сниппета, намеренно не специфицирован, за исключением того, что сгенерированный элемент всегда будет блочным элементом, например элементом div. Поэтому тег snippet в документирующем комментарии всегда следует использовать в контексте, где допустим flow content (потоковое содержимое), а не там, где разрешён только phrasing content (фразовое содержимое), например в элементе span или a (т. е. якоре).
Сгенерированный HTML для каждого сниппета будет объявлять атрибут id, чтобы на сниппет можно было ссылаться из других мест документации. Значением атрибута id в HTML будет значение атрибута id, объявленного в теге snippet, если он есть, иначе будет использоваться значение по умолчанию.
Альтернативы
-
Подсветку синтаксиса обеспечивают различные сторонние решения на JavaScript. Однако документация API JDK часто содержит примеры с новыми возможностями языка, и такие решения могут поддерживать их с опозданием. Кроме того, такие решения обычно основаны на регулярных выражениях, которые могут быть очень хрупкими, и не могут использовать дополнительные сведения, доступные при генерации документации.
-
Мы рассматривали использование блочных комментариев для задания разметки в содержимом сниппета. Однако блочные комментарии для разметки визуально загромождают исходный код и могут использоваться только во внешних сниппетах.
-
Мы рассматривали использование Text Blocks для обрамления содержимого встроенных сниппетов. Однако это было бы несогласованно с существующими встроенными тегами, принимающими текстовое содержимое, а чтобы следовать полной спецификации Text Blocks, пришлось бы ввести дополнительные правила для escape-последовательностей. Кроме того, это затруднило бы использование Text Blocks в качестве собственно содержимого встроенного сниппета.
Тестирование
Мы будем тестировать эту возможность с помощью стандартной тестовой инфраструктуры для возможностей javadoc, включая тесты jtreg и связанные инструменты для проверки корректности сгенерированной документации. Мы также преобразуем существующие простые блоки <pre>{@code ...}</pre> в простые сниппеты в существующей документации.