Документация является важной частью любого программного проекта, и в Rust ей уделяется не меньше внимания, чем самому коду. Давайте поговорим об инструментах Rust, предназначенных для создания документации к проекту.
rustdoc
Дистрибутив Rust включает в себя инструмент, rustdoc
, который генерирует
документацию. rustdoc
также используется Cargo через cargo doc
.
Документация может быть сгенерирована двумя методами: из исходного кода, и из отдельных файлов в формате Markdown.
Основной способ документирования проекта на Rust заключается в комментировании исходного кода. Для этой цели вы можете использовать документирующие комментарии:
fn main() { /// Создаёт новый `Rc<T>`. /// /// # Examples /// /// ``` /// use std::rc::Rc; /// /// let five = Rc::new(5); /// ``` pub fn new(value: T) -> Rc<T> { // здесь реализация } }/// Создаёт новый `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), }
А такой — нет:
fn main() { /// Тип `Option`. Подробнее смотрите [документацию уровня модуля](http://doc.rust-lang.org/). enum Option<T> { None, /// Нет значения Some(T), /// Некоторое значение `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 }
^
Эта досадная ошибка заключается в следующем: комментарии документации распространяются на элементы, расположенные за ними, а в данном примере нет элемента, расположенного после последнего комментария.
Давайте рассмотрим каждую часть приведенного комментария в деталях:
fn main() { /// Создаёт новый `Rc<T>`. fn foo() {} }
/// Создаёт новый `Rc<T>`.
Первая строка документирующего комментария должна представлять из себя краткую информацию о функциональности. Одно предложение. Только самое основное. Высокоуровневое.
fn main() { /// /// Подробности создания `Rc<T>`, возможно, описывающие сложности семантики, /// дополнительные опции, и всё остальное. /// fn foo() {} }/// /// Подробности создания `Rc<T>`, возможно, описывающие сложности семантики, /// дополнительные опции, и всё остальное. ///
Наш исходный пример включал только строку с краткой информацией, но если бы у нас было больше информации, о которой следует сказать, мы могли бы добавить эту информацию в новом параграфе.
/// # Examples
Далее идут специальные разделы. Они обознаются заголовком, который начинается с
#
. Существуют три вида заголовков, которые обычно используются. Они не
являются каким-либо специальным синтаксисом, на данный момент это просто
соглашение.
/// # Panics
Раздел Panics
. Неустранимые ошибки при неправильном вызове функции (так
называемые ошибки программирования) в Rust, как правило, вызывают панику,
которая, в крайнем случае, убивает весь текущий поток (thread). Если ваша
функция имеет подобное нетривиальное поведение — т.е. обнаруживает/вызывает
панику, то очень важно задокументировать это.
/// # Failures
Раздел Failures
. Если ваша функция или метод возвращает Result<T, E>
, то
хорошим тоном является описание условий, при которых она возвращает Err(E)
.
Это чуть менее важно, чем описание Panics
, потому как неудача кодируется в
системе типов, но это не значит, что стоит пренебрегать данной возможностью.
/// # Safety
Раздел Safety
. Если ваша функция является unsafe
, необходимо пояснить, какие
инварианты вызова должны поддерживаться.
/// # Examples /// /// ``` /// use std::rc::Rc; /// /// let five = Rc::new(5); /// ```
Раздел 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!["абв", "где", "жзи"]); /// ```
Давайте подробно обсудим блоки кода.
Чтобы написать код на Rust в комментарии, используйте символы ```:
fn main() { /// ``` /// println!("Привет, мир"); /// ``` fn foo() {} }/// ``` /// println!("Привет, мир"); /// ```
Если вы хотите написать код на любом другом языке (не на Rust), вы можете добавить аннотацию:
fn main() { /// ```c /// printf("Hello, world\n"); /// ``` fn foo() {} }/// ```c /// printf("Hello, world\n"); /// ```
Это позволит использовать подсветку синтаксиса, соответствующую тому языку,
который был указан в аннотации. Если же это простой текст, то в аннотации
указывается text
.
Важно выбрать правильную аннотацию, потому что rustdoc
использует ее
интересным способом: Rust может выполнять проверку работоспособности примеров на
момент создания документации. Это позволяет избежать устаревания примеров.
Предположим, у вас есть код на C. Если вы опустите аннотацию, указывающую, что
это код на C, то rustdoc
будет думать, что это код на Rust, поэтому он
пожалуется при попытке создания документации.
Давайте обсудим наш пример документации:
fn main() { /// ``` /// println!("Привет, мир"); /// ``` fn foo() {} }/// ``` /// println!("Привет, мир"); /// ```
Заметьте, что здесь нет нужды в fn main()
или чём-нибудь подобном. rustdoc
автоматически добавит оборачивающий main()
вокруг вашего кода в нужном месте.
Например:
/// ``` /// use std::rc::Rc; /// /// let five = Rc::new(5); /// ```
В конечном итоге это будет тест:
fn main() { use std::rc::Rc; let five = Rc::new(5); }fn main() { use std::rc::Rc; let five = Rc::new(5); }
Вот полный алгоритм, который rustdoc
использует для обработки примеров:
#![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 main() { /// Некоторая документация. fn foo() {} }
/// Некоторая документация.
Да, именно так: вы можете добавлять строки, которые начинаются с #
, и они
будут скрыты в выводе, но при этом будут использоваться во время компиляции
кода. Вы можете использовать это в своих интересах. Если в документирующем
комментарии необходимо обратиться к какой-то функции, то ниже нужно будет
добавить определение этой функции. В то же время, это делается только для того,
чтобы удовлетворить компилятор, поэтому сокрытие ненужных строк в выводе делает
пример более ясным. Вы можете использовать эту технику, чтобы детально объяснять
длинные примеры, сохраняя при этом тестируемость документации. Например, вот
код:
let x = 5; let y = 6; println!("{}", x + y);
Ниже приведено отрисованное объяснение этого кода.
Сперва мы устанавливаем x
равным пяти:
let x = 5;
Затем мы устанавливаем y
равным шести:
let y = 6;
В конце мы печатаем сумму x
и y
:
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() {}/// Паниковать с данным сообщением, если только выражение не является истиной. /// /// # 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),+); } }); }
В нем вы можете заметить три вещи. Во-первых, мы должны собственноручно добавить
строку с extern crate
для того, чтобы мы могли указать атрибут #[macro_use]
.
Во-вторых, мы также собственноручно должны добавить main()
. И наконец, разумно
будет использовать #
, чтобы закомментировать все, что мы добавили в первых
двух пунктах, что бы оно не отображалось в генерируемом выводе.
Для запуска тестов можно использовать одну из двух комманд
$ rustdoc --test path/to/my/crate/root.rs
# или
$ cargo test
Все верно, cargo test
также выполняет тесты, встроенные в документацию. Тем не
менее, cargo test
не будет тестировать исполняемые контейнеры, только
библиотечные. Это связано с тем, как работает rustdoc
: он компонуется с
библиотекой, которую надо протестировать, но в случае с исполняемым файлом
компоноваться не с чем.
Есть еще несколько полезных аннотаций, которые помогают rustdoc
работать
правильно при тестировании кода:
/// ```ignore /// fn foo() { /// ```
Аннотация ignore
указывает Rust, что код должен быть проигнорирован. Почти во
всех случаях это не то, что вам нужно, так как эта директива носит очень общий
характер. Вместо неё лучше использовать аннотацию text
, если это не код, или
#
, чтобы получить рабочий пример, отображающий только ту часть, которая вам
нужна.
/// ```should_panic /// assert!(false); /// ```
Аннотация should_panic
указывает rustdoc
, что код должен компилироваться, но
выполнение теста должно завершиться ошибкой.
/// ```no_run /// loop { /// println!("Привет, мир"); /// } /// ```
Аннотация 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); /// ```
преобразуется в
# Examples
```
use std::rc::Rc;
let five = Rc::new(5);
```
когда он находится в файле Markdown. Однако есть один недостаток: файлы Markdown должны иметь заголовок наподобие этого:
% Заголовок
Это пример документации.
Строка, начинающаяся с %
, должна быть самой первой строкой файла.
doc
На более глубоком уровне, комментарии документации — это синтаксический сахар для атрибутов документации:
fn main() { /// this fn foo() {} #[doc="this"] fn bar() {} }/// this #[doc="this"]
Т.е. представленные выше комментарии идентичны, также как и ниже:
fn main() { //! this #![doc="/// this"] }//! 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, который генерирует 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-литералами:
fn main() { /// <script>alert(document.cookie)</script> fn foo() {} }
/// <script>alert(document.cookie)</script>