`bumpy_road_function`
Purpose
Detects functions with multiple distinct clusters of nested conditional complexity.
Scope and behaviour
Flags a function when peak detection finds two or more separated complexity
regions above the configured threshold. Detection smooths the local complexity
signal with the configured window and only considers peaks spanning at least
min_bump_lines.
The default threshold was lowered from 3.0 to 2.5 to detect bumpy road patterns
in match expressions with nested conditionals. The moving-average smoothing
(window=3) reduces raw peaks by approximately 15–20%, so a threshold of 3.0 can
mask genuine two-bump patterns in match arms with nested if guards.
Configuration
[bumpy_road_function]
threshold = 2.5 # Raise to 3.0 or higher to reduce false positives
window = 3
min_bump_lines = 2
What is allowed
- A single complexity peak in a function.
- Simple predicates that remain below the configured threshold.
What is denied
- Two or more separated complexity peaks above the configured threshold.
How to fix
Split complex regions into helper functions and simplify branch-heavy predicates.
`conditional_max_n_branches`
Limits the complexity of conditional predicates by enforcing a maximum number of boolean branches.
Configuration:
[conditional_max_n_branches]
max_branches = 2
The default threshold is 2 branches. A predicate like a && b && c has three
branches and would trigger the lint.
How to fix: Extract complex conditions into helper functions:
// Before: Too many branches
if condition_a && condition_b && condition_c {
// action
}
// After: Extract to helper function
fn should_proceed() -> bool {
condition_a && condition_b && condition_c
}
if should_proceed() {
// action
}
`function_attrs_follow_docs`
Purpose
Ensures doc comments appear before other outer attributes on functions, methods, and trait methods.
Scope and behaviour
When attributes are generated or reordered by a procedural macro (for example,
rstest or derive), the lint recovers the original source span from the
macro expansion chain. Attributes whose spans cannot be traced back to any
user-written source location (macro-only glue) are silently excluded from the
ordering check, so the lint never fires on compiler- or macro-generated code
that the developer cannot edit.
Configuration
function_attrs_follow_docs has no configuration knobs.
What is allowed
- Doc comments that appear before every other outer attribute on the same function, method, or trait method.
- Macro-generated attributes whose spans are excluded because they are macro-only.
- Inner attributes, which are outside the lint's scope.
What is denied
- Outer attributes that appear before a doc comment on the same function, method, or trait method.
- Macro-expanded attributes that recover to a user-editable source span and sort before the doc comment.
How to fix
Move doc comments so they appear before other outer attributes:
// Wrong
#[inline]
/// This function does something.
fn example() {}
// Correct
/// This function does something.
#[inline]
fn example() {}
With rstest, place the doc comment before all attributes, including the test
annotation:
// Wrong
#[rstest]
#[case(1, 2, 3)]
/// Verifies addition.
fn adds(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
assert_eq!(a + b, expected);
}
// Correct
/// Verifies addition.
#[rstest]
#[case(1, 2, 3)]
fn adds(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
assert_eq!(a + b, expected);
}
`module_max_lines`
Warns when modules exceed a configurable line count threshold.
Configuration:
[module_max_lines]
max_lines = 400
How to fix: Split large modules into smaller, focused submodules.
`module_must_have_inner_docs`
Enforces that every module begins with an inner documentation comment (//!).
How to fix:
mod my_module {
//! Explain the module's purpose here.
pub fn value() {}
}
`no_expect_outside_tests`
Purpose
Detect test attributes correctly so no_expect_outside_tests can allow
.expect() in recognized test-only code while still flagging production use.
Scope and behaviour
Whitaker recognizes #[test], prelude-qualified #[test] forms,
#[tokio::test], #[async_std::test], #[gpui::test], #[rstest],
#[rstest::rstest], #[rstest_parametrize], #[rstest::rstest_parametrize],
#[case], and #[rstest::case] by default. The additional_test_attributes
setting extends that matching list with project-specific markers, so the lint
treats those annotated functions as tests too.
Configuration
[no_expect_outside_tests]
additional_test_attributes = ["my_framework::test", "wasm_bindgen_test"]
Set additional_test_attributes to an array of attribute paths written as
strings. Each entry should match the path Whitaker sees on the test function,
for example my_framework::test or wasm_bindgen_test.
Ancestor context propagation
additional_test_attributes now apply during ancestor context detection as
well as direct annotation matching. If a parent function is annotated with a
configured custom test attribute, Whitaker treats nested code within that
function as test context too, so .expect() remains allowed throughout that
ancestry chain.
// dylint.toml
// [no_expect_outside_tests]
// additional_test_attributes = ["my_framework::test"]
#[my_framework::test]
async fn my_test() {
helper(); // allowed — ancestor is a recognized test function
}
fn helper() {
let v: Option<u32> = Some(1);
let _ = v.expect("value present"); // allowed — called from within test ancestry
}
What is allowed
- Default markers such as
#[test],#[::test],#[::std::prelude::v1::test],#[tokio::test],#[async_std::test],#[gpui::test],#[rstest],#[rstest::rstest],#[rstest_parametrize],#[rstest::rstest_parametrize],#[case], and#[rstest::case] - Project-specific markers listed in
additional_test_attributes, such as#[wasm_bindgen_test]
What is denied
Functions using .expect() will still be flagged when their test attribute is
not in Whitaker's default list and is not listed in
additional_test_attributes.
How to fix
- Add the missing test marker to
additional_test_attributesif the function is genuinely part of a supported test framework - Change the attribute usage to a recognized form such as
#[test],#[::test],#[::std::prelude::v1::test],#[tokio::test],#[async_std::test],#[gpui::test],#[rstest],#[rstest::rstest],#[rstest_parametrize],#[rstest::rstest_parametrize],#[case], or#[rstest::case]where appropriate - If the function is not test-only code, replace
.expect()with explicit error handling such as?ormap_err
`rstest_helper_should_be_fixture`
Purpose
Bootstraps the experimental lint that will recommend converting repeated helper
calls inside #[rstest] tests into injected #[fixture] parameters.
Scope and behaviour
This lint is experimental. The current implementation registers the lint,
loads configuration defaults, and passively collects local helper calls
inside strict #[rstest] tests, fingerprinting fixture-local, literal,
const, and static arguments for later aggregation. The lint remains
diagnostic-silent: threshold evaluation and actionable diagnostics are
tracked by 8.2.3, while UI pass/fail coverage is tracked by 8.2.4.
Configuration
[rstest_helper_should_be_fixture]
min_calls = 2
min_distinct_tests = 2
require_identical_fixture_arg_names = false
provider_param_attributes = ["case", "values", "files", "future", "context"]
use_source_callee_fallback = false
provider_param_attributes lists rstest parameter attributes that should be
treated as data providers rather than fixture-local bindings. Entries may be
written either as bare names such as case or qualified names such as
rstest::case; Whitaker normalizes them to the shared detection policy.
What is allowed
- Single-use helper calls inside
#[rstest]tests - Helper calls whose totals stay below
min_callsormin_distinct_tests - Parameter-provider uses covered by
provider_param_attributes, such ascase,values,files,future, andcontext
What is denied
When diagnostic phases are implemented, rstest_helper_should_be_fixture will
deny repeated non-provider helper invocations across #[rstest] tests when
they meet or exceed both min_calls and min_distinct_tests. The
require_identical_fixture_arg_names setting controls whether candidate
fixture arguments must use the same names, and use_source_callee_fallback
controls whether source-callsite recovery may be used for macro-expanded
callee locations.
How to fix
- Replace repeated helper calls with a shared
#[fixture]parameter. - If
require_identical_fixture_arg_namesis enabled, rename helper arguments so repeated calls use the same fixture argument names. - Prefer provider attributes listed in
provider_param_attributesfor parameterized data inputs rather than modelling them as fixture-local helper calls. - Tune
min_calls,min_distinct_tests, anduse_source_callee_fallbackwhen repository conventions need stricter or looser matching.
`test_must_not_have_example`
Warns when test function documentation includes example headings (for example
# Examples) or fenced code blocks.
Configuration:
[test_must_not_have_example]
additional_test_attributes = ["actix_rt::test", "my_framework::test"]
Use additional_test_attributes for frameworks not covered by default test
markers such as #[test], #[tokio::test], #[async_std::test],
#[gpui::test], and #[rstest].
How to fix: Keep test docs focused on intent and assertions, and move example/tutorial snippets into user-facing documentation.
// Before
#[test]
/// # Examples
/// ```rust
/// assert_eq!(sum(2, 2), 4);
/// ```
fn sums_values() { /* ... */ }
// After
#[test]
/// Verifies summation handles two positive integers.
fn sums_values() { /* ... */ }
`no_std_fs_operations`
Enforces capability-based filesystem access by forbidding direct use of
std::fs operations.
Configuration:
[no_std_fs_operations]
excluded_crates = ["my_cli_entrypoint", "my_test_utilities"]
The excluded_crates option allows specified crates to use std::fs
operations without triggering diagnostics. This is useful for:
- CLI entry points where ambient filesystem access is the intended boundary
- Test support utilities that manage fixtures with ambient access
- Build scripts or code generators that require direct filesystem operations
Note: Use Rust crate names (underscores), not Cargo package names (hyphens). For example, use
my_cli_apprather thanmy-cli-app.
How to fix: Replace std::fs with cap_std:
// Before
use std::fs;
fn read_config() -> std::io::Result<String> {
fs::read_to_string("config.toml")
}
// After
use cap_std::fs::Dir;
use camino::Utf8Path;
fn read_config(config_dir: &Dir, path: &Utf8Path) -> std::io::Result<String> {
config_dir.read_to_string(path)
}
`no_unwrap_or_else_panic`
Denies panicking unwrap_or_else fallbacks on Option/Result, including
tests. Doctest runs remain exempt.
Configuration:
[no_unwrap_or_else_panic]
allow_in_main = true
What is allowed:
- Panicking
unwrap_or_elsefallbacks inside doctests - Panicking
unwrap_or_elsefallbacks insidemainwhenallow_in_main = true unwrap_or_else(|| panic!("value was {:?}", value))inside test code when the closure interpolates a runtime value into the panic message- Non-panicking
unwrap_or_elsefallbacks
What is denied:
unwrap_or_else(|| panic!(..))outside tests, subject to the standard denialunwrap_or_else(|| panic!(..))in tests unless thepanic!message interpolates runtime valuesunwrap_or_else(|| panic!("static message"))in tests; use.expect("static message")insteadunwrap_or_else(|| value.unwrap())
How to fix: Propagate errors with ? or use .expect() with a clear
message if a panic is truly intended. In tests, replace
unwrap_or_else(|| panic!("msg")) with .expect("msg") for clarity and
brevity, unless the closure needs to interpolate runtime state for a more
useful diagnostic. The rule denies only closures whose panic! message does
not meet the interpolated-only test exception. When the closure contains a
static string literal in tests, prefer .expect("static message"); only
interpolated-only panic! fallbacks are permitted there.