Skip to content

Commit

Permalink
Change the desugaring of assert! for better error output
Browse files Browse the repository at this point in the history
In the desugaring of `assert!`, we now expand to a `match` expression
instead of `if !cond {..}`.

The span of incorrect conditions will point only at the expression, and not
the whole `assert!` invocation.

```
error[E0308]: mismatched types
  --> $DIR/issue-14091.rs:2:13
   |
LL |     assert!(1,1);
   |             ^ expected `bool`, found integer
```

We no longer mention the expression needing to implement the `Not` trait.

```
error[E0308]: mismatched types
  --> $DIR/issue-14091-2.rs:15:13
   |
LL |     assert!(x, x);
   |             ^ expected `bool`, found `BytePos`
```

`assert!(val)` now desugars to:

```rust
match val {
    true => {},
    _ => $crate::panic::panic_2021!(),
}
```

Fix #122159.

We make some minor changes to some diagnostics to avoid span overlap on
type mismatch or inverted "expected"/"found" on type errors.

We remove some unnecessary parens from core, alloc and miri.
  • Loading branch information
estebank committed Jan 28, 2025
1 parent 2f348cb commit 903c367
Show file tree
Hide file tree
Showing 56 changed files with 319 additions and 247 deletions.
32 changes: 18 additions & 14 deletions compiler/rustc_builtin_macros/src/assert.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
mod context;

use rustc_ast::ptr::P;
use rustc_ast::token::Delimiter;
use rustc_ast::token::{self, Delimiter};
use rustc_ast::tokenstream::{DelimSpan, TokenStream};
use rustc_ast::{DelimArgs, Expr, ExprKind, MacCall, Path, PathSegment, UnOp, token};
use rustc_ast::{DelimArgs, Expr, ExprKind, MacCall, Path, PathSegment};
use rustc_ast_pretty::pprust;
use rustc_errors::PResult;
use rustc_expand::base::{DummyResult, ExpandResult, ExtCtxt, MacEager, MacroExpanderResult};
Expand All @@ -30,7 +30,7 @@ pub(crate) fn expand_assert<'cx>(

// `core::panic` and `std::panic` are different macros, so we use call-site
// context to pick up whichever is currently in scope.
let call_site_span = cx.with_call_site_ctxt(span);
let call_site_span = cx.with_call_site_ctxt(cond_expr.span);

let panic_path = || {
if use_panic_2021(span) {
Expand Down Expand Up @@ -64,7 +64,7 @@ pub(crate) fn expand_assert<'cx>(
}),
})),
);
expr_if_not(cx, call_site_span, cond_expr, then, None)
expr_if_not(cx, call_site_span, cond_expr, then)
}
// If `generic_assert` is enabled, generates rich captured outputs
//
Expand All @@ -89,26 +89,30 @@ pub(crate) fn expand_assert<'cx>(
)),
)],
);
expr_if_not(cx, call_site_span, cond_expr, then, None)
expr_if_not(cx, call_site_span, cond_expr, then)
};

ExpandResult::Ready(MacEager::expr(expr))
}

/// `assert!($cond_expr, $custom_message)`
struct Assert {
cond_expr: P<Expr>,
custom_message: Option<TokenStream>,
}

