Skip to content

Lints

The Gossamer linter ships 56 day-one checks. Each has a short identifier suitable for #[lint(allow(...))] and a long-form explanation available via gos lint --explain <id>. This page is auto-generated from gossamer-lint; hand edits are overwritten on the next run of cargo xtask docs-lints.

Code Identifier Default
GL0001 unused_variable warn
GL0002 unused_import warn
GL0003 unused_mut_variable warn
GL0004 needless_return warn
GL0005 needless_bool warn
GL0006 comparison_to_bool_literal warn
GL0007 single_match warn
GL0008 shadowed_binding warn
GL0009 unchecked_result warn
GL0010 empty_block warn
GL0011 panic_in_main warn
GL0012 redundant_clone warn
GL0013 double_negation warn
GL0014 self_assignment warn
GL0015 todo_macro warn
GL0054 i64_only_container_family warn
GL0056 no_effect_statement warn
GL0016 bool_literal_in_condition warn
GL0017 let_and_return warn
GL0018 collapsible_if warn
GL0019 if_same_then_else warn
GL0021 needless_else_after_return warn
GL0022 self_compare warn
GL0023 identity_op warn
GL0024 unit_let warn
GL0025 float_eq_zero warn
GL0026 empty_else warn
GL0027 match_bool warn
GL0028 needless_parens warn
GL0029 manual_not_equal warn
GL0030 nested_ternary_if warn
GL0031 absurd_range warn
GL0032 string_literal_concat warn
GL0033 chained_negation_literals warn
GL0034 if_not_else warn
GL0035 empty_string_concat warn
GL0036 println_newline_only warn
GL0037 match_same_arms warn
GL0038 manual_swap warn
GL0039 consecutive_assignment warn
GL0040 large_unreadable_literal warn
GL0041 redundant_closure warn
GL0042 empty_if_body warn
GL0043 bool_to_int_match warn
GL0044 fn_returns_unit_explicit warn
GL0045 let_with_unit_type warn
GL0046 useless_default_only_match warn
GL0047 unnecessary_parens_in_condition warn
GL0048 pattern_matching_unit warn
GL0049 panic_without_message warn
GL0050 empty_loop warn
GL0051 fill_loop warn
GL0052 substring_byte_scan warn
GL0057 non_camel_case_types warn
GL0058 non_snake_case warn
GL0059 non_upper_case_globals warn

unused_variable

Declares a let binding whose name is never read. Prefix the name with _ to silence explicitly (e.g. _tmp) when the binding is intentional but unused.

unused_import

A use declaration whose imported name is never referenced. Remove the import or reference the name in the file.

unused_mut_variable

A binding marked mut that is never reassigned. Drop the mut keyword.

needless_return

return expr at the tail of a block is the same as writing expr by itself. Prefer the tail form for symmetry with the rest of the expression language.

needless_bool

if cond { true } else { false } is the same as cond. The inverted form is !cond.

comparison_to_bool_literal

x == true reads worse than x, and x == false reads worse than !x. Drop the literal.

single_match

A match with a single arm reads better as if let PATTERN = .... Single-arm match is almost always a half-written exhaustive match.

shadowed_binding

A let binding in the same block shadows an earlier one. Rename one of them to make the data flow obvious.

unchecked_result

let _ = expr? silently discards an error. Either handle the Err branch explicitly or propagate with ? so the caller sees it.

empty_block

An empty {} block is almost always a mistake. Add an explicit () tail if the block is intentional.

panic_in_main

panic! inside main aborts without a clean exit code. Return a Result from main and use ? so the error propagates.

redundant_clone

Calling .clone() on a literal or already-copied value is redundant. Drop the call.

double_negation

!!x collapses to x when x: bool. If the double negation is intentional for truthiness coercion, use an explicit cast.

self_assignment

Assigning a variable to itself does nothing. The statement is usually the residue of a refactor - remove it.

todo_macro

todo() and unimplemented() are placeholders, not shippable expressions. Implement the branch before merging.

i64_only_container_family

std::collections::{queue,stack,deque,heap,ordered_*} hold i64 only and return a new container from every mutator, which duplicates Queue, Stack, Deque, MinHeap, BTreeSet, and BTreeMap at a narrower element type. Deliberately not auto-fixable: the two families differ in more than spelling. peek answers 0 on an empty container where the type answers None, so a mechanical rewrite would keep type-checking and change what an empty container means.

no_effect_statement

An expression statement that only computes - a path, a literal, arithmetic over them - and hands its value to nothing. A newline followed by -, *, or & starts a NEW statement, so a continuation meant as part of the line above becomes one of these. Join it to that line (end the previous line with the operator, or parenthesise), or bind what it computes. A builtin method that answers a new value and writes nothing - v.dedup(), s.trim(), s.to_uppercase() - is reported the same way when its result is dropped: the receiver is unchanged, so bind the result (v = v.dedup()).

bool_literal_in_condition

if true { ... } / while false { ... } - the branch is decided at compile time. Drop the control-flow construct.

let_and_return

let x = expr; x at the tail of a block is just expr. Drop the needless binding.

collapsible_if

if a { if b { ... } } can be combined into if a && b { ... }. Easier to scan.

if_same_then_else

