diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index 3dae20f5ce93..ab876722d5c0 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -445,6 +445,7 @@ object Trees { def forwardTo: Tree[T] = fun } + /** The kind of application */ enum ApplyKind: case Regular // r.f(x) case Using // r.f(using x) @@ -459,6 +460,9 @@ object Trees { putAttachment(untpd.KindOfApply, kind) this + /** The kind of this application. Works reliably only for untyped trees; typed trees + * are under no obligation to update it correctly. + */ def applyKind: ApplyKind = attachmentOrElse(untpd.KindOfApply, ApplyKind.Regular) } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index ded36b9fdaac..6217d0c06704 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -828,35 +828,63 @@ class Typer extends Namer // allow assignments from the primary constructor to class fields ctx.owner.name.is(TraitSetterName) || ctx.owner.isStaticConstructor - lhsCore.tpe match { - case ref: TermRef => - val lhsVal = lhsCore.denot.suchThat(!_.is(Method)) - if (canAssign(lhsVal.symbol)) { - // lhsBounds: (T .. Any) as seen from lhs prefix, where T is the type of lhsVal.symbol - // This ensures we do the as-seen-from on T with variance -1. Test case neg/i2928.scala - val lhsBounds = - TypeBounds.lower(lhsVal.symbol.info).asSeenFrom(ref.prefix, lhsVal.symbol.owner) - assignType(cpy.Assign(tree)(lhs1, typed(tree.rhs, lhsBounds.loBound))) - .computeAssignNullable() + lhsCore match + case Apply(fn, _) if fn.symbol.isExtensionMethod => + def toSetter(fn: Tree): untpd.Tree = fn match + case fn @ Ident(name: TermName) => + untpd.cpy.Ident(fn)(name.setterName) + case fn @ Select(qual, name: TermName) => + untpd.cpy.Select(fn)(untpd.TypedSplice(qual), name.setterName) + case fn @ TypeApply(fn1, targs) => + untpd.cpy.TypeApply(fn)(toSetter(fn1), targs.map(untpd.TypedSplice(_))) + case fn @ Apply(fn1, args) => + val result = untpd.cpy.Apply(fn)(toSetter(fn1), args.map(untpd.TypedSplice(_))) + fn1 match + case Apply(_, _) => // current apply is to implicit arguments + result.setApplyKind(ApplyKind.Using) + // Note that we cannot copy the apply kind of `fn` since `fn` is a typed + // tree and applyKinds are not preserved for those. + case _ => result + case _ => + EmptyTree + + val setter = toSetter(lhsCore) + if setter.isEmpty then reassignmentToVal + else tryEither { + val assign = untpd.Apply(setter, tree.rhs :: Nil) + typed(assign, IgnoredProto(pt)) + } { + (_, _) => reassignmentToVal } - else { - val pre = ref.prefix - val setterName = ref.name.setterName - val setter = pre.member(setterName) - lhsCore match { - case lhsCore: RefTree if setter.exists => - val setterTypeRaw = pre.select(setterName, setter) - val setterType = ensureAccessible(setterTypeRaw, isSuperSelection(lhsCore), tree.sourcePos) - val lhs2 = untpd.rename(lhsCore, setterName).withType(setterType) - typedUnadapted(untpd.Apply(untpd.TypedSplice(lhs2), tree.rhs :: Nil), WildcardType, locked) - case _ => - reassignmentToVal + case _ => lhsCore.tpe match { + case ref: TermRef => + val lhsVal = lhsCore.denot.suchThat(!_.is(Method)) + if (canAssign(lhsVal.symbol)) { + // lhsBounds: (T .. Any) as seen from lhs prefix, where T is the type of lhsVal.symbol + // This ensures we do the as-seen-from on T with variance -1. Test case neg/i2928.scala + val lhsBounds = + TypeBounds.lower(lhsVal.symbol.info).asSeenFrom(ref.prefix, lhsVal.symbol.owner) + assignType(cpy.Assign(tree)(lhs1, typed(tree.rhs, lhsBounds.loBound))) + .computeAssignNullable() } - } - case TryDynamicCallType => - typedDynamicAssign(tree, pt) - case tpe => - reassignmentToVal + else { + val pre = ref.prefix + val setterName = ref.name.setterName + val setter = pre.member(setterName) + lhsCore match { + case lhsCore: RefTree if setter.exists => + val setterTypeRaw = pre.select(setterName, setter) + val setterType = ensureAccessible(setterTypeRaw, isSuperSelection(lhsCore), tree.sourcePos) + val lhs2 = untpd.rename(lhsCore, setterName).withType(setterType) + typedUnadapted(untpd.Apply(untpd.TypedSplice(lhs2), tree.rhs :: Nil), WildcardType, locked) + case _ => + reassignmentToVal + } + } + case TryDynamicCallType => + typedDynamicAssign(tree, pt) + case tpe => + reassignmentToVal } } diff --git a/tests/pos/i5588a.scala b/tests/pos/i5588a.scala new file mode 100644 index 000000000000..06aeb3c8d9e1 --- /dev/null +++ b/tests/pos/i5588a.scala @@ -0,0 +1,22 @@ +object TestMain { + def main(args: Array[String]): Unit = { + testExtensionProperty() + } + + case class Foo(var foo: String) + + object fooExt { + // define `Foo`-extension property with getter and setter delegating to `Foo.foo` + extension (thisFoo: Foo) def fooExt: String = thisFoo.foo + extension (thisFoo: Foo) def fooExt_= (value: String): Unit = { thisFoo.foo = value } + } + + def testExtensionProperty(): Unit = { + import fooExt._ + val foo = Foo("initVal") + assert(foo.fooExt == "initVal") + foo.fooExt = "updatedVal" + assert(foo.foo == "updatedVal") + assert(foo.fooExt == "updatedVal") + } +} diff --git a/tests/pos/i5588b.scala b/tests/pos/i5588b.scala new file mode 100644 index 000000000000..97940677f5d7 --- /dev/null +++ b/tests/pos/i5588b.scala @@ -0,0 +1,22 @@ +object TestMain2 { + def main(args: Array[String]): Unit = { + testExtensionProperty() + } + + case class Foo(var foo: String) + + implicit object fooExt { + // define `Foo`-extension property with getter and setter delegating to `Foo.foo` + extension (thisFoo: Foo) def fooExt: String = thisFoo.foo + extension (thisFoo: Foo) def fooExt_= (value: String): Unit = { thisFoo.foo = value } + } + + def testExtensionProperty(): Unit = { + //import fooExt._ + val foo = Foo("initVal") + assert(foo.fooExt == "initVal") + foo.fooExt = "updatedVal" + assert(foo.foo == "updatedVal") + assert(foo.fooExt == "updatedVal") + } +} diff --git a/tests/pos/i9248.scala b/tests/pos/i9248.scala index edbd23552a36..163ce207d5e0 100644 --- a/tests/pos/i9248.scala +++ b/tests/pos/i9248.scala @@ -5,4 +5,20 @@ extension (c: C) def foo_=(a: Int): Unit = c.a = a val c = C(10) -val test = c.foo +val c1 = c.foo = 11 + +given C = C(0) + +// Harder case: extensions defined in local scope, with type parameters and implicits +def test = + class D[T](var a: T) + + extension [T](d: D[T])(using C) + def foo: T = d.a + def foo_=(a: T): Unit = + val c = summon[C] + d.a = a + + val d = D(10) + d.foo + d.foo = 11