From 21d48c8336840f7d9a342d8f860037a440725708 Mon Sep 17 00:00:00 2001
From: Som Snytt <som.snytt@gmail.com>
Date: Mon, 24 Mar 2025 18:29:47 -0700
Subject: [PATCH] Use other tree for actual symbol of Assign

---
 compiler/src/dotty/tools/dotc/ast/Trees.scala |  7 +-
 .../tools/dotc/printing/Formatting.scala      |  5 +-
 .../dotty/tools/dotc/reporting/messages.scala | 26 +++--
 .../src/dotty/tools/dotc/typer/Dynamic.scala  | 11 +--
 .../src/dotty/tools/dotc/typer/Typer.scala    | 14 ++-
 tests/neg/assignments.check                   | 12 +++
 tests/neg/i11561.check                        |  2 +-
 tests/neg/i16655.check                        |  2 +-
 tests/neg/i20338c.check                       |  2 +-
 tests/neg/i22671.check                        | 61 ++++++++++++
 tests/neg/i22671.explain.check                | 94 +++++++++++++++++++
 tests/neg/i22671.explain.scala                | 32 +++++++
 tests/neg/i22671.scala                        | 49 ++++++++++
 13 files changed, 287 insertions(+), 30 deletions(-)
 create mode 100644 tests/neg/assignments.check
 create mode 100644 tests/neg/i22671.check
 create mode 100644 tests/neg/i22671.explain.check
 create mode 100644 tests/neg/i22671.explain.scala
 create mode 100644 tests/neg/i22671.scala

diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala
index fdefc14aadd6..561411857688 100644
--- a/compiler/src/dotty/tools/dotc/ast/Trees.scala
+++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala
@@ -1821,16 +1821,15 @@ object Trees {
       }
     }
 
-    def rename(tree: NameTree, newName: Name)(using Context): tree.ThisTree[T] = {
-      tree match {
+    def rename(tree: NameTree, newName: Name)(using Context): tree.ThisTree[T] =
+      tree.match
         case tree: Ident => cpy.Ident(tree)(newName)
         case tree: Select => cpy.Select(tree)(tree.qualifier, newName)
         case tree: Bind => cpy.Bind(tree)(newName, tree.body)
         case tree: ValDef => cpy.ValDef(tree)(name = newName.asTermName)
         case tree: DefDef => cpy.DefDef(tree)(name = newName.asTermName)
         case tree: TypeDef => cpy.TypeDef(tree)(name = newName.asTypeName)
-      }
-    }.asInstanceOf[tree.ThisTree[T]]
+      .asInstanceOf[tree.ThisTree[T]]
 
     object TypeDefs:
       def unapply(xs: List[Tree]): Option[List[TypeDef]] = xs match
diff --git a/compiler/src/dotty/tools/dotc/printing/Formatting.scala b/compiler/src/dotty/tools/dotc/printing/Formatting.scala
index 741b997d9926..5b05982ccbaa 100644
--- a/compiler/src/dotty/tools/dotc/printing/Formatting.scala
+++ b/compiler/src/dotty/tools/dotc/printing/Formatting.scala
@@ -8,7 +8,7 @@ import core.*
 import Texts.*, Types.*, Flags.*, Symbols.*, Contexts.*
 import Decorators.*
 import reporting.Message
