Lint Rules Reference

Lint Rules Reference

8Vast's linter ships with 156 built-in rules across eight categories. This page lists every category, highlights the most important rules, and shows examples of the code they catch.

Run 8v lint explain <rule-id> to get a detailed description of any rule, including code examples and configuration options.


Error Handling (11 rules)

These rules catch patterns where errors are silently lost, incorrectly logged, or stripped of context. Most other linters don't check for these. Rust's type system makes it easy to compile code that ignores errors -- these rules find it.

RuleSeverityWhat it catches
error-swallowederrorErr(_) => {} -- error variant matched and ignored
error-chain-brokenerror.map_err(|_| ...) -- original error discarded, context lost
error-to-defaultwarning.unwrap_or_default() on Result -- missing data hidden behind a default value
error-wrong-log-levelwarningError logged at debug! or info! instead of error! or warn!
public-anyhow-resultwarningPublic function returning anyhow::Result -- callers can't match on error variants
err-with-contextwarningErr(e).into() without .context() -- error propagated without explaining where it happened
error-to-stringwarning.to_string() on an error type -- downgrades a typed error to an opaque string
result-unit-errorwarningResult<T, ()> -- error type carries no information
error-in-ok-positionerrorReturning an error inside Ok(...) -- likely a mistake
unhandled-trywarninglet _ = fallible_fn()? -- result of try expression discarded
catch-all-errorwarningcatch_unwind or catch_all used instead of proper error handling

Example: error-swallowed

This is the rule most people notice first. It fires when an Err variant is matched and nothing happens:

// This triggers error-swallowed
match do_something() {
    Ok(val) => process(val),
    Err(_) => {}  // Error silently ignored
}

// Fixed: handle or propagate the error
match do_something() {
    Ok(val) => process(val),
    Err(e) => {
        tracing::error!("do_something failed: {e}");
        return Err(e.into());
    }
}

Example: error-chain-broken

Fires when .map_err discards the original error:

// This triggers error-chain-broken
let file = File::open(path).map_err(|_| AppError::NotFound)?;

// Fixed: preserve the original error
let file = File::open(path)
    .context("failed to open config file")?;

Safety (10 rules)

These rules find code that compiles but is likely to cause runtime problems -- panics in places that shouldn't panic, unsafe code without justification, blocking in async contexts.

RuleSeverityWhat it catches
no-process-exiterrorprocess::exit() in library code -- kills the entire process without cleanup
undocumented-unsafeerrorunsafe block without a // SAFETY: comment explaining why it's sound
panic-in-result-fnerrorpanic!, unreachable!, or todo! in a function that returns Result
no-blocking-in-asyncerrorstd::fs, std::net, or thread::sleep called inside an async fn
no-unwrapwarning.unwrap() on Option or Result -- will panic on None/Err
no-expect-without-msgwarning.expect("") with an empty message -- adds a panic point with no context
no-todo-macrowarningtodo!() left in non-test code
no-unreachablewarningunreachable!() outside of exhaustive match arms
no-dbgwarningdbg!() macro left in code (debug-only, should not ship)
no-printlnwarningprintln! or eprintln! in library code -- use structured logging

Example: no-blocking-in-async

This catches a common mistake in async Rust -- calling blocking I/O inside an async function, which blocks the entire executor thread:

// This triggers no-blocking-in-async
async fn read_config() -> Config {
    let contents = std::fs::read_to_string("config.toml").unwrap();
    toml::from_str(&contents).unwrap()
}

// Fixed: use async I/O
async fn read_config() -> Config {
    let contents = tokio::fs::read_to_string("config.toml").await.unwrap();
    toml::from_str(&contents).unwrap()
}

Performance (15 rules)

Patterns that work correctly but waste CPU, memory, or executor time. These matter most in hot paths and async code.

