Skip to content

Commit 44efd68

Browse files
authored
Merge pull request #9552 from dotty-staging/fix-#9248-2
Fix #9248: Handle extension setters
2 parents 8c77b94 + c709511 commit 44efd68

File tree

5 files changed

+120
-28
lines changed

5 files changed

+120
-28
lines changed

compiler/src/dotty/tools/dotc/ast/Trees.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ object Trees {
445445
def forwardTo: Tree[T] = fun
446446
}
447447

448+
/** The kind of application */
448449
enum ApplyKind:
449450
case Regular // r.f(x)
450451
case Using // r.f(using x)
@@ -459,6 +460,9 @@ object Trees {
459460
putAttachment(untpd.KindOfApply, kind)
460461
this
461462

463+
/** The kind of this application. Works reliably only for untyped trees; typed trees
464+
* are under no obligation to update it correctly.
465+
*/
462466
def applyKind: ApplyKind =
463467
attachmentOrElse(untpd.KindOfApply, ApplyKind.Regular)
464468
}

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -828,35 +828,63 @@ class Typer extends Namer
828828
// allow assignments from the primary constructor to class fields
829829
ctx.owner.name.is(TraitSetterName) || ctx.owner.isStaticConstructor
830830

831-
lhsCore.tpe match {
832-
case ref: TermRef =>
833-
val lhsVal = lhsCore.denot.suchThat(!_.is(Method))
834-
if (canAssign(lhsVal.symbol)) {
835-
// lhsBounds: (T .. Any) as seen from lhs prefix, where T is the type of lhsVal.symbol
836-
// This ensures we do the as-seen-from on T with variance -1. Test case neg/i2928.scala
837-
val lhsBounds =
838-
TypeBounds.lower(lhsVal.symbol.info).asSeenFrom(ref.prefix, lhsVal.symbol.owner)
839-
assignType(cpy.Assign(tree)(lhs1, typed(tree.rhs, lhsBounds.loBound)))
840-
.computeAssignNullable()
831+
lhsCore match
832+
case Apply(fn, _) if fn.symbol.isExtensionMethod =>
833+
def toSetter(fn: Tree): untpd.Tree = fn match
834+
case fn @ Ident(name: TermName) =>
835+
untpd.cpy.Ident(fn)(name.setterName)
836+
case fn @ Select(qual, name: TermName) =>
837+
untpd.cpy.Select(fn)(untpd.TypedSplice(qual), name.setterName)
838+
case fn @ TypeApply(fn1, targs) =>
839+
untpd.cpy.TypeApply(fn)(toSetter(fn1), targs.map(untpd.TypedSplice(_)))
840+
case fn @ Apply(fn1, args) =>
841+
val result = untpd.cpy.Apply(fn)(toSetter(fn1), args.map(untpd.TypedSplice(_)))
842+
fn1 match
843+
case Apply(_, _) => // current apply is to implicit arguments
844+
result.setApplyKind(ApplyKind.Using)
845+
// Note that we cannot copy the apply kind of `fn` since `fn` is a typed
846+
// tree and applyKinds are not preserved for those.
847+
case _ => result
848+
case _ =>
849+
EmptyTree
850+
851+
val setter = toSetter(lhsCore)
852+
if setter.isEmpty then reassignmentToVal
853+
else tryEither {
854+
val assign = untpd.Apply(setter, tree.rhs :: Nil)
855+
typed(assign, IgnoredProto(pt))
856+
} {
857+
(_, _) => reassignmentToVal
841858
}
842-
else {
843-
val pre = ref.prefix
844-
val setterName = ref.name.setterName
845-
val setter = pre.member(setterName)
846-
lhsCore match {
847-
case lhsCore: RefTree if setter.exists =>
848-
val setterTypeRaw = pre.select(setterName, setter)
849-
val setterType = ensureAccessible(setterTypeRaw, isSuperSelection(lhsCore), tree.sourcePos)
850-
val lhs2 = untpd.rename(lhsCore, setterName).withType(setterType)
851-
typedUnadapted(untpd.Apply(untpd.TypedSplice(lhs2), tree.rhs :: Nil), WildcardType, locked)
852-
case _ =>
853-
reassignmentToVal
859+
case _ => lhsCore.tpe match {
860+
case ref: TermRef =>
861+
val lhsVal = lhsCore.denot.suchThat(!_.is(Method))
862+
if (canAssign(lhsVal.symbol)) {
863+
// lhsBounds: (T .. Any) as seen from lhs prefix, where T is the type of lhsVal.symbol
864+
// This ensures we do the as-seen-from on T with variance -1. Test case neg/i2928.scala
865+
val lhsBounds =
866+
TypeBounds.lower(lhsVal.symbol.info).asSeenFrom(ref.prefix, lhsVal.symbol.owner)
867+
assignType(cpy.Assign(tree)(lhs1, typed(tree.rhs, lhsBounds.loBound)))
868+
.computeAssignNullable()
854869
}
855-
}
856-
case TryDynamicCallType =>
857-
typedDynamicAssign(tree, pt)
858-
case tpe =>
859-
reassignmentToVal
870+
else {
871+
val pre = ref.prefix
872+
val setterName = ref.name.setterName
873+
val setter = pre.member(setterName)
874+
lhsCore match {
875+
case lhsCore: RefTree if setter.exists =>
876+
val setterTypeRaw = pre.select(setterName, setter)
877+
val setterType = ensureAccessible(setterTypeRaw, isSuperSelection(lhsCore), tree.sourcePos)
878+
val lhs2 = untpd.rename(lhsCore, setterName).withType(setterType)
879+
typedUnadapted(untpd.Apply(untpd.TypedSplice(lhs2), tree.rhs :: Nil), WildcardType, locked)
880+
case _ =>
881+
reassignmentToVal
882+
}
883+
}
884+
case TryDynamicCallType =>
885+
typedDynamicAssign(tree, pt)
886+
case tpe =>
887+
reassignmentToVal
860888
}
861889
}
862890

tests/pos/i5588a.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
object TestMain {
2+
def main(args: Array[String]): Unit = {
3+
testExtensionProperty()
4+
}
5+
6+
case class Foo(var foo: String)
7+
8+
object fooExt {
9+
// define `Foo`-extension property with getter and setter delegating to `Foo.foo`
10+
extension (thisFoo: Foo) def fooExt: String = thisFoo.foo
11+
extension (thisFoo: Foo) def fooExt_= (value: String): Unit = { thisFoo.foo = value }
12+
}
13+
14+
def testExtensionProperty(): Unit = {
15+
import fooExt._
16+
val foo = Foo("initVal")
17+
assert(foo.fooExt == "initVal")
18+
foo.fooExt = "updatedVal"
19+
assert(foo.foo == "updatedVal")
20+
assert(foo.fooExt == "updatedVal")
21+
}
22+
}

tests/pos/i5588b.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
object TestMain2 {
2+
def main(args: Array[String]): Unit = {
3+
testExtensionProperty()
4+
}
5+
6+
case class Foo(var foo: String)
7+
8+
implicit object fooExt {
9+
// define `Foo`-extension property with getter and setter delegating to `Foo.foo`
10+
extension (thisFoo: Foo) def fooExt: String = thisFoo.foo
11+
extension (thisFoo: Foo) def fooExt_= (value: String): Unit = { thisFoo.foo = value }
12+
}
13+
14+
def testExtensionProperty(): Unit = {
15+
//import fooExt._
16+
val foo = Foo("initVal")
17+
assert(foo.fooExt == "initVal")
18+
foo.fooExt = "updatedVal"
19+
assert(foo.foo == "updatedVal")
20+
assert(foo.fooExt == "updatedVal")
21+
}
22+
}

tests/pos/i9248.scala

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,20 @@ extension (c: C)
55
def foo_=(a: Int): Unit = c.a = a
66

77
val c = C(10)
8-
val test = c.foo
8+
val c1 = c.foo = 11
9+
10+
given C = C(0)
11+
12+
// Harder case: extensions defined in local scope, with type parameters and implicits
13+
def test =
14+
class D[T](var a: T)
15+
16+
extension [T](d: D[T])(using C)
17+
def foo: T = d.a
18+
def foo_=(a: T): Unit =
19+
val c = summon[C]
20+
d.a = a
21+
22+
val d = D(10)
23+
d.foo
24+
d.foo = 11

0 commit comments

Comments
 (0)