-import util.{DiffUtil, SimpleIdentitySet}
+import util.{Chars, DiffUtil, SimpleIdentitySet}
 import Highlighting.*
 
 object Formatting {
@@ -169,7 +169,8 @@ object Formatting {
     }
 
     def assemble(args: Seq[Shown])(using Context): String = {
-      def isLineBreak(c: Char) = c == '\n' || c == '\f' // compatible with StringLike#isLineBreak
+      // compatible with CharArrayReader (not StringOps)
+      inline def isLineBreak(c: Char) = c == Chars.LF || c == Chars.FF
       def stripTrailingPart(s: String) = {
         val (pre, post) = s.span(c => !isLineBreak(c))
         pre ++ post.stripMargin
diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala
index dcd7ed10987b..79d64c0d1cef 100644
--- a/compiler/src/dotty/tools/dotc/reporting/messages.scala
+++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala
@@ -1526,18 +1526,24 @@ class AmbiguousExtensionMethod(tree: untpd.Tree, expansion1: tpd.Tree, expansion
        |are possible expansions of $tree"""
   def explain(using Context) = ""
 
-class ReassignmentToVal(name: Name)(using Context)
-  extends TypeMsg(ReassignmentToValID) {
-  def msg(using Context) = i"""Reassignment to val $name"""
-  def explain(using Context) =
-    i"""|You can not assign a new value to $name as values can't be changed.
-        |Keep in mind that every statement has a value, so you may e.g. use
-        |  ${hl("val")} $name ${hl("= if (condition) 2 else 5")}
-        |In case you need a reassignable name, you can declare it as
-        |variable
+class ReassignmentToVal(sym: Symbol, usage: Name)(using Context) extends TypeMsg(ReassignmentToValID):
+  private def name = if sym.exists then sym.name else usage
+  private def addendum = if !sym.exists || !sym.owner.isClass then "" else
+    i"""|
+        |Also, assignment syntax can be used if there is a corresponding setter:
+        |  ${hl("def")} ${name}${hl("_=(x: Int): Unit = _v = x")}
+        |"""
+  def msg(using Context) =
+    if sym.exists then i"""Assignment to $sym"""
+    else i"""Bad assignment to $usage"""
+  def explain(using Context) =
+    i"""|Members defined using `val` or `def` can't be assigned to.
+        |If you need to change the value of $name, use `var` instead:
         |  ${hl("var")} $name ${hl("=")} ...
+        |However, it's more common to initialize a variable just once
+        |with a complex expression or even a block with many statements:
+        |  ${hl("val")} $name ${hl("= if (condition) 1 else -1")}$addendum
         |"""
-}
 
 class TypeDoesNotTakeParameters(tpe: Type, params: List[untpd.Tree])(using Context)
   extends TypeMsg(TypeDoesNotTakeParametersID) {
diff --git a/compiler/src/dotty/tools/dotc/typer/Dynamic.scala b/compiler/src/dotty/tools/dotc/typer/Dynamic.scala
index 14cc7bf963a6..d660fd3c41ad 100644
--- a/compiler/src/dotty/tools/dotc/typer/Dynamic.scala
+++ b/compiler/src/dotty/tools/dotc/typer/Dynamic.scala
@@ -128,18 +128,17 @@ trait Dynamic {
   /** Translate selection that does not typecheck according to the normal rules into a updateDynamic.
    *    foo.bar = baz ~~> foo.updateDynamic(bar)(baz)
    */
-  def typedDynamicAssign(tree: untpd.Assign, pt: Type)(using Context): Tree = {
+  def typedDynamicAssign(tree: untpd.Assign, pt: Type)(using Context): Tree =
     def typedDynamicAssign(qual: untpd.Tree, name: Name, selSpan: Span, targs: List[untpd.Tree]): Tree =
       typedApply(untpd.Apply(coreDynamic(qual, nme.updateDynamic, name, selSpan, targs), tree.rhs), pt)
-    tree.lhs match {
+    tree.lhs match
       case sel @ Select(qual, name) if !isDynamicMethod(name) =>
         typedDynamicAssign(qual, name, sel.span, Nil)
       case TypeApply(sel @ Select(qual, name), targs) if !isDynamicMethod(name) =>
         typedDynamicAssign(qual, name, sel.span, targs)
-      case _ =>
-        errorTree(tree, ReassignmentToVal(tree.lhs.symbol.name))
-    }
-  }
+      case lhs =>
+        val name = lhs match { case nt: NameTree => nt.name case _ => nme.NO_NAME }
+        errorTree(tree, ReassignmentToVal(lhs.symbol, name))
 
   private def coreDynamic(qual: untpd.Tree, dynName: Name, name: Name, selSpan: Span, targs: List[untpd.Tree])(using Context): untpd.Apply = {
     val select = untpd.Select(qual, dynName).withSpan(selSpan)
diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala
index 6b7b840e7606..cbbf9a8c3de0 100644
--- a/compiler/src/dotty/tools/dotc/typer/Typer.scala
+++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala
@@ -1373,9 +1373,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
 
   def typedAssign(tree: untpd.Assign, pt: Type)(using Context): Tree =
     tree.lhs match {
-      case lhs @ Apply(fn, args) =>
-        typed(untpd.Apply(untpd.Select(fn, nme.update), args :+ tree.rhs), pt)
-      case untpd.TypedSplice(Apply(MaybePoly(Select(fn, app), targs), args)) if app == nme.apply =>
+      case Apply(fn, args) =>
+        val appliedUpdate =
+          untpd.Apply(untpd.Select(fn, nme.update), args :+ tree.rhs)
+        typed(appliedUpdate, pt)
+      case untpd.TypedSplice(Apply(MaybePoly(Select(fn, nme.apply), targs), args)) =>
         val rawUpdate: untpd.Tree = untpd.Select(untpd.TypedSplice(fn), nme.update)
         val wrappedUpdate =
           if (targs.isEmpty) rawUpdate
@@ -1389,7 +1391,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
         def lhs1 = adapt(lhsCore, LhsProto, locked)
 
         def reassignmentToVal =
-          report.error(ReassignmentToVal(lhsCore.symbol.name), tree.srcPos)
+          val name = lhs match { case nt: NameTree => nt.name case _ => nme.NO_NAME }
+          report.error(ReassignmentToVal(lhs1.symbol, name), tree.srcPos)
           cpy.Assign(tree)(lhsCore, typed(tree.rhs, lhs1.tpe.widen)).withType(defn.UnitType)
 
         def canAssign(sym: Symbol) =
@@ -1478,8 +1481,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
               typedDynamicAssign(tree, pt)
             case tpe =>
               reassignmentToVal
-        }
+          }
     }
+  end typedAssign
 
   def typedBlockStats(stats: List[untpd.Tree])(using Context): (List[tpd.Tree], Context) =
     index(stats)
diff --git a/tests/neg/assignments.check b/tests/neg/assignments.check
new file mode 100644
index 000000000000..e42b50ccc8d9
--- /dev/null
+++ b/tests/neg/assignments.check
@@ -0,0 +1,12 @@
+-- [E052] Type Error: tests/neg/assignments.scala:16:8 -----------------------------------------------------------------
+16 |    x_= = 2  // error should give missing arguments
+   |    ^^^^^^^
+   |    Bad assignment to x_=
+   |
+   | longer explanation available when compiling with `-explain`
+-- [E083] Type Error: tests/neg/assignments.scala:20:9 -----------------------------------------------------------------
+20 |  import c._ // error should give: prefix is not stable
+   |         ^
+   |         (assignments.c : assignments.C) is not a valid import prefix, since it is not an immutable path
+   |
+   | longer explanation available when compiling with `-explain`
diff --git a/tests/neg/i11561.check b/tests/neg/i11561.check
index 28d7e355c499..c9d11dbc68fc 100644
--- a/tests/neg/i11561.check
+++ b/tests/neg/i11561.check
@@ -11,6 +11,6 @@
 -- [E052] Type Error: tests/neg/i11561.scala:3:30 ----------------------------------------------------------------------
 3 |  val updateText2 = copy(text = (_: String)) // error
   |                         ^^^^^^^^^^^^^^^^^^
-  |                         Reassignment to val text
+  |                         Assignment to value text
   |
   | longer explanation available when compiling with `-explain`
diff --git a/tests/neg/i16655.check b/tests/neg/i16655.check
index e1335b624244..e9fbeb56adb8 100644
--- a/tests/neg/i16655.check
+++ b/tests/neg/i16655.check
@@ -1,6 +1,6 @@
 -- [E052] Type Error: tests/neg/i16655.scala:3:4 -----------------------------------------------------------------------
 3 |  x = 5 // error
   |  ^^^^^
-  |  Reassignment to val x
+  |  Assignment to value x
   |
   | longer explanation available when compiling with `-explain`
diff --git a/tests/neg/i20338c.check b/tests/neg/i20338c.check
index 1d19ec0b3042..fa8e05de4641 100644
--- a/tests/neg/i20338c.check
+++ b/tests/neg/i20338c.check
@@ -1,6 +1,6 @@
 -- [E052] Type Error: tests/neg/i20338c.scala:9:6 ----------------------------------------------------------------------
 9 |  f.x = 42 // error
   |  ^^^^^^^^
-  |  Reassignment to val x
+  |  Assignment to value x
   |
   | longer explanation available when compiling with `-explain`
diff --git a/tests/neg/i22671.check b/tests/neg/i22671.check
new file mode 100644
index 000000000000..0ea9768a0bb8
--- /dev/null
+++ b/tests/neg/i22671.check
@@ -0,0 +1,61 @@
+-- [E007] Type Mismatch Error: tests/neg/i22671.scala:41:22 ------------------------------------------------------------
+41 |    names_times(fields(0)) += fields(1).toLong // error
+   |                ^^^^^^^^^
+   |                Found:    Char
+   |                Required: String
+   |
+   | longer explanation available when compiling with `-explain`
+-- [E008] Not Found Error: tests/neg/i22671.scala:45:6 -----------------------------------------------------------------
+45 |  x() += "42" // error
+   |  ^^^^^^
+   |  value += is not a member of Int - did you mean Int.!=? or perhaps Int.<=?
+-- [E052] Type Error: tests/neg/i22671.scala:49:6 ----------------------------------------------------------------------
+49 |    c = 42 // error
+   |    ^^^^^^
+   |    Assignment to value c
+   |
+   | longer explanation available when compiling with `-explain`
+-- [E052] Type Error: tests/neg/i22671.scala:9:6 -----------------------------------------------------------------------
+9 |  X.w = 27 // error
+  |  ^^^^^^^^
+  |  Assignment to value w
+  |
+  | longer explanation available when compiling with `-explain`
+-- [E052] Type Error: tests/neg/i22671.scala:12:6 ----------------------------------------------------------------------
+12 |  X.x = 27 // error
+   |  ^^^^^^^^
+   |  Assignment to method x
+   |
+   | longer explanation available when compiling with `-explain`
+-- [E052] Type Error: tests/neg/i22671.scala:16:4 ----------------------------------------------------------------------
+16 |  x = 27 // error
+   |  ^^^^^^
+   |  Assignment to method x
+   |
+   | longer explanation available when compiling with `-explain`
+-- [E052] Type Error: tests/neg/i22671.scala:20:4 ----------------------------------------------------------------------
+20 |  y = 27 // error
+   |  ^^^^^^
+   |  Assignment to method x
+   |
+   | longer explanation available when compiling with `-explain`
+-- [E052] Type Error: tests/neg/i22671.scala:24:4 ----------------------------------------------------------------------
+24 |  y = 27 // error
+   |  ^^^^^^
+   |  Assignment to value z
+   |
+   | longer explanation available when compiling with `-explain`
+-- [E052] Type Error: tests/neg/i22671.scala:28:4 ----------------------------------------------------------------------
+28 |  x = 27 // error
+   |  ^^^^^^
+   |  Assignment to value x
+   |
+   | longer explanation available when compiling with `-explain`
+-- [E008] Not Found Error: tests/neg/i22671.scala:31:6 -----------------------------------------------------------------
+31 |  X.x += 27 // error
+   |  ^^^^^^
+   |  value += is not a member of Int - did you mean Int.!=? or perhaps Int.<=?
+-- [E008] Not Found Error: tests/neg/i22671.scala:32:4 -----------------------------------------------------------------
+32 |  1 += 1 // error
+   |  ^^^^
+   |  value += is not a member of Int - did you mean (1 : Int).!=? or perhaps (1 : Int).<=?
diff --git a/tests/neg/i22671.explain.check b/tests/neg/i22671.explain.check
new file mode 100644
index 000000000000..72ea32eddcf2
--- /dev/null
+++ b/tests/neg/i22671.explain.check
@@ -0,0 +1,94 @@
+-- [E052] Type Error: tests/neg/i22671.explain.scala:14:6 --------------------------------------------------------------
+14 |  X.w = 27 // error
+   |  ^^^^^^^^
+   |  Assignment to value w
+   |--------------------------------------------------------------------------------------------------------------------
+   | Explanation (enabled by `-explain`)
+   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+   | Members defined using `val` or `def` can't be assigned to.
+   | If you need to change the value of w, use `var` instead:
+   |   var w = ...
+   | However, it's more common to initialize a variable just once
+   | with a complex expression or even a block with many statements:
+   |   val w = if (condition) 1 else -1
+   | Also, assignment syntax can be used if there is a corresponding setter:
+   |   def w_=(x: Int): Unit = _v = x
+    --------------------------------------------------------------------------------------------------------------------
+-- [E052] Type Error: tests/neg/i22671.explain.scala:17:6 --------------------------------------------------------------
+17 |  X.x = 27 // error
+   |  ^^^^^^^^
+   |  Assignment to method x
+   |--------------------------------------------------------------------------------------------------------------------
+   | Explanation (enabled by `-explain`)
+   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+   | Members defined using `val` or `def` can't be assigned to.
+   | If you need to change the value of x, use `var` instead:
+   |   var x = ...
+   | However, it's more common to initialize a variable just once
+   | with a complex expression or even a block with many statements:
+   |   val x = if (condition) 1 else -1
+   | Also, assignment syntax can be used if there is a corresponding setter:
+   |   def x_=(x: Int): Unit = _v = x
+    --------------------------------------------------------------------------------------------------------------------
+-- [E052] Type Error: tests/neg/i22671.explain.scala:21:4 --------------------------------------------------------------
+21 |  y = 27 // error overload renamed
+   |  ^^^^^^
+   |  Assignment to method x
+   |--------------------------------------------------------------------------------------------------------------------
+   | Explanation (enabled by `-explain`)
+   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+   | Members defined using `val` or `def` can't be assigned to.
+   | If you need to change the value of x, use `var` instead:
+   |   var x = ...
+   | However, it's more common to initialize a variable just once
+   | with a complex expression or even a block with many statements:
+   |   val x = if (condition) 1 else -1
+   | Also, assignment syntax can be used if there is a corresponding setter:
+   |   def x_=(x: Int): Unit = _v = x
+    --------------------------------------------------------------------------------------------------------------------
+-- [E052] Type Error: tests/neg/i22671.explain.scala:25:4 --------------------------------------------------------------
+25 |  y = 27 // error val renamed
+   |  ^^^^^^
+   |  Assignment to value z
+   |--------------------------------------------------------------------------------------------------------------------
+   | Explanation (enabled by `-explain`)
+   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+   | Members defined using `val` or `def` can't be assigned to.
+   | If you need to change the value of z, use `var` instead:
+   |   var z = ...
+   | However, it's more common to initialize a variable just once
+   | with a complex expression or even a block with many statements:
+   |   val z = if (condition) 1 else -1
+   | Also, assignment syntax can be used if there is a corresponding setter:
+   |   def z_=(x: Int): Unit = _v = x
+    --------------------------------------------------------------------------------------------------------------------
+-- [E052] Type Error: tests/neg/i22671.explain.scala:29:4 --------------------------------------------------------------
+29 |  x = 27 // error local
+   |  ^^^^^^
+   |  Assignment to value x
+   |--------------------------------------------------------------------------------------------------------------------
+   | Explanation (enabled by `-explain`)
+   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+   | Members defined using `val` or `def` can't be assigned to.
+   | If you need to change the value of x, use `var` instead:
+   |   var x = ...
+   | However, it's more common to initialize a variable just once
+   | with a complex expression or even a block with many statements:
+   |   val x = if (condition) 1 else -1
+    --------------------------------------------------------------------------------------------------------------------
+-- [E052] Type Error: tests/neg/i22671.explain.scala:32:6 --------------------------------------------------------------
+32 |  t.t = t // error
+   |  ^^^^^^^
+   |  Assignment to method t
+   |--------------------------------------------------------------------------------------------------------------------
+   | Explanation (enabled by `-explain`)
+   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+   | Members defined using `val` or `def` can't be assigned to.
+   | If you need to change the value of t, use `var` instead:
+   |   var t = ...
+   | However, it's more common to initialize a variable just once
+   | with a complex expression or even a block with many statements:
+   |   val t = if (condition) 1 else -1
+   | Also, assignment syntax can be used if there is a corresponding setter:
+   |   def t_=(x: Int): Unit = _v = x
+    --------------------------------------------------------------------------------------------------------------------
diff --git a/tests/neg/i22671.explain.scala b/tests/neg/i22671.explain.scala
new file mode 100644
index 000000000000..1b78caf76326
--- /dev/null
+++ b/tests/neg/i22671.explain.scala
@@ -0,0 +1,32 @@
+//> using options -explain
+
+object X:
+  val w: Int = 42
+  def w(y: Int): Int = x + y
+  def x: Int = 42
+  def x(y: Int): Int = x + y
+  val z = 26
+
+trait T:
+  def t = 42
+
+def w =
+  X.w = 27 // error
+
+def f =
+  X.x = 27 // error
+
+def h =
+  import X.x as y
+  y = 27 // error overload renamed
+
+def i =
+  import X.z as y
+  y = 27 // error val renamed
+
+def j =
+  val x = 42
+  x = 27 // error local
+
+def t(t: T) =
+  t.t = t // error
diff --git a/tests/neg/i22671.scala b/tests/neg/i22671.scala
new file mode 100644
index 000000000000..afb04f4cea73
--- /dev/null
+++ b/tests/neg/i22671.scala
@@ -0,0 +1,49 @@
+object X:
+  val w: Int = 42
+  def w(y: Int): Int = x + y
+  def x: Int = 42
+  def x(y: Int): Int = x + y
+  val z = 26
+
+def w =
+  X.w = 27 // error
+
+def f =
+  X.x = 27 // error
+
+def g =
+  import X.x
+  x = 27 // error
+
+def h =
+  import X.x as y
+  y = 27 // error
+
+def i =
+  import X.z as y
+  y = 27 // error
+
+def j =
+  val x = 42
+  x = 27 // error
+
+def k =
+  X.x += 27 // error
+  1 += 1 // error
+
+
+object t8763:
+  import collection.mutable
+  def bar(): Unit =
+    val names_times = mutable.Map.empty[String, mutable.Set[Long]]
+    val line = ""
+    val Array(fields) = line.split("\t")
+    names_times(fields(0)) += fields(1).toLong // error
+
+object t9834:
+  object x { def apply() = 42 ; def update(i: Int) = () }
+  x() += "42" // error
+
+class C(c: Int):
+  def test(): Unit =
+    c = 42 // error