Repository: rust-lang/rust-clippy (Rust)

Issue:
Assign op pattern should not apply in const context.
### Summary

Right now only a handful of traits are actually const traits. One of them is `Add`. However `AddAssign` is not one of them. Therefor 
the statement `a = a + some_constant` is not equal to `a += some_constant`.

### Lint Name

assign_op_pattern

### Reproducer

I tried this code:

```rust
impl<T> const NumberConstants for T
where
    T: Number + ~const core::ops::Add,
{
    fn constant(value: usize) -> Self {
        let mut res = Self::ZERO;
        let mut count = 0;
        while count < value {
            res = res + Self::ONE;
            count += 1;
        }
        res
    }
}
<code>
```

I saw this happen:

```
res += Self::ONE;
```

I expected to see this happen:
Clippy should not apply this pattern when in a const context.


### Version

```text
rustc 1.89.0-nightly (bc8215286 2025-05-20)
binary: rustc
commit-hash: bc821528634632b4ff8dee5ac1ea4ad90d1b3eb5
commit-date: 2025-05-20
host: x86_64-unknown-linux-gnu
release: 1.89.0-nightly
LLVM version: 20.1.5
```

### Additional Labels

@rustbot label +I-suggestion-causes-error

<!-- TRIAGEBOT_START -->

<!-- TRIAGEBOT_ASSIGN_START -->

<!-- TRIAGEBOT_ASSIGN_DATA_START$${"user":"profetia"}$$TRIAGEBOT_ASSIGN_DATA_END -->

<!-- TRIAGEBOT_ASSIGN_END -->
<!-- TRIAGEBOT_END -->


Discussion:
@rustbot claim
Can you give a more complete reproducer? I am unable to reproduce a case where `AddAssign` is unstable
Yeah sorry, I missed one detail in when simplifying the code. I should have tested the simplified version again.

`Number` inherits `AddAssign` which is crucial for the bug.

This should reproduce correctly
```rust
#![feature(const_trait_impl, const_ops)]

use std::ops::{Add, AddAssign};

pub trait Number: Copy + Add<Self, Output = Self> + AddAssign {
    const ZERO: Self;
    const ONE: Self;
}

#[const_trait]
pub trait NumberConstants {
    fn constant(value: usize) -> Self;
}

impl<T> const NumberConstants for T
where
    T: Number + ~const core::ops::Add,
{
    fn constant(value: usize) -> Self {
        let mut res = Self::ZERO;
        let mut count = 0;
        while count < value {
            res = res + Self::ONE;
            count += 1;
        }
        res
    }
}
```



Patch:
diff --git a/clippy_lints/src/operators/assign_op_pattern.rs b/clippy_lints/src/operators/assign_op_pattern.rs
index 4be42267b14b..9c6141d82226 100644
--- a/clippy_lints/src/operators/assign_op_pattern.rs
+++ b/clippy_lints/src/operators/assign_op_pattern.rs
@@ -1,8 +1,10 @@
 use clippy_utils::diagnostics::span_lint_and_then;
+use clippy_utils::msrvs::Msrv;
+use clippy_utils::qualify_min_const_fn::is_stable_const_fn;
 use clippy_utils::source::SpanRangeExt;
 use clippy_utils::ty::implements_trait;
 use clippy_utils::visitors::for_each_expr_without_closures;
-use clippy_utils::{binop_traits, eq_expr_value, trait_ref_of_method};
+use clippy_utils::{binop_traits, eq_expr_value, is_in_const_context, trait_ref_of_method};
 use core::ops::ControlFlow;
 use rustc_errors::Applicability;
 use rustc_hir as hir;
@@ -19,6 +21,7 @@ pub(super) fn check<'tcx>(
     expr: &'tcx hir::Expr<'_>,
     assignee: &'tcx hir::Expr<'_>,
     e: &'tcx hir::Expr<'_>,
