Skip to content

Commit a727425

Browse files
authored
Rollup merge of rust-lang#68108 - varkor:chained-comparison-suggestions, r=Centril
Add suggestions when encountering chained comparisons Ideally, we'd also prevent the type error, which is just extra noise, but that will require moving the error from the parser, and I think the suggestion makes things clear enough for now. Fixes rust-lang#65659.
2 parents 7dcb9eb + 088a180 commit a727425

8 files changed

+335
-49
lines changed

src/librustc_parse/parser/diagnostics.rs

+70-13
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use rustc_data_structures::fx::FxHashSet;
44
use rustc_error_codes::*;
55
use rustc_errors::{pluralize, struct_span_err};
66
use rustc_errors::{Applicability, DiagnosticBuilder, Handler, PResult};
7+
use rustc_span::source_map::Spanned;
78
use rustc_span::symbol::kw;
89
use rustc_span::{MultiSpan, Span, SpanSnippetError, DUMMY_SP};
910
use syntax::ast::{
@@ -491,6 +492,58 @@ impl<'a> Parser<'a> {
491492
}
492493
}
493494

495+
/// Check to see if a pair of chained operators looks like an attempt at chained comparison,
496+
/// e.g. `1 < x <= 3`. If so, suggest either splitting the comparison into two, or
497+
/// parenthesising the leftmost comparison.
498+
fn attempt_chained_comparison_suggestion(
499+
&mut self,
500+
err: &mut DiagnosticBuilder<'_>,
501+
inner_op: &Expr,
502+
outer_op: &Spanned<AssocOp>,
503+
) {
504+
if let ExprKind::Binary(op, ref l1, ref r1) = inner_op.kind {
505+
match (op.node, &outer_op.node) {
506+
// `x < y < z` and friends.
507+
(BinOpKind::Lt, AssocOp::Less) | (BinOpKind::Lt, AssocOp::LessEqual) |
508+
(BinOpKind::Le, AssocOp::LessEqual) | (BinOpKind::Le, AssocOp::Less) |
509+
// `x > y > z` and friends.
510+
(BinOpKind::Gt, AssocOp::Greater) | (BinOpKind::Gt, AssocOp::GreaterEqual) |
511+
(BinOpKind::Ge, AssocOp::GreaterEqual) | (BinOpKind::Ge, AssocOp::Greater) => {
512+
let expr_to_str = |e: &Expr| {
513+
self.span_to_snippet(e.span)
514+
.unwrap_or_else(|_| pprust::expr_to_string(&e))
515+
};
516+
err.span_suggestion(
517+
inner_op.span.to(outer_op.span),
518+
"split the comparison into two...",
519+
format!(
520+
"{} {} {} && {} {}",
521+
expr_to_str(&l1),
522+
op.node.to_string(),
523+
expr_to_str(&r1),
524+
expr_to_str(&r1),
525+
outer_op.node.to_ast_binop().unwrap().to_string(),
526+
),
527+
Applicability::MaybeIncorrect,
528+
);
529+
err.span_suggestion(
530+
inner_op.span.to(outer_op.span),
531+
"...or parenthesize one of the comparisons",
532+
format!(
533+
"({} {} {}) {}",
534+
expr_to_str(&l1),
535+
op.node.to_string(),
536+
expr_to_str(&r1),
537+
outer_op.node.to_ast_binop().unwrap().to_string(),
538+
),
539+
Applicability::MaybeIncorrect,
540+
);
541+
}
542+
_ => {}
543+
}
544+
}
545+
}
546+
494547
/// Produces an error if comparison operators are chained (RFC #558).
495548
/// We only need to check the LHS, not the RHS, because all comparison ops have same
496549
/// precedence (see `fn precedence`) and are left-associative (see `fn fixity`).
@@ -506,27 +559,31 @@ impl<'a> Parser<'a> {
506559
/// / \
507560
/// inner_op r2
508561
/// / \
509-
/// l1 r1
562+
/// l1 r1
510563
pub(super) fn check_no_chained_comparison(
511564
&mut self,
512-
lhs: &Expr,
513-
outer_op: &AssocOp,
565+
inner_op: &Expr,
566+
outer_op: &Spanned<AssocOp>,
514567
) -> PResult<'a, Option<P<Expr>>> {
515568
debug_assert!(
516-
outer_op.is_comparison(),
569+
outer_op.node.is_comparison(),
517570
"check_no_chained_comparison: {:?} is not comparison",
518-
outer_op,
571+
outer_op.node,
519572
);
520573

521574
let mk_err_expr =
522575
|this: &Self, span| Ok(Some(this.mk_expr(span, ExprKind::Err, AttrVec::new())));
523576

524-
match lhs.kind {
577+
match inner_op.kind {
525578
ExprKind::Binary(op, _, _) if op.node.is_comparison() => {
526579
// Respan to include both operators.
527580
let op_span = op.span.to(self.prev_span);
528-
let mut err = self
529-
.struct_span_err(op_span, "chained comparison operators require parentheses");
581+
let mut err =
582+
self.struct_span_err(op_span, "comparison operators cannot be chained");
583+
584+
// If it looks like a genuine attempt to chain operators (as opposed to a
585+
// misformatted turbofish, for instance), suggest a correct form.
586+
self.attempt_chained_comparison_suggestion(&mut err, inner_op, outer_op);
530587

531588
let suggest = |err: &mut DiagnosticBuilder<'_>| {
532589
err.span_suggestion_verbose(
@@ -538,12 +595,12 @@ impl<'a> Parser<'a> {
538595
};
539596

540597
if op.node == BinOpKind::Lt &&
541-
*outer_op == AssocOp::Less || // Include `<` to provide this recommendation
542-
*outer_op == AssocOp::Greater
598+
outer_op.node == AssocOp::Less || // Include `<` to provide this recommendation
599+
outer_op.node == AssocOp::Greater
543600
// even in a case like the following:
544601
{
545602
// Foo<Bar<Baz<Qux, ()>>>
546-
if *outer_op == AssocOp::Less {
603+
if outer_op.node == AssocOp::Less {
547604
let snapshot = self.clone();
548605
self.bump();
549606
// So far we have parsed `foo<bar<`, consume the rest of the type args.
@@ -575,7 +632,7 @@ impl<'a> Parser<'a> {
575632
// FIXME: actually check that the two expressions in the binop are
576633
// paths and resynthesize new fn call expression instead of using
577634
// `ExprKind::Err` placeholder.
578-
mk_err_expr(self, lhs.span.to(self.prev_span))
635+
mk_err_expr(self, inner_op.span.to(self.prev_span))
579636
}
580637
Err(mut expr_err) => {
581638
expr_err.cancel();
@@ -597,7 +654,7 @@ impl<'a> Parser<'a> {
597654
// FIXME: actually check that the two expressions in the binop are
598655
// paths and resynthesize new fn call expression instead of using
599656
// `ExprKind::Err` placeholder.
600-
mk_err_expr(self, lhs.span.to(self.prev_span))
657+
mk_err_expr(self, inner_op.span.to(self.prev_span))
601658
}
602659
}
603660
} else {

src/librustc_parse/parser/expr.rs

+23-19
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use super::{SemiColonMode, SeqSep, TokenExpectType};
44
use crate::maybe_recover_from_interpolated_ty_qpath;
55

66
use rustc_errors::{Applicability, PResult};
7-
use rustc_span::source_map::{self, Span};
7+
use rustc_span::source_map::{self, Span, Spanned};
88
use rustc_span::symbol::{kw, sym, Symbol};
99
use std::mem;
1010
use syntax::ast::{self, AttrStyle, AttrVec, CaptureBy, Field, Ident, Lit, DUMMY_NODE_ID};
@@ -180,17 +180,17 @@ impl<'a> Parser<'a> {
180180
};
181181

182182
let cur_op_span = self.token.span;
183-
let restrictions = if op.is_assign_like() {
183+
let restrictions = if op.node.is_assign_like() {
184184
self.restrictions & Restrictions::NO_STRUCT_LITERAL
185185
} else {
186186
self.restrictions
187187
};
188-
let prec = op.precedence();
188+
let prec = op.node.precedence();
189189
if prec < min_prec {
190190
break;
191191
}
192192
// Check for deprecated `...` syntax
193-
if self.token == token::DotDotDot && op == AssocOp::DotDotEq {
193+
if self.token == token::DotDotDot && op.node == AssocOp::DotDotEq {
194194
self.err_dotdotdot_syntax(self.token.span);
195195
}
196196

@@ -199,11 +199,12 @@ impl<'a> Parser<'a> {
199199
}
200200

201201
self.bump();
202-
if op.is_comparison() {
202+
if op.node.is_comparison() {
203203
if let Some(expr) = self.check_no_chained_comparison(&lhs, &op)? {
204204
return Ok(expr);
205205
}
206206
}
207+
let op = op.node;
207208
// Special cases:
208209
if op == AssocOp::As {
209210
lhs = self.parse_assoc_op_cast(lhs, lhs_span, ExprKind::Cast)?;
@@ -297,7 +298,7 @@ impl<'a> Parser<'a> {
297298
}
298299

299300
fn should_continue_as_assoc_expr(&mut self, lhs: &Expr) -> bool {
300-
match (self.expr_is_complete(lhs), self.check_assoc_op()) {
301+
match (self.expr_is_complete(lhs), self.check_assoc_op().map(|op| op.node)) {
301302
// Semi-statement forms are odd:
302303
// See https://github.com/rust-lang/rust/issues/29071
303304
(true, None) => false,
@@ -342,19 +343,22 @@ impl<'a> Parser<'a> {
342343
/// The method does not advance the current token.
343344
///
344345
/// Also performs recovery for `and` / `or` which are mistaken for `&&` and `||` respectively.
345-
fn check_assoc_op(&self) -> Option<AssocOp> {
346-
match (AssocOp::from_token(&self.token), &self.token.kind) {
347-
(op @ Some(_), _) => op,
348-
(None, token::Ident(sym::and, false)) => {
349-
self.error_bad_logical_op("and", "&&", "conjunction");
350-
Some(AssocOp::LAnd)
351-
}
352-
(None, token::Ident(sym::or, false)) => {
353-
self.error_bad_logical_op("or", "||", "disjunction");
354-
Some(AssocOp::LOr)
355-
}
356-
_ => None,
357-
}
346+
fn check_assoc_op(&self) -> Option<Spanned<AssocOp>> {
347+
Some(Spanned {
348+
node: match (AssocOp::from_token(&self.token), &self.token.kind) {
349+
(Some(op), _) => op,
350+
(None, token::Ident(sym::and, false)) => {
351+
self.error_bad_logical_op("and", "&&", "conjunction");
352+
AssocOp::LAnd
353+
}
354+
(None, token::Ident(sym::or, false)) => {
355+
self.error_bad_logical_op("or", "||", "disjunction");
356+
AssocOp::LOr
357+
}
358+
_ => return None,
359+
},
360+
span: self.token.span,
361+
})
358362
}
359363

360364
/// Error on `and` and `or` suggesting `&&` and `||` respectively.
+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
fn main() {
22
(0..13).collect<Vec<i32>>();
3-
//~^ ERROR chained comparison
3+
//~^ ERROR comparison operators cannot be chained
44
Vec<i32>::new();
5-
//~^ ERROR chained comparison
5+
//~^ ERROR comparison operators cannot be chained
66
(0..13).collect<Vec<i32>();
7-
//~^ ERROR chained comparison
7+
//~^ ERROR comparison operators cannot be chained
88
}

src/test/ui/did_you_mean/issue-40396.stderr

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
error: chained comparison operators require parentheses
1+
error: comparison operators cannot be chained
22
--> $DIR/issue-40396.rs:2:20
33
|
44
LL | (0..13).collect<Vec<i32>>();
55
| ^^^^^
66
|
7+
help: split the comparison into two...
8+
|
9+
LL | (0..13).collect < Vec && Vec <i32>>();
10+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11+
help: ...or parenthesize one of the comparisons
12+
|
13+
LL | ((0..13).collect < Vec) <i32>>();
14+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
715
help: use `::<...>` instead of `<...>` to specify type arguments
816
|
917
LL | (0..13).collect::<Vec<i32>>();
1018
| ^^
1119

12-
error: chained comparison operators require parentheses
20+
error: comparison operators cannot be chained
1321
--> $DIR/issue-40396.rs:4:8
1422
|
1523
LL | Vec<i32>::new();
@@ -20,12 +28,20 @@ help: use `::<...>` instead of `<...>` to specify type arguments
2028
LL | Vec::<i32>::new();
2129
| ^^
2230

23-
error: chained comparison operators require parentheses
31+
error: comparison operators cannot be chained
2432
--> $DIR/issue-40396.rs:6:20
2533
|
2634
LL | (0..13).collect<Vec<i32>();
2735
| ^^^^^
2836
|
37+
help: split the comparison into two...
38+
|
39+
LL | (0..13).collect < Vec && Vec <i32>();
40+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
41+
help: ...or parenthesize one of the comparisons
42+
|
43+
LL | ((0..13).collect < Vec) <i32>();
44+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
2945
help: use `::<...>` instead of `<...>` to specify type arguments
3046
|
3147
LL | (0..13).collect::<Vec<i32>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Check that we get nice suggestions when attempting a chained comparison.
2+
3+
fn comp1() {
4+
1 < 2 <= 3; //~ ERROR comparison operators cannot be chained
5+
//~^ ERROR mismatched types
6+
}
7+
8+
fn comp2() {
9+
1 < 2 < 3; //~ ERROR comparison operators cannot be chained
10+
}
11+
12+
fn comp3() {
13+
1 <= 2 < 3; //~ ERROR comparison operators cannot be chained
14+
//~^ ERROR mismatched types
15+
}
16+
17+
fn comp4() {
18+
1 <= 2 <= 3; //~ ERROR comparison operators cannot be chained
19+
//~^ ERROR mismatched types
20+
}
21+
22+
fn comp5() {
23+
1 > 2 >= 3; //~ ERROR comparison operators cannot be chained
24+
//~^ ERROR mismatched types
25+
}
26+
27+
fn comp6() {
28+
1 > 2 > 3; //~ ERROR comparison operators cannot be chained
29+
}
30+
31+
fn comp7() {
32+
1 >= 2 > 3; //~ ERROR comparison operators cannot be chained
33+
}
34+
35+
fn comp8() {
36+
1 >= 2 >= 3; //~ ERROR comparison operators cannot be chained
37+
//~^ ERROR mismatched types
38+
}
39+
40+
fn main() {}

0 commit comments

Comments
 (0)