From ff9a1757f13d38ea337832b0e800ab5e3b9c2904 Mon Sep 17 00:00:00 2001
From: Peter Jaszkowiak
Date: Sat, 3 May 2025 22:58:27 -0600
Subject: [PATCH 01/21] refactor ub_checks and contract_checks to share logic
---
clippy_utils/src/qualify_min_const_fn.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clippy_utils/src/qualify_min_const_fn.rs b/clippy_utils/src/qualify_min_const_fn.rs
index 90ea2616890a..0cf1ad348953 100644
--- a/clippy_utils/src/qualify_min_const_fn.rs
+++ b/clippy_utils/src/qualify_min_const_fn.rs
@@ -194,7 +194,7 @@ fn check_rvalue<'tcx>(
))
}
},
- Rvalue::NullaryOp(NullOp::OffsetOf(_) | NullOp::UbChecks | NullOp::ContractChecks, _)
+ Rvalue::NullaryOp(NullOp::OffsetOf(_) | NullOp::RuntimeChecks(_), _)
| Rvalue::ShallowInitBox(_, _) => Ok(()),
Rvalue::UnaryOp(_, operand) => {
let ty = operand.ty(body, cx.tcx);
From 07b6b325e2b92c08aedfe8db56df7e76825d191b Mon Sep 17 00:00:00 2001
From: Oli Scherer
Date: Mon, 21 Jul 2025 10:07:35 +0000
Subject: [PATCH 02/21] Trait aliases are rare large ast nodes, box them
---
clippy_utils/src/ast_utils/mod.rs | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/clippy_utils/src/ast_utils/mod.rs b/clippy_utils/src/ast_utils/mod.rs
index 22b74ab16d61..4df1eb508713 100644
--- a/clippy_utils/src/ast_utils/mod.rs
+++ b/clippy_utils/src/ast_utils/mod.rs
@@ -473,9 +473,18 @@ pub fn eq_item_kind(l: &ItemKind, r: &ItemKind) -> bool {
&& over(lb, rb, eq_generic_bound)
&& over(lis, ris, |l, r| eq_item(l, r, eq_assoc_item_kind))
},
- (TraitAlias(li, lg, lb), TraitAlias(ri, rg, rb)) => {
- eq_id(*li, *ri) && eq_generics(lg, rg) && over(lb, rb, eq_generic_bound)
- },
+ (
+ TraitAlias(box ast::TraitAlias {
+ ident: li,
+ generics: lg,
+ bounds: lb,
+ }),
+ TraitAlias(box ast::TraitAlias {
+ ident: ri,
+ generics: rg,
+ bounds: rb,
+ }),
+ ) => eq_id(*li, *ri) && eq_generics(lg, rg) && over(lb, rb, eq_generic_bound),
(
Impl(ast::Impl {
generics: lg,
From 42f074df6207d12bb708fa03be1ef6b7e78b6964 Mon Sep 17 00:00:00 2001
From: Frank King
Date: Sun, 13 Apr 2025 22:57:37 +0800
Subject: [PATCH 03/21] Implement pattern matching for `&pin mut|const T`
---
clippy_lints/src/index_refutable_slice.rs | 2 +-
clippy_lints/src/matches/match_as_ref.rs | 2 +-
clippy_lints/src/matches/needless_match.rs | 2 +-
clippy_lints/src/matches/redundant_guards.rs | 2 +-
clippy_lints/src/methods/clone_on_copy.rs | 2 +-
clippy_lints/src/question_mark.rs | 8 ++++----
clippy_lints/src/toplevel_ref_arg.rs | 4 ++--
clippy_lints/src/utils/author.rs | 4 ++++
clippy_utils/src/eager_or_lazy.rs | 7 ++++++-
clippy_utils/src/lib.rs | 4 ++--
10 files changed, 23 insertions(+), 14 deletions(-)
diff --git a/clippy_lints/src/index_refutable_slice.rs b/clippy_lints/src/index_refutable_slice.rs
index 919702c5714a..e95816353df6 100644
--- a/clippy_lints/src/index_refutable_slice.rs
+++ b/clippy_lints/src/index_refutable_slice.rs
@@ -94,7 +94,7 @@ fn find_slice_values(cx: &LateContext<'_>, pat: &hir::Pat<'_>) -> FxIndexMap, arm: &Arm<'_>) -> Option {
.qpath_res(qpath, arm.pat.hir_id)
.ctor_parent(cx)
.is_lang_item(cx, LangItem::OptionSome)
- && let PatKind::Binding(BindingMode(ByRef::Yes(mutabl), _), .., ident, _) = first_pat.kind
+ && let PatKind::Binding(BindingMode(ByRef::Yes(_, mutabl), _), .., ident, _) = first_pat.kind
&& let ExprKind::Call(e, [arg]) = peel_blocks(arm.body).kind
&& e.res(cx).ctor_parent(cx).is_lang_item(cx, LangItem::OptionSome)
&& let ExprKind::Path(QPath::Resolved(_, path2)) = arg.kind
diff --git a/clippy_lints/src/matches/needless_match.rs b/clippy_lints/src/matches/needless_match.rs
index c9b6821ad98f..9c6cf66019f0 100644
--- a/clippy_lints/src/matches/needless_match.rs
+++ b/clippy_lints/src/matches/needless_match.rs
@@ -172,7 +172,7 @@ fn pat_same_as_expr(pat: &Pat<'_>, expr: &Expr<'_>) -> bool {
},
)),
) => {
- return !matches!(annot, BindingMode(ByRef::Yes(_), _)) && pat_ident.name == first_seg.ident.name;
+ return !matches!(annot, BindingMode(ByRef::Yes(..), _)) && pat_ident.name == first_seg.ident.name;
},
// Example: `Custom::TypeA => Custom::TypeB`, or `None => None`
(
diff --git a/clippy_lints/src/matches/redundant_guards.rs b/clippy_lints/src/matches/redundant_guards.rs
index d39e315cae1f..7a1dd94567b1 100644
--- a/clippy_lints/src/matches/redundant_guards.rs
+++ b/clippy_lints/src/matches/redundant_guards.rs
@@ -176,7 +176,7 @@ fn get_pat_binding<'tcx>(
if let PatKind::Binding(bind_annot, hir_id, ident, _) = pat.kind
&& hir_id == local
{
- if matches!(bind_annot.0, rustc_ast::ByRef::Yes(_)) {
+ if matches!(bind_annot.0, rustc_ast::ByRef::Yes(..)) {
let _ = byref_ident.insert(ident);
}
// the second call of `replace()` returns a `Some(span)`, meaning a multi-binding pattern
diff --git a/clippy_lints/src/methods/clone_on_copy.rs b/clippy_lints/src/methods/clone_on_copy.rs
index 2a0ae14a4b08..9cdad980f238 100644
--- a/clippy_lints/src/methods/clone_on_copy.rs
+++ b/clippy_lints/src/methods/clone_on_copy.rs
@@ -56,7 +56,7 @@ pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, receiver: &Expr<'_>)
_ => false,
},
// local binding capturing a reference
- Node::LetStmt(l) if matches!(l.pat.kind, PatKind::Binding(BindingMode(ByRef::Yes(_), _), ..)) => {
+ Node::LetStmt(l) if matches!(l.pat.kind, PatKind::Binding(BindingMode(ByRef::Yes(..), _), ..)) => {
return;
},
_ => false,
diff --git a/clippy_lints/src/question_mark.rs b/clippy_lints/src/question_mark.rs
index e67ea1f5e370..928a4e02fa97 100644
--- a/clippy_lints/src/question_mark.rs
+++ b/clippy_lints/src/question_mark.rs
@@ -150,7 +150,7 @@ fn check_let_some_else_return_none(cx: &LateContext<'_>, stmt: &Stmt<'_>) {
let init_expr_str = Sugg::hir_with_applicability(cx, init_expr, "..", &mut applicability).maybe_paren();
// Take care when binding is `ref`
let sugg = if let PatKind::Binding(
- BindingMode(ByRef::Yes(ref_mutability), binding_mutability),
+ BindingMode(ByRef::Yes(_,ref_mutability), binding_mutability),
_hir_id,
ident,
subpattern,
@@ -169,7 +169,7 @@ fn check_let_some_else_return_none(cx: &LateContext<'_>, stmt: &Stmt<'_>) {
// Handle subpattern (@ subpattern)
let maybe_subpattern = match subpattern {
Some(Pat {
- kind: PatKind::Binding(BindingMode(ByRef::Yes(_), _), _, subident, None),
+ kind: PatKind::Binding(BindingMode(ByRef::Yes(..), _), _, subident, None),
..
}) => {
// avoid `&ref`
@@ -504,8 +504,8 @@ fn check_if_let_some_or_err_and_early_return<'tcx>(cx: &LateContext<'tcx>, expr:
let receiver_str = snippet_with_applicability(cx, let_expr.span, "..", &mut applicability);
let requires_semi = matches!(cx.tcx.parent_hir_node(expr.hir_id), Node::Stmt(_));
let method_call_str = match by_ref {
- ByRef::Yes(Mutability::Mut) => ".as_mut()",
- ByRef::Yes(Mutability::Not) => ".as_ref()",
+ ByRef::Yes(_, Mutability::Mut) => ".as_mut()",
+ ByRef::Yes(_, Mutability::Not) => ".as_ref()",
ByRef::No => "",
};
let sugg = format!(
diff --git a/clippy_lints/src/toplevel_ref_arg.rs b/clippy_lints/src/toplevel_ref_arg.rs
index 074b79263d37..250c277ab5e1 100644
--- a/clippy_lints/src/toplevel_ref_arg.rs
+++ b/clippy_lints/src/toplevel_ref_arg.rs
@@ -61,7 +61,7 @@ impl<'tcx> LateLintPass<'tcx> for ToplevelRefArg {
) {
if !matches!(k, FnKind::Closure) {
for arg in iter_input_pats(decl, body) {
- if let PatKind::Binding(BindingMode(ByRef::Yes(_), _), ..) = arg.pat.kind
+ if let PatKind::Binding(BindingMode(ByRef::Yes(..), _), ..) = arg.pat.kind
&& is_lint_allowed(cx, REF_PATTERNS, arg.pat.hir_id)
&& !arg.span.in_external_macro(cx.tcx.sess.source_map())
{
@@ -80,7 +80,7 @@ impl<'tcx> LateLintPass<'tcx> for ToplevelRefArg {
fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) {
if let StmtKind::Let(local) = stmt.kind
- && let PatKind::Binding(BindingMode(ByRef::Yes(mutabl), _), .., name, None) = local.pat.kind
+ && let PatKind::Binding(BindingMode(ByRef::Yes(_, mutabl), _), .., name, None) = local.pat.kind
&& let Some(init) = local.init
// Do not emit if clippy::ref_patterns is not allowed to avoid having two lints for the same issue.
&& is_lint_allowed(cx, REF_PATTERNS, local.pat.hir_id)
diff --git a/clippy_lints/src/utils/author.rs b/clippy_lints/src/utils/author.rs
index 68e51dace2db..92cc11dae8b1 100644
--- a/clippy_lints/src/utils/author.rs
+++ b/clippy_lints/src/utils/author.rs
@@ -745,10 +745,14 @@ impl<'a, 'tcx> PrintVisitor<'a, 'tcx> {
let ann = match ann {
BindingMode::NONE => "NONE",
BindingMode::REF => "REF",
+ BindingMode::REF_PIN => "REF_PIN",
BindingMode::MUT => "MUT",
BindingMode::REF_MUT => "REF_MUT",
+ BindingMode::REF_PIN_MUT => "REF_PIN_MUT",
BindingMode::MUT_REF => "MUT_REF",
+ BindingMode::MUT_REF_PIN => "MUT_REF_PIN",
BindingMode::MUT_REF_MUT => "MUT_REF_MUT",
+ BindingMode::MUT_REF_PIN_MUT => "MUT_REF_PIN_MUT",
};
kind!("Binding(BindingMode::{ann}, _, {name}, {sub})");
self.ident(name);
diff --git a/clippy_utils/src/eager_or_lazy.rs b/clippy_utils/src/eager_or_lazy.rs
index eb3f442ac754..6b922a20ca20 100644
--- a/clippy_utils/src/eager_or_lazy.rs
+++ b/clippy_utils/src/eager_or_lazy.rs
@@ -212,7 +212,12 @@ fn expr_eagerness<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> EagernessS
// Custom `Deref` impl might have side effects
ExprKind::Unary(UnOp::Deref, e)
- if self.cx.typeck_results().expr_ty(e).builtin_deref(true).is_none() =>
+ if self
+ .cx
+ .typeck_results()
+ .expr_ty(e)
+ .builtin_deref(true)
+ .is_none() =>
{
self.eagerness |= NoChange;
},
diff --git a/clippy_utils/src/lib.rs b/clippy_utils/src/lib.rs
index 6ee991eae137..7b3de69d9086 100644
--- a/clippy_utils/src/lib.rs
+++ b/clippy_utils/src/lib.rs
@@ -783,7 +783,7 @@ pub fn capture_local_usage(cx: &LateContext<'_>, e: &Expr<'_>) -> CaptureKind {
ByRef::No if !is_copy(cx, cx.typeck_results().node_type(id)) => {
capture = CaptureKind::Value;
},
- ByRef::Yes(Mutability::Mut) if capture != CaptureKind::Value => {
+ ByRef::Yes(_, Mutability::Mut) if capture != CaptureKind::Value => {
capture = CaptureKind::Ref(Mutability::Mut);
},
_ => (),
@@ -1831,7 +1831,7 @@ pub fn is_expr_identity_of_pat(cx: &LateContext<'_>, pat: &Pat<'_>, expr: &Expr<
.typeck_results()
.pat_binding_modes()
.get(pat.hir_id)
- .is_some_and(|mode| matches!(mode.0, ByRef::Yes(_)))
+ .is_some_and(|mode| matches!(mode.0, ByRef::Yes(..)))
{
// If the parameter is `(x, y)` of type `&(T, T)`, or `[x, y]` of type `&[T; 2]`, then
// due to match ergonomics, the inner patterns become references. Don't consider this
From 8992878605e2ffcc603f1def512e7b73754869e6 Mon Sep 17 00:00:00 2001
From: Frank King
Date: Fri, 24 Oct 2025 20:59:33 +0800
Subject: [PATCH 04/21] Rename `#[pin_project]` to `#[pin_v2]` to avoid naming
conflicts
---
tests/ui/explicit_write_in_test.stderr | 0
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 tests/ui/explicit_write_in_test.stderr
diff --git a/tests/ui/explicit_write_in_test.stderr b/tests/ui/explicit_write_in_test.stderr
deleted file mode 100644
index e69de29bb2d1..000000000000
From 0fd7406e4d646fb9711004baa157187a7802c659 Mon Sep 17 00:00:00 2001
From: Scott Schafer
Date: Wed, 22 Oct 2025 09:58:23 -0600
Subject: [PATCH 05/21] fix: Only special case single line item attribute
suggestions
---
tests/ui/new_without_default.stderr | 3 ---
1 file changed, 3 deletions(-)
diff --git a/tests/ui/new_without_default.stderr b/tests/ui/new_without_default.stderr
index 1e0d5e213199..0593dbb00fb6 100644
--- a/tests/ui/new_without_default.stderr
+++ b/tests/ui/new_without_default.stderr
@@ -191,7 +191,6 @@ LL + fn default() -> Self {
LL + Self::new()
LL + }
LL + }
-LL | impl NewWithCfg {
|
error: you should consider adding a `Default` implementation for `NewWith2Cfgs`
@@ -212,7 +211,6 @@ LL + fn default() -> Self {
LL + Self::new()
LL + }
LL + }
-LL | impl NewWith2Cfgs {
|
error: you should consider adding a `Default` implementation for `NewWithExtraneous`
@@ -250,7 +248,6 @@ LL + fn default() -> Self {
LL + Self::new()
LL + }
LL + }
-LL | impl NewWithCfgAndExtraneous {
|
error: aborting due to 13 previous errors
From 3f5ea6beb4d7944006f8eb97ba3e43fbf450f2cd Mon Sep 17 00:00:00 2001
From: Oli Scherer
Date: Mon, 21 Jul 2025 10:04:10 +0000
Subject: [PATCH 06/21] Constify trait aliases
---
clippy_lints/src/item_name_repetitions.rs | 2 +-
clippy_utils/src/ast_utils/mod.rs | 9 ++++++++-
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/clippy_lints/src/item_name_repetitions.rs b/clippy_lints/src/item_name_repetitions.rs
index 76f5fdfaa8dc..22767901614e 100644
--- a/clippy_lints/src/item_name_repetitions.rs
+++ b/clippy_lints/src/item_name_repetitions.rs
@@ -528,7 +528,7 @@ impl LateLintPass<'_> for ItemNameRepetitions {
| ItemKind::Macro(ident, ..)
| ItemKind::Static(_, ident, ..)
| ItemKind::Trait(_, _, _, ident, ..)
- | ItemKind::TraitAlias(ident, ..)
+ | ItemKind::TraitAlias(_, ident, ..)
| ItemKind::TyAlias(ident, ..)
| ItemKind::Union(ident, ..)
| ItemKind::Use(_, UseKind::Single(ident)) => ident,
diff --git a/clippy_utils/src/ast_utils/mod.rs b/clippy_utils/src/ast_utils/mod.rs
index 4df1eb508713..839b46325b5e 100644
--- a/clippy_utils/src/ast_utils/mod.rs
+++ b/clippy_utils/src/ast_utils/mod.rs
@@ -478,13 +478,20 @@ pub fn eq_item_kind(l: &ItemKind, r: &ItemKind) -> bool {
ident: li,
generics: lg,
bounds: lb,
+ constness: lc,
}),
TraitAlias(box ast::TraitAlias {
ident: ri,
generics: rg,
bounds: rb,
+ constness: rc,
}),
- ) => eq_id(*li, *ri) && eq_generics(lg, rg) && over(lb, rb, eq_generic_bound),
+ ) => {
+ matches!(lc, ast::Const::No) == matches!(rc, ast::Const::No)
+ && eq_id(*li, *ri)
+ && eq_generics(lg, rg)
+ && over(lb, rb, eq_generic_bound)
+ },
(
Impl(ast::Impl {
generics: lg,
From c71f7b63f8fb3d7f0b2b0e26d0e029646f93fb9e Mon Sep 17 00:00:00 2001
From: Philipp Krones
Date: Fri, 31 Oct 2025 19:15:42 +0100
Subject: [PATCH 07/21] Merge commit 'c936595d17413c1f08e162e117e504fb4ed126e4'
into clippy-subtree-update
---
CHANGELOG.md | 85 +++++
CODE_OF_CONDUCT.md | 2 +-
Cargo.toml | 2 +-
book/book.toml | 2 -
book/src/development/method_checking.md | 15 +-
book/src/lint_configuration.md | 1 +
clippy_config/Cargo.toml | 2 +-
clippy_config/src/conf.rs | 1 +
clippy_dev/src/deprecate_lint.rs | 32 +-
clippy_dev/src/fmt.rs | 2 +-
clippy_dev/src/lib.rs | 11 +-
clippy_dev/src/main.rs | 51 ++-
clippy_dev/src/new_lint.rs | 21 +-
clippy_dev/src/parse.rs | 285 +++++++++++++++++
clippy_dev/src/parse/cursor.rs | 263 ++++++++++++++++
clippy_dev/src/release.rs | 2 +-
clippy_dev/src/rename_lint.rs | 118 ++++---
clippy_dev/src/update_lints.rs | 296 ++----------------
clippy_dev/src/utils.rs | 201 ++----------
clippy_lints/Cargo.toml | 2 +-
clippy_lints/src/declared_lints.rs | 12 +-
clippy_lints/src/deprecated_lints.rs | 4 +
clippy_lints/src/doc/mod.rs | 117 ++++---
clippy_lints/src/doc/needless_doctest_main.rs | 203 ++++--------
clippy_lints/src/doc/test_attr_in_doctest.rs | 34 ++
clippy_lints/src/double_parens.rs | 37 ++-
.../src/{empty_enum.rs => empty_enums.rs} | 19 +-
.../src/extra_unused_type_parameters.rs | 5 +
clippy_lints/src/formatting.rs | 2 +-
clippy_lints/src/incompatible_msrv.rs | 53 +++-
.../src/integer_division_remainder_used.rs | 50 ---
clippy_lints/src/len_zero.rs | 228 ++++++++------
clippy_lints/src/lib.rs | 20 +-
clippy_lints/src/lifetimes.rs | 110 +++----
clippy_lints/src/lines_filter_map_ok.rs | 141 ---------
clippy_lints/src/loops/needless_range_loop.rs | 5 +-
clippy_lints/src/loops/never_loop.rs | 19 +-
clippy_lints/src/manual_let_else.rs | 8 +-
clippy_lints/src/manual_option_as_slice.rs | 36 ++-
clippy_lints/src/map_unit_fn.rs | 9 +-
clippy_lints/src/matches/manual_unwrap_or.rs | 32 +-
clippy_lints/src/matches/match_as_ref.rs | 104 +++---
clippy_lints/src/matches/match_wild_enum.rs | 2 +-
.../src/matches/redundant_pattern_match.rs | 2 +-
.../src/methods/lines_filter_map_ok.rs | 85 +++++
clippy_lints/src/methods/mod.rs | 102 +++++-
.../src/methods/option_as_ref_cloned.rs | 26 +-
clippy_lints/src/methods/search_is_some.rs | 122 ++++----
.../src/methods/unnecessary_filter_map.rs | 96 +++---
.../misc_early/unneeded_wildcard_pattern.rs | 2 +-
.../src/needless_borrows_for_generic_args.rs | 2 +-
.../src/{needless_if.rs => needless_ifs.rs} | 13 +-
clippy_lints/src/only_used_in_recursion.rs | 4 +-
.../src/operators/double_comparison.rs | 88 +++---
.../integer_division_remainder_used.rs | 24 ++
.../invalid_upcast_comparisons.rs | 148 ++++-----
.../src/{ => operators}/manual_div_ceil.rs | 147 +++------
clippy_lints/src/operators/mod.rs | 87 ++++-
clippy_lints/src/precedence.rs | 18 +-
clippy_lints/src/single_range_in_vec_init.rs | 3 +-
clippy_lints/src/types/option_option.rs | 18 +-
clippy_lints/src/unnested_or_patterns.rs | 4 +-
clippy_lints/src/write.rs | 25 +-
clippy_lints_internal/src/msrv_attr_impl.rs | 2 +-
clippy_utils/Cargo.toml | 2 +-
clippy_utils/README.md | 2 +-
clippy_utils/src/ast_utils/mod.rs | 8 +-
clippy_utils/src/higher.rs | 30 +-
clippy_utils/src/lib.rs | 2 +-
clippy_utils/src/res.rs | 6 +-
clippy_utils/src/source.rs | 4 +-
clippy_utils/src/sugg.rs | 8 +-
declare_clippy_lint/Cargo.toml | 2 +-
rust-toolchain.toml | 2 +-
.../excessive_nesting/excessive_nesting.rs | 2 +-
tests/ui/auxiliary/macro_rules.rs | 12 +
tests/ui/auxiliary/proc_macro_derive.rs | 11 +
tests/ui/auxiliary/proc_macros.rs | 2 +-
tests/ui/blocks_in_conditions.fixed | 2 +-
tests/ui/blocks_in_conditions.rs | 2 +-
tests/ui/bool_comparison.fixed | 2 +-
tests/ui/bool_comparison.rs | 2 +-
.../ui/cmp_owned/asymmetric_partial_eq.fixed | 2 +-
tests/ui/cmp_owned/asymmetric_partial_eq.rs | 2 +-
tests/ui/collapsible_else_if.fixed | 2 +-
tests/ui/collapsible_else_if.rs | 2 +-
tests/ui/collapsible_if.fixed | 2 +-
tests/ui/collapsible_if.rs | 2 +-
tests/ui/comparison_to_empty.fixed | 2 +-
tests/ui/comparison_to_empty.rs | 2 +-
tests/ui/crashes/ice-7169.fixed | 2 +-
tests/ui/crashes/ice-7169.rs | 2 +-
tests/ui/disallowed_names.rs | 2 +-
tests/ui/doc/needless_doctest_main.rs | 28 +-
tests/ui/doc/needless_doctest_main.stderr | 33 +-
tests/ui/double_comparison.fixed | 2 +-
tests/ui/double_comparison.rs | 2 +-
tests/ui/double_parens.fixed | 65 ++++
tests/ui/double_parens.rs | 65 ++++
tests/ui/double_parens.stderr | 55 +++-
tests/ui/empty_enum.rs | 8 -
tests/ui/empty_enums.rs | 25 ++
.../{empty_enum.stderr => empty_enums.stderr} | 6 +-
...e.rs => empty_enums_without_never_type.rs} | 3 +-
tests/ui/equatable_if_let.fixed | 2 +-
tests/ui/equatable_if_let.rs | 2 +-
tests/ui/expect_tool_lint_rfc_2383.rs | 2 +-
tests/ui/filetype_is_file.rs | 2 +-
tests/ui/if_same_then_else2.rs | 2 +-
tests/ui/ifs_same_cond.rs | 2 +-
tests/ui/impl.rs | 22 ++
tests/ui/incompatible_msrv.rs | 10 +
tests/ui/incompatible_msrv.stderr | 14 +-
.../ui/integer_division_remainder_used.stderr | 18 +-
tests/ui/len_zero.fixed | 6 +-
tests/ui/len_zero.rs | 6 +-
tests/ui/len_zero_unstable.fixed | 7 +
tests/ui/len_zero_unstable.rs | 7 +
tests/ui/len_zero_unstable.stderr | 11 +
tests/ui/lines_filter_map_ok.fixed | 18 +-
tests/ui/lines_filter_map_ok.rs | 18 +-
tests/ui/lines_filter_map_ok.stderr | 20 +-
tests/ui/manual_float_methods.rs | 2 +-
tests/ui/manual_let_else.rs | 27 +-
tests/ui/manual_let_else.stderr | 38 ++-
tests/ui/manual_option_as_slice.stderr | 79 ++++-
tests/ui/manual_unwrap_or.fixed | 14 +
tests/ui/manual_unwrap_or.rs | 14 +
tests/ui/manual_unwrap_or.stderr | 14 +-
tests/ui/manual_unwrap_or_default.fixed | 14 +
tests/ui/manual_unwrap_or_default.rs | 14 +
tests/ui/manual_unwrap_or_default.stderr | 14 +-
tests/ui/match_as_ref.fixed | 19 ++
tests/ui/match_as_ref.rs | 31 ++
tests/ui/match_as_ref.stderr | 108 ++++++-
tests/ui/match_overlapping_arm.rs | 2 +-
.../multiple_inherent_impl_cfg.normal.stderr | 31 ++
tests/ui/multiple_inherent_impl_cfg.rs | 46 +++
tests/ui/multiple_inherent_impl_cfg.stderr | 91 ++++++
...multiple_inherent_impl_cfg.withtest.stderr | 91 ++++++
tests/ui/needless_bool/fixable.fixed | 2 +-
tests/ui/needless_bool/fixable.rs | 2 +-
tests/ui/needless_borrowed_ref.fixed | 2 +-
tests/ui/needless_borrowed_ref.rs | 2 +-
tests/ui/needless_collect.fixed | 2 +-
tests/ui/needless_collect.rs | 2 +-
tests/ui/needless_collect_indirect.rs | 2 +-
tests/ui/needless_doc_main.rs | 15 +-
tests/ui/needless_doc_main.stderr | 35 +--
.../{needless_if.fixed => needless_ifs.fixed} | 25 +-
tests/ui/{needless_if.rs => needless_ifs.rs} | 25 +-
...needless_if.stderr => needless_ifs.stderr} | 32 +-
tests/ui/never_loop.rs | 38 ++-
tests/ui/never_loop.stderr | 80 +++--
tests/ui/nonminimal_bool.rs | 2 +-
tests/ui/nonminimal_bool_methods.fixed | 2 +-
tests/ui/nonminimal_bool_methods.rs | 2 +-
tests/ui/op_ref.fixed | 2 +-
tests/ui/op_ref.rs | 2 +-
tests/ui/option_map_unit_fn_fixable.fixed | 5 +-
tests/ui/option_map_unit_fn_fixable.rs | 5 +-
tests/ui/option_map_unit_fn_fixable.stderr | 253 ++++++++++-----
tests/ui/option_map_unit_fn_unfixable.rs | 20 +-
tests/ui/option_map_unit_fn_unfixable.stderr | 109 ++++++-
tests/ui/option_option.rs | 28 +-
tests/ui/option_option.stderr | 54 +++-
tests/ui/panicking_overflow_checks.rs | 2 +-
tests/ui/partialeq_to_none.fixed | 2 +-
tests/ui/partialeq_to_none.rs | 2 +-
tests/ui/precedence.fixed | 32 +-
tests/ui/precedence.rs | 32 +-
tests/ui/precedence.stderr | 38 ++-
tests/ui/print.rs | 44 ---
tests/ui/print.stderr | 56 ----
tests/ui/print_stdout.rs | 23 ++
tests/ui/print_stdout.stderr | 35 +++
...edundant_pattern_matching_drop_order.fixed | 2 +-
.../redundant_pattern_matching_drop_order.rs | 2 +-
...dundant_pattern_matching_if_let_true.fixed | 2 +-
.../redundant_pattern_matching_if_let_true.rs | 2 +-
.../redundant_pattern_matching_ipaddr.fixed | 2 +-
tests/ui/redundant_pattern_matching_ipaddr.rs | 2 +-
.../redundant_pattern_matching_option.fixed | 2 +-
tests/ui/redundant_pattern_matching_option.rs | 2 +-
.../ui/redundant_pattern_matching_poll.fixed | 2 +-
tests/ui/redundant_pattern_matching_poll.rs | 2 +-
.../redundant_pattern_matching_result.fixed | 2 +-
tests/ui/redundant_pattern_matching_result.rs | 2 +-
tests/ui/rename.fixed | 4 +
tests/ui/rename.rs | 4 +
tests/ui/rename.stderr | 162 +++++-----
tests/ui/result_map_unit_fn_fixable.fixed | 4 +-
tests/ui/result_map_unit_fn_fixable.rs | 4 +-
tests/ui/result_map_unit_fn_fixable.stderr | 217 ++++++++-----
tests/ui/result_map_unit_fn_unfixable.rs | 5 +-
tests/ui/result_map_unit_fn_unfixable.stderr | 86 +++--
tests/ui/search_is_some.rs | 65 ----
tests/ui/search_is_some.stderr | 102 +-----
tests/ui/search_is_some_fixable_none.fixed | 18 ++
tests/ui/search_is_some_fixable_none.rs | 24 ++
tests/ui/search_is_some_fixable_none.stderr | 158 +++++++---
tests/ui/search_is_some_fixable_some.fixed | 31 ++
tests/ui/search_is_some_fixable_some.rs | 35 +++
tests/ui/search_is_some_fixable_some.stderr | 156 ++++++---
tests/ui/shadow.rs | 2 +-
tests/ui/single_match.fixed | 2 +-
tests/ui/single_match.rs | 2 +-
.../ui/single_match_else_deref_patterns.fixed | 2 +-
tests/ui/single_match_else_deref_patterns.rs | 2 +-
tests/ui/starts_ends_with.fixed | 2 +-
tests/ui/starts_ends_with.rs | 2 +-
tests/ui/suspicious_else_formatting.rs | 2 +-
tests/ui/suspicious_unary_op_formatting.rs | 2 +-
tests/ui/test_attr_in_doctest.rs | 1 -
tests/ui/test_attr_in_doctest.stderr | 28 +-
tests/ui/unit_cmp.rs | 2 +-
tests/ui/unnecessary_filter_map.stderr | 24 +-
tests/ui/unnecessary_find_map.rs | 2 -
tests/ui/unnecessary_find_map.stderr | 20 +-
tests/ui/unnecessary_safety_comment.rs | 2 +-
tests/ui/unneeded_wildcard_pattern.fixed | 2 +-
tests/ui/unneeded_wildcard_pattern.rs | 2 +-
tests/ui/unnested_or_patterns.fixed | 2 +-
tests/ui/unnested_or_patterns.rs | 2 +-
tests/ui/unnested_or_patterns2.fixed | 2 +-
tests/ui/unnested_or_patterns2.rs | 2 +-
tests/ui/use_debug.rs | 50 +++
tests/ui/use_debug.stderr | 23 ++
tests/ui/useless_conversion.fixed | 2 +-
tests/ui/useless_conversion.rs | 2 +-
tests/ui/useless_conversion_try.rs | 2 +-
...e_loop.rs => while_immutable_condition.rs} | 2 +
...tderr => while_immutable_condition.stderr} | 25 +-
util/gh-pages/script.js | 50 +--
234 files changed, 4504 insertions(+), 2769 deletions(-)
create mode 100644 clippy_dev/src/parse.rs
create mode 100644 clippy_dev/src/parse/cursor.rs
create mode 100644 clippy_lints/src/doc/test_attr_in_doctest.rs
rename clippy_lints/src/{empty_enum.rs => empty_enums.rs} (83%)
delete mode 100644 clippy_lints/src/integer_division_remainder_used.rs
delete mode 100644 clippy_lints/src/lines_filter_map_ok.rs
create mode 100644 clippy_lints/src/methods/lines_filter_map_ok.rs
rename clippy_lints/src/{needless_if.rs => needless_ifs.rs} (88%)
create mode 100644 clippy_lints/src/operators/integer_division_remainder_used.rs
rename clippy_lints/src/{ => operators}/invalid_upcast_comparisons.rs (56%)
rename clippy_lints/src/{ => operators}/manual_div_ceil.rs (50%)
delete mode 100644 tests/ui/empty_enum.rs
create mode 100644 tests/ui/empty_enums.rs
rename tests/ui/{empty_enum.stderr => empty_enums.stderr} (75%)
rename tests/ui/{empty_enum_without_never_type.rs => empty_enums_without_never_type.rs} (67%)
create mode 100644 tests/ui/len_zero_unstable.fixed
create mode 100644 tests/ui/len_zero_unstable.rs
create mode 100644 tests/ui/len_zero_unstable.stderr
create mode 100644 tests/ui/multiple_inherent_impl_cfg.normal.stderr
create mode 100644 tests/ui/multiple_inherent_impl_cfg.rs
create mode 100644 tests/ui/multiple_inherent_impl_cfg.stderr
create mode 100644 tests/ui/multiple_inherent_impl_cfg.withtest.stderr
rename tests/ui/{needless_if.fixed => needless_ifs.fixed} (82%)
rename tests/ui/{needless_if.rs => needless_ifs.rs} (82%)
rename tests/ui/{needless_if.stderr => needless_ifs.stderr} (64%)
delete mode 100644 tests/ui/print.rs
delete mode 100644 tests/ui/print.stderr
create mode 100644 tests/ui/print_stdout.rs
create mode 100644 tests/ui/print_stdout.stderr
create mode 100644 tests/ui/use_debug.rs
create mode 100644 tests/ui/use_debug.stderr
rename tests/ui/{infinite_loop.rs => while_immutable_condition.rs} (98%)
rename tests/ui/{infinite_loop.stderr => while_immutable_condition.stderr} (76%)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37d46d349667..6cb2755be0ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,89 @@ document.
[e9b7045...master](https://github.com/rust-lang/rust-clippy/compare/e9b7045...master)
+## Rust 1.91
+
+Current stable, released 2025-10-30
+
+[View all 146 merged pull requests](https://github.com/rust-lang/rust-clippy/pulls?q=merged%3A2025-07-25T21%3A05%3A11Z..2025-09-04T22%3A34%3A27Z+base%3Amaster)
+
+### New Lints
+
+* Added [`possible_missing_else`] to `suspicious`
+ [#15317](https://github.com/rust-lang/rust-clippy/pull/15317)
+
+### Moves and Deprecations
+
+* Moved [`cognitive_complexity`] from `nursery` to `restriction`
+ [#15415](https://github.com/rust-lang/rust-clippy/pull/15415)
+* Moved [`declare_interior_mutable_const`] from `style` to `suspicious`
+ [#15454](https://github.com/rust-lang/rust-clippy/pull/15454)
+* Moved [`crosspointer_transmute`] from `complexity` to `suspicious`
+ [#15403](https://github.com/rust-lang/rust-clippy/pull/15403)
+
+### Enhancements
+
+* [`excessive_precision`] added `const_literal_digits_threshold` option to suppress overly precise constants.
+ [#15193](https://github.com/rust-lang/rust-clippy/pull/15193)
+* [`unwrap_in_result`] rewritten for better accuracy; now lints on `.unwrap()` and `.expect()`
+ directly and no longer mixes `Result` and `Option`.
+ [#15445](https://github.com/rust-lang/rust-clippy/pull/15445)
+* [`panic`] now works in `const` contexts.
+ [#15565](https://github.com/rust-lang/rust-clippy/pull/15565)
+* [`implicit_clone`] now also lints `to_string` calls (merging [`string_to_string`] behavior).
+ [#14177](https://github.com/rust-lang/rust-clippy/pull/14177)
+* [`collapsible_match`] improved suggestions to handle necessary ref/dereferencing.
+ [#14221](https://github.com/rust-lang/rust-clippy/pull/14221)
+* [`map_identity`] now suggests making variables mutable when required; recognizes tuple struct restructuring.
+ [#15261](https://github.com/rust-lang/rust-clippy/pull/15261)
+* [`option_map_unit_fn`] preserves `unsafe` blocks in suggestions.
+ [#15570](https://github.com/rust-lang/rust-clippy/pull/15570)
+* [`unnecessary_mut_passed`] provides structured, clearer fix suggestions.
+ [#15438](https://github.com/rust-lang/rust-clippy/pull/15438)
+* [`float_equality_without_abs`] now checks `f16` and `f128` types.
+ [#15054](https://github.com/rust-lang/rust-clippy/pull/15054)
+* [`doc_markdown`] expanded whitelist (`InfiniBand`, `RoCE`, `PowerPC`) and improved handling of
+ identifiers like NixOS.
+ [#15558](https://github.com/rust-lang/rust-clippy/pull/15558)
+* [`clone_on_ref_ptr`] now suggests fully qualified paths to avoid resolution errors.
+ [#15561](https://github.com/rust-lang/rust-clippy/pull/15561)
+* [`manual_assert`] simplifies boolean expressions in suggested fixes.
+ [#15368](https://github.com/rust-lang/rust-clippy/pull/15368)
+* [`four_forward_slashes`] warns about bare CR in comments and avoids invalid autofixes.
+ [#15175](https://github.com/rust-lang/rust-clippy/pull/15175)
+
+### False Positive Fixes
+
+* [`alloc_instead_of_core`] fixed FP when `alloc` is an alias
+ [#15581](https://github.com/rust-lang/rust-clippy/pull/15581)
+* [`needless_range_loop`] fixed FP and FN when meeting multidimensional array
+ [#15486](https://github.com/rust-lang/rust-clippy/pull/15486)
+* [`semicolon_inside_block`] fixed FP when attribute over expr is not enabled
+ [#15476](https://github.com/rust-lang/rust-clippy/pull/15476)
+* [`unnested_or_patterns`] fixed FP on structs with only shorthand field patterns
+ [#15343](https://github.com/rust-lang/rust-clippy/pull/15343)
+* [`match_ref_pats`] fixed FP on match scrutinee of never type
+ [#15474](https://github.com/rust-lang/rust-clippy/pull/15474)
+* [`infinite_loop`] fixed FP in async blocks that are not awaited
+ [#15157](https://github.com/rust-lang/rust-clippy/pull/15157)
+* [`iter_on_single_items`] fixed FP on function pointers and let statements
+ [#15013](https://github.com/rust-lang/rust-clippy/pull/15013)
+
+### ICE Fixes
+
+* [`len_zero`] fix ICE when fn len has a return type without generic type params
+ [#15660](https://github.com/rust-lang/rust-clippy/pull/15660)
+
+### Documentation Improvements
+
+* [`cognitive_complexity`] corrected documentation to state lint is in `restriction`, not `nursery`
+ [#15563](https://github.com/rust-lang/rust-clippy/pull/15563)
+
+### Performance Improvements
+
+* [`doc_broken_link`] optimized by 99.77% (12M → 27k instructions)
+ [#15385](https://github.com/rust-lang/rust-clippy/pull/15385)
+
## Rust 1.90
Current stable, released 2025-09-18
@@ -6253,6 +6336,7 @@ Released 2018-09-13
[`empty_drop`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_drop
[`empty_enum`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_enum
[`empty_enum_variants_with_brackets`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_enum_variants_with_brackets
+[`empty_enums`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_enums
[`empty_line_after_doc_comments`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_line_after_doc_comments
[`empty_line_after_outer_attr`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_line_after_outer_attr
[`empty_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_loop
@@ -6583,6 +6667,7 @@ Released 2018-09-13
[`needless_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_else
[`needless_for_each`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_for_each
[`needless_if`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_if
+[`needless_ifs`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_ifs
[`needless_late_init`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_late_init
[`needless_lifetimes`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
[`needless_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_match
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index e3708bc48539..133072e0586b 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,3 +1,3 @@
# The Rust Code of Conduct
-The Code of Conduct for this repository [can be found online](https://www.rust-lang.org/conduct.html).
+The Code of Conduct for this repository [can be found online](https://rust-lang.org/policies/code-of-conduct/).
diff --git a/Cargo.toml b/Cargo.toml
index bedcc300f856..fee885d8fa7e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "clippy"
-version = "0.1.92"
+version = "0.1.93"
description = "A bunch of helpful lints to avoid common pitfalls in Rust"
repository = "https://github.com/rust-lang/rust-clippy"
readme = "README.md"
diff --git a/book/book.toml b/book/book.toml
index c918aadf83c4..ae5fd07487ce 100644
--- a/book/book.toml
+++ b/book/book.toml
@@ -1,8 +1,6 @@
[book]
authors = ["The Rust Clippy Developers"]
language = "en"
-multilingual = false
-src = "src"
title = "Clippy Documentation"
[rust]
diff --git a/book/src/development/method_checking.md b/book/src/development/method_checking.md
index cca6d6ae7bf3..f3912f81d859 100644
--- a/book/src/development/method_checking.md
+++ b/book/src/development/method_checking.md
@@ -21,14 +21,13 @@ use clippy_utils::sym;
impl<'tcx> LateLintPass<'tcx> for OurFancyMethodLint {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) {
// Check our expr is calling a method with pattern matching
- if let hir::ExprKind::MethodCall(path, _, [self_arg, ..], _) = &expr.kind
+ if let hir::ExprKind::MethodCall(path, _, _, _) = &expr.kind
// Check if the name of this method is `our_fancy_method`
&& path.ident.name == sym::our_fancy_method
- // We can check the type of the self argument whenever necessary.
- // (It's necessary if we want to check that method is specifically belonging to a specific trait,
- // for example, a `map` method could belong to user-defined trait instead of to `Iterator`)
+ // Check if the method belongs to the `sym::OurFancyTrait` trait.
+ // (for example, a `map` method could belong to user-defined trait instead of to `Iterator`)
// See the next section for more information.
- && cx.ty_based_def(self_arg).opt_parent(cx).is_diag_item(cx, sym::OurFancyTrait)
+ && cx.ty_based_def(expr).opt_parent(cx).is_diag_item(cx, sym::OurFancyTrait)
{
println!("`expr` is a method call for `our_fancy_method`");
}
@@ -45,6 +44,12 @@ New symbols such as `our_fancy_method` need to be added to the `clippy_utils::sy
This module extends the list of symbols already provided by the compiler crates
in `rustc_span::sym`.
+If a trait defines only one method (such as the `std::ops::Deref` trait, which only has the `deref()` method),
+one might be tempted to omit the method name check. This would work, but is not always advisable because:
+- If a new method (possibly with a default implementation) were to be added to the trait, there would be a risk of
+ matching the wrong method.
+- Comparing symbols is very cheap and might prevent a more expensive lookup.
+
## Checking if a `impl` block implements a method
While sometimes we want to check whether a method is being called or not, other
diff --git a/book/src/lint_configuration.md b/book/src/lint_configuration.md
index b9ecff1fa364..6569bdabf115 100644
--- a/book/src/lint_configuration.md
+++ b/book/src/lint_configuration.md
@@ -859,6 +859,7 @@ The minimum rust version that the project supports. Defaults to the `rust-versio
* [`io_other_error`](https://rust-lang.github.io/rust-clippy/master/index.html#io_other_error)
* [`iter_kv_map`](https://rust-lang.github.io/rust-clippy/master/index.html#iter_kv_map)
* [`legacy_numeric_constants`](https://rust-lang.github.io/rust-clippy/master/index.html#legacy_numeric_constants)
+* [`len_zero`](https://rust-lang.github.io/rust-clippy/master/index.html#len_zero)
* [`lines_filter_map_ok`](https://rust-lang.github.io/rust-clippy/master/index.html#lines_filter_map_ok)
* [`manual_abs_diff`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_abs_diff)
* [`manual_bits`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_bits)
diff --git a/clippy_config/Cargo.toml b/clippy_config/Cargo.toml
index f8c748290e41..3f6b26d3334e 100644
--- a/clippy_config/Cargo.toml
+++ b/clippy_config/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "clippy_config"
-version = "0.1.92"
+version = "0.1.93"
edition = "2024"
publish = false
diff --git a/clippy_config/src/conf.rs b/clippy_config/src/conf.rs
index 9993765b4bdc..2a042e6c3d85 100644
--- a/clippy_config/src/conf.rs
+++ b/clippy_config/src/conf.rs
@@ -748,6 +748,7 @@ define_Conf! {
io_other_error,
iter_kv_map,
legacy_numeric_constants,
+ len_zero,
lines_filter_map_ok,
manual_abs_diff,
manual_bits,
diff --git a/clippy_dev/src/deprecate_lint.rs b/clippy_dev/src/deprecate_lint.rs
index 3bdc5b277232..0401cfda7080 100644
--- a/clippy_dev/src/deprecate_lint.rs
+++ b/clippy_dev/src/deprecate_lint.rs
@@ -1,4 +1,5 @@
-use crate::update_lints::{DeprecatedLint, Lint, find_lint_decls, generate_lint_files, read_deprecated_lints};
+use crate::parse::{DeprecatedLint, Lint, ParseCx};
+use crate::update_lints::generate_lint_files;
use crate::utils::{UpdateMode, Version};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
@@ -13,21 +14,20 @@ use std::{fs, io};
/// # Panics
///
/// If a file path could not read from or written to
-pub fn deprecate(clippy_version: Version, name: &str, reason: &str) {
- if let Some((prefix, _)) = name.split_once("::") {
- panic!("`{name}` should not contain the `{prefix}` prefix");
- }
-
- let mut lints = find_lint_decls();
- let (mut deprecated_lints, renamed_lints) = read_deprecated_lints();
+pub fn deprecate<'cx>(cx: ParseCx<'cx>, clippy_version: Version, name: &'cx str, reason: &'cx str) {
+ let mut lints = cx.find_lint_decls();
+ let (mut deprecated_lints, renamed_lints) = cx.read_deprecated_lints();
let Some(lint) = lints.iter().find(|l| l.name == name) else {
eprintln!("error: failed to find lint `{name}`");
return;
};
- let prefixed_name = String::from_iter(["clippy::", name]);
- match deprecated_lints.binary_search_by(|x| x.name.cmp(&prefixed_name)) {
+ let prefixed_name = cx.str_buf.with(|buf| {
+ buf.extend(["clippy::", name]);
+ cx.arena.alloc_str(buf)
+ });
+ match deprecated_lints.binary_search_by(|x| x.name.cmp(prefixed_name)) {
Ok(_) => {
println!("`{name}` is already deprecated");
return;
@@ -36,8 +36,8 @@ pub fn deprecate(clippy_version: Version, name: &str, reason: &str) {
idx,
DeprecatedLint {
name: prefixed_name,
- reason: reason.into(),
- version: clippy_version.rust_display().to_string(),
+ reason,
+ version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()),
},
),
}
@@ -61,8 +61,8 @@ pub fn deprecate(clippy_version: Version, name: &str, reason: &str) {
}
}
-fn remove_lint_declaration(name: &str, path: &Path, lints: &mut Vec) -> io::Result {
- fn remove_lint(name: &str, lints: &mut Vec) {
+fn remove_lint_declaration(name: &str, path: &Path, lints: &mut Vec>) -> io::Result {
+ fn remove_lint(name: &str, lints: &mut Vec>) {
lints.iter().position(|l| l.name == name).map(|pos| lints.remove(pos));
}
@@ -135,14 +135,14 @@ fn remove_lint_declaration(name: &str, path: &Path, lints: &mut Vec) -> io
);
assert!(
- content[lint.declaration_range.clone()].contains(&name.to_uppercase()),
+ content[lint.declaration_range].contains(&name.to_uppercase()),
"error: `{}` does not contain lint `{}`'s declaration",
path.display(),
lint.name
);
// Remove lint declaration (declare_clippy_lint!)
- content.replace_range(lint.declaration_range.clone(), "");
+ content.replace_range(lint.declaration_range, "");
// Remove the module declaration (mod xyz;)
let mod_decl = format!("\nmod {name};");
diff --git a/clippy_dev/src/fmt.rs b/clippy_dev/src/fmt.rs
index 2b2138d3108d..781e37e6144e 100644
--- a/clippy_dev/src/fmt.rs
+++ b/clippy_dev/src/fmt.rs
@@ -268,7 +268,7 @@ fn run_rustfmt(update_mode: UpdateMode) {
.expect("invalid rustfmt path");
rustfmt_path.truncate(rustfmt_path.trim_end().len());
- let args: Vec<_> = walk_dir_no_dot_or_target()
+ let args: Vec<_> = walk_dir_no_dot_or_target(".")
.filter_map(|e| {
let e = expect_action(e, ErrAction::Read, ".");
e.path()
diff --git a/clippy_dev/src/lib.rs b/clippy_dev/src/lib.rs
index 16f413e0c862..dcca08aee7e6 100644
--- a/clippy_dev/src/lib.rs
+++ b/clippy_dev/src/lib.rs
@@ -1,9 +1,12 @@
#![feature(
- rustc_private,
exit_status_error,
if_let_guard,
+ new_range,
+ new_range_api,
os_str_slice,
os_string_truncate,
+ pattern,
+ rustc_private,
slice_split_once
)]
#![warn(
@@ -15,6 +18,7 @@
)]
#![allow(clippy::missing_panics_doc)]
+extern crate rustc_arena;
#[expect(unused_extern_crates, reason = "required to link to rustc crates")]
extern crate rustc_driver;
extern crate rustc_lexer;
@@ -32,5 +36,8 @@ pub mod setup;
pub mod sync;
pub mod update_lints;
+mod parse;
mod utils;
-pub use utils::{ClippyInfo, UpdateMode};
+
+pub use self::parse::{ParseCx, new_parse_cx};
+pub use self::utils::{ClippyInfo, UpdateMode};
diff --git a/clippy_dev/src/main.rs b/clippy_dev/src/main.rs
index 1b6a590b896f..392c3aabf193 100644
--- a/clippy_dev/src/main.rs
+++ b/clippy_dev/src/main.rs
@@ -4,10 +4,9 @@
use clap::{Args, Parser, Subcommand};
use clippy_dev::{
- ClippyInfo, UpdateMode, deprecate_lint, dogfood, fmt, lint, new_lint, release, rename_lint, serve, setup, sync,
- update_lints,
+ ClippyInfo, UpdateMode, deprecate_lint, dogfood, fmt, lint, new_lint, new_parse_cx, release, rename_lint, serve,
+ setup, sync, update_lints,
};
-use std::convert::Infallible;
use std::env;
fn main() {
@@ -28,7 +27,7 @@ fn main() {
allow_no_vcs,
} => dogfood::dogfood(fix, allow_dirty, allow_staged, allow_no_vcs),
DevCommand::Fmt { check } => fmt::run(UpdateMode::from_check(check)),
- DevCommand::UpdateLints { check } => update_lints::update(UpdateMode::from_check(check)),
+ DevCommand::UpdateLints { check } => new_parse_cx(|cx| update_lints::update(cx, UpdateMode::from_check(check))),
DevCommand::NewLint {
pass,
name,
@@ -36,7 +35,7 @@ fn main() {
r#type,
msrv,
} => match new_lint::create(clippy.version, pass, &name, &category, r#type.as_deref(), msrv) {
- Ok(()) => update_lints::update(UpdateMode::Change),
+ Ok(()) => new_parse_cx(|cx| update_lints::update(cx, UpdateMode::Change)),
Err(e) => eprintln!("Unable to create lint: {e}"),
},
DevCommand::Setup(SetupCommand { subcommand }) => match subcommand {
@@ -79,13 +78,18 @@ fn main() {
old_name,
new_name,
uplift,
- } => rename_lint::rename(
- clippy.version,
- &old_name,
- new_name.as_ref().unwrap_or(&old_name),
- uplift,
- ),
- DevCommand::Deprecate { name, reason } => deprecate_lint::deprecate(clippy.version, &name, &reason),
+ } => new_parse_cx(|cx| {
+ rename_lint::rename(
+ cx,
+ clippy.version,
+ &old_name,
+ new_name.as_ref().unwrap_or(&old_name),
+ uplift,
+ );
+ }),
+ DevCommand::Deprecate { name, reason } => {
+ new_parse_cx(|cx| deprecate_lint::deprecate(cx, clippy.version, &name, &reason));
+ },
DevCommand::Sync(SyncCommand { subcommand }) => match subcommand {
SyncSubcommand::UpdateNightly => sync::update_nightly(),
},
@@ -95,6 +99,20 @@ fn main() {
}
}
+fn lint_name(name: &str) -> Result {
+ let name = name.replace('-', "_");
+ if let Some((pre, _)) = name.split_once("::") {
+ Err(format!("lint name should not contain the `{pre}` prefix"))
+ } else if name
+ .bytes()
+ .any(|x| !matches!(x, b'_' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z'))
+ {
+ Err("lint name contains invalid characters".to_owned())
+ } else {
+ Ok(name)
+ }
+}
+
#[derive(Parser)]
#[command(name = "dev", about)]
struct Dev {
@@ -150,7 +168,7 @@ enum DevCommand {
#[arg(
short,
long,
- value_parser = |name: &str| Ok::<_, Infallible>(name.replace('-', "_")),
+ value_parser = lint_name,
)]
/// Name of the new lint in snake case, ex: `fn_too_long`
name: String,
@@ -223,8 +241,12 @@ enum DevCommand {
/// Rename a lint
RenameLint {
/// The name of the lint to rename
+ #[arg(value_parser = lint_name)]
old_name: String,
- #[arg(required_unless_present = "uplift")]
+ #[arg(
+ required_unless_present = "uplift",
+ value_parser = lint_name,
+ )]
/// The new name of the lint
new_name: Option,
#[arg(long)]
@@ -234,6 +256,7 @@ enum DevCommand {
/// Deprecate the given lint
Deprecate {
/// The name of the lint to deprecate
+ #[arg(value_parser = lint_name)]
name: String,
#[arg(long, short)]
/// The reason for deprecation
diff --git a/clippy_dev/src/new_lint.rs b/clippy_dev/src/new_lint.rs
index a14afd8c5f41..a180db6ad062 100644
--- a/clippy_dev/src/new_lint.rs
+++ b/clippy_dev/src/new_lint.rs
@@ -1,4 +1,5 @@
-use crate::utils::{RustSearcher, Token, Version};
+use crate::parse::cursor::{self, Capture, Cursor};
+use crate::utils::Version;
use clap::ValueEnum;
use indoc::{formatdoc, writedoc};
use std::fmt::{self, Write as _};
@@ -516,22 +517,22 @@ fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str>
// Find both the last lint declaration (declare_clippy_lint!) and the lint pass impl
fn parse_mod_file(path: &Path, contents: &str) -> (&'static str, usize) {
#[allow(clippy::enum_glob_use)]
- use Token::*;
+ use cursor::Pat::*;
let mut context = None;
let mut decl_end = None;
- let mut searcher = RustSearcher::new(contents);
- while let Some(name) = searcher.find_capture_token(CaptureIdent) {
- match name {
+ let mut cursor = Cursor::new(contents);
+ let mut captures = [Capture::EMPTY];
+ while let Some(name) = cursor.find_any_ident() {
+ match cursor.get_text(name) {
"declare_clippy_lint" => {
- if searcher.match_tokens(&[Bang, OpenBrace], &mut []) && searcher.find_token(CloseBrace) {
- decl_end = Some(searcher.pos());
+ if cursor.match_all(&[Bang, OpenBrace], &mut []) && cursor.find_pat(CloseBrace) {
+ decl_end = Some(cursor.pos());
}
},
"impl" => {
- let mut capture = "";
- if searcher.match_tokens(&[Lt, Lifetime, Gt, CaptureIdent], &mut [&mut capture]) {
- match capture {
+ if cursor.match_all(&[Lt, Lifetime, Gt, CaptureIdent], &mut captures) {
+ match cursor.get_text(captures[0]) {
"LateLintPass" => context = Some("LateContext"),
"EarlyLintPass" => context = Some("EarlyContext"),
_ => {},
diff --git a/clippy_dev/src/parse.rs b/clippy_dev/src/parse.rs
new file mode 100644
index 000000000000..de5caf4e1ef6
--- /dev/null
+++ b/clippy_dev/src/parse.rs
@@ -0,0 +1,285 @@
+pub mod cursor;
+
+use self::cursor::{Capture, Cursor};
+use crate::utils::{ErrAction, File, Scoped, expect_action, walk_dir_no_dot_or_target};
+use core::fmt::{Display, Write as _};
+use core::range::Range;
+use rustc_arena::DroplessArena;
+use std::fs;
+use std::path::{self, Path, PathBuf};
+use std::str::pattern::Pattern;
+
+pub struct ParseCxImpl<'cx> {
+ pub arena: &'cx DroplessArena,
+ pub str_buf: StrBuf,
+}
+pub type ParseCx<'cx> = &'cx mut ParseCxImpl<'cx>;
+
+/// Calls the given function inside a newly created parsing context.
+pub fn new_parse_cx<'env, T>(f: impl for<'cx> FnOnce(&'cx mut Scoped<'cx, 'env, ParseCxImpl<'cx>>) -> T) -> T {
+ let arena = DroplessArena::default();
+ f(&mut Scoped::new(ParseCxImpl {
+ arena: &arena,
+ str_buf: StrBuf::with_capacity(128),
+ }))
+}
+
+/// A string used as a temporary buffer used to avoid allocating for short lived strings.
+pub struct StrBuf(String);
+impl StrBuf {
+ /// Creates a new buffer with the specified initial capacity.
+ pub fn with_capacity(cap: usize) -> Self {
+ Self(String::with_capacity(cap))
+ }
+
+ /// Allocates the result of formatting the given value onto the arena.
+ pub fn alloc_display<'cx>(&mut self, arena: &'cx DroplessArena, value: impl Display) -> &'cx str {
+ self.0.clear();
+ write!(self.0, "{value}").expect("`Display` impl returned an error");
+ arena.alloc_str(&self.0)
+ }
+
+ /// Allocates the string onto the arena with all ascii characters converted to
+ /// lowercase.
+ pub fn alloc_ascii_lower<'cx>(&mut self, arena: &'cx DroplessArena, s: &str) -> &'cx str {
+ self.0.clear();
+ self.0.push_str(s);
+ self.0.make_ascii_lowercase();
+ arena.alloc_str(&self.0)
+ }
+
+ /// Allocates the result of replacing all instances the pattern with the given string
+ /// onto the arena.
+ pub fn alloc_replaced<'cx>(
+ &mut self,
+ arena: &'cx DroplessArena,
+ s: &str,
+ pat: impl Pattern,
+ replacement: &str,
+ ) -> &'cx str {
+ let mut parts = s.split(pat);
+ let Some(first) = parts.next() else {
+ return "";
+ };
+ self.0.clear();
+ self.0.push_str(first);
+ for part in parts {
+ self.0.push_str(replacement);
+ self.0.push_str(part);
+ }
+ if self.0.is_empty() {
+ ""
+ } else {
+ arena.alloc_str(&self.0)
+ }
+ }
+
+ /// Performs an operation with the freshly cleared buffer.
+ pub fn with(&mut self, f: impl FnOnce(&mut String) -> T) -> T {
+ self.0.clear();
+ f(&mut self.0)
+ }
+}
+
+pub struct Lint<'cx> {
+ pub name: &'cx str,
+ pub group: &'cx str,
+ pub module: &'cx str,
+ pub path: PathBuf,
+ pub declaration_range: Range,
+}
+
+pub struct DeprecatedLint<'cx> {
+ pub name: &'cx str,
+ pub reason: &'cx str,
+ pub version: &'cx str,
+}
+
+pub struct RenamedLint<'cx> {
+ pub old_name: &'cx str,
+ pub new_name: &'cx str,
+ pub version: &'cx str,
+}
+
+impl<'cx> ParseCxImpl<'cx> {
+ /// Finds all lint declarations (`declare_clippy_lint!`)
+ #[must_use]
+ pub fn find_lint_decls(&mut self) -> Vec> {
+ let mut lints = Vec::with_capacity(1000);
+ let mut contents = String::new();
+ for e in expect_action(fs::read_dir("."), ErrAction::Read, ".") {
+ let e = expect_action(e, ErrAction::Read, ".");
+
+ // Skip if this isn't a lint crate's directory.
+ let mut crate_path = if expect_action(e.file_type(), ErrAction::Read, ".").is_dir()
+ && let Ok(crate_path) = e.file_name().into_string()
+ && crate_path.starts_with("clippy_lints")
+ && crate_path != "clippy_lints_internal"
+ {
+ crate_path
+ } else {
+ continue;
+ };
+
+ crate_path.push(path::MAIN_SEPARATOR);
+ crate_path.push_str("src");
+ for e in walk_dir_no_dot_or_target(&crate_path) {
+ let e = expect_action(e, ErrAction::Read, &crate_path);
+ if let Some(path) = e.path().to_str()
+ && let Some(path) = path.strip_suffix(".rs")
+ && let Some(path) = path.get(crate_path.len() + 1..)
+ {
+ let module = if path == "lib" {
+ ""
+ } else {
+ let path = path
+ .strip_suffix("mod")
+ .and_then(|x| x.strip_suffix(path::MAIN_SEPARATOR))
+ .unwrap_or(path);
+ self.str_buf
+ .alloc_replaced(self.arena, path, path::MAIN_SEPARATOR, "::")
+ };
+ self.parse_clippy_lint_decls(
+ e.path(),
+ File::open_read_to_cleared_string(e.path(), &mut contents),
+ module,
+ &mut lints,
+ );
+ }
+ }
+ }
+ lints.sort_by(|lhs, rhs| lhs.name.cmp(rhs.name));
+ lints
+ }
+
+ /// Parse a source file looking for `declare_clippy_lint` macro invocations.
+ fn parse_clippy_lint_decls(&mut self, path: &Path, contents: &str, module: &'cx str, lints: &mut Vec>) {
+ #[allow(clippy::enum_glob_use)]
+ use cursor::Pat::*;
+ #[rustfmt::skip]
+ static DECL_TOKENS: &[cursor::Pat<'_>] = &[
+ // !{ /// docs
+ Bang, OpenBrace, AnyComment,
+ // #[clippy::version = "version"]
+ Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, LitStr, CloseBracket,
+ // pub NAME, GROUP,
+ Ident("pub"), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma,
+ ];
+
+ let mut cursor = Cursor::new(contents);
+ let mut captures = [Capture::EMPTY; 2];
+ while let Some(start) = cursor.find_ident("declare_clippy_lint") {
+ if cursor.match_all(DECL_TOKENS, &mut captures) && cursor.find_pat(CloseBrace) {
+ lints.push(Lint {
+ name: self.str_buf.alloc_ascii_lower(self.arena, cursor.get_text(captures[0])),
+ group: self.arena.alloc_str(cursor.get_text(captures[1])),
+ module,
+ path: path.into(),
+ declaration_range: start as usize..cursor.pos() as usize,
+ });
+ }
+ }
+ }
+
+ #[must_use]
+ pub fn read_deprecated_lints(&mut self) -> (Vec>, Vec>) {
+ #[allow(clippy::enum_glob_use)]
+ use cursor::Pat::*;
+ #[rustfmt::skip]
+ static DECL_TOKENS: &[cursor::Pat<'_>] = &[
+ // #[clippy::version = "version"]
+ Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, CaptureLitStr, CloseBracket,
+ // ("first", "second"),
+ OpenParen, CaptureLitStr, Comma, CaptureLitStr, CloseParen, Comma,
+ ];
+ #[rustfmt::skip]
+ static DEPRECATED_TOKENS: &[cursor::Pat<'_>] = &[
+ // !{ DEPRECATED(DEPRECATED_VERSION) = [
+ Bang, OpenBrace, Ident("DEPRECATED"), OpenParen, Ident("DEPRECATED_VERSION"), CloseParen, Eq, OpenBracket,
+ ];
+ #[rustfmt::skip]
+ static RENAMED_TOKENS: &[cursor::Pat<'_>] = &[
+ // !{ RENAMED(RENAMED_VERSION) = [
+ Bang, OpenBrace, Ident("RENAMED"), OpenParen, Ident("RENAMED_VERSION"), CloseParen, Eq, OpenBracket,
+ ];
+
+ let path = "clippy_lints/src/deprecated_lints.rs";
+ let mut deprecated = Vec::with_capacity(30);
+ let mut renamed = Vec::with_capacity(80);
+ let mut contents = String::new();
+ File::open_read_to_cleared_string(path, &mut contents);
+
+ let mut cursor = Cursor::new(&contents);
+ let mut captures = [Capture::EMPTY; 3];
+
+ // First instance is the macro definition.
+ assert!(
+ cursor.find_ident("declare_with_version").is_some(),
+ "error reading deprecated lints"
+ );
+
+ if cursor.find_ident("declare_with_version").is_some() && cursor.match_all(DEPRECATED_TOKENS, &mut []) {
+ while cursor.match_all(DECL_TOKENS, &mut captures) {
+ deprecated.push(DeprecatedLint {
+ name: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[1])),
+ reason: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[2])),
+ version: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[0])),
+ });
+ }
+ } else {
+ panic!("error reading deprecated lints");
+ }
+
+ if cursor.find_ident("declare_with_version").is_some() && cursor.match_all(RENAMED_TOKENS, &mut []) {
+ while cursor.match_all(DECL_TOKENS, &mut captures) {
+ renamed.push(RenamedLint {
+ old_name: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[1])),
+ new_name: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[2])),
+ version: self.parse_str_single_line(path.as_ref(), cursor.get_text(captures[0])),
+ });
+ }
+ } else {
+ panic!("error reading renamed lints");
+ }
+
+ deprecated.sort_by(|lhs, rhs| lhs.name.cmp(rhs.name));
+ renamed.sort_by(|lhs, rhs| lhs.old_name.cmp(rhs.old_name));
+ (deprecated, renamed)
+ }
+
+ /// Removes the line splices and surrounding quotes from a string literal
+ fn parse_str_lit(&mut self, s: &str) -> &'cx str {
+ let (s, is_raw) = if let Some(s) = s.strip_prefix("r") {
+ (s.trim_matches('#'), true)
+ } else {
+ (s, false)
+ };
+ let s = s
+ .strip_prefix('"')
+ .and_then(|s| s.strip_suffix('"'))
+ .unwrap_or_else(|| panic!("expected quoted string, found `{s}`"));
+
+ if is_raw {
+ if s.is_empty() { "" } else { self.arena.alloc_str(s) }
+ } else {
+ self.str_buf.with(|buf| {
+ rustc_literal_escaper::unescape_str(s, &mut |_, ch| {
+ if let Ok(ch) = ch {
+ buf.push(ch);
+ }
+ });
+ if buf.is_empty() { "" } else { self.arena.alloc_str(buf) }
+ })
+ }
+ }
+
+ fn parse_str_single_line(&mut self, path: &Path, s: &str) -> &'cx str {
+ let value = self.parse_str_lit(s);
+ assert!(
+ !value.contains('\n'),
+ "error parsing `{}`: `{s}` should be a single line string",
+ path.display(),
+ );
+ value
+ }
+}
diff --git a/clippy_dev/src/parse/cursor.rs b/clippy_dev/src/parse/cursor.rs
new file mode 100644
index 000000000000..6dc003f326de
--- /dev/null
+++ b/clippy_dev/src/parse/cursor.rs
@@ -0,0 +1,263 @@
+use core::slice;
+use rustc_lexer::{self as lex, LiteralKind, Token, TokenKind};
+
+/// A token pattern used for searching and matching by the [`Cursor`].
+///
+/// In the event that a pattern is a multi-token sequence, earlier tokens will be consumed
+/// even if the pattern ultimately isn't matched. e.g. With the sequence `:*` matching
+/// `DoubleColon` will consume the first `:` and then fail to match, leaving the cursor at
+/// the `*`.
+#[derive(Clone, Copy)]
+pub enum Pat<'a> {
+ /// Matches any number of comments and doc comments.
+ AnyComment,
+ Ident(&'a str),
+ CaptureIdent,
+ LitStr,
+ CaptureLitStr,
+ Bang,
+ CloseBrace,
+ CloseBracket,
+ CloseParen,
+ Comma,
+ DoubleColon,
+ Eq,
+ Lifetime,
+ Lt,
+ Gt,
+ OpenBrace,
+ OpenBracket,
+ OpenParen,
+ Pound,
+ Semi,
+}
+
+#[derive(Clone, Copy)]
+pub struct Capture {
+ pub pos: u32,
+ pub len: u32,
+}
+impl Capture {
+ pub const EMPTY: Self = Self { pos: 0, len: 0 };
+}
+
+/// A unidirectional cursor over a token stream that is lexed on demand.
+pub struct Cursor<'txt> {
+ next_token: Token,
+ pos: u32,
+ inner: lex::Cursor<'txt>,
+ text: &'txt str,
+}
+impl<'txt> Cursor<'txt> {
+ #[must_use]
+ pub fn new(text: &'txt str) -> Self {
+ let mut inner = lex::Cursor::new(text, lex::FrontmatterAllowed::Yes);
+ Self {
+ next_token: inner.advance_token(),
+ pos: 0,
+ inner,
+ text,
+ }
+ }
+
+ /// Gets the text of the captured token assuming it came from this cursor.
+ #[must_use]
+ pub fn get_text(&self, capture: Capture) -> &'txt str {
+ &self.text[capture.pos as usize..(capture.pos + capture.len) as usize]
+ }
+
+ /// Gets the text that makes up the next token in the stream, or the empty string if
+ /// stream is exhausted.
+ #[must_use]
+ pub fn peek_text(&self) -> &'txt str {
+ &self.text[self.pos as usize..(self.pos + self.next_token.len) as usize]
+ }
+
+ /// Gets the length of the next token in bytes, or zero if the stream is exhausted.
+ #[must_use]
+ pub fn peek_len(&self) -> u32 {
+ self.next_token.len
+ }
+
+ /// Gets the next token in the stream, or [`TokenKind::Eof`] if the stream is
+ /// exhausted.
+ #[must_use]
+ pub fn peek(&self) -> TokenKind {
+ self.next_token.kind
+ }
+
+ /// Gets the offset of the next token in the source string, or the string's length if
+ /// the stream is exhausted.
+ #[must_use]
+ pub fn pos(&self) -> u32 {
+ self.pos
+ }
+
+ /// Gets whether the cursor has exhausted its input.
+ #[must_use]
+ pub fn at_end(&self) -> bool {
+ self.next_token.kind == TokenKind::Eof
+ }
+
+ /// Advances the cursor to the next token. If the stream is exhausted this will set
+ /// the next token to [`TokenKind::Eof`].
+ pub fn step(&mut self) {
+ // `next_token.len` is zero for the eof marker.
+ self.pos += self.next_token.len;
+ self.next_token = self.inner.advance_token();
+ }
+
+ /// Consumes tokens until the given pattern is either fully matched of fails to match.
+ /// Returns whether the pattern was fully matched.
+ ///
+ /// For each capture made by the pattern one item will be taken from the capture
+ /// sequence with the result placed inside.
+ fn match_impl(&mut self, pat: Pat<'_>, captures: &mut slice::IterMut<'_, Capture>) -> bool {
+ loop {
+ match (pat, self.next_token.kind) {
+ #[rustfmt::skip] // rustfmt bug: https://github.com/rust-lang/rustfmt/issues/6697
+ (_, TokenKind::Whitespace)
+ | (
+ Pat::AnyComment,
+ TokenKind::BlockComment { terminated: true, .. } | TokenKind::LineComment { .. },
+ ) => self.step(),
+ (Pat::AnyComment, _) => return true,
+ (Pat::Bang, TokenKind::Bang)
+ | (Pat::CloseBrace, TokenKind::CloseBrace)
+ | (Pat::CloseBracket, TokenKind::CloseBracket)
+ | (Pat::CloseParen, TokenKind::CloseParen)
+ | (Pat::Comma, TokenKind::Comma)
+ | (Pat::Eq, TokenKind::Eq)
+ | (Pat::Lifetime, TokenKind::Lifetime { .. })
+ | (Pat::Lt, TokenKind::Lt)
+ | (Pat::Gt, TokenKind::Gt)
+ | (Pat::OpenBrace, TokenKind::OpenBrace)
+ | (Pat::OpenBracket, TokenKind::OpenBracket)
+ | (Pat::OpenParen, TokenKind::OpenParen)
+ | (Pat::Pound, TokenKind::Pound)
+ | (Pat::Semi, TokenKind::Semi)
+ | (
+ Pat::LitStr,
+ TokenKind::Literal {
+ kind: LiteralKind::Str { terminated: true } | LiteralKind::RawStr { .. },
+ ..
+ },
+ ) => {
+ self.step();
+ return true;
+ },
+ (Pat::Ident(x), TokenKind::Ident) if x == self.peek_text() => {
+ self.step();
+ return true;
+ },
+ (Pat::DoubleColon, TokenKind::Colon) => {
+ self.step();
+ if !self.at_end() && matches!(self.next_token.kind, TokenKind::Colon) {
+ self.step();
+ return true;
+ }
+ return false;
+ },
+ #[rustfmt::skip]
+ (
+ Pat::CaptureLitStr,
+ TokenKind::Literal {
+ kind:
+ LiteralKind::Str { terminated: true }
+ | LiteralKind::RawStr { n_hashes: Some(_) },
+ ..
+ },
+ )
+ | (Pat::CaptureIdent, TokenKind::Ident) => {
+ *captures.next().unwrap() = Capture { pos: self.pos, len: self.next_token.len };
+ self.step();
+ return true;
+ },
+ _ => return false,
+ }
+ }
+ }
+
+ /// Consumes all tokens until the specified identifier is found and returns its
+ /// position. Returns `None` if the identifier could not be found.
+ ///
+ /// The cursor will be positioned immediately after the identifier, or at the end if
+ /// it is not.
+ pub fn find_ident(&mut self, ident: &str) -> Option {
+ loop {
+ match self.next_token.kind {
+ TokenKind::Ident if self.peek_text() == ident => {
+ let pos = self.pos;
+ self.step();
+ return Some(pos);
+ },
+ TokenKind::Eof => return None,
+ _ => self.step(),
+ }
+ }
+ }
+
+ /// Consumes all tokens until the next identifier is found and captures it. Returns
+ /// `None` if no identifier could be found.
+ ///
+ /// The cursor will be positioned immediately after the identifier, or at the end if
+ /// it is not.
+ pub fn find_any_ident(&mut self) -> Option {
+ loop {
+ match self.next_token.kind {
+ TokenKind::Ident => {
+ let res = Capture {
+ pos: self.pos,
+ len: self.next_token.len,
+ };
+ self.step();
+ return Some(res);
+ },
+ TokenKind::Eof => return None,
+ _ => self.step(),
+ }
+ }
+ }
+
+ /// Continually attempt to match the pattern on subsequent tokens until a match is
+ /// found. Returns whether the pattern was successfully matched.
+ ///
+ /// Not generally suitable for multi-token patterns or patterns that can match
+ /// nothing.
+ #[must_use]
+ pub fn find_pat(&mut self, pat: Pat<'_>) -> bool {
+ let mut capture = [].iter_mut();
+ while !self.match_impl(pat, &mut capture) {
+ self.step();
+ if self.at_end() {
+ return false;
+ }
+ }
+ true
+ }
+
+ /// Attempts to match a sequence of patterns at the current position. Returns whether
+ /// all patterns were successfully matched.
+ ///
+ /// Captures will be written to the given slice in the order they're matched. If a
+ /// capture is matched, but there are no more capture slots this will panic. If the
+ /// match is completed without filling all the capture slots they will be left
+ /// unmodified.
+ ///
+ /// If the match fails the cursor will be positioned at the first failing token.
+ #[must_use]
+ pub fn match_all(&mut self, pats: &[Pat<'_>], captures: &mut [Capture]) -> bool {
+ let mut captures = captures.iter_mut();
+ pats.iter().all(|&p| self.match_impl(p, &mut captures))
+ }
+
+ /// Attempts to match a single pattern at the current position. Returns whether the
+ /// pattern was successfully matched.
+ ///
+ /// If the pattern attempts to capture anything this will panic. If the match fails
+ /// the cursor will be positioned at the first failing token.
+ #[must_use]
+ pub fn match_pat(&mut self, pat: Pat<'_>) -> bool {
+ self.match_impl(pat, &mut [].iter_mut())
+ }
+}
diff --git a/clippy_dev/src/release.rs b/clippy_dev/src/release.rs
index 15392dd1d292..d11070bab85b 100644
--- a/clippy_dev/src/release.rs
+++ b/clippy_dev/src/release.rs
@@ -23,7 +23,7 @@ pub fn bump_version(mut version: Version) {
dst.push_str(&src[..package.version_range.start]);
write!(dst, "\"{}\"", version.toml_display()).unwrap();
dst.push_str(&src[package.version_range.end..]);
- UpdateStatus::from_changed(src.get(package.version_range.clone()) != dst.get(package.version_range))
+ UpdateStatus::from_changed(src.get(package.version_range) != dst.get(package.version_range))
}
});
}
diff --git a/clippy_dev/src/rename_lint.rs b/clippy_dev/src/rename_lint.rs
index d62597428e21..8e30eb7ce95b 100644
--- a/clippy_dev/src/rename_lint.rs
+++ b/clippy_dev/src/rename_lint.rs
@@ -1,7 +1,9 @@
-use crate::update_lints::{RenamedLint, find_lint_decls, generate_lint_files, read_deprecated_lints};
+use crate::parse::cursor::{self, Capture, Cursor};
+use crate::parse::{ParseCx, RenamedLint};
+use crate::update_lints::generate_lint_files;
use crate::utils::{
- ErrAction, FileUpdater, RustSearcher, Token, UpdateMode, UpdateStatus, Version, delete_dir_if_exists,
- delete_file_if_exists, expect_action, try_rename_dir, try_rename_file, walk_dir_no_dot_or_target,
+ ErrAction, FileUpdater, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists,
+ expect_action, try_rename_dir, try_rename_file, walk_dir_no_dot_or_target,
};
use rustc_lexer::TokenKind;
use std::ffi::OsString;
@@ -24,36 +26,35 @@ use std::path::Path;
/// * If `old_name` doesn't name an existing lint.
/// * If `old_name` names a deprecated or renamed lint.
#[expect(clippy::too_many_lines)]
-pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: bool) {
- if let Some((prefix, _)) = old_name.split_once("::") {
- panic!("`{old_name}` should not contain the `{prefix}` prefix");
- }
- if let Some((prefix, _)) = new_name.split_once("::") {
- panic!("`{new_name}` should not contain the `{prefix}` prefix");
- }
-
+pub fn rename<'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_name: &'cx str, new_name: &'cx str, uplift: bool) {
let mut updater = FileUpdater::default();
- let mut lints = find_lint_decls();
- let (deprecated_lints, mut renamed_lints) = read_deprecated_lints();
+ let mut lints = cx.find_lint_decls();
+ let (deprecated_lints, mut renamed_lints) = cx.read_deprecated_lints();
- let Ok(lint_idx) = lints.binary_search_by(|x| x.name.as_str().cmp(old_name)) else {
+ let Ok(lint_idx) = lints.binary_search_by(|x| x.name.cmp(old_name)) else {
panic!("could not find lint `{old_name}`");
};
let lint = &lints[lint_idx];
- let old_name_prefixed = String::from_iter(["clippy::", old_name]);
+ let old_name_prefixed = cx.str_buf.with(|buf| {
+ buf.extend(["clippy::", old_name]);
+ cx.arena.alloc_str(buf)
+ });
let new_name_prefixed = if uplift {
- new_name.to_owned()
+ new_name
} else {
- String::from_iter(["clippy::", new_name])
+ cx.str_buf.with(|buf| {
+ buf.extend(["clippy::", new_name]);
+ cx.arena.alloc_str(buf)
+ })
};
for lint in &mut renamed_lints {
if lint.new_name == old_name_prefixed {
- lint.new_name.clone_from(&new_name_prefixed);
+ lint.new_name = new_name_prefixed;
}
}
- match renamed_lints.binary_search_by(|x| x.old_name.cmp(&old_name_prefixed)) {
+ match renamed_lints.binary_search_by(|x| x.old_name.cmp(old_name_prefixed)) {
Ok(_) => {
println!("`{old_name}` already has a rename registered");
return;
@@ -63,12 +64,8 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b
idx,
RenamedLint {
old_name: old_name_prefixed,
- new_name: if uplift {
- new_name.to_owned()
- } else {
- String::from_iter(["clippy::", new_name])
- },
- version: clippy_version.rust_display().to_string(),
+ new_name: new_name_prefixed,
+ version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()),
},
);
},
@@ -104,7 +101,7 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b
}
delete_test_files(old_name, change_prefixed_tests);
lints.remove(lint_idx);
- } else if lints.binary_search_by(|x| x.name.as_str().cmp(new_name)).is_err() {
+ } else if lints.binary_search_by(|x| x.name.cmp(new_name)).is_err() {
let lint = &mut lints[lint_idx];
if lint.module.ends_with(old_name)
&& lint
@@ -118,13 +115,15 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b
mod_edit = ModEdit::Rename;
}
- let mod_len = lint.module.len();
- lint.module.truncate(mod_len - old_name.len());
- lint.module.push_str(new_name);
+ lint.module = cx.str_buf.with(|buf| {
+ buf.push_str(&lint.module[..lint.module.len() - old_name.len()]);
+ buf.push_str(new_name);
+ cx.arena.alloc_str(buf)
+ });
}
rename_test_files(old_name, new_name, change_prefixed_tests);
- new_name.clone_into(&mut lints[lint_idx].name);
- lints.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
+ lints[lint_idx].name = new_name;
+ lints.sort_by(|lhs, rhs| lhs.name.cmp(rhs.name));
} else {
println!("Renamed `clippy::{old_name}` to `clippy::{new_name}`");
println!("Since `{new_name}` already exists the existing code has not been changed");
@@ -132,7 +131,7 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b
}
let mut update_fn = file_update_fn(old_name, new_name, mod_edit);
- for e in walk_dir_no_dot_or_target() {
+ for e in walk_dir_no_dot_or_target(".") {
let e = expect_action(e, ErrAction::Read, ".");
if e.path().as_os_str().as_encoded_bytes().ends_with(b".rs") {
updater.update_file(e.path(), &mut update_fn);
@@ -285,47 +284,46 @@ fn file_update_fn<'a, 'b>(
move |_, src, dst| {
let mut copy_pos = 0u32;
let mut changed = false;
- let mut searcher = RustSearcher::new(src);
- let mut capture = "";
+ let mut cursor = Cursor::new(src);
+ let mut captures = [Capture::EMPTY];
loop {
- match searcher.peek() {
+ match cursor.peek() {
TokenKind::Eof => break,
TokenKind::Ident => {
- let match_start = searcher.pos();
- let text = searcher.peek_text();
- searcher.step();
+ let match_start = cursor.pos();
+ let text = cursor.peek_text();
+ cursor.step();
match text {
// clippy::line_name or clippy::lint-name
"clippy" => {
- if searcher.match_tokens(&[Token::DoubleColon, Token::CaptureIdent], &mut [&mut capture])
- && capture == old_name
+ if cursor.match_all(&[cursor::Pat::DoubleColon, cursor::Pat::CaptureIdent], &mut captures)
+ && cursor.get_text(captures[0]) == old_name
{
- dst.push_str(&src[copy_pos as usize..searcher.pos() as usize - capture.len()]);
+ dst.push_str(&src[copy_pos as usize..captures[0].pos as usize]);
dst.push_str(new_name);
- copy_pos = searcher.pos();
+ copy_pos = cursor.pos();
changed = true;
}
},
// mod lint_name
"mod" => {
if !matches!(mod_edit, ModEdit::None)
- && searcher.match_tokens(&[Token::CaptureIdent], &mut [&mut capture])
- && capture == old_name
+ && let Some(pos) = cursor.find_ident(old_name)
{
match mod_edit {
ModEdit::Rename => {
- dst.push_str(&src[copy_pos as usize..searcher.pos() as usize - capture.len()]);
+ dst.push_str(&src[copy_pos as usize..pos as usize]);
dst.push_str(new_name);
- copy_pos = searcher.pos();
+ copy_pos = cursor.pos();
changed = true;
},
- ModEdit::Delete if searcher.match_tokens(&[Token::Semi], &mut []) => {
+ ModEdit::Delete if cursor.match_pat(cursor::Pat::Semi) => {
let mut start = &src[copy_pos as usize..match_start as usize];
if start.ends_with("\n\n") {
start = &start[..start.len() - 1];
}
dst.push_str(start);
- copy_pos = searcher.pos();
+ copy_pos = cursor.pos();
if src[copy_pos as usize..].starts_with("\n\n") {
copy_pos += 1;
}
@@ -337,8 +335,8 @@ fn file_update_fn<'a, 'b>(
},
// lint_name::
name if matches!(mod_edit, ModEdit::Rename) && name == old_name => {
- let name_end = searcher.pos();
- if searcher.match_tokens(&[Token::DoubleColon], &mut []) {
+ let name_end = cursor.pos();
+ if cursor.match_pat(cursor::Pat::DoubleColon) {
dst.push_str(&src[copy_pos as usize..match_start as usize]);
dst.push_str(new_name);
copy_pos = name_end;
@@ -356,36 +354,36 @@ fn file_update_fn<'a, 'b>(
};
dst.push_str(&src[copy_pos as usize..match_start as usize]);
dst.push_str(replacement);
- copy_pos = searcher.pos();
+ copy_pos = cursor.pos();
changed = true;
},
}
},
// //~ lint_name
TokenKind::LineComment { doc_style: None } => {
- let text = searcher.peek_text();
+ let text = cursor.peek_text();
if text.starts_with("//~")
&& let Some(text) = text.strip_suffix(old_name)
&& !text.ends_with(|c| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_'))
{
- dst.push_str(&src[copy_pos as usize..searcher.pos() as usize + text.len()]);
+ dst.push_str(&src[copy_pos as usize..cursor.pos() as usize + text.len()]);
dst.push_str(new_name);
- copy_pos = searcher.pos() + searcher.peek_len();
+ copy_pos = cursor.pos() + cursor.peek_len();
changed = true;
}
- searcher.step();
+ cursor.step();
},
// ::lint_name
TokenKind::Colon
- if searcher.match_tokens(&[Token::DoubleColon, Token::CaptureIdent], &mut [&mut capture])
- && capture == old_name =>
+ if cursor.match_all(&[cursor::Pat::DoubleColon, cursor::Pat::CaptureIdent], &mut captures)
+ && cursor.get_text(captures[0]) == old_name =>
{
- dst.push_str(&src[copy_pos as usize..searcher.pos() as usize - capture.len()]);
+ dst.push_str(&src[copy_pos as usize..captures[0].pos as usize]);
dst.push_str(new_name);
- copy_pos = searcher.pos();
+ copy_pos = cursor.pos();
changed = true;
},
- _ => searcher.step(),
+ _ => cursor.step(),
}
}
diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs
index 5f6e874ffe25..3d0da6846114 100644
--- a/clippy_dev/src/update_lints.rs
+++ b/clippy_dev/src/update_lints.rs
@@ -1,13 +1,10 @@
-use crate::utils::{
- ErrAction, File, FileUpdater, RustSearcher, Token, UpdateMode, UpdateStatus, expect_action, update_text_region_fn,
-};
+use crate::parse::cursor::Cursor;
+use crate::parse::{DeprecatedLint, Lint, ParseCx, RenamedLint};
+use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, update_text_region_fn};
use itertools::Itertools;
use std::collections::HashSet;
use std::fmt::Write;
-use std::fs;
-use std::ops::Range;
-use std::path::{self, Path, PathBuf};
-use walkdir::{DirEntry, WalkDir};
+use std::path::{self, Path};
const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\
// Use that command to update this file and do not edit by hand.\n\
@@ -24,18 +21,18 @@ const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.ht
/// # Panics
///
/// Panics if a file path could not read from or then written to
-pub fn update(update_mode: UpdateMode) {
- let lints = find_lint_decls();
- let (deprecated, renamed) = read_deprecated_lints();
+pub fn update(cx: ParseCx<'_>, update_mode: UpdateMode) {
+ let lints = cx.find_lint_decls();
+ let (deprecated, renamed) = cx.read_deprecated_lints();
generate_lint_files(update_mode, &lints, &deprecated, &renamed);
}
#[expect(clippy::too_many_lines)]
pub fn generate_lint_files(
update_mode: UpdateMode,
- lints: &[Lint],
- deprecated: &[DeprecatedLint],
- renamed: &[RenamedLint],
+ lints: &[Lint<'_>],
+ deprecated: &[DeprecatedLint<'_>],
+ renamed: &[RenamedLint<'_>],
) {
let mut updater = FileUpdater::default();
updater.update_file_checked(
@@ -64,7 +61,7 @@ pub fn generate_lint_files(
|dst| {
for lint in lints
.iter()
- .map(|l| &*l.name)
+ .map(|l| l.name)
.chain(deprecated.iter().filter_map(|l| l.name.strip_prefix("clippy::")))
.chain(renamed.iter().filter_map(|l| l.old_name.strip_prefix("clippy::")))
.sorted()
@@ -79,13 +76,13 @@ pub fn generate_lint_files(
update_mode,
"clippy_lints/src/deprecated_lints.rs",
&mut |_, src, dst| {
- let mut searcher = RustSearcher::new(src);
+ let mut cursor = Cursor::new(src);
assert!(
- searcher.find_token(Token::Ident("declare_with_version"))
- && searcher.find_token(Token::Ident("declare_with_version")),
+ cursor.find_ident("declare_with_version").is_some()
+ && cursor.find_ident("declare_with_version").is_some(),
"error reading deprecated lints"
);
- dst.push_str(&src[..searcher.pos() as usize]);
+ dst.push_str(&src[..cursor.pos() as usize]);
dst.push_str("! { DEPRECATED(DEPRECATED_VERSION) = [\n");
for lint in deprecated {
write!(
@@ -135,13 +132,13 @@ pub fn generate_lint_files(
dst.push_str(GENERATED_FILE_COMMENT);
dst.push_str("#![allow(clippy::duplicated_attributes)]\n");
for lint in renamed {
- if seen_lints.insert(&lint.new_name) {
+ if seen_lints.insert(lint.new_name) {
writeln!(dst, "#![allow({})]", lint.new_name).unwrap();
}
}
seen_lints.clear();
for lint in renamed {
- if seen_lints.insert(&lint.old_name) {
+ if seen_lints.insert(lint.old_name) {
writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.old_name, lint.old_name).unwrap();
}
}
@@ -167,7 +164,7 @@ pub fn generate_lint_files(
for lint_mod in lints
.iter()
.filter(|l| !l.module.is_empty())
- .map(|l| l.module.split_once("::").map_or(&*l.module, |x| x.0))
+ .map(|l| l.module.split_once("::").map_or(l.module, |x| x.0))
.sorted()
.dedup()
{
@@ -200,260 +197,3 @@ pub fn generate_lint_files(
fn round_to_fifty(count: usize) -> usize {
count / 50 * 50
}
-
-/// Lint data parsed from the Clippy source code.
-#[derive(PartialEq, Eq, Debug)]
-pub struct Lint {
- pub name: String,
- pub group: String,
- pub module: String,
- pub path: PathBuf,
- pub declaration_range: Range,
-}
-
-pub struct DeprecatedLint {
- pub name: String,
- pub reason: String,
- pub version: String,
-}
-
-pub struct RenamedLint {
- pub old_name: String,
- pub new_name: String,
- pub version: String,
-}
-
-/// Finds all lint declarations (`declare_clippy_lint!`)
-#[must_use]
-pub fn find_lint_decls() -> Vec {
- let mut lints = Vec::with_capacity(1000);
- let mut contents = String::new();
- for e in expect_action(fs::read_dir("."), ErrAction::Read, ".") {
- let e = expect_action(e, ErrAction::Read, ".");
- if !expect_action(e.file_type(), ErrAction::Read, ".").is_dir() {
- continue;
- }
- let Ok(mut name) = e.file_name().into_string() else {
- continue;
- };
- if name.starts_with("clippy_lints") && name != "clippy_lints_internal" {
- name.push_str("/src");
- for (file, module) in read_src_with_module(name.as_ref()) {
- parse_clippy_lint_decls(
- file.path(),
- File::open_read_to_cleared_string(file.path(), &mut contents),
- &module,
- &mut lints,
- );
- }
- }
- }
- lints.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
- lints
-}
-
-/// Reads the source files from the given root directory
-fn read_src_with_module(src_root: &Path) -> impl use<'_> + Iterator- {
- WalkDir::new(src_root).into_iter().filter_map(move |e| {
- let e = expect_action(e, ErrAction::Read, src_root);
- let path = e.path().as_os_str().as_encoded_bytes();
- if let Some(path) = path.strip_suffix(b".rs")
- && let Some(path) = path.get(src_root.as_os_str().len() + 1..)
- {
- if path == b"lib" {
- Some((e, String::new()))
- } else {
- let path = if let Some(path) = path.strip_suffix(b"mod")
- && let Some(path) = path.strip_suffix(b"/").or_else(|| path.strip_suffix(b"\\"))
- {
- path
- } else {
- path
- };
- if let Ok(path) = str::from_utf8(path) {
- let path = path.replace(['/', '\\'], "::");
- Some((e, path))
- } else {
- None
- }
- }
- } else {
- None
- }
- })
-}
-
-/// Parse a source file looking for `declare_clippy_lint` macro invocations.
-fn parse_clippy_lint_decls(path: &Path, contents: &str, module: &str, lints: &mut Vec) {
- #[allow(clippy::enum_glob_use)]
- use Token::*;
- #[rustfmt::skip]
- static DECL_TOKENS: &[Token<'_>] = &[
- // !{ /// docs
- Bang, OpenBrace, AnyComment,
- // #[clippy::version = "version"]
- Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, LitStr, CloseBracket,
- // pub NAME, GROUP,
- Ident("pub"), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma,
- ];
-
- let mut searcher = RustSearcher::new(contents);
- while searcher.find_token(Ident("declare_clippy_lint")) {
- let start = searcher.pos() as usize - "declare_clippy_lint".len();
- let (mut name, mut group) = ("", "");
- if searcher.match_tokens(DECL_TOKENS, &mut [&mut name, &mut group]) && searcher.find_token(CloseBrace) {
- lints.push(Lint {
- name: name.to_lowercase(),
- group: group.into(),
- module: module.into(),
- path: path.into(),
- declaration_range: start..searcher.pos() as usize,
- });
- }
- }
-}
-
-#[must_use]
-pub fn read_deprecated_lints() -> (Vec, Vec) {
- #[allow(clippy::enum_glob_use)]
- use Token::*;
- #[rustfmt::skip]
- static DECL_TOKENS: &[Token<'_>] = &[
- // #[clippy::version = "version"]
- Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, CaptureLitStr, CloseBracket,
- // ("first", "second"),
- OpenParen, CaptureLitStr, Comma, CaptureLitStr, CloseParen, Comma,
- ];
- #[rustfmt::skip]
- static DEPRECATED_TOKENS: &[Token<'_>] = &[
- // !{ DEPRECATED(DEPRECATED_VERSION) = [
- Bang, OpenBrace, Ident("DEPRECATED"), OpenParen, Ident("DEPRECATED_VERSION"), CloseParen, Eq, OpenBracket,
- ];
- #[rustfmt::skip]
- static RENAMED_TOKENS: &[Token<'_>] = &[
- // !{ RENAMED(RENAMED_VERSION) = [
- Bang, OpenBrace, Ident("RENAMED"), OpenParen, Ident("RENAMED_VERSION"), CloseParen, Eq, OpenBracket,
- ];
-
- let path = "clippy_lints/src/deprecated_lints.rs";
- let mut deprecated = Vec::with_capacity(30);
- let mut renamed = Vec::with_capacity(80);
- let mut contents = String::new();
- File::open_read_to_cleared_string(path, &mut contents);
-
- let mut searcher = RustSearcher::new(&contents);
-
- // First instance is the macro definition.
- assert!(
- searcher.find_token(Ident("declare_with_version")),
- "error reading deprecated lints"
- );
-
- if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(DEPRECATED_TOKENS, &mut []) {
- let mut version = "";
- let mut name = "";
- let mut reason = "";
- while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut name, &mut reason]) {
- deprecated.push(DeprecatedLint {
- name: parse_str_single_line(path.as_ref(), name),
- reason: parse_str_single_line(path.as_ref(), reason),
- version: parse_str_single_line(path.as_ref(), version),
- });
- }
- } else {
- panic!("error reading deprecated lints");
- }
-
- if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(RENAMED_TOKENS, &mut []) {
- let mut version = "";
- let mut old_name = "";
- let mut new_name = "";
- while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut old_name, &mut new_name]) {
- renamed.push(RenamedLint {
- old_name: parse_str_single_line(path.as_ref(), old_name),
- new_name: parse_str_single_line(path.as_ref(), new_name),
- version: parse_str_single_line(path.as_ref(), version),
- });
- }
- } else {
- panic!("error reading renamed lints");
- }
-
- deprecated.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
- renamed.sort_by(|lhs, rhs| lhs.old_name.cmp(&rhs.old_name));
- (deprecated, renamed)
-}
-
-/// Removes the line splices and surrounding quotes from a string literal
-fn parse_str_lit(s: &str) -> String {
- let s = s.strip_prefix("r").unwrap_or(s).trim_matches('#');
- let s = s
- .strip_prefix('"')
- .and_then(|s| s.strip_suffix('"'))
- .unwrap_or_else(|| panic!("expected quoted string, found `{s}`"));
- let mut res = String::with_capacity(s.len());
- rustc_literal_escaper::unescape_str(s, &mut |_, ch| {
- if let Ok(ch) = ch {
- res.push(ch);
- }
- });
- res
-}
-
-fn parse_str_single_line(path: &Path, s: &str) -> String {
- let value = parse_str_lit(s);
- assert!(
- !value.contains('\n'),
- "error parsing `{}`: `{s}` should be a single line string",
- path.display(),
- );
- value
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_clippy_lint_decls() {
- static CONTENTS: &str = r#"
- declare_clippy_lint! {
- #[clippy::version = "Hello Clippy!"]
- pub PTR_ARG,
- style,
- "really long \
- text"
- }
-
- declare_clippy_lint!{
- #[clippy::version = "Test version"]
- pub DOC_MARKDOWN,
- pedantic,
- "single line"
- }
- "#;
- let mut result = Vec::new();
- parse_clippy_lint_decls("".as_ref(), CONTENTS, "module_name", &mut result);
- for r in &mut result {
- r.declaration_range = Range::default();
- }
-
- let expected = vec![
- Lint {
- name: "ptr_arg".into(),
- group: "style".into(),
- module: "module_name".into(),
- path: PathBuf::new(),
- declaration_range: Range::default(),
- },
- Lint {
- name: "doc_markdown".into(),
- group: "pedantic".into(),
- module: "module_name".into(),
- path: PathBuf::new(),
- declaration_range: Range::default(),
- },
- ];
- assert_eq!(expected, result);
- }
-}
diff --git a/clippy_dev/src/utils.rs b/clippy_dev/src/utils.rs
index 057951d0e33b..52452dd86b49 100644
--- a/clippy_dev/src/utils.rs
+++ b/clippy_dev/src/utils.rs
@@ -1,9 +1,9 @@
use core::fmt::{self, Display};
+use core::marker::PhantomData;
use core::num::NonZero;
-use core::ops::Range;
-use core::slice;
+use core::ops::{Deref, DerefMut};
+use core::range::Range;
use core::str::FromStr;
-use rustc_lexer::{self as lexer, FrontmatterAllowed};
use std::ffi::OsStr;
use std::fs::{self, OpenOptions};
use std::io::{self, Read as _, Seek as _, SeekFrom, Write};
@@ -12,6 +12,24 @@ use std::process::{self, Command, Stdio};
use std::{env, thread};
use walkdir::WalkDir;
+pub struct Scoped<'inner, 'outer: 'inner, T>(T, PhantomData<&'inner mut T>, PhantomData<&'outer mut ()>);
+impl Scoped<'_, '_, T> {
+ pub fn new(value: T) -> Self {
+ Self(value, PhantomData, PhantomData)
+ }
+}
+impl Deref for Scoped<'_, '_, T> {
+ type Target = T;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+impl DerefMut for Scoped<'_, '_, T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
#[derive(Clone, Copy)]
pub enum ErrAction {
Open,
@@ -410,179 +428,6 @@ pub fn update_text_region_fn(
move |path, src, dst| update_text_region(path, start, end, src, dst, &mut insert)
}
-#[derive(Clone, Copy)]
-pub enum Token<'a> {
- /// Matches any number of comments / doc comments.
- AnyComment,
- Ident(&'a str),
- CaptureIdent,
- LitStr,
- CaptureLitStr,
- Bang,
- CloseBrace,
- CloseBracket,
- CloseParen,
- /// This will consume the first colon even if the second doesn't exist.
- DoubleColon,
- Comma,
- Eq,
- Lifetime,
- Lt,
- Gt,
- OpenBrace,
- OpenBracket,
- OpenParen,
- Pound,
- Semi,
-}
-
-pub struct RustSearcher<'txt> {
- text: &'txt str,
- cursor: lexer::Cursor<'txt>,
- pos: u32,
- next_token: lexer::Token,
-}
-impl<'txt> RustSearcher<'txt> {
- #[must_use]
- #[expect(clippy::inconsistent_struct_constructor)]
- pub fn new(text: &'txt str) -> Self {
- let mut cursor = lexer::Cursor::new(text, FrontmatterAllowed::Yes);
- Self {
- text,
- pos: 0,
- next_token: cursor.advance_token(),
- cursor,
- }
- }
-
- #[must_use]
- pub fn peek_text(&self) -> &'txt str {
- &self.text[self.pos as usize..(self.pos + self.next_token.len) as usize]
- }
-
- #[must_use]
- pub fn peek_len(&self) -> u32 {
- self.next_token.len
- }
-
- #[must_use]
- pub fn peek(&self) -> lexer::TokenKind {
- self.next_token.kind
- }
-
- #[must_use]
- pub fn pos(&self) -> u32 {
- self.pos
- }
-
- #[must_use]
- pub fn at_end(&self) -> bool {
- self.next_token.kind == lexer::TokenKind::Eof
- }
-
- pub fn step(&mut self) {
- // `next_len` is zero for the sentinel value and the eof marker.
- self.pos += self.next_token.len;
- self.next_token = self.cursor.advance_token();
- }
-
- /// Consumes the next token if it matches the requested value and captures the value if
- /// requested. Returns true if a token was matched.
- fn read_token(&mut self, token: Token<'_>, captures: &mut slice::IterMut<'_, &mut &'txt str>) -> bool {
- loop {
- match (token, self.next_token.kind) {
- (_, lexer::TokenKind::Whitespace)
- | (
- Token::AnyComment,
- lexer::TokenKind::BlockComment { terminated: true, .. } | lexer::TokenKind::LineComment { .. },
- ) => self.step(),
- (Token::AnyComment, _) => return true,
- (Token::Bang, lexer::TokenKind::Bang)
- | (Token::CloseBrace, lexer::TokenKind::CloseBrace)
- | (Token::CloseBracket, lexer::TokenKind::CloseBracket)
- | (Token::CloseParen, lexer::TokenKind::CloseParen)
- | (Token::Comma, lexer::TokenKind::Comma)
- | (Token::Eq, lexer::TokenKind::Eq)
- | (Token::Lifetime, lexer::TokenKind::Lifetime { .. })
- | (Token::Lt, lexer::TokenKind::Lt)
- | (Token::Gt, lexer::TokenKind::Gt)
- | (Token::OpenBrace, lexer::TokenKind::OpenBrace)
- | (Token::OpenBracket, lexer::TokenKind::OpenBracket)
- | (Token::OpenParen, lexer::TokenKind::OpenParen)
- | (Token::Pound, lexer::TokenKind::Pound)
- | (Token::Semi, lexer::TokenKind::Semi)
- | (
- Token::LitStr,
- lexer::TokenKind::Literal {
- kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. },
- ..
- },
- ) => {
- self.step();
- return true;
- },
- (Token::Ident(x), lexer::TokenKind::Ident) if x == self.peek_text() => {
- self.step();
- return true;
- },
- (Token::DoubleColon, lexer::TokenKind::Colon) => {
- self.step();
- if !self.at_end() && matches!(self.next_token.kind, lexer::TokenKind::Colon) {
- self.step();
- return true;
- }
- return false;
- },
- (
- Token::CaptureLitStr,
- lexer::TokenKind::Literal {
- kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. },
- ..
- },
- )
- | (Token::CaptureIdent, lexer::TokenKind::Ident) => {
- **captures.next().unwrap() = self.peek_text();
- self.step();
- return true;
- },
- _ => return false,
- }
- }
- }
-
- #[must_use]
- pub fn find_token(&mut self, token: Token<'_>) -> bool {
- let mut capture = [].iter_mut();
- while !self.read_token(token, &mut capture) {
- self.step();
- if self.at_end() {
- return false;
- }
- }
- true
- }
-
- #[must_use]
- pub fn find_capture_token(&mut self, token: Token<'_>) -> Option<&'txt str> {
- let mut res = "";
- let mut capture = &mut res;
- let mut capture = slice::from_mut(&mut capture).iter_mut();
- while !self.read_token(token, &mut capture) {
- self.step();
- if self.at_end() {
- return None;
- }
- }
- Some(res)
- }
-
- #[must_use]
- pub fn match_tokens(&mut self, tokens: &[Token<'_>], captures: &mut [&mut &'txt str]) -> bool {
- let mut captures = captures.iter_mut();
- tokens.iter().all(|&t| self.read_token(t, &mut captures))
- }
-}
-
#[track_caller]
pub fn try_rename_file(old_name: &Path, new_name: &Path) -> bool {
match OpenOptions::new().create_new(true).write(true).open(new_name) {
@@ -748,8 +593,8 @@ pub fn delete_dir_if_exists(path: &Path) {
}
/// Walks all items excluding top-level dot files/directories and any target directories.
-pub fn walk_dir_no_dot_or_target() -> impl Iterator
- > {
- WalkDir::new(".").into_iter().filter_entry(|e| {
+pub fn walk_dir_no_dot_or_target(p: impl AsRef) -> impl Iterator
- > {
+ WalkDir::new(p).into_iter().filter_entry(|e| {
e.path()
.file_name()
.is_none_or(|x| x != "target" && x.as_encoded_bytes().first().copied() != Some(b'.'))
diff --git a/clippy_lints/Cargo.toml b/clippy_lints/Cargo.toml
index 42486e182ee3..bc97746a1cba 100644
--- a/clippy_lints/Cargo.toml
+++ b/clippy_lints/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "clippy_lints"
-version = "0.1.92"
+version = "0.1.93"
description = "A bunch of helpful lints to avoid common pitfalls in Rust"
repository = "https://github.com/rust-lang/rust-clippy"
readme = "README.md"
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index 375d179681da..a754eea31165 100644
--- a/clippy_lints/src/declared_lints.rs
+++ b/clippy_lints/src/declared_lints.rs
@@ -135,7 +135,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::duplicate_mod::DUPLICATE_MOD_INFO,
crate::else_if_without_else::ELSE_IF_WITHOUT_ELSE_INFO,
crate::empty_drop::EMPTY_DROP_INFO,
- crate::empty_enum::EMPTY_ENUM_INFO,
+ crate::empty_enums::EMPTY_ENUMS_INFO,
crate::empty_line_after::EMPTY_LINE_AFTER_DOC_COMMENTS_INFO,
crate::empty_line_after::EMPTY_LINE_AFTER_OUTER_ATTR_INFO,
crate::empty_with_brackets::EMPTY_ENUM_VARIANTS_WITH_BRACKETS_INFO,
@@ -227,8 +227,6 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::init_numbered_fields::INIT_NUMBERED_FIELDS_INFO,
crate::inline_fn_without_body::INLINE_FN_WITHOUT_BODY_INFO,
crate::int_plus_one::INT_PLUS_ONE_INFO,
- crate::integer_division_remainder_used::INTEGER_DIVISION_REMAINDER_USED_INFO,
- crate::invalid_upcast_comparisons::INVALID_UPCAST_COMPARISONS_INFO,
crate::item_name_repetitions::ENUM_VARIANT_NAMES_INFO,
crate::item_name_repetitions::MODULE_INCEPTION_INFO,
crate::item_name_repetitions::MODULE_NAME_REPETITIONS_INFO,
@@ -258,7 +256,6 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::lifetimes::ELIDABLE_LIFETIME_NAMES_INFO,
crate::lifetimes::EXTRA_UNUSED_LIFETIMES_INFO,
crate::lifetimes::NEEDLESS_LIFETIMES_INFO,
- crate::lines_filter_map_ok::LINES_FILTER_MAP_OK_INFO,
crate::literal_representation::DECIMAL_LITERAL_REPRESENTATION_INFO,
crate::literal_representation::INCONSISTENT_DIGIT_GROUPING_INFO,
crate::literal_representation::LARGE_DIGIT_GROUPS_INFO,
@@ -298,7 +295,6 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::manual_async_fn::MANUAL_ASYNC_FN_INFO,
crate::manual_bits::MANUAL_BITS_INFO,
crate::manual_clamp::MANUAL_CLAMP_INFO,
- crate::manual_div_ceil::MANUAL_DIV_CEIL_INFO,
crate::manual_float_methods::MANUAL_IS_FINITE_INFO,
crate::manual_float_methods::MANUAL_IS_INFINITE_INFO,
crate::manual_hash_one::MANUAL_HASH_ONE_INFO,
@@ -404,6 +400,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::methods::ITER_SKIP_ZERO_INFO,
crate::methods::ITER_WITH_DRAIN_INFO,
crate::methods::JOIN_ABSOLUTE_PATHS_INFO,
+ crate::methods::LINES_FILTER_MAP_OK_INFO,
crate::methods::MANUAL_CONTAINS_INFO,
crate::methods::MANUAL_C_STR_LITERALS_INFO,
crate::methods::MANUAL_FILTER_MAP_INFO,
@@ -545,7 +542,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::needless_continue::NEEDLESS_CONTINUE_INFO,
crate::needless_else::NEEDLESS_ELSE_INFO,
crate::needless_for_each::NEEDLESS_FOR_EACH_INFO,
- crate::needless_if::NEEDLESS_IF_INFO,
+ crate::needless_ifs::NEEDLESS_IFS_INFO,
crate::needless_late_init::NEEDLESS_LATE_INIT_INFO,
crate::needless_maybe_sized::NEEDLESS_MAYBE_SIZED_INFO,
crate::needless_parens_on_range_literals::NEEDLESS_PARENS_ON_RANGE_LITERALS_INFO,
@@ -592,6 +589,9 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::operators::IMPOSSIBLE_COMPARISONS_INFO,
crate::operators::INEFFECTIVE_BIT_MASK_INFO,
crate::operators::INTEGER_DIVISION_INFO,
+ crate::operators::INTEGER_DIVISION_REMAINDER_USED_INFO,
+ crate::operators::INVALID_UPCAST_COMPARISONS_INFO,
+ crate::operators::MANUAL_DIV_CEIL_INFO,
crate::operators::MANUAL_IS_MULTIPLE_OF_INFO,
crate::operators::MANUAL_MIDPOINT_INFO,
crate::operators::MISREFACTORED_ASSIGN_OP_INFO,
diff --git a/clippy_lints/src/deprecated_lints.rs b/clippy_lints/src/deprecated_lints.rs
index f087a894d76a..f010d17917f9 100644
--- a/clippy_lints/src/deprecated_lints.rs
+++ b/clippy_lints/src/deprecated_lints.rs
@@ -85,6 +85,8 @@ declare_with_version! { RENAMED(RENAMED_VERSION) = [
("clippy::drop_copy", "dropping_copy_types"),
#[clippy::version = ""]
("clippy::drop_ref", "dropping_references"),
+ #[clippy::version = "1.92.0"]
+ ("clippy::empty_enum", "clippy::empty_enums"),
#[clippy::version = ""]
("clippy::eval_order_dependence", "clippy::mixed_read_write_in_expression"),
#[clippy::version = "1.53.0"]
@@ -137,6 +139,8 @@ declare_with_version! { RENAMED(RENAMED_VERSION) = [
("clippy::mem_discriminant_non_enum", "enum_intrinsics_non_enums"),
#[clippy::version = "1.80.0"]
("clippy::mismatched_target_os", "unexpected_cfgs"),
+ #[clippy::version = "1.92.0"]
+ ("clippy::needless_if", "clippy::needless_ifs"),
#[clippy::version = ""]
("clippy::new_without_default_derive", "clippy::new_without_default"),
#[clippy::version = ""]
diff --git a/clippy_lints/src/doc/mod.rs b/clippy_lints/src/doc/mod.rs
index 2a3fb8294611..1e1d6e69cc91 100644
--- a/clippy_lints/src/doc/mod.rs
+++ b/clippy_lints/src/doc/mod.rs
@@ -22,7 +22,6 @@ use rustc_resolve::rustdoc::{
};
use rustc_session::impl_lint_pass;
use rustc_span::Span;
-use rustc_span::edition::Edition;
use std::ops::Range;
use url::Url;
@@ -36,6 +35,7 @@ mod markdown;
mod missing_headers;
mod needless_doctest_main;
mod suspicious_doc_comments;
+mod test_attr_in_doctest;
mod too_long_first_doc_paragraph;
declare_clippy_lint! {
@@ -900,8 +900,6 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet, attrs: &[
))
}
-const RUST_CODE: &[&str] = &["rust", "no_run", "should_panic", "compile_fail"];
-
enum Container {
Blockquote,
List(usize),
@@ -966,6 +964,70 @@ fn check_for_code_clusters<'a, Events: Iterator
- Self {
+ Self {
+ no_run: false,
+ ignore: false,
+ compile_fail: false,
+
+ rust: true,
+ }
+ }
+}
+
+impl CodeTags {
+ /// Based on
+ fn parse(lang: &str) -> Self {
+ let mut tags = Self::default();
+
+ let mut seen_rust_tags = false;
+ let mut seen_other_tags = false;
+ for item in lang.split([',', ' ', '\t']) {
+ match item.trim() {
+ "" => {},
+ "rust" => {
+ tags.rust = true;
+ seen_rust_tags = true;
+ },
+ "ignore" => {
+ tags.ignore = true;
+ seen_rust_tags = !seen_other_tags;
+ },
+ "no_run" => {
+ tags.no_run = true;
+ seen_rust_tags = !seen_other_tags;
+ },
+ "should_panic" => seen_rust_tags = !seen_other_tags,
+ "compile_fail" => {
+ tags.compile_fail = true;
+ seen_rust_tags = !seen_other_tags || seen_rust_tags;
+ },
+ "test_harness" | "standalone_crate" => {
+ seen_rust_tags = !seen_other_tags || seen_rust_tags;
+ },
+ _ if item.starts_with("ignore-") => seen_rust_tags = true,
+ _ if item.starts_with("edition") => {},
+ _ => seen_other_tags = true,
+ }
+ }
+
+ tags.rust &= seen_rust_tags || !seen_other_tags;
+
+ tags
+ }
+}
+
/// Checks parsed documentation.
/// This walks the "events" (think sections of markdown) produced by `pulldown_cmark`,
/// so lints here will generally access that information.
@@ -981,14 +1043,10 @@ fn check_doc<'a, Events: Iterator
- , Range DocHeaders {
// true if a safety header was found
let mut headers = DocHeaders::default();
- let mut in_code = false;
+ let mut code = None;
let mut in_link = None;
let mut in_heading = false;
let mut in_footnote_definition = false;
- let mut is_rust = false;
- let mut no_test = false;
- let mut ignore = false;
- let mut edition = None;
let mut ticks_unbalanced = false;
let mut text_to_check: Vec<(CowStr<'_>, Range, isize)> = Vec::new();
let mut paragraph_range = 0..0;
@@ -1048,31 +1106,12 @@ fn check_doc<'a, Events: Iterator
- , Range {
- in_code = true;
- if let CodeBlockKind::Fenced(lang) = kind {
- for item in lang.split(',') {
- if item == "ignore" {
- is_rust = false;
- break;
- } else if item == "no_test" {
- no_test = true;
- } else if item == "no_run" || item == "compile_fail" {
- ignore = true;
- }
- if let Some(stripped) = item.strip_prefix("edition") {
- is_rust = true;
- edition = stripped.parse::().ok();
- } else if item.is_empty() || RUST_CODE.contains(&item) {
- is_rust = true;
- }
- }
- }
- },
- End(TagEnd::CodeBlock) => {
- in_code = false;
- is_rust = false;
- ignore = false;
+ code = Some(match kind {
+ CodeBlockKind::Indented => CodeTags::default(),
+ CodeBlockKind::Fenced(lang) => CodeTags::parse(lang),
+ });
},
+ End(TagEnd::CodeBlock) => code = None,
Start(Link { dest_url, .. }) => in_link = Some(dest_url),
End(TagEnd::Link) => in_link = None,
Start(Heading { .. } | Paragraph | Item) => {
@@ -1182,7 +1221,7 @@ fn check_doc<'a, Events: Iterator
- , Range, Range>) {
- test_attr_spans.extend(
- item.attrs
- .iter()
- .find(|attr| attr.has_name(sym::test))
- .map(|attr| attr.span.lo().to_usize()..ident.span.hi().to_usize()),
- );
-}
-
-pub fn check(
- cx: &LateContext<'_>,
- text: &str,
- edition: Edition,
- range: Range,
- fragments: Fragments<'_>,
- ignore: bool,
-) {
- // return whether the code contains a needless `fn main` plus a vector of byte position ranges
- // of all `#[test]` attributes in not ignored code examples
- fn check_code_sample(code: String, edition: Edition, ignore: bool) -> (bool, Vec>) {
- rustc_driver::catch_fatal_errors(|| {
- rustc_span::create_session_globals_then(edition, &[], None, || {
- let mut test_attr_spans = vec![];
- let filename = FileName::anon_source_code(&code);
-
- let translator = rustc_driver::default_translator();
- let emitter = HumanEmitter::new(AutoStream::never(Box::new(io::sink())), translator);
- let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
- #[expect(clippy::arc_with_non_send_sync)] // `Arc` is expected by with_dcx
- let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
- let psess = ParseSess::with_dcx(dcx, sm);
-
- let mut parser =
- match new_parser_from_source_str(&psess, filename, code, StripTokens::ShebangAndFrontmatter) {
- Ok(p) => p,
- Err(errs) => {
- errs.into_iter().for_each(Diag::cancel);
- return (false, test_attr_spans);
- },
- };
-
- let mut relevant_main_found = false;
- let mut eligible = true;
- loop {
- match parser.parse_item(ForceCollect::No) {
- Ok(Some(item)) => match &item.kind {
- ItemKind::Fn(box Fn {
- ident,
- sig,
- body: Some(block),
- ..
- }) if ident.name == sym::main => {
- if !ignore {
- get_test_spans(&item, *ident, &mut test_attr_spans);
- }
-
- let is_async = matches!(sig.header.coroutine_kind, Some(CoroutineKind::Async { .. }));
- let returns_nothing = match &sig.decl.output {
- FnRetTy::Default(..) => true,
- FnRetTy::Ty(ty) if ty.kind.is_unit() => true,
- FnRetTy::Ty(_) => false,
- };
-
- if returns_nothing && !is_async && !block.stmts.is_empty() {
- // This main function should be linted, but only if there are no other functions
- relevant_main_found = true;
- } else {
- // This main function should not be linted, we're done
- eligible = false;
- }
- },
- // Another function was found; this case is ignored for needless_doctest_main
- ItemKind::Fn(fn_) => {
- eligible = false;
- if ignore {
- // If ignore is active invalidating one lint,
- // and we already found another function thus
- // invalidating the other one, we have no
- // business continuing.
- return (false, test_attr_spans);
- }
- get_test_spans(&item, fn_.ident, &mut test_attr_spans);
- },
- // Tests with one of these items are ignored
- ItemKind::Static(..)
- | ItemKind::Const(..)
- | ItemKind::ExternCrate(..)
- | ItemKind::ForeignMod(..) => {
- eligible = false;
- },
- _ => {},
- },
- Ok(None) => break,
- Err(e) => {
- // See issue #15041. When calling `.cancel()` on the `Diag`, Clippy will unexpectedly panic
- // when the `Diag` is unwinded. Meanwhile, we can just call `.emit()`, since the `DiagCtxt`
- // is just a sink, nothing will be printed.
- e.emit();
- return (false, test_attr_spans);
- },
- }
- }
-
- (relevant_main_found & eligible, test_attr_spans)
- })
- })
- .ok()
- .unwrap_or_default()
+use rustc_span::InnerSpan;
+
+fn returns_unit<'a>(mut tokens: impl Iterator
- ) -> bool {
+ let mut next = || tokens.next().map_or(TokenKind::Whitespace, |(kind, ..)| kind);
+
+ match next() {
+ // {
+ TokenKind::OpenBrace => true,
+ // - > ( ) {
+ TokenKind::Minus => {
+ next() == TokenKind::Gt
+ && next() == TokenKind::OpenParen
+ && next() == TokenKind::CloseParen
+ && next() == TokenKind::OpenBrace
+ },
+ _ => false,
}
+}
- let trailing_whitespace = text.len() - text.trim_end().len();
-
- // We currently only test for "fn main". Checking for the real
- // entrypoint (with tcx.entry_fn(())) in each block would be unnecessarily
- // expensive, as those are probably intended and relevant. Same goes for
- // macros and other weird ways of declaring a main function.
- //
- // Also, as we only check for attribute names and don't do macro expansion,
- // we can check only for #[test]
-
- if !((text.contains("main") && text.contains("fn")) || text.contains("#[test]")) {
+pub fn check(cx: &LateContext<'_>, text: &str, offset: usize, fragments: Fragments<'_>) {
+ if !text.contains("main") {
return;
}
- // Because of the global session, we need to create a new session in a different thread with
- // the edition we need.
- let text = text.to_owned();
- let (has_main, test_attr_spans) = thread::spawn(move || check_code_sample(text, edition, ignore))
- .join()
- .expect("thread::spawn failed");
- if has_main && let Some(span) = fragments.span(cx, range.start..range.end - trailing_whitespace) {
- span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest");
- }
- for span in test_attr_spans {
- let span = (range.start + span.start)..(range.start + span.end);
- if let Some(span) = fragments.span(cx, span) {
- span_lint(cx, TEST_ATTR_IN_DOCTEST, span, "unit tests in doctest are not executed");
+ let mut tokens = tokenize_with_text(text).filter(|&(kind, ..)| {
+ !matches!(
+ kind,
+ TokenKind::Whitespace | TokenKind::BlockComment { .. } | TokenKind::LineComment { .. }
+ )
+ });
+ if let Some((TokenKind::Ident, "fn", fn_span)) = tokens.next()
+ && let Some((TokenKind::Ident, "main", main_span)) = tokens.next()
+ && let Some((TokenKind::OpenParen, ..)) = tokens.next()
+ && let Some((TokenKind::CloseParen, ..)) = tokens.next()
+ && returns_unit(&mut tokens)
+ {
+ let mut depth = 1;
+ for (kind, ..) in &mut tokens {
+ match kind {
+ TokenKind::OpenBrace => depth += 1,
+ TokenKind::CloseBrace => {
+ depth -= 1;
+ if depth == 0 {
+ break;
+ }
+ },
+ _ => {},
+ }
+ }
+
+ if tokens.next().is_none()
+ && let Some(span) = fragments.span(cx, fn_span.start + offset..main_span.end + offset)
+ {
+ span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest");
}
}
}
diff --git a/clippy_lints/src/doc/test_attr_in_doctest.rs b/clippy_lints/src/doc/test_attr_in_doctest.rs
new file mode 100644
index 000000000000..65738434ac28
--- /dev/null
+++ b/clippy_lints/src/doc/test_attr_in_doctest.rs
@@ -0,0 +1,34 @@
+use super::Fragments;
+use crate::doc::TEST_ATTR_IN_DOCTEST;
+use clippy_utils::diagnostics::span_lint;
+use clippy_utils::tokenize_with_text;
+use rustc_lexer::TokenKind;
+use rustc_lint::LateContext;
+
+pub fn check(cx: &LateContext<'_>, text: &str, offset: usize, fragments: Fragments<'_>) {
+ if !text.contains("#[test]") {
+ return;
+ }
+
+ let mut spans = Vec::new();
+ let mut tokens = tokenize_with_text(text).filter(|&(kind, ..)| kind != TokenKind::Whitespace);
+ while let Some(token) = tokens.next() {
+ if let (TokenKind::Pound, _, pound_span) = token
+ && let Some((TokenKind::OpenBracket, ..)) = tokens.next()
+ && let Some((TokenKind::Ident, "test", _)) = tokens.next()
+ && let Some((TokenKind::CloseBracket, _, close_span)) = tokens.next()
+ && let Some(span) = fragments.span(cx, pound_span.start + offset..close_span.end + offset)
+ {
+ spans.push(span);
+ }
+ }
+
+ if !spans.is_empty() {
+ span_lint(
+ cx,
+ TEST_ATTR_IN_DOCTEST,
+ spans,
+ "unit tests in doctest are not executed",
+ );
+ }
+}
diff --git a/clippy_lints/src/double_parens.rs b/clippy_lints/src/double_parens.rs
index bddf4702fb34..8defbeeaa5f2 100644
--- a/clippy_lints/src/double_parens.rs
+++ b/clippy_lints/src/double_parens.rs
@@ -1,5 +1,5 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
-use clippy_utils::source::{HasSession, snippet_with_applicability, snippet_with_context};
+use clippy_utils::source::{HasSession, SpanRangeExt, snippet_with_applicability, snippet_with_context};
use rustc_ast::ast::{Expr, ExprKind, MethodCall};
use rustc_errors::Applicability;
use rustc_lint::{EarlyContext, EarlyLintPass};
@@ -47,8 +47,12 @@ impl EarlyLintPass for DoubleParens {
// ^^^^^^ expr
// ^^^^ inner
ExprKind::Paren(inner) if matches!(inner.kind, ExprKind::Paren(_) | ExprKind::Tup(_)) => {
- // suggest removing the outer parens
- if expr.span.eq_ctxt(inner.span) {
+ if expr.span.eq_ctxt(inner.span)
+ && !expr.span.in_external_macro(cx.sess().source_map())
+ && check_source(cx, inner)
+ {
+ // suggest removing the outer parens
+
let mut applicability = Applicability::MachineApplicable;
// We don't need to use `snippet_with_context` here, because:
// - if `inner`'s `ctxt` is from macro, we don't lint in the first place (see the check above)
@@ -74,8 +78,12 @@ impl EarlyLintPass for DoubleParens {
if let [arg] = &**args
&& let ExprKind::Paren(inner) = &arg.kind =>
{
- // suggest removing the inner parens
- if expr.span.eq_ctxt(arg.span) {
+ if expr.span.eq_ctxt(arg.span)
+ && !arg.span.in_external_macro(cx.sess().source_map())
+ && check_source(cx, arg)
+ {
+ // suggest removing the inner parens
+
let mut applicability = Applicability::MachineApplicable;
let sugg = snippet_with_context(cx.sess(), inner.span, arg.span.ctxt(), "_", &mut applicability).0;
span_lint_and_sugg(
@@ -93,3 +101,22 @@ impl EarlyLintPass for DoubleParens {
}
}
}
+
+/// Check that the span does indeed look like `( (..) )`
+fn check_source(cx: &EarlyContext<'_>, inner: &Expr) -> bool {
+ if let Some(sfr) = inner.span.get_source_range(cx)
+ // this is the same as `SourceFileRange::as_str`, but doesn't apply the range right away, because
+ // we're interested in the source code outside it
+ && let Some(src) = sfr.sf.src.as_ref().map(|src| src.as_str())
+ && let Some((start, outer_after_inner)) = src.split_at_checked(sfr.range.end)
+ && let Some((outer_before_inner, inner)) = start.split_at_checked(sfr.range.start)
+ && outer_before_inner.trim_end().ends_with('(')
+ && inner.starts_with('(')
+ && inner.ends_with(')')
+ && outer_after_inner.trim_start().starts_with(')')
+ {
+ true
+ } else {
+ false
+ }
+}
diff --git a/clippy_lints/src/empty_enum.rs b/clippy_lints/src/empty_enums.rs
similarity index 83%
rename from clippy_lints/src/empty_enum.rs
rename to clippy_lints/src/empty_enums.rs
index b0389fd9a2f6..f96854411fe6 100644
--- a/clippy_lints/src/empty_enum.rs
+++ b/clippy_lints/src/empty_enums.rs
@@ -1,4 +1,5 @@
use clippy_utils::diagnostics::span_lint_and_help;
+use clippy_utils::span_contains_cfg;
use rustc_hir::{Item, ItemKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
@@ -25,10 +26,6 @@ declare_clippy_lint! {
/// matched to mark code unreachable. If the field is not visible, then the struct
/// acts like any other struct with private fields.
///
- /// * If the enum has no variants only because all variants happen to be
- /// [disabled by conditional compilation][cfg], then it would be appropriate
- /// to allow the lint, with `#[allow(empty_enum)]`.
- ///
/// For further information, visit
/// [the never type’s documentation][`!`].
///
@@ -53,24 +50,24 @@ declare_clippy_lint! {
/// [newtype]: https://doc.rust-lang.org/book/ch19-04-advanced-types.html#using-the-newtype-pattern-for-type-safety-and-abstraction
/// [visibility]: https://doc.rust-lang.org/reference/visibility-and-privacy.html
#[clippy::version = "pre 1.29.0"]
- pub EMPTY_ENUM,
+ pub EMPTY_ENUMS,
pedantic,
"enum with no variants"
}
-declare_lint_pass!(EmptyEnum => [EMPTY_ENUM]);
+declare_lint_pass!(EmptyEnums => [EMPTY_ENUMS]);
-impl LateLintPass<'_> for EmptyEnum {
+impl LateLintPass<'_> for EmptyEnums {
fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
- if let ItemKind::Enum(..) = item.kind
+ if let ItemKind::Enum(.., def) = item.kind
+ && def.variants.is_empty()
// Only suggest the `never_type` if the feature is enabled
&& cx.tcx.features().never_type()
- && let Some(adt) = cx.tcx.type_of(item.owner_id).instantiate_identity().ty_adt_def()
- && adt.variants().is_empty()
+ && !span_contains_cfg(cx, item.span)
{
span_lint_and_help(
cx,
- EMPTY_ENUM,
+ EMPTY_ENUMS,
item.span,
"enum with no variants",
None,
diff --git a/clippy_lints/src/extra_unused_type_parameters.rs b/clippy_lints/src/extra_unused_type_parameters.rs
index c0b0fd88d9e1..fb98cb30ae90 100644
--- a/clippy_lints/src/extra_unused_type_parameters.rs
+++ b/clippy_lints/src/extra_unused_type_parameters.rs
@@ -157,6 +157,11 @@ impl<'cx, 'tcx> TypeWalker<'cx, 'tcx> {
let spans = if explicit_params.len() == extra_params.len() {
vec![self.generics.span] // Remove the entire list of generics
} else {
+ // 1. Start from the last extra param
+ // 2. While the params preceding it are also extra, construct spans going from the current param to
+ // the comma before it
+ // 3. Once this chain of extra params stops, switch to constructing spans going from the current
+ // param to the comma _after_ it
let mut end: Option = None;
extra_params
.iter()
diff --git a/clippy_lints/src/formatting.rs b/clippy_lints/src/formatting.rs
index 1c751643becb..3b9c70e23e20 100644
--- a/clippy_lints/src/formatting.rs
+++ b/clippy_lints/src/formatting.rs
@@ -110,7 +110,7 @@ declare_clippy_lint! {
/// } if bar { // looks like an `else` is missing here
/// }
/// ```
- #[clippy::version = "1.90.0"]
+ #[clippy::version = "1.91.0"]
pub POSSIBLE_MISSING_ELSE,
suspicious,
"possibly missing `else`"
diff --git a/clippy_lints/src/incompatible_msrv.rs b/clippy_lints/src/incompatible_msrv.rs
index bfe7a6355c00..716334656926 100644
--- a/clippy_lints/src/incompatible_msrv.rs
+++ b/clippy_lints/src/incompatible_msrv.rs
@@ -3,10 +3,9 @@ use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::msrvs::Msrv;
use clippy_utils::{is_in_const_context, is_in_test};
use rustc_data_structures::fx::FxHashMap;
-use rustc_hir::def::DefKind;
use rustc_hir::{self as hir, AmbigArg, Expr, ExprKind, HirId, RustcVersion, StabilityLevel, StableSince};
use rustc_lint::{LateContext, LateLintPass};
-use rustc_middle::ty::TyCtxt;
+use rustc_middle::ty::{self, TyCtxt};
use rustc_session::impl_lint_pass;
use rustc_span::def_id::{CrateNum, DefId};
use rustc_span::{ExpnKind, Span, sym};
@@ -83,6 +82,10 @@ pub struct IncompatibleMsrv {
availability_cache: FxHashMap<(DefId, bool), Availability>,
check_in_tests: bool,
core_crate: Option,
+
+ // The most recently called path. Used to skip checking the path after it's
+ // been checked when visiting the call expression.
+ called_path: Option,
}
impl_lint_pass!(IncompatibleMsrv => [INCOMPATIBLE_MSRV]);
@@ -98,6 +101,7 @@ impl IncompatibleMsrv {
.iter()
.find(|krate| tcx.crate_name(**krate) == sym::core)
.copied(),
+ called_path: None,
}
}
@@ -140,7 +144,14 @@ impl IncompatibleMsrv {
}
/// Emit lint if `def_id`, associated with `node` and `span`, is below the current MSRV.
- fn emit_lint_if_under_msrv(&mut self, cx: &LateContext<'_>, def_id: DefId, node: HirId, span: Span) {
+ fn emit_lint_if_under_msrv(
+ &mut self,
+ cx: &LateContext<'_>,
+ needs_const: bool,
+ def_id: DefId,
+ node: HirId,
+ span: Span,
+ ) {
if def_id.is_local() {
// We don't check local items since their MSRV is supposed to always be valid.
return;
@@ -158,10 +169,6 @@ impl IncompatibleMsrv {
return;
}
- let needs_const = cx.enclosing_body.is_some()
- && is_in_const_context(cx)
- && matches!(cx.tcx.def_kind(def_id), DefKind::AssocFn | DefKind::Fn);
-
if (self.check_in_tests || !is_in_test(cx.tcx, node))
&& let Some(current) = self.msrv.current(cx)
&& let Availability::Since(version) = self.get_def_id_availability(cx.tcx, def_id, needs_const)
@@ -190,13 +197,33 @@ impl<'tcx> LateLintPass<'tcx> for IncompatibleMsrv {
match expr.kind {
ExprKind::MethodCall(_, _, _, span) => {
if let Some(method_did) = cx.typeck_results().type_dependent_def_id(expr.hir_id) {
- self.emit_lint_if_under_msrv(cx, method_did, expr.hir_id, span);
+ self.emit_lint_if_under_msrv(cx, is_in_const_context(cx), method_did, expr.hir_id, span);
}
},
- ExprKind::Path(qpath) => {
- if let Some(path_def_id) = cx.qpath_res(&qpath, expr.hir_id).opt_def_id() {
- self.emit_lint_if_under_msrv(cx, path_def_id, expr.hir_id, expr.span);
- }
+ ExprKind::Call(callee, _) if let ExprKind::Path(qpath) = callee.kind => {
+ self.called_path = Some(callee.hir_id);
+ let needs_const = is_in_const_context(cx);
+ let def_id = if let Some(def_id) = cx.qpath_res(&qpath, callee.hir_id).opt_def_id() {
+ def_id
+ } else if needs_const && let ty::FnDef(def_id, _) = *cx.typeck_results().expr_ty(callee).kind() {
+ // Edge case where a function is first assigned then called.
+ // We previously would have warned for the non-const MSRV, when
+ // checking the path, but now that it's called the const MSRV
+ // must also be met.
+ def_id
+ } else {
+ return;
+ };
+ self.emit_lint_if_under_msrv(cx, needs_const, def_id, expr.hir_id, callee.span);
+ },
+ // Desugaring into function calls by the compiler will use `QPath::LangItem` variants. Those should
+ // not be linted as they will not be generated in older compilers if the function is not available,
+ // and the compiler is allowed to call unstable functions.
+ ExprKind::Path(qpath)
+ if let Some(path_def_id) = cx.qpath_res(&qpath, expr.hir_id).opt_def_id()
+ && self.called_path != Some(expr.hir_id) =>
+ {
+ self.emit_lint_if_under_msrv(cx, false, path_def_id, expr.hir_id, expr.span);
},
_ => {},
}
@@ -208,7 +235,7 @@ impl<'tcx> LateLintPass<'tcx> for IncompatibleMsrv {
// `CStr` and `CString` have been moved around but have been available since Rust 1.0.0
&& !matches!(cx.tcx.get_diagnostic_name(ty_def_id), Some(sym::cstr_type | sym::cstring_type))
{
- self.emit_lint_if_under_msrv(cx, ty_def_id, hir_ty.hir_id, hir_ty.span);
+ self.emit_lint_if_under_msrv(cx, false, ty_def_id, hir_ty.hir_id, hir_ty.span);
}
}
}
diff --git a/clippy_lints/src/integer_division_remainder_used.rs b/clippy_lints/src/integer_division_remainder_used.rs
deleted file mode 100644
index a1215491b48c..000000000000
--- a/clippy_lints/src/integer_division_remainder_used.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-use clippy_utils::diagnostics::span_lint;
-use rustc_ast::BinOpKind;
-use rustc_hir::{Expr, ExprKind};
-use rustc_lint::{LateContext, LateLintPass};
-use rustc_middle::ty::{self};
-use rustc_session::declare_lint_pass;
-
-declare_clippy_lint! {
- /// ### What it does
- /// Checks for the usage of division (`/`) and remainder (`%`) operations
- /// when performed on any integer types using the default `Div` and `Rem` trait implementations.
- ///
- /// ### Why restrict this?
- /// In cryptographic contexts, division can result in timing sidechannel vulnerabilities,
- /// and needs to be replaced with constant-time code instead (e.g. Barrett reduction).
- ///
- /// ### Example
- /// ```no_run
- /// let my_div = 10 / 2;
- /// ```
- /// Use instead:
- /// ```no_run
- /// let my_div = 10 >> 1;
- /// ```
- #[clippy::version = "1.79.0"]
- pub INTEGER_DIVISION_REMAINDER_USED,
- restriction,
- "use of disallowed default division and remainder operations"
-}
-
-declare_lint_pass!(IntegerDivisionRemainderUsed => [INTEGER_DIVISION_REMAINDER_USED]);
-
-impl LateLintPass<'_> for IntegerDivisionRemainderUsed {
- fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
- if let ExprKind::Binary(op, lhs, rhs) = &expr.kind
- && let BinOpKind::Div | BinOpKind::Rem = op.node
- && let lhs_ty = cx.typeck_results().expr_ty(lhs)
- && let rhs_ty = cx.typeck_results().expr_ty(rhs)
- && let ty::Int(_) | ty::Uint(_) = lhs_ty.peel_refs().kind()
- && let ty::Int(_) | ty::Uint(_) = rhs_ty.peel_refs().kind()
- {
- span_lint(
- cx,
- INTEGER_DIVISION_REMAINDER_USED,
- expr.span.source_callsite(),
- format!("use of {} has been disallowed in this context", op.node.as_str()),
- );
- }
- }
-}
diff --git a/clippy_lints/src/len_zero.rs b/clippy_lints/src/len_zero.rs
index 04a8e4739b85..877bd34a732b 100644
--- a/clippy_lints/src/len_zero.rs
+++ b/clippy_lints/src/len_zero.rs
@@ -1,4 +1,6 @@
+use clippy_config::Conf;
use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_then};
+use clippy_utils::msrvs::Msrv;
use clippy_utils::res::{MaybeDef, MaybeTypeckRes};
use clippy_utils::source::{SpanRangeExt, snippet_with_context};
use clippy_utils::sugg::{Sugg, has_enclosing_paren};
@@ -10,12 +12,12 @@ use rustc_hir::def::Res;
use rustc_hir::def_id::{DefId, DefIdSet};
use rustc_hir::{
BinOpKind, Expr, ExprKind, FnRetTy, GenericArg, GenericBound, HirId, ImplItem, ImplItemKind, ImplicitSelfKind,
- Item, ItemKind, Mutability, Node, OpaqueTyOrigin, PatExprKind, PatKind, PathSegment, PrimTy, QPath, TraitItemId,
- TyKind,
+ Item, ItemKind, Mutability, Node, OpaqueTyOrigin, PatExprKind, PatKind, PathSegment, PrimTy, QPath, RustcVersion,
+ StabilityLevel, StableSince, TraitItemId, TyKind,
};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{self, FnSig, Ty};
-use rustc_session::declare_lint_pass;
+use rustc_session::impl_lint_pass;
use rustc_span::source_map::Spanned;
use rustc_span::symbol::kw;
use rustc_span::{Ident, Span, Symbol};
@@ -120,7 +122,17 @@ declare_clippy_lint! {
"checking `x == \"\"` or `x == []` (or similar) when `.is_empty()` could be used instead"
}
-declare_lint_pass!(LenZero => [LEN_ZERO, LEN_WITHOUT_IS_EMPTY, COMPARISON_TO_EMPTY]);
+pub struct LenZero {
+ msrv: Msrv,
+}
+
+impl_lint_pass!(LenZero => [LEN_ZERO, LEN_WITHOUT_IS_EMPTY, COMPARISON_TO_EMPTY]);
+
+impl LenZero {
+ pub fn new(conf: &'static Conf) -> Self {
+ Self { msrv: conf.msrv }
+ }
+}
impl<'tcx> LateLintPass<'tcx> for LenZero {
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) {
@@ -184,7 +196,7 @@ impl<'tcx> LateLintPass<'tcx> for LenZero {
_ => false,
}
&& !expr.span.from_expansion()
- && has_is_empty(cx, lt.init)
+ && has_is_empty(cx, lt.init, self.msrv)
{
let mut applicability = Applicability::MachineApplicable;
@@ -206,7 +218,7 @@ impl<'tcx> LateLintPass<'tcx> for LenZero {
&& cx.ty_based_def(expr).opt_parent(cx).is_diag_item(cx, sym::PartialEq)
&& !expr.span.from_expansion()
{
- check_empty_expr(
+ self.check_empty_expr(
cx,
expr.span,
lhs_expr,
@@ -226,29 +238,110 @@ impl<'tcx> LateLintPass<'tcx> for LenZero {
let actual_span = span_without_enclosing_paren(cx, expr.span);
match cmp {
BinOpKind::Eq => {
- check_cmp(cx, actual_span, left, right, "", 0); // len == 0
- check_cmp(cx, actual_span, right, left, "", 0); // 0 == len
+ self.check_cmp(cx, actual_span, left, right, "", 0); // len == 0
+ self.check_cmp(cx, actual_span, right, left, "", 0); // 0 == len
},
BinOpKind::Ne => {
- check_cmp(cx, actual_span, left, right, "!", 0); // len != 0
- check_cmp(cx, actual_span, right, left, "!", 0); // 0 != len
+ self.check_cmp(cx, actual_span, left, right, "!", 0); // len != 0
+ self.check_cmp(cx, actual_span, right, left, "!", 0); // 0 != len
},
BinOpKind::Gt => {
- check_cmp(cx, actual_span, left, right, "!", 0); // len > 0
- check_cmp(cx, actual_span, right, left, "", 1); // 1 > len
+ self.check_cmp(cx, actual_span, left, right, "!", 0); // len > 0
+ self.check_cmp(cx, actual_span, right, left, "", 1); // 1 > len
},
BinOpKind::Lt => {
- check_cmp(cx, actual_span, left, right, "", 1); // len < 1
- check_cmp(cx, actual_span, right, left, "!", 0); // 0 < len
+ self.check_cmp(cx, actual_span, left, right, "", 1); // len < 1
+ self.check_cmp(cx, actual_span, right, left, "!", 0); // 0 < len
},
- BinOpKind::Ge => check_cmp(cx, actual_span, left, right, "!", 1), // len >= 1
- BinOpKind::Le => check_cmp(cx, actual_span, right, left, "!", 1), // 1 <= len
+ BinOpKind::Ge => self.check_cmp(cx, actual_span, left, right, "!", 1), // len >= 1
+ BinOpKind::Le => self.check_cmp(cx, actual_span, right, left, "!", 1), // 1 <= len
_ => (),
}
}
}
}
+impl LenZero {
+ fn check_cmp(
+ &self,
+ cx: &LateContext<'_>,
+ span: Span,
+ method: &Expr<'_>,
+ lit: &Expr<'_>,
+ op: &str,
+ compare_to: u32,
+ ) {
+ if method.span.from_expansion() {
+ return;
+ }
+
+ if let (&ExprKind::MethodCall(method_path, receiver, [], _), ExprKind::Lit(lit)) = (&method.kind, &lit.kind) {
+ // check if we are in an is_empty() method
+ if parent_item_name(cx, method) == Some(sym::is_empty) {
+ return;
+ }
+
+ self.check_len(cx, span, method_path.ident.name, receiver, &lit.node, op, compare_to);
+ } else {
+ self.check_empty_expr(cx, span, method, lit, op);
+ }
+ }
+
+ #[expect(clippy::too_many_arguments)]
+ fn check_len(
+ &self,
+ cx: &LateContext<'_>,
+ span: Span,
+ method_name: Symbol,
+ receiver: &Expr<'_>,
+ lit: &LitKind,
+ op: &str,
+ compare_to: u32,
+ ) {
+ if let LitKind::Int(lit, _) = *lit {
+ // check if length is compared to the specified number
+ if lit != u128::from(compare_to) {
+ return;
+ }
+
+ if method_name == sym::len && has_is_empty(cx, receiver, self.msrv) {
+ let mut applicability = Applicability::MachineApplicable;
+ span_lint_and_sugg(
+ cx,
+ LEN_ZERO,
+ span,
+ format!("length comparison to {}", if compare_to == 0 { "zero" } else { "one" }),
+ format!("using `{op}is_empty` is clearer and more explicit"),
+ format!(
+ "{op}{}.is_empty()",
+ snippet_with_context(cx, receiver.span, span.ctxt(), "_", &mut applicability).0,
+ ),
+ applicability,
+ );
+ }
+ }
+ }
+
+ fn check_empty_expr(&self, cx: &LateContext<'_>, span: Span, lit1: &Expr<'_>, lit2: &Expr<'_>, op: &str) {
+ if (is_empty_array(lit2) || is_empty_string(lit2)) && has_is_empty(cx, lit1, self.msrv) {
+ let mut applicability = Applicability::MachineApplicable;
+
+ let lit1 = peel_ref_operators(cx, lit1);
+ let lit_str = Sugg::hir_with_context(cx, lit1, span.ctxt(), "_", &mut applicability).maybe_paren();
+
+ span_lint_and_sugg(
+ cx,
+ COMPARISON_TO_EMPTY,
+ span,
+ "comparison to empty slice",
+ format!("using `{op}is_empty` is clearer and more explicit"),
+ format!("{op}{lit_str}.is_empty()"),
+ applicability,
+ );
+ }
+ }
+}
+
fn span_without_enclosing_paren(cx: &LateContext<'_>, span: Span) -> Span {
let Some(snippet) = span.get_source_text(cx) else {
return span;
@@ -513,75 +606,6 @@ fn check_for_is_empty(
}
}
-fn check_cmp(cx: &LateContext<'_>, span: Span, method: &Expr<'_>, lit: &Expr<'_>, op: &str, compare_to: u32) {
- if method.span.from_expansion() {
- return;
- }
-
- if let (&ExprKind::MethodCall(method_path, receiver, [], _), ExprKind::Lit(lit)) = (&method.kind, &lit.kind) {
- // check if we are in an is_empty() method
- if parent_item_name(cx, method) == Some(sym::is_empty) {
- return;
- }
-
- check_len(cx, span, method_path.ident.name, receiver, &lit.node, op, compare_to);
- } else {
- check_empty_expr(cx, span, method, lit, op);
- }
-}
-
-fn check_len(
- cx: &LateContext<'_>,
- span: Span,
- method_name: Symbol,
- receiver: &Expr<'_>,
- lit: &LitKind,
- op: &str,
- compare_to: u32,
-) {
- if let LitKind::Int(lit, _) = *lit {
- // check if length is compared to the specified number
- if lit != u128::from(compare_to) {
- return;
- }
-
- if method_name == sym::len && has_is_empty(cx, receiver) {
- let mut applicability = Applicability::MachineApplicable;
- span_lint_and_sugg(
- cx,
- LEN_ZERO,
- span,
- format!("length comparison to {}", if compare_to == 0 { "zero" } else { "one" }),
- format!("using `{op}is_empty` is clearer and more explicit"),
- format!(
- "{op}{}.is_empty()",
- snippet_with_context(cx, receiver.span, span.ctxt(), "_", &mut applicability).0,
- ),
- applicability,
- );
- }
- }
-}
-
-fn check_empty_expr(cx: &LateContext<'_>, span: Span, lit1: &Expr<'_>, lit2: &Expr<'_>, op: &str) {
- if (is_empty_array(lit2) || is_empty_string(lit2)) && has_is_empty(cx, lit1) {
- let mut applicability = Applicability::MachineApplicable;
-
- let lit1 = peel_ref_operators(cx, lit1);
- let lit_str = Sugg::hir_with_context(cx, lit1, span.ctxt(), "_", &mut applicability).maybe_paren();
-
- span_lint_and_sugg(
- cx,
- COMPARISON_TO_EMPTY,
- span,
- "comparison to empty slice",
- format!("using `{op}is_empty` is clearer and more explicit"),
- format!("{op}{lit_str}.is_empty()"),
- applicability,
- );
- }
-}
-
fn is_empty_string(expr: &Expr<'_>) -> bool {
if let ExprKind::Lit(lit) = expr.kind
&& let LitKind::Str(lit, _) = lit.node
@@ -600,45 +624,59 @@ fn is_empty_array(expr: &Expr<'_>) -> bool {
}
/// Checks if this type has an `is_empty` method.
-fn has_is_empty(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
+fn has_is_empty(cx: &LateContext<'_>, expr: &Expr<'_>, msrv: Msrv) -> bool {
/// Gets an `AssocItem` and return true if it matches `is_empty(self)`.
- fn is_is_empty(cx: &LateContext<'_>, item: &ty::AssocItem) -> bool {
+ fn is_is_empty_and_stable(cx: &LateContext<'_>, item: &ty::AssocItem, msrv: Msrv) -> bool {
if item.is_fn() {
let sig = cx.tcx.fn_sig(item.def_id).skip_binder();
let ty = sig.skip_binder();
ty.inputs().len() == 1
+ && cx.tcx.lookup_stability(item.def_id).is_none_or(|stability| {
+ if let StabilityLevel::Stable { since, .. } = stability.level {
+ let version = match since {
+ StableSince::Version(version) => version,
+ StableSince::Current => RustcVersion::CURRENT,
+ StableSince::Err(_) => return false,
+ };
+
+ msrv.meets(cx, version)
+ } else {
+ // Unstable fn, check if the feature is enabled.
+ cx.tcx.features().enabled(stability.feature) && msrv.current(cx).is_none()
+ }
+ })
} else {
false
}
}
/// Checks the inherent impl's items for an `is_empty(self)` method.
- fn has_is_empty_impl(cx: &LateContext<'_>, id: DefId) -> bool {
+ fn has_is_empty_impl(cx: &LateContext<'_>, id: DefId, msrv: Msrv) -> bool {
cx.tcx.inherent_impls(id).iter().any(|imp| {
cx.tcx
.associated_items(*imp)
.filter_by_name_unhygienic(sym::is_empty)
- .any(|item| is_is_empty(cx, item))
+ .any(|item| is_is_empty_and_stable(cx, item, msrv))
})
}
- fn ty_has_is_empty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, depth: usize) -> bool {
+ fn ty_has_is_empty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, depth: usize, msrv: Msrv) -> bool {
match ty.kind() {
ty::Dynamic(tt, ..) => tt.principal().is_some_and(|principal| {
cx.tcx
.associated_items(principal.def_id())
.filter_by_name_unhygienic(sym::is_empty)
- .any(|item| is_is_empty(cx, item))
+ .any(|item| is_is_empty_and_stable(cx, item, msrv))
}),
- ty::Alias(ty::Projection, proj) => has_is_empty_impl(cx, proj.def_id),
+ ty::Alias(ty::Projection, proj) => has_is_empty_impl(cx, proj.def_id, msrv),
ty::Adt(id, _) => {
- has_is_empty_impl(cx, id.did())
+ has_is_empty_impl(cx, id.did(), msrv)
|| (cx.tcx.recursion_limit().value_within_limit(depth)
&& cx.tcx.get_diagnostic_item(sym::Deref).is_some_and(|deref_id| {
implements_trait(cx, ty, deref_id, &[])
&& cx
.get_associated_type(ty, deref_id, sym::Target)
- .is_some_and(|deref_ty| ty_has_is_empty(cx, deref_ty, depth + 1))
+ .is_some_and(|deref_ty| ty_has_is_empty(cx, deref_ty, depth + 1, msrv))
}))
},
ty::Array(..) | ty::Slice(..) | ty::Str => true,
@@ -646,5 +684,5 @@ fn has_is_empty(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
}
}
- ty_has_is_empty(cx, cx.typeck_results().expr_ty(expr).peel_refs(), 0)
+ ty_has_is_empty(cx, cx.typeck_results().expr_ty(expr).peel_refs(), 0, msrv)
}
diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs
index a4ad9424b3eb..99cafc7fc6d8 100644
--- a/clippy_lints/src/lib.rs
+++ b/clippy_lints/src/lib.rs
@@ -32,7 +32,6 @@ extern crate rustc_arena;
extern crate rustc_ast;
extern crate rustc_ast_pretty;
extern crate rustc_data_structures;
-extern crate rustc_driver;
extern crate rustc_errors;
extern crate rustc_hir;
extern crate rustc_hir_analysis;
@@ -43,7 +42,6 @@ extern crate rustc_infer;
extern crate rustc_lexer;
extern crate rustc_lint;
extern crate rustc_middle;
-extern crate rustc_parse;
extern crate rustc_parse_format;
extern crate rustc_resolve;
extern crate rustc_session;
@@ -117,7 +115,7 @@ mod drop_forget_ref;
mod duplicate_mod;
mod else_if_without_else;
mod empty_drop;
-mod empty_enum;
+mod empty_enums;
mod empty_line_after;
mod empty_with_brackets;
mod endian_bytes;
@@ -171,8 +169,6 @@ mod inherent_to_string;
mod init_numbered_fields;
mod inline_fn_without_body;
mod int_plus_one;
-mod integer_division_remainder_used;
-mod invalid_upcast_comparisons;
mod item_name_repetitions;
mod items_after_statements;
mod items_after_test_module;
@@ -191,7 +187,6 @@ mod let_if_seq;
mod let_underscore;
mod let_with_type_underscore;
mod lifetimes;
-mod lines_filter_map_ok;
mod literal_representation;
mod literal_string_with_formatting_args;
mod loops;
@@ -203,7 +198,6 @@ mod manual_assert;
mod manual_async_fn;
mod manual_bits;
mod manual_clamp;
-mod manual_div_ceil;
mod manual_float_methods;
mod manual_hash_one;
mod manual_ignore_case_cmp;
@@ -255,7 +249,7 @@ mod needless_borrows_for_generic_args;
mod needless_continue;
mod needless_else;
mod needless_for_each;
-mod needless_if;
+mod needless_ifs;
mod needless_late_init;
mod needless_maybe_sized;
mod needless_parens_on_range_literals;
@@ -481,7 +475,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_late_pass(|_| Box::new(mut_mut::MutMut::default()));
store.register_late_pass(|_| Box::new(unnecessary_mut_passed::UnnecessaryMutPassed));
store.register_late_pass(|_| Box::>::default());
- store.register_late_pass(|_| Box::new(len_zero::LenZero));
+ store.register_late_pass(move |_| Box::new(len_zero::LenZero::new(conf)));
store.register_late_pass(move |_| Box::new(attrs::Attributes::new(conf)));
store.register_late_pass(|_| Box::new(blocks_in_conditions::BlocksInConditions));
store.register_late_pass(|_| Box::new(unicode::Unicode));
@@ -542,8 +536,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_late_pass(|_| Box::new(derive::Derive));
store.register_late_pass(move |_| Box::new(derivable_impls::DerivableImpls::new(conf)));
store.register_late_pass(|_| Box::new(drop_forget_ref::DropForgetRef));
- store.register_late_pass(|_| Box::new(empty_enum::EmptyEnum));
- store.register_late_pass(|_| Box::new(invalid_upcast_comparisons::InvalidUpcastComparisons));
+ store.register_late_pass(|_| Box::new(empty_enums::EmptyEnums));
store.register_late_pass(|_| Box::::default());
store.register_late_pass(move |tcx| Box::new(ifs::CopyAndPaste::new(tcx, conf)));
store.register_late_pass(|_| Box::new(copy_iterator::CopyIterator));
@@ -743,7 +736,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_late_pass(move |_| Box::new(manual_main_separator_str::ManualMainSeparatorStr::new(conf)));
store.register_late_pass(|_| Box::new(unnecessary_struct_initialization::UnnecessaryStruct));
store.register_late_pass(move |_| Box::new(unnecessary_box_returns::UnnecessaryBoxReturns::new(conf)));
- store.register_late_pass(move |_| Box::new(lines_filter_map_ok::LinesFilterMapOk::new(conf)));
store.register_late_pass(|_| Box::new(tests_outside_test_module::TestsOutsideTestModule));
store.register_late_pass(|_| Box::new(manual_slice_size_calculation::ManualSliceSizeCalculation::new(conf)));
store.register_early_pass(move || Box::new(excessive_nesting::ExcessiveNesting::new(conf)));
@@ -755,7 +747,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_late_pass(|_| Box::new(endian_bytes::EndianBytes));
store.register_late_pass(|_| Box::new(redundant_type_annotations::RedundantTypeAnnotations));
store.register_late_pass(|_| Box::new(arc_with_non_send_sync::ArcWithNonSendSync));
- store.register_late_pass(|_| Box::new(needless_if::NeedlessIf));
+ store.register_late_pass(|_| Box::new(needless_ifs::NeedlessIfs));
store.register_late_pass(move |_| Box::new(min_ident_chars::MinIdentChars::new(conf)));
store.register_late_pass(move |_| Box::new(large_stack_frames::LargeStackFrames::new(conf)));
store.register_late_pass(|_| Box::new(single_range_in_vec_init::SingleRangeInVecInit));
@@ -798,7 +790,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_early_pass(|| Box::new(multiple_bound_locations::MultipleBoundLocations));
store.register_late_pass(move |_| Box::new(assigning_clones::AssigningClones::new(conf)));
store.register_late_pass(|_| Box::new(zero_repeat_side_effects::ZeroRepeatSideEffects));
- store.register_late_pass(|_| Box::new(integer_division_remainder_used::IntegerDivisionRemainderUsed));
store.register_late_pass(move |_| Box::new(macro_metavars_in_unsafe::ExprMetavarsInUnsafe::new(conf)));
store.register_late_pass(move |_| Box::new(string_patterns::StringPatterns::new(conf)));
store.register_early_pass(|| Box::new(field_scoped_visibility_modifiers::FieldScopedVisibilityModifiers));
@@ -807,7 +798,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_early_pass(|| Box::new(cfg_not_test::CfgNotTest));
store.register_late_pass(|_| Box::new(zombie_processes::ZombieProcesses));
store.register_late_pass(|_| Box::new(pointers_in_nomem_asm_block::PointersInNomemAsmBlock));
- store.register_late_pass(move |_| Box::new(manual_div_ceil::ManualDivCeil::new(conf)));
store.register_late_pass(move |_| Box::new(manual_is_power_of_two::ManualIsPowerOfTwo::new(conf)));
store.register_late_pass(|_| Box::new(non_zero_suggestions::NonZeroSuggestions));
store.register_late_pass(|_| Box::new(literal_string_with_formatting_args::LiteralStringWithFormattingArg));
diff --git a/clippy_lints/src/lifetimes.rs b/clippy_lints/src/lifetimes.rs
index 519ec228c884..727e9b172a87 100644
--- a/clippy_lints/src/lifetimes.rs
+++ b/clippy_lints/src/lifetimes.rs
@@ -856,89 +856,49 @@ fn elision_suggestions(
.filter(|param| !param.is_elided_lifetime() && !param.is_impl_trait())
.collect::>();
- if !elidable_lts
- .iter()
- .all(|lt| explicit_params.iter().any(|param| param.def_id == *lt))
- {
- return None;
- }
-
- let mut suggestions = if elidable_lts.is_empty() {
- vec![]
- } else if elidable_lts.len() == explicit_params.len() {
+ let mut suggestions = if elidable_lts.len() == explicit_params.len() {
// if all the params are elided remove the whole generic block
//
// fn x<'a>() {}
// ^^^^
vec![(generics.span, String::new())]
} else {
- match &explicit_params[..] {
- // no params, nothing to elide
- [] => unreachable!("handled by `elidable_lts.is_empty()`"),
- [param] => {
- if elidable_lts.contains(¶m.def_id) {
- unreachable!("handled by `elidable_lts.len() == explicit_params.len()`")
+ // 1. Start from the last elidable lifetime
+ // 2. While the lifetimes preceding it are also elidable, construct spans going from the current
+ // lifetime to the comma before it
+ // 3. Once this chain of elidable lifetimes stops, switch to constructing spans going from the
+ // current lifetime to the comma _after_ it
+ let mut end: Option = None;
+ elidable_lts
+ .iter()
+ .rev()
+ .map(|&id| {
+ let (idx, param) = explicit_params.iter().find_position(|param| param.def_id == id)?;
+
+ let span = if let Some(next) = explicit_params.get(idx + 1)
+ && end != Some(next.def_id)
+ {
+ // Extend the current span forward, up until the next param in the list.
+ // fn x<'prev, 'a, 'next>() {}
+ // ^^^^
+ param.span.until(next.span)
} else {
- unreachable!("handled by `elidable_lts.is_empty()`")
- }
- },
- [_, _, ..] => {
- // Given a list like `<'a, 'b, 'c, 'd, ..>`,
- //
- // If there is a cluster of elidable lifetimes at the beginning, say `'a` and `'b`, we should
- // suggest removing them _and_ the trailing comma. The span for that is `a.span.until(c.span)`:
- // <'a, 'b, 'c, 'd, ..> => <'a, 'b, 'c, 'd, ..>
- // ^^ ^^ ^^^^^^^^
- //
- // And since we know that `'c` isn't elidable--otherwise it would've been in the cluster--we can go
- // over all the lifetimes after it, and for each elidable one, add a suggestion spanning the
- // lifetime itself and the comma before, because each individual suggestion is guaranteed to leave
- // the list valid:
- // <.., 'c, 'd, 'e, 'f, 'g, ..> => <.., 'c, 'd, 'e, 'f, 'g, ..>
- // ^^ ^^ ^^ ^^^^ ^^^^^^^^
- //
- // In case there is no such starting cluster, we only need to do the second part of the algorithm:
- // <'a, 'b, 'c, 'd, 'e, 'f, 'g, ..> => <'a, 'b , 'c, 'd, 'e, 'f, 'g, ..>
- // ^^ ^^ ^^ ^^ ^^^^^^^^^ ^^^^^^^^
-
- // Split off the starting cluster
- // TODO: use `slice::split_once` once stabilized (github.com/rust-lang/rust/issues/112811):
- // ```
- // let Some(split) = explicit_params.split_once(|param| !elidable_lts.contains(¶m.def_id)) else {
- // // there were no lifetime param that couldn't be elided
- // unreachable!("handled by `elidable_lts.len() == explicit_params.len()`")
- // };
- // match split { /* .. */ }
- // ```
- let Some(split_pos) = explicit_params
- .iter()
- .position(|param| !elidable_lts.contains(¶m.def_id))
- else {
- // there were no lifetime param that couldn't be elided
- unreachable!("handled by `elidable_lts.len() == explicit_params.len()`")
+ // Extend the current span back to include the comma following the previous
+ // param. If the span of the next param in the list has already been
+ // extended, we continue the chain. This is why we're iterating in reverse.
+ end = Some(param.def_id);
+
+ // `idx` will never be 0, else we'd be removing the entire list of generics
+ let prev = explicit_params.get(idx - 1)?;
+
+ // fn x<'prev, 'a>() {}
+ // ^^^^
+ param.span.with_lo(prev.span.hi())
};
- let split = explicit_params
- .split_at_checked(split_pos)
- .expect("got `split_pos` from `position` on the same Vec");
-
- match split {
- ([..], []) => unreachable!("handled by `elidable_lts.len() == explicit_params.len()`"),
- ([], [_]) => unreachable!("handled by `explicit_params.len() == 1`"),
- (cluster, rest @ [rest_first, ..]) => {
- // the span for the cluster
- (cluster.first().map(|fw| fw.span.until(rest_first.span)).into_iter())
- // the span for the remaining lifetimes (calculations independent of the cluster)
- .chain(
- rest.array_windows()
- .filter(|[_, curr]| elidable_lts.contains(&curr.def_id))
- .map(|[prev, curr]| curr.span.with_lo(prev.span.hi())),
- )
- .map(|sp| (sp, String::new()))
- .collect()
- },
- }
- },
- }
+
+ Some((span, String::new()))
+ })
+ .collect::