Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7ec5821
add misbound_arg
alii Aug 15, 2026
9999b90
add bypassed_conversion
alii Aug 15, 2026
13aaef5
add same_match_twice and reimplemented_helper
alii Aug 15, 2026
0778c2c
add dependent_field
alii Aug 15, 2026
a1476dc
add collapsed_error
alii Aug 15, 2026
4108cac
add uneven_narrowing
alii Aug 15, 2026
70bee8e
add crossed_index
alii Aug 15, 2026
2292bdf
dependent_field stays quiet on fields assigned outside the case
alii Aug 15, 2026
7a36e3f
add bool_beside_option
alii Aug 15, 2026
3b1a9b8
add parallel_vecs
alii Aug 15, 2026
18b09ff
collapsed_error stays quiet on zero-sized errors
alii Aug 15, 2026
62c007d
add sentinel_int
alii Aug 15, 2026
7caf758
add stringly_state
alii Aug 15, 2026
e5ba762
add parallel_params
alii Aug 15, 2026
9e7b301
add bool_params
alii Aug 15, 2026
f22af50
misbound_arg stays quiet on receivers and symmetric pairs
alii Aug 15, 2026
9d7e19c
add unnamed_tuple
alii Aug 15, 2026
4defed7
misbound_arg stays quiet when the namesake slot holds a literal
alii Aug 15, 2026
5ee8690
parallel_params counts plain data handed between functions
alii Aug 15, 2026
aaf0785
bypassed_conversion matches the transmute's own types
alii Aug 15, 2026
9657f5b
uneven_narrowing skips repr structs, round trips and if-let ranges
alii Aug 15, 2026
842e5ba
stringly_state sees ref mut bindings, indexed writes and field copies
alii Aug 15, 2026
1330be9
dependent_field counts ..base sites and the values assigned
alii Aug 15, 2026
d1bd233
compare parameter patterns and shared locals in structural clones
alii Aug 15, 2026
fda3a81
sentinel_int reads asserts, match arms and sums, skips keyed lookups
alii Aug 15, 2026
a07881a
bool_beside_option sees ref mut bindings, bare arms, exits and derives
alii Aug 15, 2026
35cb902
unnamed_tuple reads destructuring assignments and skips closure returns
alii Aug 15, 2026
44d1581
collapsed_error skips kind-naming arms, sees loop bodies and &str errors
alii Aug 15, 2026
088d6af
crossed_index reads place names first, skips typed indices
alii Aug 15, 2026
4cb3876
parallel_vecs counts any mutable access as a length change
alii Aug 15, 2026
9b04fcb
misbound_arg drops the literal carve-out
alii Aug 15, 2026
be4f71e
add crossed_alias
alii Aug 15, 2026
c711fba
crossed_alias reads comparisons, skips cfg-selected aliases
alii Aug 15, 2026
5d96649
crossed_alias sees tails after statements, branches and tuple structs
alii Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ Mordant will not find every defect, but what it reports is real: a lint that can
| `stale_across_reentry` | a length, flag, or pointer read off a field of `self`, then a call that can re-enter (closure, fn pointer, `dyn`, `.await`, configured), then the field used through it |
| `defaulted_failure` | `f(x).unwrap_or(0)` or `let Ok(v) = f(x) else { return Ok(()) }` where `f`'s own body rejects some of `x`: the rejection becomes a value and processing carries on |
| `unchecked_input_len` | opt-in via `unchecked-input-len-enabled`: a received integer bounded on one path and turned into memory (`split_at`, `set_len`, `ptr.add`) on a path no check dominates |
| `misbound_arg` | `resize(height, width)` against `fn resize(width: u32, height: u32)`: an argument named as another parameter of the same type, so only its position says which it is |
| `bypassed_conversion` | `mem::transmute` or a pointer cast into a type outside its own module and impls, when a `From`/`TryFrom` impl or constructor already converts that same source into it |
| `same_match_twice` | the same `match` over one enum written out arm for arm in two places: a mapping the enum should state once as a method, kept in step by hand instead |
| `reimplemented_helper` | a function whose signature and body repeat another function in the crate under a different name: one helper written twice, so a fix to one copy misses the other |
| `dependent_field` | a field every reader tests a sibling for one value before touching, and every other construction fills with a placeholder: an enum payload stored flat beside its tag |
| `collapsed_error` | `f(x);` or `let _ = f(x)` on a crate fn whose `false`/`None` is the bare `Err` arm of a `Result` it held: the typed error became one bit, and this call drops the bit |
| `uneven_narrowing` | an integer field or local converted with `try_from` at one site and a bare `as` at another: the check says the value may not fit, and `as` wraps silently when it doesn't |
| `crossed_index` | `parts[source_index]` in a function that indexes `parts` by `part_index` and `sources` by `source_index`: two index kinds cross, and both are plain integers |
| `parallel_vecs` | sequence fields of one struct that only change length side by side and are read at one index: element `i` of each is one record, so the type lets the lengths differ |
| `bool_beside_option` | a bool field written only beside an `Option` field, `true` with `Some(..)` and `false` with `None`: it is that field's `is_some()` stored twice, kept equal only by habit |
| `sentinel_int` | an integer field one function tests against `MAX`, `-1` or an `INVALID` constant and another indexes with or offsets a pointer by untested: `Option` spelled as an int |
| `stringly_state` | a string field or local only ever storing one of a closed set of literals and then compared against them: an undeclared enum, so a misspelt state still compiles |
| `parallel_params` | opt-in via `parallel-params-enabled`: parameters several functions declare alike and hand each other unchanged in one call: one value with no type, passable by halves |
| `bool_params` | a crate-private fn with two or more `bool` parameters that a call fills with bare `true`/`false`: `f(x, true, false)` names neither flag, and the swapped call compiles |
| `unnamed_tuple` | a private fn's tuple return with two members of one type that every caller destructures under the same names: only the type lacks them, and it accepts them transposed |
| `crossed_alias` | a `DependencyId` value passed, stored, bound, returned or compared where a `PackageId` is declared, both aliasing one integer: two id kinds only the aliases tell apart |

