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.
| Rule | Severity | What it catches |
|---|---|---|
error-swallowed | error | Err(_) => {} -- error variant matched and ignored |
error-chain-broken | error | .map_err(|_| ...) -- original error discarded, context lost |
error-to-default | warning | .unwrap_or_default() on Result -- missing data hidden behind a default value |
error-wrong-log-level | warning | Error logged at debug! or info! instead of error! or warn! |
public-anyhow-result | warning | Public function returning anyhow::Result -- callers can't match on error variants |
err-with-context | warning | Err(e).into() without .context() -- error propagated without explaining where it happened |
error-to-string | warning | .to_string() on an error type -- downgrades a typed error to an opaque string |
result-unit-error | warning | Result<T, ()> -- error type carries no information |
error-in-ok-position | error | Returning an error inside Ok(...) -- likely a mistake |
unhandled-try | warning | let _ = fallible_fn()? -- result of try expression discarded |
catch-all-error | warning | catch_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.
| Rule | Severity | What it catches |
|---|---|---|
no-process-exit | error | process::exit() in library code -- kills the entire process without cleanup |
undocumented-unsafe | error | unsafe block without a // SAFETY: comment explaining why it's sound |
panic-in-result-fn | error | panic!, unreachable!, or todo! in a function that returns Result |
no-blocking-in-async | error | std::fs, std::net, or thread::sleep called inside an async fn |
no-unwrap | warning | .unwrap() on Option or Result -- will panic on None/Err |
no-expect-without-msg | warning | .expect("") with an empty message -- adds a panic point with no context |
no-todo-macro | warning | todo!() left in non-test code |
no-unreachable | warning | unreachable!() outside of exhaustive match arms |
no-dbg | warning | dbg!() macro left in code (debug-only, should not ship) |
no-println | warning | println! 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.
| Rule | Severity | What it catches |
|---|---|---|
clone-in-loop | warning | .clone() called inside a loop body -- often allocates per iteration |
await-holding-lock | error | .await while holding a MutexGuard or RwLockGuard |
large-future | warning | Future exceeds 1KB on the stack (causes expensive allocations when spawned) |
no-blocking-io-in-async | warning | std::io::Read/Write in async context -- use async equivalents |
collect-then-iter | warning | .collect::<Vec<_>>().iter() -- collects into a vec only to iterate it again |
redundant-clone | warning | .clone() on a value that's not used after the clone |
string-format-in-loop | warning | format!() inside a loop -- consider pre-allocating or using write! |
vec-push-in-loop | warning | Vec::push in a loop without with_capacity -- may reallocate multiple times |
box-default | warning | Box::new(Default::default()) -- use Box::default() instead |
large-stack-array | warning | Stack-allocated array exceeding 4KB |
unnecessary-allocation | warning | Heap allocation where a reference would work (e.g., &String param instead of &str) |
map-entry | warning | .contains_key() followed by .insert() -- use the entry API |
single-char-pattern | warning | .split("x") where .split('x') (char literal) is faster |
manual-memcpy | warning | Loop copying elements between slices -- use copy_from_slice |
large-enum-variant | warning | Enum 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.
| Rule | Severity | What it catches |
|---|---|---|
no-sql-string-concat | error | SQL query built with format! or string concatenation -- use parameterized queries |
no-command-injection | error | Command::new with user-controlled input in arguments |
no-hardcoded-secrets | error | String literals matching patterns for API keys, passwords, tokens |
no-path-traversal | warning | File path constructed from user input without sanitization |
no-crypto-weak | warning | Use of MD5, SHA1, or other weak cryptographic algorithms |
no-http-plaintext | warning | http:// URLs in non-test code (should be https://) |
no-world-readable-permissions | warning | File 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.
| Rule | Severity | Default | What it measures |
|---|---|---|---|
function-too-long | warning | 50 lines | Function body length |
file-too-long | warning | 500 lines | Total file length |
too-many-params | warning | 6 | Number of function parameters |
nesting-too-deep | warning | 3 levels | Deepest nesting of control flow (if, match, for, while, loop) |
too-many-let-bindings | warning | 15 | Let bindings in a single function |
cognitive-complexity | warning | 15 | Branching complexity (counts branches, nesting, and recursion) |
trait-too-many-methods | warning | 8 | Methods defined on a single trait |
struct-excessive-bools | warning | 3 | Boolean fields on a single struct |
test-too-long | warning | 50 lines | Test 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,matcharm) - Each level of nesting (nested
ifinsideforinsidematch) - Each
break,continue, or earlyreturn - 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.
| Rule | Severity | What it catches |
|---|---|---|
test-no-assertions | warning | Test function with no assert!, assert_eq!, or assert_ne! |
test-assert-true | warning | assert!(true) or assert_eq!(1, 1) -- always passes, tests nothing |
test-assert-eq-bool | warning | assert_eq!(result, true) -- use assert!(result) instead |
sleepy-test | warning | thread::sleep or tokio::time::sleep in a test -- fragile timing dependency |
no-ignore-without-reason | warning | #[ignore] without a comment explaining why |
test-too-many-asserts | warning | Test with more than 10 assertions -- probably tests multiple things |
test-duplicate-name | warning | Two test functions with the same name in the same module |
test-no-description | warning | Test name is just test_it or test1 -- not descriptive |
test-panics-as-assertion | warning | Test 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.
| Rule | Severity | What it catches |
|---|---|---|
no-unwrap-or-default | warning | .unwrap_or_default() on Result -- error replaced with empty/zero value |
no-filter-map-ok | warning | .filter_map(Result::ok) -- silently drops all errors from an iterator |
no-map-or-default | warning | .map(...).unwrap_or_default() -- failure hidden behind a default |
no-ok-or-default | warning | .ok().unwrap_or_default() -- converts error to None then to a default |
no-silent-drop | warning | let _ = fallible_fn() -- result explicitly discarded |
no-or-default-on-option | warning | .or(Some(default)) where the default hides a meaningful None |
no-flatten-result | warning | .flatten() on nested Result or Option -- inner error lost |
no-and-then-ignore | warning | .and_then(|_| ...) -- input value ignored |
no-silent-ok | warning | .ok() converting Result to Option -- error information discarded |
no-match-wildcard-err | warning | _ => arm in a match on Result -- catches Err without handling it |
no-if-let-ignore-err | warning | if let Ok(v) = expr without an else -- error path does nothing |
no-while-let-ignore-err | warning | while let Ok(v) = expr -- loop silently stops on first error |
no-option-map-result | warning | .map() on Option that returns Result -- inner error unhandled |
no-try-in-map | warning | ? inside .map() closure -- error propagates out of the closure unexpectedly |
no-ignore-join-error | warning | tokio::join! result not checked -- task panic silently ignored |
no-detach-without-log | warning | tokio::spawn without logging or handling the JoinHandle error |
no-silent-timeout | warning | .timeout().await where the timeout error is discarded |
no-collect-ignore-errors | warning | .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.
| Rule | Severity | What it catches |
|---|---|---|
ts-no-any | warning | any type annotation -- defeats TypeScript's type system |
ts-no-non-null-assertion | warning | value!.property -- non-null assertion operator, will throw at runtime if null |
ts-no-console-log | warning | console.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
- Getting Started -- run your first lint
- Configuration -- adjust thresholds and disable rules
- Custom Rules -- write your own rules in YAML