RuleSeverityWhat it catches
clone-in-loopwarning.clone() called inside a loop body -- often allocates per iteration
await-holding-lockerror.await while holding a MutexGuard or RwLockGuard
large-futurewarningFuture exceeds 1KB on the stack (causes expensive allocations when spawned)
no-blocking-io-in-asyncwarningstd::io::Read/Write in async context -- use async equivalents
collect-then-iterwarning.collect::<Vec<_>>().iter() -- collects into a vec only to iterate it again
redundant-clonewarning.clone() on a value that's not used after the clone
string-format-in-loopwarningformat!() inside a loop -- consider pre-allocating or using write!
vec-push-in-loopwarningVec::push in a loop without with_capacity -- may reallocate multiple times
box-defaultwarningBox::new(Default::default()) -- use Box::default() instead
large-stack-arraywarningStack-allocated array exceeding 4KB
unnecessary-allocationwarningHeap allocation where a reference would work (e.g., &String param instead of &str)
map-entrywarning.contains_key() followed by .insert() -- use the entry API
single-char-patternwarning.split("x") where .split('x') (char literal) is faster
manual-memcpywarningLoop copying elements between slices -- use copy_from_slice
large-enum-variantwarningEnum variant significantly larger than others -- consider boxing

Example: await-holding-lock

This is an error, not a warning, because it can deadlock your application:

// This triggers await-holding-lock
async fn update_state(state: &Mutex<AppState>) {
    let mut guard = state.lock().await;
    let data = fetch_from_db().await;  // Still holding the lock
    guard.data = data;
}

// Fixed: drop the lock before awaiting
async fn update_state(state: &Mutex<AppState>) {
    let data = fetch_from_db().await;
    let mut guard = state.lock().await;
    guard.data = data;
}

Security (7 rules)

These rules flag patterns that create attack surfaces. They err on the side of false positives -- better to review and dismiss than to miss a real vulnerability.

RuleSeverityWhat it catches
no-sql-string-concaterrorSQL query built with format! or string concatenation -- use parameterized queries
no-command-injectionerrorCommand::new with user-controlled input in arguments
no-hardcoded-secretserrorString literals matching patterns for API keys, passwords, tokens
no-path-traversalwarningFile path constructed from user input without sanitization
no-crypto-weakwarningUse of MD5, SHA1, or other weak cryptographic algorithms
no-http-plaintextwarninghttp:// URLs in non-test code (should be https://)
no-world-readable-permissionswarningFile created with 0o777 or similarly broad permissions

Example: no-sql-string-concat

// This triggers no-sql-string-concat
let query = format!("SELECT * FROM users WHERE name = '{name}'");
sqlx::query(&query).fetch_one(&pool).await?;

// Fixed: use parameterized queries
sqlx::query("SELECT * FROM users WHERE name = $1")
    .bind(&name)
    .fetch_one(&pool)
    .await?;

Metrics (9 rules)

These rules measure code complexity and flag when it exceeds a threshold. Every threshold is configurable in lint.toml -- see Configuration.

RuleSeverityDefaultWhat it measures
function-too-longwarning50 linesFunction body length
file-too-longwarning500 linesTotal file length
too-many-paramswarning6Number of function parameters
nesting-too-deepwarning3 levelsDeepest nesting of control flow (if, match, for, while, loop)
too-many-let-bindingswarning15Let bindings in a single function
cognitive-complexitywarning15Branching complexity (counts branches, nesting, and recursion)
trait-too-many-methodswarning8Methods defined on a single trait
struct-excessive-boolswarning3Boolean fields on a single struct
test-too-longwarning50 linesTest function body length

Metric rules are all warnings by default. They're advisory -- complexity is sometimes necessary. Adjust the thresholds to match your codebase:

[rules.function-too-long]
threshold = 100

[rules.cognitive-complexity]
threshold = 25

Cognitive complexity

This measures how hard a function is to understand. It's not just line count -- a 40-line function with straightforward sequential logic scores lower than a 20-line function with deeply nested if/match/for blocks.

The score increases for:

  • Each branch (if, else, match arm)
  • Each level of nesting (nested if inside for inside match)
  • Each break, continue, or early return
  • Recursion

A score of 15 (the default limit) roughly corresponds to "needs to be split or restructured."


Test Quality (9 rules)

Tests that don't test anything give false confidence. These rules flag common patterns that make tests unreliable.

