Skip to content

Commit 4b236fc

Browse files
authored
fix(collapsible{,_else}_if): respect #[expect] on inner if (#15647)
Fixes #13365 Pretty much the exact approach described in #13365 (comment), thank you @Jarcho! r? @Jarcho changelog: [`collapsible_if`]: respect `#[expect]` on inner `if` changelog: [`collapsible_else_if`]: respect `#[expect]` on inner `if`
2 parents 2b03bc1 + 593a9c9 commit 4b236fc

14 files changed

+256
-27
lines changed

clippy_lints/src/collapsible_if.rs

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
use clippy_config::Conf;
2-
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::diagnostics::span_lint_hir_and_then;
33
use clippy_utils::msrvs::{self, Msrv};
44
use 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};
77
use rustc_errors::Applicability;
88
use rustc_hir::{Block, Expr, ExprKind, StmtKind};
99
use rustc_lexer::TokenKind;
10-
use rustc_lint::{LateContext, LateLintPass};
10+
use rustc_lint::{LateContext, LateLintPass, Level};
1111
use rustc_session::impl_lint_pass;
1212
use rustc_span::source_map::SourceMap;
13-
use rustc_span::{BytePos, Span};
13+
use rustc_span::{BytePos, Span, Symbol};
1414

1515
declare_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

224263
impl_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.
259286
fn expr_block<'tcx>(block: &Block<'tcx>) -> Option<&'tcx Expr<'tcx>> {

clippy_utils/src/sym.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ generate! {
111111
clone_into,
112112
cloned,
113113
cognitive_complexity,
114+
collapsible_else_if,
115+
collapsible_if,
114116
collect,
115117
const_ptr,
116118
contains,

tests/ui-toml/collapsible_if/collapsible_else_if.fixed

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![allow(clippy::eq_op, clippy::nonminimal_bool)]
2+
#![warn(clippy::collapsible_if)]
23

34
#[rustfmt::skip]
4-
#[warn(clippy::collapsible_if)]
55
fn main() {
66
let (x, y) = ("hello", "world");
77

@@ -48,3 +48,20 @@ fn main() {
4848
}
4949
//~^^^^^^ collapsible_else_if
5050
}
51+
52+
fn issue_13365() {
53+
// the comments don't stop us from linting, so the the `expect` *will* be fulfilled
54+
if true {
55+
} else {
56+
// some other text before
57+
#[expect(clippy::collapsible_else_if)]
58+
if false {}
59+
}
60+
61+
if true {
62+
} else {
63+
#[expect(clippy::collapsible_else_if)]
64+
// some other text after
65+
if false {}
66+
}
67+
}

tests/ui-toml/collapsible_if/collapsible_else_if.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![allow(clippy::eq_op, clippy::nonminimal_bool)]
2+
#![warn(clippy::collapsible_if)]
23

34
#[rustfmt::skip]
4-
#[warn(clippy::collapsible_if)]
55
fn main() {
66
let (x, y) = ("hello", "world");
77

@@ -53,3 +53,20 @@ fn main() {
5353
}
5454
//~^^^^^^ collapsible_else_if
5555
}
56+
57+
fn issue_13365() {
58+
// the comments don't stop us from linting, so the the `expect` *will* be fulfilled
59+
if true {
60+
} else {
61+
// some other text before
62+
#[expect(clippy::collapsible_else_if)]
63+
if false {}
64+
}
65+
66+
if true {
67+
} else {
68+
#[expect(clippy::collapsible_else_if)]
69+
// some other text after
70+
if false {}
71+
}
72+
}

tests/ui/collapsible_else_if.fixed

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,33 @@ fn issue_7318() {
8787
//~^^^ collapsible_else_if
8888
}
8989

90+
fn issue_13365() {
91+
// all the `expect`s that we should fulfill
92+
if true {
93+
} else {
94+
#[expect(clippy::collapsible_else_if)]
95+
if false {}
96+
}
97+
98+
if true {
99+
} else {
100+
#[expect(clippy::style)]
101+
if false {}
102+
}
103+
104+
if true {
105+
} else {
106+
#[expect(clippy::all)]
107+
if false {}
108+
}
109+
110+
if true {
111+
} else {
112+
#[expect(warnings)]
113+
if false {}
114+
}
115+
}
116+
90117
fn issue14799() {
91118
use std::ops::ControlFlow;
92119

tests/ui/collapsible_else_if.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,33 @@ fn issue_7318() {
103103
//~^^^ collapsible_else_if
104104
}
105105

106+
fn issue_13365() {
107+
// all the `expect`s that we should fulfill
108+
if true {
109+
} else {
110+
#[expect(clippy::collapsible_else_if)]
111+
if false {}
112+
}
113+
114+
if true {
115+
} else {
116+
#[expect(clippy::style)]
117+
if false {}
118+
}
119+
120+
if true {
121+
} else {
122+
#[expect(clippy::all)]
123+
if false {}
124+
}
125+
126+
if true {
127+
} else {
128+
#[expect(warnings)]
129+
if false {}
130+
}
131+
}
132+
106133
fn issue14799() {
107134
use std::ops::ControlFlow;
108135

tests/ui/collapsible_else_if.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ LL | | }
151151
| |_____^ help: collapse nested if block: `if false {}`
152152

153153
error: this `else { if .. }` block can be collapsed
154-
--> tests/ui/collapsible_else_if.rs:130:12
154+
--> tests/ui/collapsible_else_if.rs:157:12
155155
|
156156
LL | } else {
157157
| ____________^
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//@no-rustfix
2+
#![warn(clippy::collapsible_else_if)]
3+
4+
fn issue_13365() {
5+
// in the following examples, we won't lint because of the comments,
6+
// so the the `expect` will be unfulfilled
7+
if true {
8+
} else {
9+
// some other text before
10+
#[expect(clippy::collapsible_else_if)]
11+
if false {}
12+
}
13+
//~^^^ ERROR: this lint expectation is unfulfilled
14+
15+
if true {
16+
} else {
17+
#[expect(clippy::collapsible_else_if)]
18+
// some other text after
19+
if false {}
20+
}
21+
//~^^^^ ERROR: this lint expectation is unfulfilled
22+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
error: this lint expectation is unfulfilled
2+
--> tests/ui/collapsible_else_if_unfixable.rs:10:18
3+
|
4+
LL | #[expect(clippy::collapsible_else_if)]
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
= note: `-D unfulfilled-lint-expectations` implied by `-D warnings`
8+
= help: to override `-D warnings` add `#[allow(unfulfilled_lint_expectations)]`
9+
10+
error: this lint expectation is unfulfilled
11+
--> tests/ui/collapsible_else_if_unfixable.rs:17:18
12+
|
13+
LL | #[expect(clippy::collapsible_else_if)]
14+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
15+
16+
error: aborting due to 2 previous errors
17+

tests/ui/collapsible_if.fixed

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,24 @@ fn layout_check() -> u32 {
144144
//~^^^^^ collapsible_if
145145
}
146146

147+
fn issue13365() {
148+
// all the `expect`s that we should fulfill
149+
if true {
150+
#[expect(clippy::collapsible_if)]
151+
if true {}
152+
}
153+
154+
if true {
155+
#[expect(clippy::style)]
156+
if true {}
157+
}
158+
159+
if true {
160+
#[expect(clippy::all)]
161+
if true {}
162+
}
163+
}
164+
147165
fn issue14722() {
148166
let x = if true {
149167
Some(1)

0 commit comments

Comments
 (0)