Each diagnostic states what the lint found, why the type is wrong, and the type that replaces it.

Expand Down Expand Up @@ -82,18 +98,22 @@ wildcard-local-enum-max-variants = 12
exclusive-options-min-fields = 2
flag-cluster-min-bools = 3
stored-projection-min-sites = 2
reimplemented-helper-min-nodes = 12
parallel-params-min-fns = 3

# Opt-in: also count `Box<dyn Error>` as a stringly error type.
stringly-error-include-box-dyn = true

# Opt-in: `flag_cluster`, `stale_safety_comment` and `unchecked_input_len` are
# surveys to run once over a codebase (most of what they name is legitimate
# once the real cases are fixed; for the last, a length the caller vouches for
# that the function also uses as some other value's limit), so they are off
# until turned on here.
# Opt-in: `flag_cluster`, `stale_safety_comment`, `unchecked_input_len` and
# `parallel_params` are surveys to run once over a codebase (most of what they
# name is legitimate once the real cases are fixed; for the third, a length the
# caller vouches for that the function also uses as some other value's limit;
# for the last, a buffer and a cursor into it, passed along together by
# design), so they are off until turned on here.
flag-cluster-enabled = true
stale-safety-comment-enabled = true
unchecked-input-len-enabled = true
parallel-params-enabled = true

# Opt-in: flag composite keys (tuples, structs one level deep) that carry a
# denied type unless one of the fixing types sits beside it. With these two
Expand Down
57 changes: 57 additions & 0 deletions src/adt_facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
//! lint fire or not -- privacy, `is_struct`, a minimum field count -- stays in
//! the lint, so this module only ever answers, never decides.

use rustc_hir::def::DefKind;
use rustc_hir::def_id::DefId;
use rustc_hir::{HirId, find_attr};
use rustc_lint::LateContext;
use rustc_middle::ty::{self, AdtDef, FieldDef, Ty, TyCtxt, VariantDef};
use rustc_span::{Symbol, sym};
Expand Down Expand Up @@ -107,3 +109,58 @@ pub(crate) fn result_err_ty<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> Option<Ty<
};
(tcx.is_diagnostic_item(sym::Result, adt.did()) && args.len() == 2).then(|| args.type_at(1))
}

/// Whether the code at `at` is the ADT's own: inside the module defining it
/// (when this crate defines it) or inside any impl block, inherent or trait,
/// whose self type it is.
pub(crate) fn in_own_code_of(cx: &LateContext<'_>, at: HirId, adt: DefId) -> bool {
if let Some(local) = adt.as_local()
&& cx.tcx.parent_module(at) == cx.tcx.parent_module_from_def_id(local)
{
return true;
}
let mut cur = cx.tcx.hir_enclosing_body_owner(at).to_def_id();
while let Some(parent) = cx.tcx.opt_parent(cur) {
if matches!(cx.tcx.def_kind(parent), DefKind::Impl { .. })
&& impl_self_adt(cx, parent).is_some_and(|a| a.did() == adt)
{
return true;
}
cur = parent;
}
false
}

/// True when `hir_id` sits inside a TRAIT impl whose self type is `adt_did`.
/// `Display`, `Debug`, `From` and derive expansions must match every variant
/// to exist, so their patterns prove nothing. Inherent methods are not
/// excluded: an accessor like `fn tenths(&self)` is the crate genuinely
/// reading the structure.
pub(crate) fn inside_own_trait_impl(cx: &LateContext<'_>, hir_id: HirId, adt_did: DefId) -> bool {
let mut cur = hir_id.owner.def_id.to_def_id();
loop {
if matches!(cx.tcx.def_kind(cur), DefKind::Impl { of_trait: true })
&& impl_self_adt(cx, cur).is_some_and(|adt| adt.did() == adt_did)
{
return true;
}
match cx.tcx.opt_parent(cur) {
Some(p) => cur = p,
None => return false,
}
}
}

/// The definition, or a module enclosing it, carries a `#[cfg]`: what it
/// names is chosen per platform or feature rather than fixed by the program.
pub(crate) fn cfg_selected(cx: &LateContext<'_>, mut did: DefId) -> bool {
loop {
if find_attr!(cx.tcx, did, CfgTrace(..)) {
return true;
}
match cx.tcx.opt_parent(did) {
Some(p) => did = p,
None => return false,
}
}
}
4 changes: 2 additions & 2 deletions src/asymmetric_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use rustc_span::{Span, Symbol};
use crate::adt_facts::impl_self_adt;
use crate::baseline::emit;
use crate::hir_shapes::{
Callee, callee_of, ends_in_return, is_self_path, peel_not, self_field, stmt_expr,
Callee, SelfField, callee_of, ends_in_return, is_self_path, peel_not, self_field, stmt_expr,
};

rustc_session::declare_lint! {
Expand Down Expand Up @@ -205,7 +205,7 @@ impl<'tcx> LateLintPass<'tcx> for AsymmetricGuard {
// other `self` reaching the walk is one that got away.
let mut followed: HashSet<HirId> = HashSet::new();
for_each_expr(cx, body.value, |e: &Expr<'_>| {
if let Some((base, ident)) = self_field(e) {
if let Some(SelfField { base, ident }) = self_field(e) {
facts.touched.insert(ident.name);
followed.insert(base.hir_id);
} else if let Some((m, _)) = self_method_call(cx, e)
Expand Down
Loading
Loading