RuleSeverityWhat it catches
test-no-assertionswarningTest function with no assert!, assert_eq!, or assert_ne!
test-assert-truewarningassert!(true) or assert_eq!(1, 1) -- always passes, tests nothing
test-assert-eq-boolwarningassert_eq!(result, true) -- use assert!(result) instead
sleepy-testwarningthread::sleep or tokio::time::sleep in a test -- fragile timing dependency
no-ignore-without-reasonwarning#[ignore] without a comment explaining why
test-too-many-assertswarningTest with more than 10 assertions -- probably tests multiple things
test-duplicate-namewarningTwo test functions with the same name in the same module
test-no-descriptionwarningTest name is just test_it or test1 -- not descriptive
test-panics-as-assertionwarningTest relies on should_panic for logic that could be checked with assert!

Example: test-no-assertions

// This triggers test-no-assertions
#[test]
fn test_parse_config() {
    let config = parse_config("test.toml");
    // ... no assertion. The test "passes" even if parse_config
    // returns garbage.
}

// Fixed: assert on the result
#[test]
fn test_parse_config() {
    let config = parse_config("test.toml");
    assert_eq!(config.name, "my-project");
    assert!(config.features.contains("search"));
}

Silent Failures (18 rules)

These rules catch Rust-specific patterns where errors are quietly swallowed through idiomatic-looking code. The code compiles, clippy doesn't flag it, but the error is gone.

RuleSeverityWhat it catches
no-unwrap-or-defaultwarning.unwrap_or_default() on Result -- error replaced with empty/zero value
no-filter-map-okwarning.filter_map(Result::ok) -- silently drops all errors from an iterator
no-map-or-defaultwarning.map(...).unwrap_or_default() -- failure hidden behind a default
no-ok-or-defaultwarning.ok().unwrap_or_default() -- converts error to None then to a default
no-silent-dropwarninglet _ = fallible_fn() -- result explicitly discarded
no-or-default-on-optionwarning.or(Some(default)) where the default hides a meaningful None
no-flatten-resultwarning.flatten() on nested Result or Option -- inner error lost
no-and-then-ignorewarning.and_then(|_| ...) -- input value ignored
no-silent-okwarning.ok() converting Result to Option -- error information discarded
no-match-wildcard-errwarning_ => arm in a match on Result -- catches Err without handling it
no-if-let-ignore-errwarningif let Ok(v) = expr without an else -- error path does nothing
no-while-let-ignore-errwarningwhile let Ok(v) = expr -- loop silently stops on first error
no-option-map-resultwarning.map() on Option that returns Result -- inner error unhandled
no-try-in-mapwarning? inside .map() closure -- error propagates out of the closure unexpectedly
no-ignore-join-errorwarningtokio::join! result not checked -- task panic silently ignored
no-detach-without-logwarningtokio::spawn without logging or handling the JoinHandle error
no-silent-timeoutwarning.timeout().await where the timeout error is discarded
no-collect-ignore-errorswarning.collect::<Vec<_>>() on an iterator of Result -- errors silently filtered

Example: no-filter-map-ok

This pattern looks clean but silently drops every error in the iterator:

// This triggers no-filter-map-ok
let valid_configs: Vec<Config> = paths
    .iter()
    .map(|p| parse_config(p))
    .filter_map(Result::ok)  // Errors? What errors?
    .collect();

// Fixed: handle errors explicitly
let (configs, errors): (Vec<_>, Vec<_>) = paths
    .iter()
    .map(|p| parse_config(p))
    .partition_result();

if !errors.is_empty() {
    tracing::warn!("{} configs failed to parse", errors.len());
}

TypeScript (3 rules)

8Vast includes a small set of TypeScript rules for mixed Rust/TypeScript codebases. For comprehensive TypeScript linting, ESLint is more appropriate -- 8Vast runs it automatically if installed.

RuleSeverityWhat it catches
ts-no-anywarningany type annotation -- defeats TypeScript's type system
ts-no-non-null-assertionwarningvalue!.property -- non-null assertion operator, will throw at runtime if null
ts-no-console-logwarningconsole.log() left in production code

Listing and exploring rules

# List all rules with their categories and severities
8v lint rules

# Filter by category
8v lint rules --category error-handling

# Get detailed info on a specific rule
8v lint explain error-swallowed

8v lint explain shows the rule's description, code examples (what it catches and what it allows), the default severity, and how to configure or suppress it.

Next