Skip to content

Commit cad9083

Browse files
authored
Lint more cases in collapsible_if (rust-lang#14231)
Replace the use of `Sugg::ast()` which prevented combining `if` together when they contained comments by span manipulation. A new configuration option `lint_commented_code`, which is `true` by default, allows opting out from this behavior. If reviewed on GitHub, the second commit of this PR is best looked at side by side, with whitespace differences turned off. changelog: [`collapsible_if`]: lint more cases
2 parents c033a4c + 8238160 commit cad9083

File tree

13 files changed

+410
-195
lines changed

13 files changed

+410
-195
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6363,6 +6363,7 @@ Released 2018-09-13
63636363
[`future-size-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#future-size-threshold
63646364
[`ignore-interior-mutability`]: https://doc.rust-lang.org/clippy/lint_configuration.html#ignore-interior-mutability
63656365
[`large-error-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#large-error-threshold
6366+
[`lint-commented-code`]: https://doc.rust-lang.org/clippy/lint_configuration.html#lint-commented-code
63666367
[`lint-inconsistent-struct-field-initializers`]: https://doc.rust-lang.org/clippy/lint_configuration.html#lint-inconsistent-struct-field-initializers
63676368
[`literal-representation-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#literal-representation-threshold
63686369
[`matches-for-let-else`]: https://doc.rust-lang.org/clippy/lint_configuration.html#matches-for-let-else

book/src/lint_configuration.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,17 @@ The maximum size of the `Err`-variant in a `Result` returned from a function
613613
* [`result_large_err`](https://rust-lang.github.io/rust-clippy/master/index.html#result_large_err)
614614

615615

616+
## `lint-commented-code`
617+
Whether collapsible `if` chains are linted if they contain comments inside the parts
618+
that would be collapsed.
619+
620+
**Default Value:** `false`
621+
622+
---
623+
**Affected lints:**
624+
* [`collapsible_if`](https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if)
625+
626+
616627
## `lint-inconsistent-struct-field-initializers`
617628
Whether to suggest reordering constructor fields when initializers are present.
618629

clippy_config/src/conf.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,10 @@ define_Conf! {
549549
/// The maximum size of the `Err`-variant in a `Result` returned from a function
550550
#[lints(result_large_err)]
551551
large_error_threshold: u64 = 128,
552+
/// Whether collapsible `if` chains are linted if they contain comments inside the parts
553+
/// that would be collapsed.
554+
#[lints(collapsible_if)]
555+
lint_commented_code: bool = false,
552556
/// Whether to suggest reordering constructor fields when initializers are present.
553557
///
554558
/// Warnings produced by this configuration aren't necessarily fixed by just reordering the fields. Even if the

clippy_lints/src/collapsible_if.rs

Lines changed: 112 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
use clippy_config::Conf;
12
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
2-
use clippy_utils::source::{snippet, snippet_block, snippet_block_with_applicability};
3-
use clippy_utils::sugg::Sugg;
3+
use clippy_utils::source::{
4+
HasSession, IntoSpan as _, SpanRangeExt, snippet, snippet_block, snippet_block_with_applicability,
5+
};
6+
use clippy_utils::span_contains_comment;
47
use rustc_ast::ast;
58
use rustc_errors::Applicability;
69
use rustc_lint::{EarlyContext, EarlyLintPass};
7-
use rustc_session::declare_lint_pass;
10+
use rustc_session::impl_lint_pass;
811
use rustc_span::Span;
912

1013
declare_clippy_lint! {
@@ -75,17 +78,106 @@ declare_clippy_lint! {
7578
"nested `else`-`if` expressions that can be collapsed (e.g., `else { if x { ... } }`)"
7679
}
7780

78-
declare_lint_pass!(CollapsibleIf => [COLLAPSIBLE_IF, COLLAPSIBLE_ELSE_IF]);
81+
pub struct CollapsibleIf {
82+
lint_commented_code: bool,
83+
}
84+
85+
impl CollapsibleIf {
86+
pub fn new(conf: &'static Conf) -> Self {
87+
Self {
88+
lint_commented_code: conf.lint_commented_code,
89+
}
90+
}
91+
92+
fn check_collapsible_else_if(cx: &EarlyContext<'_>, then_span: Span, else_: &ast::Expr) {
93+
if let ast::ExprKind::Block(ref block, _) = else_.kind
94+
&& !block_starts_with_comment(cx, block)
95+
&& let Some(else_) = expr_block(block)
96+
&& else_.attrs.is_empty()
97+
&& !else_.span.from_expansion()
98+
&& let ast::ExprKind::If(..) = else_.kind
99+
{
100+
// Prevent "elseif"
101+
// Check that the "else" is followed by whitespace
102+
let up_to_else = then_span.between(block.span);
103+
let requires_space = if let Some(c) = snippet(cx, up_to_else, "..").chars().last() {
104+
!c.is_whitespace()
105+
} else {
106+
false
107+
};
108+
109+
let mut applicability = Applicability::MachineApplicable;
110+
span_lint_and_sugg(
111+
cx,
112+
COLLAPSIBLE_ELSE_IF,
113+
block.span,
114+
"this `else { if .. }` block can be collapsed",
115+
"collapse nested if block",
116+
format!(
117+
"{}{}",
118+
if requires_space { " " } else { "" },
119+
snippet_block_with_applicability(cx, else_.span, "..", Some(block.span), &mut applicability)
120+
),
121+
applicability,
122+
);
123+
}
124+
}
125+
126+
fn check_collapsible_if_if(&self, cx: &EarlyContext<'_>, expr: &ast::Expr, check: &ast::Expr, then: &ast::Block) {
127+
if let Some(inner) = expr_block(then)
128+
&& inner.attrs.is_empty()
129+
&& let ast::ExprKind::If(check_inner, _, None) = &inner.kind
130+
// Prevent triggering on `if c { if let a = b { .. } }`.
131+
&& !matches!(check_inner.kind, ast::ExprKind::Let(..))
132+
&& let ctxt = expr.span.ctxt()
133+
&& inner.span.ctxt() == ctxt
134+
&& let contains_comment = span_contains_comment(cx.sess().source_map(), check.span.to(check_inner.span))
135+
&& (!contains_comment || self.lint_commented_code)
136+
{
137+
span_lint_and_then(
138+
cx,
139+
COLLAPSIBLE_IF,
140+
expr.span,
141+
"this `if` statement can be collapsed",
142+
|diag| {
143+
let then_open_bracket = then.span.split_at(1).0.with_leading_whitespace(cx).into_span();
144+
let then_closing_bracket = {
145+
let end = then.span.shrink_to_hi();
146+
end.with_lo(end.lo() - rustc_span::BytePos(1))
147+
.with_leading_whitespace(cx)
148+
.into_span()
149+
};
150+
let inner_if = inner.span.split_at(2).0;
151+
let mut sugg = vec![
152+
// Remove the outer then block `{`
153+
(then_open_bracket, String::new()),
154+
// Remove the outer then block '}'
155+
(then_closing_bracket, String::new()),
156+
// Replace inner `if` by `&&`
157+
(inner_if, String::from("&&")),
158+
];
159+
sugg.extend(parens_around(check));
160+
sugg.extend(parens_around(check_inner));
161+
162+
diag.multipart_suggestion("collapse nested if block", sugg, Applicability::MachineApplicable);
163+
},
164+
);
165+
}
166+
}
167+
}
168+
169+
impl_lint_pass!(CollapsibleIf => [COLLAPSIBLE_IF, COLLAPSIBLE_ELSE_IF]);
79170

80171
impl EarlyLintPass for CollapsibleIf {
81172
fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) {
82173
if let ast::ExprKind::If(cond, then, else_) = &expr.kind
83174
&& !expr.span.from_expansion()
84175
{
85176
if let Some(else_) = else_ {
86-
check_collapsible_maybe_if_let(cx, then.span, else_);
177+
Self::check_collapsible_else_if(cx, then.span, else_);
87178
} else if !matches!(cond.kind, ast::ExprKind::Let(..)) {
88-
check_collapsible_no_if_let(cx, expr, cond, then);
179+
// Prevent triggering on `if c { if let a = b { .. } }`.
180+
self.check_collapsible_if_if(cx, expr, cond, then);
89181
}
90182
}
91183
}
@@ -99,74 +191,6 @@ fn block_starts_with_comment(cx: &EarlyContext<'_>, expr: &ast::Block) -> bool {
99191
trimmed_block_text.starts_with("//") || trimmed_block_text.starts_with("/*")
100192
}
101193

102-
fn check_collapsible_maybe_if_let(cx: &EarlyContext<'_>, then_span: Span, else_: &ast::Expr) {
103-
if let ast::ExprKind::Block(ref block, _) = else_.kind
104-
&& !block_starts_with_comment(cx, block)
105-
&& let Some(else_) = expr_block(block)
106-
&& else_.attrs.is_empty()
107-
&& !else_.span.from_expansion()
108-
&& let ast::ExprKind::If(..) = else_.kind
109-
{
110-
// Prevent "elseif"
111-
// Check that the "else" is followed by whitespace
112-
let up_to_else = then_span.between(block.span);
113-
let requires_space = if let Some(c) = snippet(cx, up_to_else, "..").chars().last() {
114-
!c.is_whitespace()
115-
} else {
116-
false
117-
};
118-
119-
let mut applicability = Applicability::MachineApplicable;
120-
span_lint_and_sugg(
121-
cx,
122-
COLLAPSIBLE_ELSE_IF,
123-
block.span,
124-
"this `else { if .. }` block can be collapsed",
125-
"collapse nested if block",
126-
format!(
127-
"{}{}",
128-
if requires_space { " " } else { "" },
129-
snippet_block_with_applicability(cx, else_.span, "..", Some(block.span), &mut applicability)
130-
),
131-
applicability,
132-
);
133-
}
134-
}
135-
136-
fn check_collapsible_no_if_let(cx: &EarlyContext<'_>, expr: &ast::Expr, check: &ast::Expr, then: &ast::Block) {
137-
if !block_starts_with_comment(cx, then)
138-
&& let Some(inner) = expr_block(then)
139-
&& inner.attrs.is_empty()
140-
&& let ast::ExprKind::If(ref check_inner, ref content, None) = inner.kind
141-
// Prevent triggering on `if c { if let a = b { .. } }`.
142-
&& !matches!(check_inner.kind, ast::ExprKind::Let(..))
143-
&& let ctxt = expr.span.ctxt()
144-
&& inner.span.ctxt() == ctxt
145-
{
146-
span_lint_and_then(
147-
cx,
148-
COLLAPSIBLE_IF,
149-
expr.span,
150-
"this `if` statement can be collapsed",
151-
|diag| {
152-
let mut app = Applicability::MachineApplicable;
153-
let lhs = Sugg::ast(cx, check, "..", ctxt, &mut app);
154-
let rhs = Sugg::ast(cx, check_inner, "..", ctxt, &mut app);
155-
diag.span_suggestion(
156-
expr.span,
157-
"collapse nested if block",
158-
format!(
159-
"if {} {}",
160-
lhs.and(&rhs),
161-
snippet_block(cx, content.span, "..", Some(expr.span)),
162-
),
163-
app, // snippet
164-
);
165-
},
166-
);
167-
}
168-
}
169-
170194
/// If the block contains only one expression, return it.
171195
fn expr_block(block: &ast::Block) -> Option<&ast::Expr> {
172196
if let [stmt] = &*block.stmts
@@ -177,3 +201,17 @@ fn expr_block(block: &ast::Block) -> Option<&ast::Expr> {
177201
None
178202
}
179203
}
204+
205+
/// If the expression is a `||`, suggest parentheses around it.
206+
fn parens_around(expr: &ast::Expr) -> Vec<(Span, String)> {
207+
if let ast::ExprKind::Binary(op, _, _) = expr.kind
208+
&& op.node == ast::BinOpKind::Or
209+
{
210+
vec![
211+
(expr.span.shrink_to_lo(), String::from("(")),
212+
(expr.span.shrink_to_hi(), String::from(")")),
213+
]
214+
} else {
215+
vec![]
216+
}
217+
}

clippy_lints/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
772772
store.register_late_pass(|_| Box::new(redundant_closure_call::RedundantClosureCall));
773773
store.register_early_pass(|| Box::new(unused_unit::UnusedUnit));
774774
store.register_late_pass(|_| Box::new(returns::Return));
775-
store.register_early_pass(|| Box::new(collapsible_if::CollapsibleIf));
775+
store.register_early_pass(move || Box::new(collapsible_if::CollapsibleIf::new(conf)));
776776
store.register_late_pass(|_| Box::new(items_after_statements::ItemsAfterStatements));
777777
store.register_early_pass(|| Box::new(precedence::Precedence));
778778
store.register_late_pass(|_| Box::new(needless_parens_on_range_literals::NeedlessParensOnRangeLiterals));
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lint-commented-code = true
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
// Comment must be kept
10+
&& y == "world" {
11+
println!("Hello world!");
12+
}
13+
//~^^^^^^ collapsible_if
14+
15+
// The following tests check for the fix of https://github.com/rust-lang/rust-clippy/issues/798
16+
if x == "hello" // Inner comment
17+
&& y == "world" {
18+
println!("Hello world!");
19+
}
20+
//~^^^^^ collapsible_if
21+
22+
if x == "hello"
23+
/* Inner comment */
24+
&& y == "world" {
25+
println!("Hello world!");
26+
}
27+
//~^^^^^^ collapsible_if
28+
29+
if x == "hello" /* Inner comment */
30+
&& y == "world" {
31+
println!("Hello world!");
32+
}
33+
//~^^^^^ collapsible_if
34+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
// Comment must be kept
10+
if y == "world" {
11+
println!("Hello world!");
12+
}
13+
}
14+
//~^^^^^^ collapsible_if
15+
16+
// The following tests check for the fix of https://github.com/rust-lang/rust-clippy/issues/798
17+
if x == "hello" { // Inner comment
18+
if y == "world" {
19+
println!("Hello world!");
20+
}
21+
}
22+
//~^^^^^ collapsible_if
23+
24+
if x == "hello" {
25+
/* Inner comment */
26+
if y == "world" {
27+
println!("Hello world!");
28+
}
29+
}
30+
//~^^^^^^ collapsible_if
31+
32+
if x == "hello" { /* Inner comment */
33+
if y == "world" {
34+
println!("Hello world!");
35+
}
36+
}
37+
//~^^^^^ collapsible_if
38+
}

0 commit comments

Comments
 (0)