Available Lints

Version 0.2.7 Updated Jul 22, 2026

`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_attributes if 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 ? or map_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_calls or min_distinct_tests
  • Parameter-provider uses covered by provider_param_attributes, such as case, values, files, future, and context

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_names is enabled, rename helper arguments so repeated calls use the same fixture argument names.
  • Prefer provider attributes listed in provider_param_attributes for parameterized data inputs rather than modelling them as fixture-local helper calls.
  • Tune min_calls, min_distinct_tests, and use_source_callee_fallback when 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_app rather than my-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_else fallbacks inside doctests
  • Panicking unwrap_or_else fallbacks inside main when allow_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_else fallbacks

What is denied:

  • unwrap_or_else(|| panic!(..)) outside tests, subject to the standard denial
  • unwrap_or_else(|| panic!(..)) in tests unless the panic! message interpolates runtime values
  • unwrap_or_else(|| panic!("static message")) in tests; use .expect("static message") instead
  • unwrap_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.