Skip to content

Commit 506411d

Browse files
Fix collapsible_else_if FP on conditionally compiled stmt (#14906)
Closes #14799 changelog: [`collapsible_else_if`] fix FP on conditionally compiled stmt
2 parents 4ef7529 + ee80deb commit 506411d

14 files changed

+418
-84
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: 93 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
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};
4+
use clippy_utils::source::{IntoSpan as _, SpanRangeExt, snippet, snippet_block_with_applicability};
5+
use clippy_utils::{span_contains_non_whitespace, tokenize_with_text};
56
use rustc_ast::BinOpKind;
67
use rustc_errors::Applicability;
78
use rustc_hir::{Block, Expr, ExprKind, Stmt, StmtKind};
9+
use rustc_lexer::TokenKind;
810
use rustc_lint::{LateContext, LateLintPass};
911
use rustc_session::impl_lint_pass;
10-
use rustc_span::Span;
12+
use rustc_span::source_map::SourceMap;
13+
use rustc_span::{BytePos, Span};
1114

1215
declare_clippy_lint! {
1316
/// ### What it does
@@ -90,35 +93,74 @@ impl CollapsibleIf {
9093
}
9194
}
9295

93-
fn check_collapsible_else_if(cx: &LateContext<'_>, then_span: Span, else_block: &Block<'_>) {
94-
if !block_starts_with_comment(cx, else_block)
95-
&& 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)
9698
&& cx.tcx.hir_attrs(else_.hir_id).is_empty()
9799
&& !else_.span.from_expansion()
98-
&& let ExprKind::If(..) = else_.kind
100+
&& let ExprKind::If(else_if_cond, ..) = else_.kind
101+
&& !block_starts_with_significant_tokens(cx, else_block, else_, self.lint_commented_code)
99102
{
100-
// Prevent "elseif"
101-
// Check that the "else" is followed by whitespace
102-
let up_to_else = then_span.between(else_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(
103+
span_lint_and_then(
111104
cx,
112105
COLLAPSIBLE_ELSE_IF,
113106
else_block.span,
114107
"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(else_block.span), &mut applicability)
120-
),
121-
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+
},
122164
);
123165
}
124166
}
@@ -130,7 +172,7 @@ impl CollapsibleIf {
130172
&& self.eligible_condition(cx, check_inner)
131173
&& let ctxt = expr.span.ctxt()
132174
&& inner.span.ctxt() == ctxt
133-
&& (self.lint_commented_code || !block_starts_with_comment(cx, then))
175+
&& !block_starts_with_significant_tokens(cx, then, inner, self.lint_commented_code)
134176
{
135177
span_lint_and_then(
136178
cx,
@@ -141,7 +183,7 @@ impl CollapsibleIf {
141183
let then_open_bracket = then.span.split_at(1).0.with_leading_whitespace(cx).into_span();
142184
let then_closing_bracket = {
143185
let end = then.span.shrink_to_hi();
144-
end.with_lo(end.lo() - rustc_span::BytePos(1))
186+
end.with_lo(end.lo() - BytePos(1))
145187
.with_leading_whitespace(cx)
146188
.into_span()
147189
};
@@ -179,7 +221,7 @@ impl LateLintPass<'_> for CollapsibleIf {
179221
if let Some(else_) = else_
180222
&& let ExprKind::Block(else_, None) = else_.kind
181223
{
182-
Self::check_collapsible_else_if(cx, then.span, else_);
224+
self.check_collapsible_else_if(cx, then.span, else_);
183225
} else if else_.is_none()
184226
&& self.eligible_condition(cx, cond)
185227
&& let ExprKind::Block(then, None) = then.kind
@@ -190,12 +232,16 @@ impl LateLintPass<'_> for CollapsibleIf {
190232
}
191233
}
192234

193-
fn block_starts_with_comment(cx: &LateContext<'_>, block: &Block<'_>) -> bool {
194-
// We trim all opening braces and whitespaces and then check if the next string is a comment.
195-
let trimmed_block_text = snippet_block(cx, block.span, "..", None)
196-
.trim_start_matches(|c: char| c.is_whitespace() || c == '{')
197-
.to_owned();
198-
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)
199245
}
200246

201247
/// If `block` is a block with either one expression or a statement containing an expression,
@@ -226,3 +272,16 @@ fn parens_around(expr: &Expr<'_>) -> Vec<(Span, String)> {
226272
vec![]
227273
}
228274
}
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_lints/src/loops/same_item_push.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,14 @@ impl<'tcx> Visitor<'tcx> for SameItemPushVisitor<'_, 'tcx> {
163163
StmtKind::Expr(expr) | StmtKind::Semi(expr) => self.visit_expr(expr),
164164
_ => {},
165165
}
166+
}
167+
// Current statement is a push ...check whether another
168+
// push had been previously done
169+
else if self.vec_push.is_none() {
170+
self.vec_push = vec_push_option;
166171
} else {
167-
// Current statement is a push ...check whether another
168-
// push had been previously done
169-
if self.vec_push.is_none() {
170-
self.vec_push = vec_push_option;
171-
} else {
172-
// There are multiple pushes ... don't lint
173-
self.multiple_pushes = true;
174-
}
172+
// There are multiple pushes ... don't lint
173+
self.multiple_pushes = true;
175174
}
176175
}
177176
}