+    msrv: Msrv,
 ) {
     if let hir::ExprKind::Binary(op, l, r) = &e.kind {
         let lint = |assignee: &hir::Expr<'_>, rhs: &hir::Expr<'_>| {
@@ -40,6 +43,15 @@ pub(super) fn check<'tcx>(
                         return;
                     }
                 }
+
+                // Skip if the trait is not stable in const contexts
+                if is_in_const_context(cx)
+                    && let Some(binop_id) = cx.tcx.associated_item_def_ids(trait_id).first()
+                    && !is_stable_const_fn(cx, *binop_id, msrv)
+                {
+                    return;
+                }
+
                 span_lint_and_then(
                     cx,
                     ASSIGN_OP_PATTERN,
diff --git a/clippy_lints/src/operators/mod.rs b/clippy_lints/src/operators/mod.rs
index d32c062cf56a..2f4e8e995886 100644
--- a/clippy_lints/src/operators/mod.rs
+++ b/clippy_lints/src/operators/mod.rs
@@ -919,7 +919,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
                 modulo_arithmetic::check(cx, e, bin_op, lhs, rhs, false);
             },
             ExprKind::Assign(lhs, rhs, _) => {
-                assign_op_pattern::check(cx, e, lhs, rhs);
+                assign_op_pattern::check(cx, e, lhs, rhs, self.msrv);
                 self_assignment::check(cx, e, lhs, rhs);
             },
             ExprKind::Unary(op, arg) => {
diff --git a/clippy_utils/src/qualify_min_const_fn.rs b/clippy_utils/src/qualify_min_const_fn.rs
index 45da266fd8a9..2bf7eb703d4b 100644
--- a/clippy_utils/src/qualify_min_const_fn.rs
+++ b/clippy_utils/src/qualify_min_const_fn.rs
@@ -393,7 +393,8 @@ fn check_terminator<'tcx>(
     }
 }
 
-fn is_stable_const_fn(cx: &LateContext<'_>, def_id: DefId, msrv: Msrv) -> bool {
+/// Checks if the given `def_id` is a stable const fn, in respect to the given MSRV.
+pub fn is_stable_const_fn(cx: &LateContext<'_>, def_id: DefId, msrv: Msrv) -> bool {
     cx.tcx.is_const_fn(def_id)
         && cx
             .tcx


Test patch:
diff --git a/tests/ui/assign_ops.fixed b/tests/ui/assign_ops.fixed
index 429c20f95e91..3bc6885d7c3e 100644
--- a/tests/ui/assign_ops.fixed
+++ b/tests/ui/assign_ops.fixed
@@ -1,5 +1,6 @@
 #![allow(clippy::useless_vec)]
 #![warn(clippy::assign_op_pattern)]
+#![feature(const_trait_impl, const_ops)]
 
 use core::num::Wrapping;
 use std::ops::{Mul, MulAssign};
@@ -73,3 +74,33 @@ impl MulAssign<i64> for Wrap {
         *self = *self * rhs
     }
 }
+
+mod issue14871 {
+
+    use std::ops::{Add, AddAssign};
+
+    pub trait Number: Copy + Add<Self, Output = Self> + AddAssign {
+        const ZERO: Self;
+        const ONE: Self;
+    }
+
+    #[const_trait]
+    pub trait NumberConstants {
+        fn constant(value: usize) -> Self;
+    }
+
+    impl<T> const NumberConstants for T
+    where
+        T: Number + ~const core::ops::Add,
+    {
+        fn constant(value: usize) -> Self {
+            let mut res = Self::ZERO;
+            let mut count = 0;
+            while count < value {
+                res = res + Self::ONE;
+                count += 1;
+            }
+            res
+        }
+    }
+}
diff --git a/tests/ui/assign_ops.rs b/tests/ui/assign_ops.rs
index 480ff07f150e..f1f8f9daff95 100644
--- a/tests/ui/assign_ops.rs
+++ b/tests/ui/assign_ops.rs
@@ -1,5 +1,6 @@
 #![allow(clippy::useless_vec)]
 #![warn(clippy::assign_op_pattern)]
+#![feature(const_trait_impl, const_ops)]
 
 use core::num::Wrapping;
 use std::ops::{Mul, MulAssign};
@@ -73,3 +74,33 @@ impl MulAssign<i64> for Wrap {
         *self = *self * rhs
     }
 }
