% Документация
Документация является важной частью любого программного проекта, и в Rust ей уделяется не меньше внимания, чем самому коду. Давайте поговорим об инструментах Rust, предназначенных для создания документации к проекту.
О rustdoc
Дистрибутив Rust включает в себя инструмент, rustdoc
, который генерирует
документацию. rustdoc
также используется Cargo через cargo doc
.
Документация может быть сгенерирована двумя методами: из исходного кода, и из отдельных файлов в формате Markdown.
Документирование исходного кода
Основной способ документирования проекта на Rust заключается в комментировании исходного кода. Для этой цели вы можете использовать документирующие комментарии:
/// Создаёт новый `Rc<T>`.
///
/// # Examples
///
///
/// use std::rc::Rc;
///
/// let five = Rc::new(5);
/// ```
pub fn new(value: T) -> Rc
Этот код генерирует документацию, которая выглядит [так][rc-new]. В приведенном
коде реализация метода была заменена на обычный комментарий. Первое, на что
следует обратить внимание в этом примере, это на использование `///` вместо
`//`. Символы `///` указывают, что это документирующий комментарий.
[rc-new]: http://doc.rust-lang.org/std/rc/struct.Rc.html#method.new
Документирующие комментарии пишутся на Markdown.
Rust отслеживает такие комментарии, и использует их при создании документации.
При документировании таких вещей, как перечисления, нужно учитывать некоторые
особенности работы `rustdoc`. Такой код работает:
```rust
/// Тип `Option`. Подробнее смотрите [документацию уровня модуля](http://doc.rust-lang.org/).
enum Option<T> {
/// Нет значения
None,
/// Некоторое значение `T`
Some(T),
}
А такой — нет:
/// Тип `Option`. Подробнее смотрите [документацию уровня модуля](http://doc.rust-lang.org/).
enum Option<T> {
None, /// Нет значения
Some(T), /// Некоторое значение `T`
}
Вы получите ошибку:
hello.rs:4:1: 4:2 error: expected ident, found `}`
hello.rs:4 }
^
Эта досадная ошибка заключается в следующем: комментарии документации распространяются на элементы, расположенные за ними, а в данном примере нет элемента, расположенного после последнего комментария.
Написание комментариев документации
Давайте рассмотрим каждую часть приведенного комментария в деталях:
/// Создаёт новый `Rc<T>`.
# fn foo() {}
Первая строка документирующего комментария должна представлять из себя краткую информацию о функциональности. Одно предложение. Только самое основное. Высокоуровневое.
///
/// Подробности создания `Rc<T>`, возможно, описывающие сложности семантики,
/// дополнительные опции, и всё остальное.
///
# fn foo() {}
Наш исходный пример включал только строку с краткой информацией, но если бы у нас было больше информации, о которой следует сказать, мы могли бы добавить эту информацию в новом параграфе.
Специальные разделы
/// # Examples
# fn foo() {}
Далее идут специальные разделы. Они обознаются заголовком, который начинается с
#
. Существуют три вида заголовков, которые обычно используются. Они не
являются каким-либо специальным синтаксисом, на данный момент это просто
соглашение.
/// # Panics
# fn foo() {}
Раздел Panics
. Неустранимые ошибки при неправильном вызове функции (так
называемые ошибки программирования) в Rust, как правило, вызывают панику,
которая, в крайнем случае, убивает весь текущий поток (thread). Если ваша
функция имеет подобное нетривиальное поведение — т.е. обнаруживает/вызывает
панику, то очень важно задокументировать это.
/// # Failures
# fn foo() {}
Раздел Failures
. Если ваша функция или метод возвращает Result<T, E>
, то
хорошим тоном является описание условий, при которых она возвращает Err(E)
.
Это чуть менее важно, чем описание Panics
, потому как неудача кодируется в
системе типов, но это не значит, что стоит пренебрегать данной возможностью.
/// # Safety
# fn foo() {}
Раздел Safety
. Если ваша функция является unsafe
, необходимо пояснить, какие
инварианты вызова должны поддерживаться.
/// # Examples
///
///
/// use std::rc::Rc; /// /// let five = Rc::new(5); /// ```
fn foo() {}
Раздел `Examples`. Включите в этот раздел один или несколько примеров
использования функции или метода, и ваши пользователи будут вам благодарны.
Примеры должны размещаться внутри блоков кода, о которых мы сейчас поговорим.
Этот раздел может иметь более одного подраздела:
```rust
/// # Examples
///
/// Простые образцы типа `&str`:
///
///
/// let v: Vec<&str> = "И была у них курочка Ряба".split(' ').collect();
/// assert_eq!(v, vec!["И", "была", "у", "них", "курочка", "Ряба"]);
/// ///
/// Более сложные образцы с замыканиями:
///
///
/// let v: Vec<&str> = "абв1где2жзи".split(|c: char| c.is_numeric()).collect();
/// assert_eq!(v, vec!["абв", "где", "жзи"]);
/// ```
fn foo() {}
Давайте подробно обсудим блоки кода.
#### Блок кода
Чтобы написать код на Rust в комментарии, используйте символы ```:
```rust
///
/// println!("Привет, мир"); /// ```
fn foo() {}
Если вы хотите написать код на любом другом языке (не на Rust), вы можете
добавить аннотацию:
```rust
/// ```c
/// printf("Hello, world\n");
///
fn foo() {}
Это позволит использовать подсветку синтаксиса, соответствующую тому языку,
который был указан в аннотации. Если же это простой текст, то в аннотации
указывается `text`.
Важно выбрать правильную аннотацию, потому что `rustdoc` использует ее
интересным способом: Rust может выполнять проверку работоспособности примеров на
момент создания документации. Это позволяет избежать устаревания примеров.
Предположим, у вас есть код на C. Если вы опустите аннотацию, указывающую, что
это код на C, то `rustdoc` будет думать, что это код на Rust, поэтому он
пожалуется при попытке создания документации.
## Тесты в документации
Давайте обсудим наш пример документации:
```rust
///
/// println!("Привет, мир"); /// ```
fn foo() {}
Заметьте, что здесь нет нужды в `fn main()` или чём-нибудь подобном. `rustdoc`
автоматически добавит оборачивающий `main()` вокруг вашего кода в нужном месте.
Например:
```rust
///
/// use std::rc::Rc; /// /// let five = Rc::new(5); /// ```
fn foo() {}
В конечном итоге это будет тест:
```rust
fn main() {
use std::rc::Rc;
let five = Rc::new(5);
}
Вот полный алгоритм, который rustdoc
использует для обработки примеров:
- Любые ведущие (leading) атрибуты
#![foo]
остаются без изменений в качестве атрибутов контейнера. - Будут вставлены некоторые общие атрибуты
allow
, в том числе:unused_variables
,unused_assignments
,unused_mut
,unused_attributes
,dead_code
. Небольшие примеры часто приводят к срабатыванию этих анализов. - Если пример не содержит
extern crate
, то будет вставленоextern crate <mycrate>;
. - Наконец, если пример не содержит
fn main
, то оставшаяся часть текста будет обернута вfn main() { your_code }
Хотя иногда этого не достаточно. Например, что насчёт всех этих примеров кода с
///
, о которых мы говорили? Простой текст, обработанный rustdoc
, выглядит
так:
/// Некоторая документация.
# fn foo() {}
А исходный текст на Rust после обработки выглядит так:
/// Некоторая документация.
# fn foo() {}
Да, именно так: вы можете добавлять строки, которые начинаются с #
, и они
будут скрыты в выводе, но при этом будут использоваться во время компиляции
кода. Вы можете использовать это в своих интересах. Если в документирующем
комментарии необходимо обратиться к какой-то функции, то ниже нужно будет
добавить определение этой функции. В то же время, это делается только для того,
чтобы удовлетворить компилятор, поэтому сокрытие ненужных строк в выводе делает
пример более ясным. Вы можете использовать эту технику, чтобы детально объяснять
длинные примеры, сохраняя при этом тестируемость документации. Например, вот
код:
let x = 5;
let y = 6;
println!("{}", x + y);
Ниже приведено отрисованное объяснение этого кода.
Сперва мы устанавливаем x
равным пяти:
let x = 5;
# let y = 6;
# println!("{}", x + y);
Затем мы устанавливаем y
равным шести:
# let x = 5;
let y = 6;
# println!("{}", x + y);
В конце мы печатаем сумму x
и y
:
# let x = 5;
# let y = 6;
println!("{}", x + y);
А вот то же самое объяснение, но в виде простого текста:
Сперва мы устанавливаем
x
равным пяти:let x = 5; # let y = 6; # println!("{}", x + y);
Затем мы устанавливаем
y
равным шести:# let x = 5; let y = 6; # println!("{}", x + y);
В конце мы печатаем сумму
x
иy
:# let x = 5; # let y = 6; println!("{}", x + y);
Повторяя все части примера, вы можете быть уверены, что ваш пример компилируется, а не просто отображает кусочки кода, которые как-то относятся к той или иной части вашего объяснения.
Документирование макросов
Вот пример документирования макроса:
/// Паниковать с данным сообщением, если только выражение не является истиной.
///
/// # Examples
///
///
/// # #[macro_use] extern crate foo;
/// # fn main() {
/// panic_unless!(1 + 1 == 2, "Математика сломалась.");
/// # }
/// ///
///
should_panic
/// # #[macro_use] extern crate foo;
/// # fn main() {
/// panic_unless!(true == false, "Я сломан.");
/// # }
/// ```
[macro_export]
macro_rules! panic_unless { ($condition:expr, $($rest:expr),+) => ({ if ! $condition { panic!($($rest),+); } }); }
fn main() {}
В нем вы можете заметить три вещи. Во-первых, мы должны собственноручно добавить
строку с `extern crate` для того, чтобы мы могли указать атрибут `#[macro_use]`.
Во-вторых, мы также собственноручно должны добавить `main()`. И наконец, разумно
будет использовать `#`, чтобы закомментировать все, что мы добавили в первых
двух пунктах, что бы оно не отображалось в генерируемом выводе.
### Запуск тестов в документации
Для запуска тестов можно использовать одну из двух комманд
```bash
$ rustdoc --test path/to/my/crate/root.rs
# или
$ cargo test
Все верно, cargo test
также выполняет тесты, встроенные в документацию. Тем не
менее, cargo test
не будет тестировать исполняемые контейнеры, только
библиотечные. Это связано с тем, как работает rustdoc
: он компонуется с
библиотекой, которую надо протестировать, но в случае с исполняемым файлом
компоноваться не с чем.
Есть еще несколько полезных аннотаций, которые помогают rustdoc
работать
правильно при тестировании кода:
/// ```ignore
/// fn foo() {
///
fn foo() {}
Аннотация `ignore` указывает Rust, что код должен быть проигнорирован. Почти во
всех случаях это не то, что вам нужно, так как эта директива носит очень общий
характер. Вместо неё лучше использовать аннотацию `text`, если это не код, или
`#`, чтобы получить рабочий пример, отображающий только ту часть, которая вам
нужна.
```rust
/// ```should_panic
/// assert!(false);
///
fn foo() {}
Аннотация `should_panic` указывает `rustdoc`, что код должен компилироваться, но
выполнение теста должно завершиться ошибкой.
```rust
/// ```no_run
/// loop {
/// println!("Привет, мир");
/// }
///
fn foo() {}
Аннотация `no_run` указывает, что код должен компилироваться, но запускать его
на выполнение не требуется. Это важно для таких примеров, которые должны успешно
компилироваться, но выполнение которых оказывается бесконечным циклом! Например:
«Вот как запустить сетевой сервис».
### Документирование модулей
Rust предоставляет ещё один вид документирующих комментариев, `//!`. Этот
комментарий относится не к следующему за ним элементу, а к элементу, который его
включает. Другими словами:
```rust
mod foo {
//! Это документация для модуля `foo`.
//!
//! # Examples
// ...
}
Приведённый пример демонстрирует наиболее распространённое использование //!
:
документирование модуля. Если же модуль расположен в файле foo.rs
, то вы,
открывая его код, часто будете видеть следующее:
//! Модуль использования разных `foo`.
//!
//! Модуль `foo` содержит много полезной функциональности ла-ла-ла
Стиль документирующих комментариев
Изучите RFC 505 для получения полных сведений о соглашениях по стилю и формату документации.
Другая документация
Все эти правила поведения также применимы и в отношении исходных файлов не на
Rust. Так как комментарии пишутся на Markdown, то часто эти файлы имеют
расширение .md
.
Когда вы пишете документацию в файлах Markdown, вам не нужно добавлять префикс
документирующего комментария, ///
. Например:
/// # Examples
///
///
/// use std::rc::Rc; /// /// let five = Rc::new(5); /// ```
fn foo() {}
преобразуется в
~~~markdown
# Examples
use std::rc::Rc;
let five = Rc::new(5);
~~~
когда он находится в файле Markdown. Однако есть один недостаток: файлы Markdown
должны иметь заголовок наподобие этого:
```markdown
% Заголовок
Это пример документации.
Строка, начинающаяся с %
, должна быть самой первой строкой файла.
Атрибуты doc
На более глубоком уровне, комментарии документации — это синтаксический сахар для атрибутов документации:
/// this
# fn foo() {}
#[doc="this"]
# fn bar() {}
Т.е. представленные выше комментарии идентичны, также как и ниже:
//! this
#![doc="/// this"]
Вы не часто будете видеть этот атрибут, используемый для написания документации, но он может быть полезен для изменения некоторых настроек, или при написании макроса.
Ре-экспорт
rustdoc
будет показывать документацию для общедоступного (public) ре-экспорта
в двух местах:
extern crate foo;
pub use foo::bar;
Это создаст документацию для bar
как в документации для контейнера foo
, так
и в документации к вашему контейнеру. То есть в обоих местах будет использована
одна и та же документация.
Такое поведение может быть подавлено с помощью no_inline
:
extern crate foo;
#[doc(no_inline)]
pub use foo::bar;
Управление HTML
Вы можете управлять некоторыми аспектами HTML, который генерирует rustdoc
,
через атрибут #![doc]
:
#![doc(html_logo_url = "http://www.rust-lang.org/logos/rust-logo-128x128-blk-v2.png",
html_favicon_url = "http://www.rust-lang.org/favicon.ico",
html_root_url = "http://doc.rust-lang.org/")];
В этом примере устанавливается несколько различных опций: логотип, иконка и корневой URL.
Опции генерации
rustdoc
также содержит несколько опций командной строки для дальнейшей
настройки:
--html-in-header FILE
: включить содержимое FILE в конец раздела<head>...</head>
.--html-before-content FILE
: включить содержимое FILE сразу после<body>
, перед отображаемым содержимым (в том числе строки поиска).--html-after-content FILE
: включить содержимое FILE после всего отображаемого содержимого.
Замечание по безопасности
Комментарии в документации в формате Markdown помещаются в конечную веб-страницу без обработки. Будьте осторожны с HTML-литералами:
/// <script>alert(document.cookie)</script>
# fn foo() {}