clippy_lints/src/single_component_path_imports.rs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -219,22 +219,21 @@ impl SingleComponentPathImports {
219219
}
220220
}
221221
}
222-
} else {
223-
// keep track of `use self::some_module` usages
224-
if segments[0].ident.name == kw::SelfLower {
225-
// simple case such as `use self::module::SomeStruct`
226-
if segments.len() > 1 {
227-
imports_reused_with_self.push(segments[1].ident.name);
228-
return;
229-
}
222+
}
223+
// keep track of `use self::some_module` usages
224+
else if segments[0].ident.name == kw::SelfLower {
225+
// simple case such as `use self::module::SomeStruct`
226+
if segments.len() > 1 {
227+
imports_reused_with_self.push(segments[1].ident.name);
228+
return;
229+
}
230230

231-
// nested case such as `use self::{module1::Struct1, module2::Struct2}`
232-
if let UseTreeKind::Nested { items, .. } = &use_tree.kind {
233-
for tree in items {
234-
let segments = &tree.0.prefix.segments;
235-
if !segments.is_empty() {
236-
imports_reused_with_self.push(segments[0].ident.name);
237-
}
231+
// nested case such as `use self::{module1::Struct1, module2::Struct2}`
232+
if let UseTreeKind::Nested { items, .. } = &use_tree.kind {
233+
for tree in items {
234+
let segments = &tree.0.prefix.segments;
235+
if !segments.is_empty() {
236+
imports_reused_with_self.push(segments[0].ident.name);
238237
}
239238
}
240239
}

clippy_lints/src/undocumented_unsafe_blocks.rs

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -606,32 +606,31 @@ fn span_from_macro_expansion_has_safety_comment(cx: &LateContext<'_>, span: Span
606606
let ctxt = span.ctxt();
607607
if ctxt == SyntaxContext::root() {
608608
HasSafetyComment::Maybe
609-
} else {
610-
// From a macro expansion. Get the text from the start of the macro declaration to start of the
611-
// unsafe block.
612-
// macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; }
613-
// ^--------------------------------------------^
614-
if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
615-
&& let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo())
616-
&& Arc::ptr_eq(&unsafe_line.sf, &macro_line.sf)
617-
&& let Some(src) = unsafe_line.sf.src.as_deref()
618-
{
619-
if macro_line.line < unsafe_line.line {
620-
match text_has_safety_comment(
621-
src,
622-
&unsafe_line.sf.lines()[macro_line.line + 1..=unsafe_line.line],
623-
unsafe_line.sf.start_pos,
624-
) {
625-
Some(b) => HasSafetyComment::Yes(b),
626-
None => HasSafetyComment::No,
627-
}
628-
} else {
629-
HasSafetyComment::No
609+
}
610+
// From a macro expansion. Get the text from the start of the macro declaration to start of the
611+
// unsafe block.
612+
// macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; }
613+
// ^--------------------------------------------^
614+
else if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
615+
&& let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo())
616+
&& Arc::ptr_eq(&unsafe_line.sf, &macro_line.sf)
617+
&& let Some(src) = unsafe_line.sf.src.as_deref()
618+
{
619+
if macro_line.line < unsafe_line.line {
620+
match text_has_safety_comment(
621+
src,
622+
&unsafe_line.sf.lines()[macro_line.line + 1..=unsafe_line.line],
623+
unsafe_line.sf.start_pos,
624+
) {
625+
Some(b) => HasSafetyComment::Yes(b),
626+
None => HasSafetyComment::No,
630627
}
631628
} else {
632-
// Problem getting source text. Pretend a comment was found.
633-
HasSafetyComment::Maybe
629+
HasSafetyComment::No
634630
}
631+
} else {
632+
// Problem getting source text. Pretend a comment was found.
633+
HasSafetyComment::Maybe
635634
}
636635
}
637636

clippy_utils/src/lib.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ use rustc_span::hygiene::{ExpnKind, MacroKind};
122122
use rustc_span::source_map::SourceMap;
123123
use rustc_span::symbol::{Ident, Symbol, kw};
124124
use rustc_span::{InnerSpan, Span};
125-
use source::walk_span_to_context;
125+
use source::{SpanRangeExt, walk_span_to_context};
126126
use visitors::{Visitable, for_each_unconsumed_temporary};
127127

128128
use crate::consts::{ConstEvalCtxt, Constant, mir_to_const};
@@ -2787,6 +2787,19 @@ pub fn span_contains_comment(sm: &SourceMap, span: Span) -> bool {
27872787
});
27882788
}
27892789

2790+
/// Checks whether a given span has any significant token. A significant token is a non-whitespace
2791+
/// token, including comments unless `skip_comments` is set.
2792+
/// This is useful to determine if there are any actual code tokens in the span that are omitted in
2793+
/// the late pass, such as platform-specific code.
2794+
pub fn span_contains_non_whitespace(cx: &impl source::HasSession, span: Span, skip_comments: bool) -> bool {
2795+
matches!(span.get_source_text(cx), Some(snippet) if tokenize_with_text(&snippet).any(|(token, _, _)|
2796+
match token {
2797+
TokenKind::Whitespace => false,
2798+
TokenKind::BlockComment { .. } | TokenKind::LineComment { .. } => !skip_comments,
2799+
_ => true,
2800+
}
2801+
))
2802+
}
27902803
/// Returns all the comments a given span contains
27912804
///
27922805
/// Comments are returned wrapped with their relevant delimiters

0 commit comments

Comments
 (0)