+
+mod issue14871 {
+
+    use std::ops::{Add, AddAssign};
+
+    pub trait Number: Copy + Add<Self, Output = Self> + AddAssign {
+        const ZERO: Self;
+        const ONE: Self;
+    }
+
+    #[const_trait]
+    pub trait NumberConstants {
+        fn constant(value: usize) -> Self;
+    }
+
+    impl<T> const NumberConstants for T
+    where
+        T: Number + ~const core::ops::Add,
+    {
+        fn constant(value: usize) -> Self {
+            let mut res = Self::ZERO;
+            let mut count = 0;
+            while count < value {
+                res = res + Self::ONE;
+                count += 1;
+            }
+            res
+        }
+    }
+}
diff --git a/tests/ui/assign_ops.stderr b/tests/ui/assign_ops.stderr
index 881a333fbe4b..c5e698b3ee11 100644
--- a/tests/ui/assign_ops.stderr
+++ b/tests/ui/assign_ops.stderr
@@ -1,5 +1,5 @@
 error: manual implementation of an assign operation
-  --> tests/ui/assign_ops.rs:9:5
+  --> tests/ui/assign_ops.rs:10:5
    |
 LL |     a = a + 1;
    |     ^^^^^^^^^ help: replace it with: `a += 1`
@@ -8,67 +8,67 @@ LL |     a = a + 1;
    = help: to override `-D warnings` add `#[allow(clippy::assign_op_pattern)]`
 
 error: manual implementation of an assign operation
-  --> tests/ui/assign_ops.rs:11:5
+  --> tests/ui/assign_ops.rs:12:5
    |
 LL |     a = 1 + a;
    |     ^^^^^^^^^ help: replace it with: `a += 1`
 
 error: manual implementation of an assign operation
-  --> tests/ui/assign_ops.rs:13:5
+  --> tests/ui/assign_ops.rs:14:5
    |
 LL |     a = a - 1;
    |     ^^^^^^^^^ help: replace it with: `a -= 1`
 
 error: manual implementation of an assign operation
-  --> tests/ui/assign_ops.rs:15:5
+  --> tests/ui/assign_ops.rs:16:5
    |
 LL |     a = a * 99;
    |     ^^^^^^^^^^ help: replace it with: `a *= 99`
 
 error: manual implementation of an assign operation
-  --> tests/ui/assign_ops.rs:17:5
+  --> tests/ui/assign_ops.rs:18:5
    |
 LL |     a = 42 * a;
    |     ^^^^^^^^^^ help: replace it with: `a *= 42`
 
 error: manual implementation of an assign operation
-  --> tests/ui/assign_ops.rs:19:5
+  --> tests/ui/assign_ops.rs:20:5
    |
 LL |     a = a / 2;
    |     ^^^^^^^^^ help: replace it with: `a /= 2`
 
 error: manual implementation of an assign operation
-  --> tests/ui/assign_ops.rs:21:5
+  --> tests/ui/assign_ops.rs:22:5
    |
 LL |     a = a % 5;
    |     ^^^^^^^^^ help: replace it with: `a %= 5`
 
 error: manual implementation of an assign operation
-  --> tests/ui/assign_ops.rs:23:5
+  --> tests/ui/assign_ops.rs:24:5
    |
 LL |     a = a & 1;
    |     ^^^^^^^^^ help: replace it with: `a &= 1`
 
 error: manual implementation of an assign operation
-  --> tests/ui/assign_ops.rs:30:5
+  --> tests/ui/assign_ops.rs:31:5
    |
 LL |     s = s + "bla";
    |     ^^^^^^^^^^^^^ help: replace it with: `s += "bla"`
 
 error: manual implementation of an assign operation
-  --> tests/ui/assign_ops.rs:35:5
+  --> tests/ui/assign_ops.rs:36:5
    |
 LL |     a = a + Wrapping(1u32);
    |     ^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `a += Wrapping(1u32)`
 
 error: manual implementation of an assign operation
-  --> tests/ui/assign_ops.rs:38:5
+  --> tests/ui/assign_ops.rs:39:5
    |
 LL |     v[0] = v[0] + v[1];
    |     ^^^^^^^^^^^^^^^^^^ help: replace it with: `v[0] += v[1]`
 
 error: manual implementation of an assign operation
-  --> tests/ui/assign_ops.rs:51:5
+  --> tests/ui/assign_ops.rs:52:5
    |
 LL |     buf = buf + cows.clone();
    |     ^^^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `buf += cows.clone()`