Rust
We use rustfmt
to format our code. This tool is part of the Rust toolchain and will format your
code according to the Rust style guide. We also use clippy
to catch common mistakes and improve
the quality of our code.
This document covers some additional guidelines that are not enforced by rustfmt
or clippy
. And
will generally allow you to write more idiomatic Rust code.
Documentation
We strictly enforce documentation of all public items using the missing_docs
linting rule. This
helps ensure that our code is well-documented and easy to understand. In rare cases where
documentation is unhelpful or simply duplicates enum variants, it may be omitted using
#[allow(missing_docs)]
but you should have a valid reason.
Rust is generally quite unopinionated in how you document your code. This can result in many different documentation styles which can negatively impact readability and maintainability. We therefore have some guidelines and suggestions for documenting your code. These should not be seen as strict rules but rather as best practices to follow in our codebases.
General guidelines
Avoid starting documentation with This module
, This function
, This field
or similar phrases.
The context will be inferred implicitly by the reader. Instead, focus on describing the purpose and
behavior of the item directly.
When possible document from the consumer's perspective. This helps ensure that the documentation is relevant and useful to those who will be using the code.
Documenting Examples
Rust has built in support for including code blocks in documentation. This can be useful when you want to include suggested usage examples or other relevant code snippets. As a bonus rust generally executes the code blocks in the documentation to ensure they are correct.
Modules
We encourage you to document modules, including their purpose and any important details about their
contents. This helps other developers understand the context and functionality of the module. When
documenting modules use the //!
syntax to write documentation comments at the top of the file.
// crates/bitwarden-crypto/src/aes.rs
//! # AES operations
//!
//! Contains low level AES operations used by the rest of the library.
//!
//! In most cases you should use the [EncString][crate::EncString] with
//! [KeyEncryptable][crate::KeyEncryptable] & [KeyDecryptable][crate::KeyDecryptable] instead.
Functions
Please ensure functions and their arguments have descriptive names. It's often better to use longer names that convey more meaning if it improves clarity.
Arguments
We avoid documenting function arguments since rust does not have a good convention for doing so.
Instead focus on using well descriptive names and appropriate types for the arguments. For example
rather than using i32
for a positive integer, use u32
. If the value should never be 0 use
NonZeroU32
. The NewType pattern can be useful here in case there is no built in
representation of the type.
If you still feel that a comment would be helpful, another technique is to extract the arguments into a separate struct to improve clarity and enforce type safety.
// Good
/// Sums two arguments.
fn sum_arguments(arg1: i32, arg2: i32) -> i32 {
arg1 + arg2
}
// Bad
/// Sums two arguments
///
/// # Arguments
///
/// * `x` - The first argument.
/// * `y` - The second argument.
///
/// # Returns
///
/// Returns the sum of the arguments.
fn do_something(arg1: i32, arg2: i32) -> i32 {
arg1 + arg2
}
Returns
Similar to arguments, do not document returns. If you find the return value can be mistaken or misused consider using the NewType pattern.
Avoid panics
Panics are highly discouraged in the Bitwarden codebase outside of tests. Errors should be handled
gracefully and returned to the caller. clippy
will forbid you from using unwrap
. While expect
is allowed, it should be used sparingly and should always provide a helpful message indicating why
it can never occur.
Some scenarios where expect
are allowed are:
- Calling libraries that guarantee that the allowed inputs never results in
Err
orNone
. - Operating on slices or arrays where the index is guaranteed to be within bounds.
Pattern matching
Rust provides a powerful pattern matching system that can be used for a variety of tasks. However,
care should be taken to not rely overly on match
statements. In many cases there are more concise
and readable alternatives especially when working with Option
and Result
.
if let
Use if let
when you only
care about one arm of a match.
// Bad
match result {
Ok(value) => outer.append(value),
_ => (),
}
// Good
if let Ok(value) = result {
outer.append(value);
}
Options
We use clippy
to enforce general guidelines for working with
Option
. Generally reach for methods like:
map
Use map
when you want to
transform the value inside the Option
.
// Bad
match Some(0) {
Some(x) => Some(x + 1),
None => None,
};
// Good
Some(0).map(|x| x + 1);
and_then
Use and_then
when you
want to chain multiple operations that return an Option
.
func(x: i32) -> Option<i32>;
// Bad
match Some(0) {
Some(x) => func(x + 1),
None => None,
};
// Good
Some(0).and_then(|x| func(x + 1));
unwrap_or
Use unwrap_or
when you
want to provide a default value.
// Bad
match Some(0) {
Some(x) => x,
None => 1,
};
// Good
Some(0).unwrap_or(1);
ok_or
Use ok_or
when you want to
convert an Option
to a Result
.
// Bad
match Some(0) {
Some(x) => x,
None => Err("error"),
};
// Good
foo.ok_or("error");
Results
When working with Result
it might be
tempting to use match to get the value out of the result. Instead, use the ?
operator to propagate
the error.
map_err
Use map_err
when you want
to transform the error inside the Result
.
// Bad
match result {
Ok(value) => value,
Err(_) => return Err("Another error"),
}
// Good
do_something().map_err(|e| "Another error")?
NewType
The NewType pattern is a way to create a distinct type that wraps a value of another type. This is useful for providing additional type safety and clarity in your code. For an in-depth introduction to the NewType pattern please read the Rust Book.
We use NewType extensively throughout the Bitwarden codebase. Some good examples are IDs, which
uses the uuid!
macro to NewType UUID
which ensure you can't mix up different types of IDs.
use bitwarden_uuid::uuid;
uuid!(pub CipherId);
uuid!(pub FolderId);
fn test(folder_id: FolderId) {
// Use the folder_id here
}
// This is fine
test(FolderId::new())
// Compile error.
test(CipherId::new())
The bitwarden-crypto
crate contains a bunch of other examples, such as UserKey
which is a
NewType wrapper around SymmetricCryptoKey
. Which also exposes some additional functionality.