// if !{ ... } { ... } else { ... }
fn expr_if_not(
cx: &ExtCtxt<'_>,
span: Span,
cond: P<Expr>,
then: P<Expr>,
els: Option<P<Expr>>,
) -> P<Expr> {
cx.expr_if(span, cx.expr(span, ExprKind::Unary(UnOp::Not, cond)), then, els)
/// `match <cond> { true => {} _ => <then> }`
fn expr_if_not(cx: &ExtCtxt<'_>, span: Span, cond: P<Expr>, then: P<Expr>) -> P<Expr> {
// Instead of expanding to `if !<cond> { <then> }`, we expand to
// `match <cond> { true => {} _ <then> }`.
// This allows us to always complain about mismatched types instead of "cannot apply unary
// operator `!` to type `X`" when passing an invalid `<cond>`, while also allowing `<cond>` to
// be `&true`.
let els = cx.expr_block(cx.block(span, thin_vec![]));
let mut arms = thin_vec![];
arms.push(cx.arm(span, cx.pat_lit(span, cx.expr_bool(span, true)), els));
arms.push(cx.arm(span, cx.pat_wild(span), then));
cx.expr_match(span, cond, arms)
}

fn parse_assert<'a>(cx: &ExtCtxt<'a>, sp: Span, stream: TokenStream) -> PResult<'a, Assert> {
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_hir_typeck/src/demand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
) {
match (self.tcx.parent_hir_node(expr.hir_id), error) {
(hir::Node::LetStmt(hir::LetStmt { ty: Some(ty), init: Some(init), .. }), _)
if init.hir_id == expr.hir_id =>
if init.hir_id == expr.hir_id && !ty.span.source_equal(init.span) =>
{
// Point at `let` assignment type.
err.span_label(ty.span, "expected due to this");
Expand Down
30 changes: 25 additions & 5 deletions compiler/rustc_trait_selection/src/error_reporting/infer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> {
span,
format!("this is an iterator with items of type `{}`", args.type_at(0)),
);
} else {
} else if !span.overlaps(cause.span) {
let expected_ty = self.tcx.short_ty_string(expected_ty, path);
err.span_label(span, format!("this expression has type `{expected_ty}`"));
}
Expand Down Expand Up @@ -1581,8 +1581,16 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> {
{
let e = self.tcx.erase_regions(e);
let f = self.tcx.erase_regions(f);
let expected = with_forced_trimmed_paths!(e.sort_string(self.tcx));
let found = with_forced_trimmed_paths!(f.sort_string(self.tcx));
let mut expected = with_forced_trimmed_paths!(e.sort_string(self.tcx));
let mut found = with_forced_trimmed_paths!(f.sort_string(self.tcx));
if let Some(def_id) = cause.span.ctxt().outer_expn_data().macro_def_id
&& self.tcx.is_diagnostic_item(sym::assert_macro, def_id)
{
// When the type error comes from `assert!()`, the cause and effect are reversed
// because that macro expands to `match val { false => {panic!()}, _ => {} }`, which
// would say something like "expected `Type`, found `bool`", confusing the user.
(found, expected) = (expected, found);
}
if expected == found {
label_or_note(span, terr.to_string(self.tcx));
} else {
Expand Down Expand Up @@ -2098,7 +2106,7 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> {
) -> Option<(DiagStyledString, DiagStyledString, Option<PathBuf>)> {
match values {
ValuePairs::Regions(exp_found) => self.expected_found_str(exp_found),
ValuePairs::Terms(exp_found) => self.expected_found_str_term(exp_found),
ValuePairs::Terms(exp_found) => self.expected_found_str_term(cause, exp_found),
ValuePairs::Aliases(exp_found) => self.expected_found_str(exp_found),
ValuePairs::ExistentialTraitRef(exp_found) => self.expected_found_str(exp_found),
ValuePairs::ExistentialProjection(exp_found) => self.expected_found_str(exp_found),
Expand Down Expand Up @@ -2139,14 +2147,26 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> {

fn expected_found_str_term(
&self,
cause: &ObligationCause<'tcx>,
exp_found: ty::error::ExpectedFound<ty::Term<'tcx>>,
) -> Option<(DiagStyledString, DiagStyledString, Option<PathBuf>)> {
let exp_found = self.resolve_vars_if_possible(exp_found);
if exp_found.references_error() {
return None;
}
let (mut expected, mut found) = (exp_found.expected, exp_found.found);
if let Some(def_id) = cause.span.ctxt().outer_expn_data().macro_def_id
&& self.tcx.is_diagnostic_item(sym::assert_macro, def_id)
{
// When the type error comes from `assert!()`, the cause and effect are reversed
// because that macro expands to `match val { false => {panic!()}, _ => {} }`, which
// would say something like
// = note: expected `Type`
// found `bool`"
(expected, found) = (found, expected);
}

Some(match (exp_found.expected.unpack(), exp_found.found.unpack()) {
Some(match (expected.unpack(), found.unpack()) {
(ty::TermKind::Ty(expected), ty::TermKind::Ty(found)) => {
let (mut exp, mut fnd) = self.cmp(expected, found);
// Use the terminal width as the basis to determine when to compress the printed
Expand Down
22 changes: 11 additions & 11 deletions library/alloc/tests/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2297,21 +2297,21 @@ fn utf8_chars() {
assert_eq!(schs.len(), 4);
assert_eq!(schs.iter().cloned().collect::<String>(), s);

assert!((from_utf8(s.as_bytes()).is_ok()));
assert!(from_utf8(s.as_bytes()).is_ok());
// invalid prefix
assert!((!from_utf8(&[0x80]).is_ok()));
assert!(!from_utf8(&[0x80]).is_ok());
// invalid 2 byte prefix
assert!((!from_utf8(&[0xc0]).is_ok()));
assert!((!from_utf8(&[0xc0, 0x10]).is_ok()));
assert!(!from_utf8(&[0xc0]).is_ok());
assert!(!from_utf8(&[0xc0, 0x10]).is_ok());
// invalid 3 byte prefix
assert!((!from_utf8(&[0xe0]).is_ok()));
assert!((!from_utf8(&[0xe0, 0x10]).is_ok()));
assert!((!from_utf8(&[0xe0, 0xff, 0x10]).is_ok()));
assert!(!from_utf8(&[0xe0]).is_ok());
assert!(!from_utf8(&[0xe0, 0x10]).is_ok());
assert!(!from_utf8(&[0xe0, 0xff, 0x10]).is_ok());
// invalid 4 byte prefix
assert!((!from_utf8(&[0xf0]).is_ok()));
assert!((!from_utf8(&[0xf0, 0x10]).is_ok()));
assert!((!from_utf8(&[0xf0, 0xff, 0x10]).is_ok()));
assert!((!from_utf8(&[0xf0, 0xff, 0xff, 0x10]).is_ok()));
assert!(!from_utf8(&[0xf0]).is_ok());
assert!(!from_utf8(&[0xf0, 0x10]).is_ok());
assert!(!from_utf8(&[0xf0, 0xff, 0x10]).is_ok());
assert!(!from_utf8(&[0xf0, 0xff, 0xff, 0x10]).is_ok());
}

#[test]
Expand Down
8 changes: 4 additions & 4 deletions library/coretests/tests/bool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,14 @@ fn test_bool() {
#[test]
pub fn test_bool_not() {
if !false {
assert!((true));
assert!(true);
} else {
assert!((false));
assert!(false);
}
if !true {
assert!((false));
assert!(false);
} else {
assert!((true));
assert!(true);
}
}

Expand Down
6 changes: 3 additions & 3 deletions library/coretests/tests/ptr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ fn test() {
let mut v1 = vec![0u16, 0u16, 0u16];

copy(v0.as_ptr().offset(1), v1.as_mut_ptr().offset(1), 1);
assert!((v1[0] == 0u16 && v1[1] == 32001u16 && v1[2] == 0u16));
assert!(v1[0] == 0u16 && v1[1] == 32001u16 && v1[2] == 0u16);
copy(v0.as_ptr().offset(2), v1.as_mut_ptr(), 1);
assert!((v1[0] == 32002u16 && v1[1] == 32001u16 && v1[2] == 0u16));
assert!(v1[0] == 32002u16 && v1[1] == 32001u16 && v1[2] == 0u16);
copy(v0.as_ptr(), v1.as_mut_ptr().offset(2), 1);
assert!((v1[0] == 32002u16 && v1[1] == 32001u16 && v1[2] == 32000u16));
assert!(v1[0] == 32002u16 && v1[1] == 32001u16 && v1[2] == 32000u16);
}
}

Expand Down
2 changes: 1 addition & 1 deletion library/coretests/tests/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fn test_partial_ord() {
assert!(!((1.0f64, 2.0f64) <= (f64::NAN, 3.0)));
assert!(!((1.0f64, 2.0f64) > (f64::NAN, 3.0)));
assert!(!((1.0f64, 2.0f64) >= (f64::NAN, 3.0)));
assert!(((1.0f64, 2.0f64) < (2.0, f64::NAN)));
assert!((1.0f64, 2.0f64) < (2.0, f64::NAN));
assert!(!((2.0f64, 2.0f64) < (2.0, f64::NAN)));
}

Expand Down
2 changes: 1 addition & 1 deletion library/std/src/env/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ fn test_self_exe_path() {

#[test]
fn test() {
assert!((!Path::new("test-path").is_absolute()));
assert!(!Path::new("test-path").is_absolute());

#[cfg(not(target_env = "sgx"))]
current_dir().unwrap();
Expand Down
4 changes: 2 additions & 2 deletions library/std/tests/istr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ fn test_stack_assign() {
let t: String = "a".to_string();
assert_eq!(s, t);
let u: String = "b".to_string();
assert!((s != u));
assert!(s != u);
}

#[test]
Expand All @@ -19,7 +19,7 @@ fn test_heap_assign() {
let t: String = "a big ol' string".to_string();
assert_eq!(s, t);
let u: String = "a bad ol' string".to_string();
assert!((s != u));
assert!(s != u);
}

#[test]
Expand Down
22 changes: 11 additions & 11 deletions library/std/tests/seq-compare.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
#[test]
fn seq_compare() {
assert!(("hello".to_string() < "hellr".to_string()));
assert!(("hello ".to_string() > "hello".to_string()));
assert!(("hello".to_string() != "there".to_string()));
assert!((vec![1, 2, 3, 4] > vec![1, 2, 3]));
assert!((vec![1, 2, 3] < vec![1, 2, 3, 4]));
assert!((vec![1, 2, 4, 4] > vec![1, 2, 3, 4]));
assert!((vec![1, 2, 3, 4] < vec![1, 2, 4, 4]));
assert!((vec![1, 2, 3] <= vec![1, 2, 3]));
assert!((vec![1, 2, 3] <= vec![1, 2, 3, 3]));
assert!((vec![1, 2, 3, 4] > vec![1, 2, 3]));
assert!("hello".to_string() < "hellr".to_string());
assert!("hello ".to_string() > "hello".to_string());
assert!("hello".to_string() != "there".to_string());
assert!(vec![1, 2, 3, 4] > vec![1, 2, 3]);
assert!(vec![1, 2, 3] < vec![1, 2, 3, 4]);
assert!(vec![1, 2, 4, 4] > vec![1, 2, 3, 4]);
assert!(vec![1, 2, 3, 4] < vec![1, 2, 4, 4]);
assert!(vec![1, 2, 3] <= vec![1, 2, 3]);
assert!(vec![1, 2, 3] <= vec![1, 2, 3, 3]);
assert!(vec![1, 2, 3, 4] > vec![1, 2, 3]);
assert_eq!(vec![1, 2, 3], vec![1, 2, 3]);
assert!((vec![1, 2, 3] != vec![1, 1, 3]));
assert!(vec![1, 2, 3] != vec![1, 1, 3]);
}
24 changes: 19 additions & 5 deletions src/tools/clippy/clippy_lints/src/bool_assert_comparison.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,27 @@ impl<'tcx> LateLintPass<'tcx> for BoolAssertComparison {

let mut suggestions = vec![(name_span, non_eq_mac.to_string()), (lit_span, String::new())];

if bool_value ^ eq_macro {
let Some(sugg) = Sugg::hir_opt(cx, non_lit_expr) else {
return;
};
suggestions.push((non_lit_expr.span, (!sugg).to_string()));
if let ty::Bool = non_lit_ty.kind() {
if bool_value ^ eq_macro {
let Some(sugg) = Sugg::hir_opt(cx, non_lit_expr) else {
return;
};
suggestions.push((non_lit_expr.span, (!sugg).to_string()));
}
} else {
// If we have a `value` that is *not* `bool` but that `!value` *is*, we suggest
// `!!value`.
suggestions.push((
non_lit_expr.span.shrink_to_lo(),
if bool_value ^ eq_macro {
"!".to_string()
} else {
"!!".to_string()
},
));
}


diag.multipart_suggestion(
format!("replace it with `{non_eq_mac}!(..)`"),
suggestions,
Expand Down
18 changes: 8 additions & 10 deletions src/tools/clippy/clippy_lints/src/missing_asserts_for_indexing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use rustc_ast::{LitKind, RangeLimits};
use rustc_data_structures::packed::Pu128;
use rustc_data_structures::unhash::UnindexMap;
use rustc_errors::{Applicability, Diag};
use rustc_hir::{BinOp, Block, Body, Expr, ExprKind, UnOp};
use rustc_hir::{BinOp, Body, Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
use rustc_span::source_map::Spanned;
Expand Down Expand Up @@ -134,18 +134,16 @@ fn assert_len_expr<'hir>(
cx: &LateContext<'_>,
expr: &'hir Expr<'hir>,
) -> Option<(LengthComparison, usize, &'hir Expr<'hir>)> {
if let Some(higher::If { cond, then, .. }) = higher::If::hir(expr)
&& let ExprKind::Unary(UnOp::Not, condition) = &cond.kind
&& let ExprKind::Binary(bin_op, left, right) = &condition.kind
if let higher::IfLetOrMatch::Match(cond, [_, then], _) = higher::IfLetOrMatch::parse(cx, expr)?
&& let ExprKind::Binary(bin_op, left, right) = &cond.kind

&& let Some((cmp, asserted_len, slice_len)) = len_comparison(*bin_op, left, right)
&& let ExprKind::MethodCall(method, recv, [], _) = &slice_len.kind
&& cx.typeck_results().expr_ty_adjusted(recv).peel_refs().is_slice()
&& method.ident.name == sym::len

// check if `then` block has a never type expression
&& let ExprKind::Block(Block { expr: Some(then_expr), .. }, _) = then.kind
&& cx.typeck_results().expr_ty(then_expr).is_never()
&& cx.typeck_results().expr_ty(then.body).is_never()
{
Some((cmp, asserted_len, recv))
} else {
Expand Down Expand Up @@ -337,21 +335,21 @@ fn report_indexes(cx: &LateContext<'_>, map: &UnindexMap<u64, Vec<IndexEntry<'_>
// `v.len() < 5` and `v.len() <= 5` does nothing in terms of bounds checks.
// The user probably meant `v.len() > 5`
LengthComparison::LengthLessThanInt | LengthComparison::LengthLessThanOrEqualInt => Some(
format!("assert!({}.len() > {highest_index})", snippet(cx, slice.span, "..")),
format!("{}.len() > {highest_index}", snippet(cx, slice.span, "..")),
),
// `5 < v.len()` == `v.len() > 5`
LengthComparison::IntLessThanLength if asserted_len < highest_index => Some(format!(
"assert!({}.len() > {highest_index})",
"{}.len() > {highest_index}",
snippet(cx, slice.span, "..")
)),
// `5 <= v.len() == `v.len() >= 5`
LengthComparison::IntLessThanOrEqualLength if asserted_len <= highest_index => Some(format!(
"assert!({}.len() > {highest_index})",
"{}.len() > {highest_index}",
snippet(cx, slice.span, "..")
)),
// `highest_index` here is rather a length, so we need to add 1 to it
LengthComparison::LengthEqualInt if asserted_len < highest_index + 1 => Some(format!(
"assert!({}.len() == {})",
"{}.len() == {}",
snippet(cx, slice.span, ".."),
highest_index + 1
)),
Expand Down
6 changes: 3 additions & 3 deletions src/tools/clippy/tests/ui/bool_assert_comparison.fixed
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ fn main() {
assert_eq!(a!(), "".is_empty());
assert_eq!("".is_empty(), b!());
assert_eq!(a, true);
assert!(b);
assert!(!!b);

assert_ne!("a".len(), 1);
assert!("a".is_empty());
Expand All @@ -111,7 +111,7 @@ fn main() {
debug_assert_eq!(a!(), "".is_empty());
debug_assert_eq!("".is_empty(), b!());
debug_assert_eq!(a, true);
debug_assert!(b);
debug_assert!(!!b);

debug_assert_ne!("a".len(), 1);
debug_assert!("a".is_empty());
Expand Down Expand Up @@ -143,7 +143,7 @@ fn main() {

use debug_assert_eq as renamed;
renamed!(a, true);
debug_assert!(b);
debug_assert!(!!b);

let non_copy = NonCopy;
assert_eq!(non_copy, true);
Expand Down
Loading

0 comments on commit 903c367

Please sign in to comment.