Thank you for contributing to the manta-rs
codebase! Here are some guidelines to follow when adding code or documentation to this repository.
Please use conventional commits. We use at least the following types:
feat
: adding a new feature, new functionality to the codebasefix
: fixing old codechore
: small changes/commits that are left over from other commitswip
: marked whenever a commit should be considered part of a set of commits that together implement a feature or fix
See the conventional commits specification for more details on how to write and use conventional commits. We use squashing for our PRs so we can add types to commits and reformat them according to the spec if you forget to include them. PR titles should also follow conventional commits with the major category that the PR belongs to (except for wip
which should only be for commits).
We use the Keep a Changelog specification for CHANGELOG.md
. Whenever we add a new PR we want to make sure to add one line to the changelog with the following format:
- [\#3](https://github.com/Manta-Network/manta-rs/pull/3) Setup Initial Rust CI Pipeline
in any of the relevant categories outlined in the changelog spec:
Added
for new features.Changed
for changes in existing functionality.Deprecated
for soon-to-be removed features.Removed
for now removed features.Fixed
for any bug fixes.Security
in case of vulnerabilities.
Like the rest of the specification entails, all changes should be presented in reverse-chronological order. To label a PR as belonging to any one of these categories for inclusion in the GitHub auto-generated release notes, use the following labels:
changelog:added
changelog:changed
changelog:deprecated
changelog:removed
changelog:fixed
changelog:security
to place each PR in its respective category or use changelog:skip
if it should not be included in the GitHub auto-generated release notes.
We use pull-request templates to standardize the PR process. See the PULL_REQUEST_TEMPLATE.md
for more details on how to build a good PR.
When writing a new PR, the Continuous Integration (CI) system will trigger linting and tests to run on every commit. See the .github/workflows/ci.yml
for more detail on this workflow.
To keep code and documentation style consistent across all the code in the repository, we are adopting the following style guide. We begin with the formatting style enforced by the Nightly version of rustfmt
with configuration specified in the .rustfmt.toml
file. Beyond what rustfmt
currently enforces we have specified other rules below.
The Cargo.toml
file should ahere to the following template:
[package]
name = "..."
version = "..."
edition = "..."
authors = ["Manta Network <[email protected]>"]
readme = "README.md"
license-file = "LICENSE"
repository = "https://github.com/Manta-Network/manta-rs"
homepage = "https://github.com/Manta-Network"
documentation = "https://github.com/Manta-Network/manta-rs"
categories = ["..."]
keywords = ["..."]
description = "..."
publish = false
[package.metadata.docs.rs]
# To build locally:
# RUSTDOCFLAGS="--cfg doc_cfg" cargo +nightly doc --all-features --open
all-features = true
rustdoc-args = ["--cfg", "doc_cfg"]
[badges]
is-it-maintained-issue-resolution = { repository = "Manta-Network/manta-rs" }
is-it-maintained-open-issues = { repository = "Manta-Network/manta-rs" }
maintenance = { status = "actively-developed" }
[[bin]]
...
[features]
...
[dependencies]
...
[dev-dependencies]
...
[build-dependencies]
...
[profile....]
...
Specifically, we have:
- Use double quotes instead of single quotes.
- Use the above as the standard ordering of the
[package]
map. [[bin]]
before[features]
before[dependencies]
before[dev-dependencies]
before[build-dependencies]
before[profile]
settings.- Order features and dependencies alphabetically.
- When selecting features for a
[features]
entry or when selecting the features on a dependency, order the features alphabetically. - For a given dependency use the following structure with
optional
andfeatures
keys as needed:If the crate is acrate-name = { version = "...", optional = true, default-features = false, features = ["..."] }
path
orgit
dependency, replace those keys with theversion
key and add atag
,branch
, orrev
as needed following thegit
key. - When adding a feature, add a doc string in title case and a newline between each feature.
When using features, be sure to attach a doc_cfg
feature declaration as well unless the code is not exported to pub
.
#[cfg(feature = "...")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "...")))]
pub mod feature_gated_public_module;
Imports (use
) and exports (mod
) should be ordered as follows:
- External Crate Declarations
- Private Imports
- Private Imports with Features
- Private Exports
- Private Exports with Features
- Public Exports
- Public Exports with Features
- Reexports
- Reexports with Features
Here's an example set of declarations:
extern crate crate_name;
use module::submodule::entry;
#[cfg(feature = "...")]
use module::feature_gated_submodule;
mod another_module;
mod module;
mod the_third_module;
#[cfg(feature = "...")]
mod feature_gated_module;
pub mod public_module;
#[cfg(feature = "...")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "...")))]
pub mod feature_gated_public_module;
pub use reexported_objects;
#[cfg(feature = "...")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "...")))]
pub use feature_gated_reexported_objects;
Ensure that there are newlines between each category. Be sure that if there are imports or exports that are feature-gated, that they are sorted by feature alphabetically. If there is a feature gated import that requires importing multiple objects use the following pattern:
#[cfg(feature = "...")]
use {
thing1, thing2, thing3, thing4,
};
NOTE: All imports should occur at the top of any module and a newline should be added between the last import and the first declared object.
When defining a trait use the following syntax:
/// DOCS
trait Trait<T> {
/// DOCS
type Type1: Default;
/// DOCS
type Type2;
/// DOCS
const CONST_1: usize;
/// DOCS
const CONST_2: usize;
/// DOCS
fn required_method(&self, argument: Self::Type1) -> T;
/// DOCS
#[inline]
fn optional_method(&self) -> T {
Self::required_method(Self::Type1::default())
}
}
Notice the ordering of components:
- Associated Types
- Associated Constants
- Methods
Depending on the context and presentation, you can mix the ordering of required and optional methods. Also, notice the name formatting, although clippy
should detect if naming differs from this pattern.
When implementing traits use the following syntax:
impl<T> Trait for Struct<T> {
type Type1 = B;
type Type2 = C;
const CONST_1: usize = 3;
const CONST_2: usize = 4;
#[inline]
fn required_method(&self, argument: Self::Type1) -> T {
self.struct_method(argument).clone()
}
#[inline]
fn optional_method(&self) -> T {
short_cut_optimization(self)
}
}
Notice the lack of space between implementaions of the same category except for methods which always get a newline between them (like all methods). Only add space between types and constants if a comment is necessary like in this example:
impl Configuration {
const SPECIAL_CONSTANT: usize = 1234249;
/// In this case we have to use this constant because it's very special.
const ANOTHER_SPECIAL_CONSTANT: usize = 10000023;
}
but otherwise it should look like
impl Configuration {
const SPECIAL_CONSTANT: usize = 1234249;
const ANOTHER_SPECIAL_CONSTANT: usize = 10000023;
}
Every crate has at least a lib.rs
(or a main.rs
if there is no library) and it should include at least these macro invocations:
//! Module Documentation
#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(doc_cfg, feature(doc_cfg))]
#![forbid(rustdoc::broken_intra_doc_links)]
#![forbid(missing_docs)]
// IMPORTS AND EXPORTS GO HERE
or with #![no_std]
instead of the first macro if there is no std
feature.
In certain cases we may want to ignore a particular compiler warning or clippy
warning. This is especially true in because of some false-positive error or because we are writing some generic macro code. In either case we need to mark the #[allow(...)]
clause with a note on why we want to ignore this warning.
#[allow(clippy::some_lint)] // NOTE: Here's the reason why this is ok.
fn some_function() {}
In the case of allow
we want to be careful of it's scope so as to not ignore warnings except in the exact place where the unexpected behavior exists. Therefore, #[allow(...)]
should be marked on functions and not modules, even if that means it is repeated multiple times. In some rare cases where this repetition would be too cumbersome, and adding it to the module is cleaner, then also be sure to state in a note why this is better than marking it on the functions themselves.
This kind of pattern will eventually be enforced by clippy
itself, but is currently an unstable feature. See allow_attributes_without_reason and rust-lang/rust/54503 for more information. Once it is stable we can move to this pattern.
-
Always use where clauses instead of inline trait constraints. So instead of
fn function<T: Clone>(t: &T) -> T { t.clone() }
you should use
fn function<T>(t: &T) -> T where T: Clone, { t.clone() }
This is also true for any part of the code where generic types can be declared, like in
fn
,struct
,enum
,trait
, andimpl
. The only "exception" is for supertraits, so use:trait Trait: Clone + Default + Sized {}
instead of using
trait Trait where Self: Clone + Default + Sized, {}
-
Order
where
clause entries by declaration order, then by associated types and then by other constraints. Here's an examplefn function<A, B, C>(a: &A, b: &mut B) -> Option<C> where A: Clone + Iterator, B: Default + Eq, C: IntoIterator, A::Item: Clone, C::IntoIter: ExactSizeIterator, Object<B, C>: Copy,
NOTE: This rule is not so strict, and these
where
clauses should be organized in a way that makes most sense but must follow this general rule. -
Order each entries constraints alphabetically. Here's an example:
F: 'a + Copy + Trait + FnOnce(T) -> S
The ordering should be lifetimes first, then regular traits, then the function traits.
In general, we should avoid magic numbers and constants in general but when they are necessary, they should be declared as such in some module instead of being used in-line with no explanation. Instead of
/// Checks that all the contributions in the round were valid.
pub fn check_all_contributions() -> Result<(), ContributionError> {
for x in 0..7 {
check_contribution(x)?;
}
Ok(())
}
you should use
/// Contribution Count for the Round-Robin Protocol
pub const PARTICIPANT_COUNT: usize = 7;
/// Checks that all the contributions in the round were valid.
pub fn check_all_contributions() -> Result<(), ContributionError> {
for x in 0..PARTICIPANT_COUNT {
check_contribution(x)?;
}
Ok(())
}
Avoid situations where an arbitrary number needs to be chosen, and if so prefer empirically measured numbers. If for some reason an arbitrary number needs to be chosen, and it should have a known order of magnitude, chose a power of two for the arbitrary number, or something close to a power of two unless the situation calls for something distinctly not a power of two.
In general, documentation should be added on function/interface boundaries instead of inside code blocks which should be written in a way that explains itself. Sometimes however, we have to do something specific that is counter-intuitive or against some known principle in which case we should comment the code to explain ourselves.
IMPORTANT: Documentation should explain what behavior an interface provides, and comments explain why the implementation provides this behavior.
When formatting comments we have a few comment types:
NOTE
: Explains some unintuitive behavior or makes note of some invariants that are being preserved by this particular piece of code.SAFETY
: LikeNOTE
, except it reflects a safety-critical assumption or invariant and is required for Rustunsafe
blocks or for some concrete cryptographic code.TODO
: LikeNOTE
, but involves relaying the information relevant for a fix or future optimization.FIXME
: Something is critically broken or inconveniently written and should be changed whenever possible.
These four kinds of comments should be formatted as follows:
#[inline]
fn last(self) -> Option<Self::Item> {
// NOTE: Although this iterator can never be completed, it has a well-defined final element
// "at infinity".
Some(Default::default())
}
The NOTE
marker and SAFETY
marker have documentation forms as well but instead are formatted as # Note
and # Safety
subsections as follows:
/// Returns the leaf digests currently stored in the merkle tree.
///
/// # Note
///
/// Since this tree does not start its leaf nodes from the first possible index, indexing into
/// this slice will not be the same as indexing into a slice from a full tree. For all other
/// indexing, use the full indexing scheme.
#[inline]
pub fn leaf_digests(&self) -> &[LeafDigest<C>] {
&self.leaf_digests
}
For documentation headers we also have # Panics
and # Errors
as standard headers that describe the conditions under which the function calls panic
or the error conditions.
Here are some important guidelines to follow for general documentation:
- All module documentation should exist in the module itself in the header with
//!
doc strings. - Be sure to link all documentation that refers to objects in the code.
- Function documentation should be in present tense. It should answer the question "What does this function do?".