11use clippy_config:: Conf ;
2- use clippy_utils:: diagnostics:: { span_lint_and_sugg , span_lint_and_then} ;
2+ use clippy_utils:: diagnostics:: span_lint_and_then;
33use clippy_utils:: msrvs:: { self , Msrv } ;
4- use clippy_utils:: source:: { IntoSpan as _, SpanRangeExt , snippet, snippet_block , snippet_block_with_applicability} ;
5- use clippy_utils:: span_contains_non_comment ;
4+ use clippy_utils:: source:: { IntoSpan as _, SpanRangeExt , snippet, snippet_block_with_applicability} ;
5+ use clippy_utils:: { span_contains_non_whitespace , tokenize_with_text } ;
66use rustc_ast:: BinOpKind ;
77use rustc_errors:: Applicability ;
88use rustc_hir:: { Block , Expr , ExprKind , Stmt , StmtKind } ;
9+ use rustc_lexer:: TokenKind ;
910use rustc_lint:: { LateContext , LateLintPass } ;
1011use rustc_session:: impl_lint_pass;
12+ use rustc_span:: source_map:: SourceMap ;
1113use rustc_span:: { BytePos , Span } ;
1214
1315declare_clippy_lint ! {
@@ -91,37 +93,74 @@ impl CollapsibleIf {
9193 }
9294 }
9395
94- fn check_collapsible_else_if ( cx : & LateContext < ' _ > , then_span : Span , else_block : & Block < ' _ > ) {
95- if !block_starts_with_comment ( cx, else_block)
96- && let Some ( else_) = expr_block ( else_block)
96+ fn check_collapsible_else_if ( & self , cx : & LateContext < ' _ > , then_span : Span , else_block : & Block < ' _ > ) {
97+ if let Some ( else_) = expr_block ( else_block)
9798 && cx. tcx . hir_attrs ( else_. hir_id ) . is_empty ( )
9899 && !else_. span . from_expansion ( )
99- && let ExprKind :: If ( ..) = else_. kind
100- && let up_to_if = else_block. span . until ( else_. span )
101- && !span_contains_non_comment ( cx, up_to_if. with_lo ( BytePos ( up_to_if. lo ( ) . 0 + 1 ) ) )
100+ && let ExprKind :: If ( else_if_cond, ..) = else_. kind
101+ && !block_starts_with_significant_tokens ( cx, else_block, else_, self . lint_commented_code )
102102 {
103- // Prevent "elseif"
104- // Check that the "else" is followed by whitespace
105- let up_to_else = then_span. between ( else_block. span ) ;
106- let requires_space = if let Some ( c) = snippet ( cx, up_to_else, ".." ) . chars ( ) . last ( ) {
107- !c. is_whitespace ( )
108- } else {
109- false
110- } ;
111-
112- let mut applicability = Applicability :: MachineApplicable ;
113- span_lint_and_sugg (
103+ span_lint_and_then (
114104 cx,
115105 COLLAPSIBLE_ELSE_IF ,
116106 else_block. span ,
117107 "this `else { if .. }` block can be collapsed" ,
118- "collapse nested if block" ,
119- format ! (
120- "{}{}" ,
121- if requires_space { " " } else { "" } ,
122- snippet_block_with_applicability( cx, else_. span, ".." , Some ( else_block. span) , & mut applicability)
123- ) ,
124- applicability,
108+ |diag| {
109+ let up_to_else = then_span. between ( else_block. span ) ;
110+ let else_before_if = else_. span . shrink_to_lo ( ) . with_hi ( else_if_cond. span . lo ( ) - BytePos ( 1 ) ) ;
111+ if self . lint_commented_code
112+ && let Some ( else_keyword_span) =
113+ span_extract_keyword ( cx. tcx . sess . source_map ( ) , up_to_else, "else" )
114+ && let Some ( else_if_keyword_span) =
115+ span_extract_keyword ( cx. tcx . sess . source_map ( ) , else_before_if, "if" )
116+ {
117+ let else_keyword_span = else_keyword_span. with_leading_whitespace ( cx) . into_span ( ) ;
118+ let else_open_bracket = else_block. span . split_at ( 1 ) . 0 . with_leading_whitespace ( cx) . into_span ( ) ;
119+ let else_closing_bracket = {
120+ let end = else_block. span . shrink_to_hi ( ) ;
121+ end. with_lo ( end. lo ( ) - BytePos ( 1 ) )
122+ . with_leading_whitespace ( cx)
123+ . into_span ( )
124+ } ;
125+ let sugg = vec ! [
126+ // Remove the outer else block `else`
127+ ( else_keyword_span, String :: new( ) ) ,
128+ // Replace the inner `if` by `else if`
129+ ( else_if_keyword_span, String :: from( "else if" ) ) ,
130+ // Remove the outer else block `{`
131+ ( else_open_bracket, String :: new( ) ) ,
132+ // Remove the outer else block '}'
133+ ( else_closing_bracket, String :: new( ) ) ,
134+ ] ;
135+ diag. multipart_suggestion ( "collapse nested if block" , sugg, Applicability :: MachineApplicable ) ;
136+ return ;
137+ }
138+
139+ // Prevent "elseif"
140+ // Check that the "else" is followed by whitespace
141+ let requires_space = if let Some ( c) = snippet ( cx, up_to_else, ".." ) . chars ( ) . last ( ) {
142+ !c. is_whitespace ( )
143+ } else {
144+ false
145+ } ;
146+ let mut applicability = Applicability :: MachineApplicable ;
147+ diag. span_suggestion (
148+ else_block. span ,
149+ "collapse nested if block" ,
150+ format ! (
151+ "{}{}" ,
152+ if requires_space { " " } else { "" } ,
153+ snippet_block_with_applicability(
154+ cx,
155+ else_. span,
156+ ".." ,
157+ Some ( else_block. span) ,
158+ & mut applicability
159+ )
160+ ) ,
161+ applicability,
162+ ) ;
163+ } ,
125164 ) ;
126165 }
127166 }
@@ -133,7 +172,7 @@ impl CollapsibleIf {
133172 && self . eligible_condition ( cx, check_inner)
134173 && let ctxt = expr. span . ctxt ( )
135174 && inner. span . ctxt ( ) == ctxt
136- && ( self . lint_commented_code || ! block_starts_with_comment ( cx, then) )
175+ && ! block_starts_with_significant_tokens ( cx, then, inner , self . lint_commented_code )
137176 {
138177 span_lint_and_then (
139178 cx,
@@ -182,7 +221,7 @@ impl LateLintPass<'_> for CollapsibleIf {
182221 if let Some ( else_) = else_
183222 && let ExprKind :: Block ( else_, None ) = else_. kind
184223 {
185- Self :: check_collapsible_else_if ( cx, then. span , else_) ;
224+ self . check_collapsible_else_if ( cx, then. span , else_) ;
186225 } else if else_. is_none ( )
187226 && self . eligible_condition ( cx, cond)
188227 && let ExprKind :: Block ( then, None ) = then. kind
@@ -193,12 +232,16 @@ impl LateLintPass<'_> for CollapsibleIf {
193232 }
194233}
195234
196- fn block_starts_with_comment ( cx : & LateContext < ' _ > , block : & Block < ' _ > ) -> bool {
197- // We trim all opening braces and whitespaces and then check if the next string is a comment.
198- let trimmed_block_text = snippet_block ( cx, block. span , ".." , None )
199- . trim_start_matches ( |c : char | c. is_whitespace ( ) || c == '{' )
200- . to_owned ( ) ;
201- trimmed_block_text. starts_with ( "//" ) || trimmed_block_text. starts_with ( "/*" )
235+ // Check that nothing significant can be found but whitespaces between the initial `{` of `block`
236+ // and the beginning of `stop_at`.
237+ fn block_starts_with_significant_tokens (
238+ cx : & LateContext < ' _ > ,
239+ block : & Block < ' _ > ,
240+ stop_at : & Expr < ' _ > ,
241+ lint_commented_code : bool ,
242+ ) -> bool {
243+ let span = block. span . split_at ( 1 ) . 1 . until ( stop_at. span ) ;
244+ span_contains_non_whitespace ( cx, span, lint_commented_code)
202245}
203246
204247/// If `block` is a block with either one expression or a statement containing an expression,
@@ -229,3 +272,16 @@ fn parens_around(expr: &Expr<'_>) -> Vec<(Span, String)> {
229272 vec ! [ ]
230273 }
231274}
275+
276+ fn span_extract_keyword ( sm : & SourceMap , span : Span , keyword : & str ) -> Option < Span > {
277+ let snippet = sm. span_to_snippet ( span) . ok ( ) ?;
278+ tokenize_with_text ( & snippet)
279+ . filter ( |( t, s, _) | matches ! ( t, TokenKind :: Ident if * s == keyword) )
280+ . map ( |( _, _, inner) | {
281+ span. split_at ( u32:: try_from ( inner. start ) . unwrap ( ) )
282+ . 1
283+ . split_at ( u32:: try_from ( inner. end - inner. start ) . unwrap ( ) )
284+ . 0
285+ } )
286+ . next ( )
287+ }
0 commit comments