Документация

Документация является важной частью любого программного проекта, и в 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<T> {
    // здесь реализация
}

Этот код генерирует документацию, которая выглядит так. В приведенном коде реализация метода была заменена на обычный комментарий. Первое, на что следует обратить внимание в этом примере, это на использование /// вместо //. Символы /// указывают, что это документирующий комментарий.

Документирующие комментарии пишутся на Markdown.

Rust отслеживает такие комментарии, и использует их при создании документации.

При документировании таких вещей, как перечисления, нужно учитывать некоторые особенности работы rustdoc. Такой код работает:

/// Тип `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. Включите в этот раздел один или несколько примеров использования функции или метода, и ваши пользователи будут вам благодарны. Примеры должны размещаться внутри блоков кода, о которых мы сейчас поговорим. Этот раздел может иметь более одного подраздела:

/// # 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 в комментарии, используйте символы ```:

/// ```
/// println!("Привет, мир");
/// ```
# fn foo() {}

Если вы хотите написать код на любом другом языке (не на Rust), вы можете добавить аннотацию:

/// ```c
/// printf("Hello, world\n");
/// ```
# fn foo() {}

Это позволит использовать подсветку синтаксиса, соответствующую тому языку, который был указан в аннотации. Если же это простой текст, то в аннотации указывается text.

Важно выбрать правильную аннотацию, потому что rustdoc использует ее интересным способом: Rust может выполнять проверку работоспособности примеров на момент создания документации. Это позволяет избежать устаревания примеров. Предположим, у вас есть код на C. Если вы опустите аннотацию, указывающую, что это код на C, то rustdoc будет думать, что это код на Rust, поэтому он пожалуется при попытке создания документации.

Тесты в документации

Давайте обсудим наш пример документации:

/// ```
/// println!("Привет, мир");
/// ```
# fn foo() {}

Заметьте, что здесь нет нужды в fn main() или чём-нибудь подобном. rustdoc автоматически добавит оборачивающий main() вокруг вашего кода в нужном месте. Например:

/// ```
/// use std::rc::Rc;
///
/// let five = Rc::new(5);
/// ```
# fn foo() {}

В конечном итоге это будет тест:

fn main() {
    use std::rc::Rc;
    let five = Rc::new(5);
}

Вот полный алгоритм, который rustdoc использует для обработки примеров:

  1. Любые ведущие (leading) атрибуты #![foo] остаются без изменений в качестве атрибутов контейнера.
  2. Будут вставлены некоторые общие атрибуты allow, в том числе: unused_variables, unused_assignments, unused_mut, unused_attributes, dead_code. Небольшие примеры часто приводят к срабатыванию этих анализов.
  3. Если пример не содержит extern crate, то будет вставлено extern crate <mycrate>;.
  4. Наконец, если пример не содержит 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(). И наконец, разумно будет использовать #, чтобы закомментировать все, что мы добавили в первых двух пунктах, что бы оно не отображалось в генерируемом выводе.

Запуск тестов в документации

Для запуска тестов можно использовать одну из двух комманд

$ rustdoc --test path/to/my/crate/root.rs
# или
$ cargo test

Все верно, cargo test также выполняет тесты, встроенные в документацию. Тем не менее, cargo test не будет тестировать исполняемые контейнеры, только библиотечные. Это связано с тем, как работает rustdoc: он компонуется с библиотекой, которую надо протестировать, но в случае с исполняемым файлом компоноваться не с чем.

Есть еще несколько полезных аннотаций, которые помогают rustdoc работать правильно при тестировании кода:

/// ```ignore
/// fn foo() {
/// ```
# fn foo() {}

Аннотация ignore указывает Rust, что код должен быть проигнорирован. Почти во всех случаях это не то, что вам нужно, так как эта директива носит очень общий характер. Вместо неё лучше использовать аннотацию text, если это не код, или #, чтобы получить рабочий пример, отображающий только ту часть, которая вам нужна.

/// ```should_panic
/// assert!(false);
/// ```
# fn foo() {}

Аннотация should_panic указывает rustdoc, что код должен компилироваться, но выполнение теста должно завершиться ошибкой.

/// ```no_run
/// loop {
///     println!("Привет, мир");
/// }
/// ```
# fn foo() {}

Аннотация no_run указывает, что код должен компилироваться, но запускать его на выполнение не требуется. Это важно для таких примеров, которые должны успешно компилироваться, но выполнение которых оказывается бесконечным циклом! Например: «Вот как запустить сетевой сервис».

Документирование модулей

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() {}

преобразуется в

# Examples

```
use std::rc::Rc;

let five = Rc::new(5);
```

когда он находится в файле 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() {}