diff --git a/compiler/rustc_error_messages/locales/en-US/lint.ftl b/compiler/rustc_error_messages/locales/en-US/lint.ftl index 0fd9b0ead167c..8b4d643c48b0c 100644 --- a/compiler/rustc_error_messages/locales/en-US/lint.ftl +++ b/compiler/rustc_error_messages/locales/en-US/lint.ftl @@ -13,6 +13,11 @@ lint_enum_intrinsics_mem_variant = the return value of `mem::variant_count` is unspecified when called with a non-enum type .note = the type parameter of `variant_count` should be an enum, but it was instantiated with the type `{$ty_param}`, which is not an enum. +lint_empty_iter_ranges = this `for` loop is never run + .clarification = this loop is never run because it's iterating over an empty iterator + .note = ranges that have a bigger start than their end will produce an empty iterator + .help = if you want a decreasing range sequence, create an increasing range and call `.rev()` on it + lint_expectation = this lint expectation is unfulfilled .note = the `unfulfilled_lint_expectations` lint can't be expected and will always produce this message diff --git a/compiler/rustc_lint/src/empty_iterator.rs b/compiler/rustc_lint/src/empty_iterator.rs new file mode 100644 index 0000000000000..5ddadecbf9633 --- /dev/null +++ b/compiler/rustc_lint/src/empty_iterator.rs @@ -0,0 +1,81 @@ +use crate::{LateContext, LateLintPass, LintContext}; + +use rustc_ast::ast::LitKind; +use rustc_errors::fluent; +use rustc_hir::{Expr, ExprField, ExprKind, MatchSource, PatKind, StmtKind}; +use rustc_span::Span; + +declare_lint! { + /// The `empty_iterator_range` lint checks for empty iterator ranges. + /// + /// ### Example + /// + /// ```rust + /// for i in 10..0 { /* ... */ } + /// ``` + /// + /// {{produces}} + /// + /// ### Explanation + /// + /// It is usually a mistake to have a statement that has no effect. + pub EMPTY_ITERATOR_RANGE, + Warn, + "empty iterator range" +} + +declare_lint_pass!(EmptyIteratorRange => [EMPTY_ITERATOR_RANGE]); + +impl<'tcx> LateLintPass<'tcx> for EmptyIteratorRange { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let Some(for_expr) = extract_for_loop(expr) else { return }; + let span = expr.span.with_hi(for_expr.span.hi()); + if let ExprKind::Struct(_, expr_fields, _) = &for_expr.kind { + is_empty_range(cx, span, expr_fields); + } else if let ExprKind::MethodCall(_, expr, _, _) = &for_expr.kind { + if let ExprKind::Struct(_, expr_fields, _) = &expr.kind { + is_empty_range(cx, span, expr_fields); + } + } + } +} + +fn extract_for_loop<'tcx>(expr: &Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> { + if let ExprKind::DropTemps(e) = expr.kind + && let ExprKind::Match(iterexpr, [arm], MatchSource::ForLoopDesugar) = e.kind + && let ExprKind::Call(_, [arg]) = iterexpr.kind + && let ExprKind::Loop(block, ..) = arm.body.kind + && let [stmt] = block.stmts + && let StmtKind::Expr(e) = stmt.kind + && let ExprKind::Match(_, [_, some_arm], _) = e.kind + && let PatKind::Struct(..) = some_arm.pat.kind + { + Some(arg) + } else { + None + } +} + +fn is_empty_range(cx: &LateContext<'_>, span: Span, expr_fields: &[ExprField<'_>]) { + let mut prev = 0u128; + for (index, expr_field) in expr_fields.iter().enumerate() { + if let ExprKind::Lit(lit) = &expr_field.expr.kind { + if let LitKind::Int(u, _) = lit.node { + if index == 0 { + prev = u; + } else if prev > u { + cx.struct_span_lint( + EMPTY_ITERATOR_RANGE, + span, + fluent::lint::empty_iter_ranges, + |lint| { + lint.span_label(span, fluent::lint::clarification) + .note(fluent::lint::note) + .help(fluent::lint::help) + }, + ); + } + } + } + } +} diff --git a/compiler/rustc_lint/src/lib.rs b/compiler/rustc_lint/src/lib.rs index 9148c42195fbe..e1b0656ab1e8b 100644 --- a/compiler/rustc_lint/src/lib.rs +++ b/compiler/rustc_lint/src/lib.rs @@ -49,6 +49,7 @@ mod array_into_iter; pub mod builtin; mod context; mod early; +mod empty_iterator; mod enum_intrinsics_non_enums; mod errors; mod expect; @@ -85,6 +86,7 @@ use rustc_span::Span; use array_into_iter::ArrayIntoIter; use builtin::*; +use empty_iterator::EmptyIteratorRange; use enum_intrinsics_non_enums::EnumIntrinsicsNonEnums; use hidden_unicode_codepoints::*; use internal::*; @@ -226,6 +228,7 @@ macro_rules! late_lint_mod_passes { InvalidAtomicOrdering: InvalidAtomicOrdering, NamedAsmLabels: NamedAsmLabels, OpaqueHiddenInferredBound: OpaqueHiddenInferredBound, + EmptyIteratorRange: EmptyIteratorRange, ] ); }; diff --git a/library/core/tests/iter/range.rs b/library/core/tests/iter/range.rs index 84498a8eae52e..0cf92f213968d 100644 --- a/library/core/tests/iter/range.rs +++ b/library/core/tests/iter/range.rs @@ -460,6 +460,8 @@ fn test_range_inclusive_size_hint() { #[test] fn test_double_ended_range() { + #![cfg_attr(not(bootstrap), allow(empty_iterator_range))] + assert_eq!((11..14).rev().collect::>(), [13, 12, 11]); for _ in (10..0).rev() { panic!("unreachable"); diff --git a/src/test/ui/lint/empty-iterator-range.rs b/src/test/ui/lint/empty-iterator-range.rs new file mode 100644 index 0000000000000..ec11257339dec --- /dev/null +++ b/src/test/ui/lint/empty-iterator-range.rs @@ -0,0 +1,13 @@ +#![deny(empty_iterator_range)] + +fn main() { + for _i in 10..0 { + //~^ ERROR this `for` loop is never run [empty_iterator_range] + } + for _i in (10..0).rev() { + //~^ ERROR this `for` loop is never run [empty_iterator_range] + } + for _i in (10..0).step_by(1) { + //~^ ERROR this `for` loop is never run [empty_iterator_range] + } +} diff --git a/src/test/ui/lint/empty-iterator-range.stderr b/src/test/ui/lint/empty-iterator-range.stderr new file mode 100644 index 0000000000000..3ead20c009cbc --- /dev/null +++ b/src/test/ui/lint/empty-iterator-range.stderr @@ -0,0 +1,34 @@ +error: this `for` loop is never run + --> $DIR/empty-iterator-range.rs:4:5 + | +LL | for _i in 10..0 { + | ^^^^^^^^^^^^^^^ this loop is never run because it's iterating over an empty iterator + | + = note: ranges that have a bigger start than their end will produce an empty iterator + = help: if you want a decreasing range sequence, create an increasing range and call `.rev()` on it +note: the lint level is defined here + --> $DIR/empty-iterator-range.rs:1:9 + | +LL | #![deny(empty_iterator_range)] + | ^^^^^^^^^^^^^^^^^^^^ + +error: this `for` loop is never run + --> $DIR/empty-iterator-range.rs:7:5 + | +LL | for _i in (10..0).rev() { + | ^^^^^^^^^^^^^^^^^^^^^^^ this loop is never run because it's iterating over an empty iterator + | + = note: ranges that have a bigger start than their end will produce an empty iterator + = help: if you want a decreasing range sequence, create an increasing range and call `.rev()` on it + +error: this `for` loop is never run + --> $DIR/empty-iterator-range.rs:10:5 + | +LL | for _i in (10..0).step_by(1) { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this loop is never run because it's iterating over an empty iterator + | + = note: ranges that have a bigger start than their end will produce an empty iterator + = help: if you want a decreasing range sequence, create an increasing range and call `.rev()` on it + +error: aborting due to 3 previous errors +