Skip to content

Commit fea8dd2

Browse files
committed
Lint more cases in collapsible_else_if
1 parent 5290b1e commit fea8dd2

File tree

9 files changed

+333
-45
lines changed

9 files changed

+333
-45
lines changed

book/src/lint_configuration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,13 +651,14 @@ The maximum size of the `Err`-variant in a `Result` returned from a function
651651

652652

653653
## `lint-commented-code`
654-
Whether collapsible `if` chains are linted if they contain comments inside the parts
654+
Whether collapsible `if` and `else if` chains are linted if they contain comments inside the parts
655655
that would be collapsed.
656656

657657
**Default Value:** `false`
658658

659659
---
660660
**Affected lints:**
661+
* [`collapsible_else_if`](https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_else_if)
661662
* [`collapsible_if`](https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if)
662663

663664

clippy_config/src/conf.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -641,9 +641,9 @@ define_Conf! {
641641
/// The maximum size of the `Err`-variant in a `Result` returned from a function
642642
#[lints(result_large_err)]
643643
large_error_threshold: u64 = 128,
644-
/// Whether collapsible `if` chains are linted if they contain comments inside the parts
644+
/// Whether collapsible `if` and `else if` chains are linted if they contain comments inside the parts
645645
/// that would be collapsed.
646-
#[lints(collapsible_if)]
646+
#[lints(collapsible_else_if, collapsible_if)]
647647
lint_commented_code: bool = false,
648648
/// Whether to suggest reordering constructor fields when initializers are present.
649649
/// DEPRECATED CONFIGURATION: lint-inconsistent-struct-field-initializers

clippy_lints/src/collapsible_if.rs

Lines changed: 91 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
use 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;
33
use 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};
66
use rustc_ast::BinOpKind;
77
use rustc_errors::Applicability;
88
use rustc_hir::{Block, Expr, ExprKind, Stmt, StmtKind};
9+
use rustc_lexer::TokenKind;
910
use rustc_lint::{LateContext, LateLintPass};
1011
use rustc_session::impl_lint_pass;
12+
use rustc_span::source_map::SourceMap;
1113
use rustc_span::{BytePos, Span};
1214

1315
declare_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+
}

clippy_utils/src/lib.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2788,16 +2788,19 @@ pub fn span_contains_comment(sm: &SourceMap, span: Span) -> bool {
27882788
});
27892789
}
27902790

2791-
/// Checks whether a given span has any non-comment token. This checks for all types of token other
2792-
/// than line comment "//", block comment "/**", doc "///" "//!" and whitespace
2791+
/// Checks whether a given span has any significant token. A significant token is a non-whitespace
2792+
/// token, including comments unless `skip_comments` is set.
27932793
/// This is useful to determine if there are any actual code tokens in the span that are omitted in
27942794
/// the late pass, such as platform-specific code.
2795-
pub fn span_contains_non_comment(cx: &impl source::HasSession, span: Span) -> bool {
2796-
matches!(span.get_source_text(cx), Some(snippet) if tokenize_with_text(&snippet).any(|(token, _, _)| {
2797-
!matches!(token, TokenKind::LineComment { .. } | TokenKind::BlockComment { .. } | TokenKind::Whitespace)
2798-
}))
2795+
pub fn span_contains_non_whitespace(cx: &impl source::HasSession, span: Span, skip_comments: bool) -> bool {
2796+
matches!(span.get_source_text(cx), Some(snippet) if tokenize_with_text(&snippet).any(|(token, _, _)|
2797+
match token {
2798+
TokenKind::Whitespace => false,
2799+
TokenKind::BlockComment { .. } | TokenKind::LineComment { .. } => !skip_comments,
2800+
_ => true,
2801+
}
2802+
))
27992803
}
2800-
28012804
/// Returns all the comments a given span contains
28022805
///
28032806
/// Comments are returned wrapped with their relevant delimiters
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#![allow(clippy::eq_op, clippy::nonminimal_bool)]
2+
3+
#[rustfmt::skip]
4+
#[warn(clippy::collapsible_if)]
5+
fn main() {
6+
let (x, y) = ("hello", "world");
7+
8+
if x == "hello" {
9+
todo!()
10+
}
11+
// Comment must be kept
12+
else if y == "world" {
13+
println!("Hello world!");
14+
}
15+
//~^^^^^^ collapsible_else_if
16+
17+
if x == "hello" {
18+
todo!()
19+
} // Inner comment
20+
else if y == "world" {
21+
println!("Hello world!");
22+
}
23+
//~^^^^^ collapsible_else_if
24+
25+
if x == "hello" {
26+
todo!()
27+
}
28+
/* Inner comment */
29+
else if y == "world" {
30+
println!("Hello world!");
31+
}
32+
//~^^^^^^ collapsible_else_if
33+
34+
if x == "hello" {
35+
todo!()
36+
} /* Inner comment */
37+
else if y == "world" {
38+
println!("Hello world!");
39+
}
40+
//~^^^^^ collapsible_else_if
41+
42+
if x == "hello" {
43+
todo!()
44+
} /* This should not be removed */ /* So does this */
45+
// Comment must be kept
46+
else if y == "world" {
47+
println!("Hello world!");
48+
}
49+
//~^^^^^^ collapsible_else_if
50+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#![allow(clippy::eq_op, clippy::nonminimal_bool)]
2+
3+
#[rustfmt::skip]
4+
#[warn(clippy::collapsible_if)]
5+
fn main() {
6+
let (x, y) = ("hello", "world");
7+
8+
if x == "hello" {
9+
todo!()
10+
} else {
11+
// Comment must be kept
12+
if y == "world" {
13+
println!("Hello world!");
14+
}
15+
}
16+
//~^^^^^^ collapsible_else_if
17+
18+
if x == "hello" {
19+
todo!()
20+
} else { // Inner comment
21+
if y == "world" {
22+
println!("Hello world!");
23+
}
24+
}
25+
//~^^^^^ collapsible_else_if
26+
27+
if x == "hello" {
28+
todo!()
29+
} else {
30+
/* Inner comment */
31+
if y == "world" {
32+
println!("Hello world!");
33+
}
34+
}
35+
//~^^^^^^ collapsible_else_if
36+
37+
if x == "hello" {
38+
todo!()
39+
} else { /* Inner comment */
40+
if y == "world" {
41+
println!("Hello world!");
42+
}
43+
}
44+
//~^^^^^ collapsible_else_if
45+
46+
if x == "hello" {
47+
todo!()
48+
} /* This should not be removed */ else /* So does this */ {
49+
// Comment must be kept
50+
if y == "world" {
51+
println!("Hello world!");
52+
}
53+
}
54+
//~^^^^^^ collapsible_else_if
55+
}

0 commit comments

Comments
 (0)