11use clippy_config:: Conf ;
2- use clippy_utils:: diagnostics:: span_lint_and_then ;
2+ use clippy_utils:: diagnostics:: span_lint_hir_and_then ;
33use clippy_utils:: msrvs:: { self , Msrv } ;
44use clippy_utils:: source:: { IntoSpan as _, SpanRangeExt , snippet, snippet_block_with_applicability} ;
5- use clippy_utils:: { span_contains_non_whitespace, tokenize_with_text} ;
6- use rustc_ast:: BinOpKind ;
5+ use clippy_utils:: { span_contains_non_whitespace, sym , tokenize_with_text} ;
6+ use rustc_ast:: { BinOpKind , MetaItemInner } ;
77use rustc_errors:: Applicability ;
88use rustc_hir:: { Block , Expr , ExprKind , StmtKind } ;
99use rustc_lexer:: TokenKind ;
10- use rustc_lint:: { LateContext , LateLintPass } ;
10+ use rustc_lint:: { LateContext , LateLintPass , Level } ;
1111use rustc_session:: impl_lint_pass;
1212use rustc_span:: source_map:: SourceMap ;
13- use rustc_span:: { BytePos , Span } ;
13+ use rustc_span:: { BytePos , Span , Symbol } ;
1414
1515declare_clippy_lint ! {
1616 /// ### What it does
@@ -95,14 +95,14 @@ impl CollapsibleIf {
9595
9696 fn check_collapsible_else_if ( & self , cx : & LateContext < ' _ > , then_span : Span , else_block : & Block < ' _ > ) {
9797 if let Some ( else_) = expr_block ( else_block)
98- && cx. tcx . hir_attrs ( else_. hir_id ) . is_empty ( )
9998 && !else_. span . from_expansion ( )
10099 && let ExprKind :: If ( else_if_cond, ..) = else_. kind
101- && ! block_starts_with_significant_tokens ( cx, else_block, else_, self . lint_commented_code )
100+ && self . check_significant_tokens_and_expect_attrs ( cx, else_block, else_, sym :: collapsible_else_if )
102101 {
103- span_lint_and_then (
102+ span_lint_hir_and_then (
104103 cx,
105104 COLLAPSIBLE_ELSE_IF ,
105+ else_. hir_id ,
106106 else_block. span ,
107107 "this `else { if .. }` block can be collapsed" ,
108108 |diag| {
@@ -166,15 +166,15 @@ impl CollapsibleIf {
166166
167167 fn check_collapsible_if_if ( & self , cx : & LateContext < ' _ > , expr : & Expr < ' _ > , check : & Expr < ' _ > , then : & Block < ' _ > ) {
168168 if let Some ( inner) = expr_block ( then)
169- && cx. tcx . hir_attrs ( inner. hir_id ) . is_empty ( )
170169 && let ExprKind :: If ( check_inner, _, None ) = & inner. kind
171170 && self . eligible_condition ( cx, check_inner)
172171 && expr. span . eq_ctxt ( inner. span )
173- && ! block_starts_with_significant_tokens ( cx, then, inner, self . lint_commented_code )
172+ && self . check_significant_tokens_and_expect_attrs ( cx, then, inner, sym :: collapsible_if )
174173 {
175- span_lint_and_then (
174+ span_lint_hir_and_then (
176175 cx,
177176 COLLAPSIBLE_IF ,
177+ inner. hir_id ,
178178 expr. span ,
179179 "this `if` statement can be collapsed" ,
180180 |diag| {
@@ -219,6 +219,45 @@ impl CollapsibleIf {
219219 !matches ! ( cond. kind, ExprKind :: Let ( ..) )
220220 || ( cx. tcx . sess . edition ( ) . at_least_rust_2024 ( ) && self . msrv . meets ( cx, msrvs:: LET_CHAINS ) )
221221 }
222+
223+ // Check that nothing significant can be found between the initial `{` of `inner_if` and
224+ // the beginning of `inner_if_expr`...
225+ //
226+ // Unless it's only an `#[expect(clippy::collapsible{,_else}_if)]` attribute, in which case we
227+ // _do_ need to lint, in order to actually fulfill its expectation (#13365)
228+ fn check_significant_tokens_and_expect_attrs (
229+ & self ,
230+ cx : & LateContext < ' _ > ,
231+ inner_if : & Block < ' _ > ,
232+ inner_if_expr : & Expr < ' _ > ,
233+ expected_lint_name : Symbol ,
234+ ) -> bool {
235+ match cx. tcx . hir_attrs ( inner_if_expr. hir_id ) {
236+ [ ] => {
237+ // There aren't any attributes, so just check for significant tokens
238+ let span = inner_if. span . split_at ( 1 ) . 1 . until ( inner_if_expr. span ) ;
239+ !span_contains_non_whitespace ( cx, span, self . lint_commented_code )
240+ } ,
241+
242+ [ attr]
243+ if matches ! ( Level :: from_attr( attr) , Some ( ( Level :: Expect , _) ) )
244+ && let Some ( metas) = attr. meta_item_list ( )
245+ && let Some ( MetaItemInner :: MetaItem ( meta_item) ) = metas. first ( )
246+ && let [ tool, lint_name] = meta_item. path . segments . as_slice ( )
247+ && tool. ident . name == sym:: clippy
248+ && [ expected_lint_name, sym:: style, sym:: all] . contains ( & lint_name. ident . name ) =>
249+ {
250+ // There is an `expect` attribute -- check that there is no _other_ significant text
251+ let span_before_attr = inner_if. span . split_at ( 1 ) . 1 . until ( attr. span ( ) ) ;
252+ let span_after_attr = attr. span ( ) . between ( inner_if_expr. span ) ;
253+ !span_contains_non_whitespace ( cx, span_before_attr, self . lint_commented_code )
254+ && !span_contains_non_whitespace ( cx, span_after_attr, self . lint_commented_code )
255+ } ,
256+
257+ // There are other attributes, which are significant tokens -- check failed
258+ _ => false ,
259+ }
260+ }
222261}
223262
224263impl_lint_pass ! ( CollapsibleIf => [ COLLAPSIBLE_IF , COLLAPSIBLE_ELSE_IF ] ) ;
@@ -242,18 +281,6 @@ impl LateLintPass<'_> for CollapsibleIf {
242281 }
243282}
244283
245- // Check that nothing significant can be found but whitespaces between the initial `{` of `block`
246- // and the beginning of `stop_at`.
247- fn block_starts_with_significant_tokens (
248- cx : & LateContext < ' _ > ,
249- block : & Block < ' _ > ,
250- stop_at : & Expr < ' _ > ,
251- lint_commented_code : bool ,
252- ) -> bool {
253- let span = block. span . split_at ( 1 ) . 1 . until ( stop_at. span ) ;
254- span_contains_non_whitespace ( cx, span, lint_commented_code)
255- }
256-
257284/// If `block` is a block with either one expression or a statement containing an expression,
258285/// return the expression. We don't peel blocks recursively, as extra blocks might be intentional.
259286fn expr_block < ' tcx > ( block : & Block < ' tcx > ) -> Option < & ' tcx Expr < ' tcx > > {
0 commit comments