Both branches of the if are identical. Drop the branch and keep the body once.

needless_else_after_return

if cond { return X } else { Y } - the else is unreachable fall-through. Un-nest the else body.

self_compare

Comparing a value to itself is always true (for ==, <=, >=) or false (for !=, <, >). Use the constant.

identity_op

x + 0, x - 0, x * 1, x / 1 all equal x. The operation adds nothing but noise.

unit_let

let x = () binds the unit value, which is almost never useful. Drop the let.

float_eq_zero

Equality against a nonzero float literal is almost never what you want - floating-point arithmetic rarely produces the exact bit pattern. Compare (x - y).abs() < eps with an explicit tolerance. Exact zero is exempt: a guard such as if d == 0.0 before a division tests the one value that matters exactly.

empty_else

else {} adds no information. Drop the else and let the if stand alone.

match_bool

match b { true => ... false => ... } is an if in disguise. Rewrite as if b { ... } else { ... }.

needless_parens

(x) without a trailing comma is a needless pair of parens - x reads the same. (x,) is a one-tuple and means something different. Closure parameters follow the same rule: write |t|, not |(t)|.

manual_not_equal

!(a == b) is just a != b. Prefer the direct operator.

nested_ternary_if

Three or more if / else if layers that all test one value against a pattern are a match written the long way. Rewrite them as match on that value. A chain whose arms test unrelated conditions, or compare with < and >, is left alone: match would only move those into guards.

absurd_range

A literal range whose lower bound exceeds its upper bound is empty. Swap the bounds or double-check the intent.

string_literal_concat

"a" + "b" can be written directly as "ab". Let the source reflect the final value.

chained_negation_literals

-(-x) is x. The extra unary does nothing.

if_not_else

if !cond { A } else { B } scans better as if cond { B } else { A }. Flip the branches and drop the !.

empty_string_concat

Concatenating an empty string literal is a no-op. Drop the "" + or + "".

println_newline_only

println("") already writes a newline. Don't pass "\n" and don't call it twice to emit a blank line.

match_same_arms

Two match arms share the same body. Either collapse them with | alternation or extract the shared body into a helper.

manual_swap

Three consecutive statements let tmp = a; a = b; b = tmp swap two bindings via a temporary. Prefer a destructuring assignment once the language supports it, or at minimum document why the swap is needed.

consecutive_assignment

Two back-to-back assignments to the same place - the earlier value is dead before it's read. Drop the first or consolidate the logic into one statement.

large_unreadable_literal

Integer literals of five or more digits are easier to scan with _ as thousands separators: 1_000_000 instead of 1000000.

redundant_closure

|x| f(x) is a closure that just forwards to f. Pass f directly.

empty_if_body

An if cond { } else { body } is the same as if !cond { body }. Invert the condition and drop the empty branch.

bool_to_int_match

match b { true => 1, false => 0 } is an if in disguise that happens to return an integer. Prefer if b { 1 } else { 0 }.

fn_returns_unit_explicit

fn f() -> () { ... } is the same as fn f() { ... }. The explicit -> () is noise.

let_with_unit_type

let _: () = expr annotates the binding with the unit type. If expr was going to return () anyway, the annotation is noise. If it wasn't, the annotation forces a coercion - use a plain statement instead.

useless_default_only_match

match x { _ => expr } always runs expr - the match adds nothing. Drop the match (and add let _ = x if evaluating the scrutinee has side effects).

unnecessary_parens_in_condition

if (cond) { ... } wraps the condition in a single-tuple expression. Drop the parens: if cond { ... }.

pattern_matching_unit

match () { ... } has exactly one reachable arm. Drop the match and run the body directly.

panic_without_message

panic() with no argument leaves the post-mortem with nothing to render. Always pass a brief explanation.

empty_loop

loop {} with no body busy-waits forever at 100% CPU. Add a break, a continue, or replace with a real wait primitive.

fill_loop

A for _ in 0..n { xs.push(value) } loop that pushes the same literal every iteration is the repeat literal spelled long: let xs = [value; n] builds the identical vec in one expression (byte-packed for bool / u8 elements).

substring_byte_scan

s.substring(i, i + 1) allocates a String per step of a scan. substring takes byte offsets, so s.byte_at(i) reads the same position directly as an i64: compare with byte literals (s.byte_at(i) >= b'0'), do arithmetic on it (s.byte_at(i) - b'0'), and render with s.byte_at(i) as char. s[i] is not that byte: indexing counts Unicode scalars, so it yields the char at character index i.

non_camel_case_types

A type, trait, enum variant, associated type, or type parameter whose name is not UpperCamelCase, as Rust spells them: Point, HttpServer, KeyA. A foreign type keeps its C spelling.

non_snake_case

A function, method, parameter, local, field, or module whose name is not snake_case, as Rust spells them. A single name in a pattern matches a variant, struct, or const it names and binds otherwise, so a binding written Width reads as a variant it is not. A foreign or #[export] function and a #[repr(C)] struct's fields keep their C spelling.

non_upper_case_globals

A const or static whose name is not SCREAMING_SNAKE_CASE, as Rust spells them. A single name in a pattern matches the const it names, so limit => compares against a const limit rather than binding; the upper case spelling keeps that visible. A foreign static keeps its C spelling.