Coding Conventions
Basis
Unless otherwise specified below, our Rust coding convention is identical to the following resources:
- Official Rust Style Guide
- Official Rust API Guidelines
Note that some of the guidance in the API Guidelines document is specifically applicable to public APIs of libraries. This doesn't necessarily apply to all of the code we write, although if we deviate from the API guidelines it should be deliberately and with a good reason.
Guiding Principle
The purpose of having a style guide and conventions is to produce consistent, readable, clear, and maintainable code. There will be situations in which a strict adherence to style conventions will be counter-productive to these objectives. Fortunately, Elastio team members are highly intelligent, educated, rational people who are capable of making subjective judgement calls based on the best information available to them.
Simply put, use your best judgement. If a rule seems to make code worse, then don't follow it (but be prepared to defend your decision in code review, and to reverse it if you're wrong).
Basics
All code should be formatted with the rustfmt
utility with the default settings. This can be made automatic in most
editors. Code that isn’t formatted this way will fail the CI build.
Code should compile without any warnings, and CI builds will treat warnings as errors.
All code should pass clippy
static analysis with the default lints enabled. Failure to pass any of these will fail
the CI build. If a particular clippy link is not applicable it should be disabled with the finest granularity possible
(eg, at the variable or function or statement or at most module level; never at the crate level).
Using use
use
is used to import modules or specific types into the current scope.
For large code files this can get complicated, so the goal of our conventions is to ensure we import other types in a way that maximizes readability and clarity.
Grouping
There should be one single block of use
statements at the beginning of the module to which they apply. There should
be no white space between the use
statements. They should be sorted alphabetically, though as long as you adhere to
the preceding rules rustfmt
will handle sorting them automatically.
Importing a module
In the majority of cases, it's preferable to import a module, not the item(s) in that module.
For example, if you want to call foo
in the bar
module in the widget
crate, you should do this:
use widget::bar;
// ...
bar::foo();
This makes it very clear that foo
is not an item in the current module but is imported from elsewhere.
Importing a type
Exceptions to the above rule do exist. Remember that the goal is clarity. If you're implementing Debug
, it's okay to
use std::fmt::Debug
; we all know what Debug
is and won't need to scratch our heads to figure out what it means.
Another exception is types that are used frequently throughout a particular crate. For example in the RocksDB wrapper
crate, there's a trait BinaryStr
that is used all over in various modules. There's not much value in qualifying it as
ops::BinaryStr
, as its origin and purpose are readily apparent to anyone working in the code.
A third and very common exception is when calling methods on a trait. Rust requires that the trait be in scope in order
to call methods on it, and in these cases there is no reasonable alternative but to explicitly use
the trait itself.
Lastly, it's often the case that the name of a type is sufficient to make it clear where it's from. For example,
Serialize
and Deserialize
are obviously from the serde
crate. MyCrateError
is obviously an error type from
my_crate
, and does not need to be qualified as my_crate::MyCrateError
for clarity. This is a matter of good
judgement and may be questioned in code review.
Wildcard imports
Wildcard imports are to be considered forbidden except under specific circumstances:
-
When a crate exposes a
prelude
module for convenience. An example of this is the Rayon crate, which hasrayon::prelude
specifically to provide a single place to import all of the helper traits and extensions to existing traits. Especially if this module is calledprelude
it's apparent by convention what it's for, and thus a wildcard import of that module can be used whenever it makes sense. -
When bringing into scope types whose actual location are obvious from context or not relevant to understanding the code. This is obviously a very subjective standard, and you should be prepared to defend this assessment in a code review if you rely on it for justification of a wildcard import.
-
In a
lib.rs
file when re-exporting items from private modules. It's almost always good practice to organize code into logical modules, even if those modules themselves are not public. In that case one must re-export the relevant types, and there's usually not any descriptive or clarifying value in explicitly listing each of the items one is re-exporting.
In general wildcard imports obscure the origin of the types being used and make it harder to understand the code at
a glance. Especially since tools like rust-analyze
and IntelliJ can automatically generate import statements for
specific types as they are used, there's no excuse for lazily pulling in everything in a module.
Error Handling
How to represent, handle, and raise errors is a big topic. See the Errors chapter.