Skip to content

Fix #9248: Handle extension setters #9552

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/Trees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down
82 changes: 55 additions & 27 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
22 changes: 22 additions & 0 deletions tests/pos/i5588a.scala
Original file line number Diff line number Diff line change
@@ -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")
}
}
22 changes: 22 additions & 0 deletions tests/pos/i5588b.scala
Original file line number Diff line number Diff line change
@@ -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")
}
}
18 changes: 17 additions & 1 deletion tests/pos/i9248.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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