Code and API conventions
Bikeshed auto-painting
In general, we try to automate as much as possible during CI. This ensures a consistent code style and avoids unnecessary work during pull request reviews.
In particular, we use the following tools:
- rustfmt for code formatting (config options).
- clippy for lints and style warnings (list of lints).
- Clang's AddressSanitizer and LeakSanitizer for memory safety.
- Various specialized tools:
- skywalking-eyes to enforce license headers.
- cargo-deny and cargo-machete for dependency verification.
In addition, we have unit tests (#[test]), doctests and Godot integration tests (#[itest]).
See Dev tools for more information.
Technicalities
This section lists specific style conventions that have caused some confusion in the past. Following them is nice for consistency, but it's not the top priority of this project. Hopefully, we can automate some of them over time.
Formatting
rustfmt is the authority on formatting decisions. If there are good reasons to deviate from it, e.g. data-driven tables in tests,
use #[rustfmt::skip]. rustfmt does not work very well with macro invocations, but such code should still follow rustfmt's
formatting choices where possible.
Line width is 120-145 characters (mostly relevant for comments).
We use separators starting with // --- to visually divide sections of related code.
Code organization
-
Anything that is not intended to be accessible by the user, but must be
pubfor technical reasons, should be marked as#[doc(hidden)].- This does not constitute part of the public API.
-
We do not use the
preludeinside the project, except in examples and doctests. -
Inside
implblocks, we roughly try to follow the order:- Type aliases in traits (
type) - Constants (
const) - Constructors and associated functions
- Public methods
- Private methods (
pub(crate), private,#[doc(hidden)])
- Type aliases in traits (
-
Inside files, there is no strict order yet, except
useandmodat the top. Prefer to declare public-facing symbols before private ones. -
Use flat import statements. If multiple paths have different prefixes, put them on separate lines. Avoid
self.#![allow(unused)] fn main() { // Good: use crate::module; use crate::module::{Type, function}; use crate::module::nested::{Trait, some_macro}; // Bad: use crate::module::{self, Type, function, nested::{Trait, some_macro}}; }
Types
-
Avoid tuple-enums
enum E { Var(u32, u32) }and tuple-structsstruct S(u32, u32)with more than 1 field. Use named fields instead. -
Derive order is
#[derive(GdextTrait, ExternTrait, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)].GdextTraitis a custom derive defined by godot-rust itself (in any of the crates).ExternTraitis a custom derive by a third-party crate, e.g.nanoserde.- The standard traits follow order construction, comparison, hashing, debug display.
More expressive ones (
Copy,Eq) precede their implied counterparts (Clone,PartialEq).
Functions
-
Getters don't have a
get_prefix. -
Use
selfinstead of&selfforCopytypes, unless they are really big (such asTransform3D). -
For
Copytypes, avoid in-place mutationvector.normalize().
Instead, usevector = vector.normalized(). The past tense indicates a copy. -
Annotate with
#[must_use]when ignoring the return value is likely an error.
Example: builder APIs.
Attributes
Concerns both #[proc_macro_attribute] and the attributes attached to a #[proc_macro_derive].
-
Attributes always have the same syntax:
#[attr(key = "value", key2, key_three = 20)]attris the outer name grouping different key-value pairs in parentheses.
A symbol can have multiple attributes, but they cannot share the same name.key = valueis a key-value pair. justkeyis a key-value pair without a value.- Keys are always
snake_caseidentifiers. - Values are typically strings or numbers, but can be more complex expressions.
- Multiple key-value pairs are separated by commas. Trailing commas are allowed.
- Keys are always
-
In particular, avoid these forms:
#[attr = "value"](top-level assignment)#[attr("value")](no key -- note that#[attr(key)]is allowed)#[attr(key(value))]#[attr(key = value, key = value)](repeated keys)
The reason for this choice is that each attribute maps nicely to a map, where values can have different types.
This allows for a recognizable and consistent syntax across all proc-macro APIs. Implementation-wise, this pattern is
directly supported by the KvParser type in godot-rust, which makes it easy to parse and interpret attributes.