Skip to content

Commit 14cc28f

Browse files
committed
Disallow direct unqualified mentions of extension methods.
Allow only qualified references `p.f` to an extension method `f`.
1 parent 300e9a2 commit 14cc28f

File tree

16 files changed

+111
-81
lines changed

16 files changed

+111
-81
lines changed

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,20 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
2424
}
2525

2626
/** A typed subtree of an untyped tree needs to be wrapped in a TypedSplice
27-
* @param owner The current owner at the time the tree was defined
27+
* @param owner The current owner at the time the tree was defined
28+
* @param isExtensionReceiver The splice was created from the receiver `e` in an extension
29+
* method call `e.f(...)`
2830
*/
29-
abstract case class TypedSplice(splice: tpd.Tree)(val owner: Symbol)(implicit @constructorOnly src: SourceFile) extends ProxyTree {
31+
abstract case class TypedSplice(splice: tpd.Tree)(val owner: Symbol, val isExtensionReceiver: Boolean)(implicit @constructorOnly src: SourceFile) extends ProxyTree {
3032
def forwardTo: tpd.Tree = splice
33+
override def toString =
34+
def ext = if isExtensionReceiver then ", isExtensionReceiver = true" else ""
35+
s"TypedSplice($splice$ext)"
3136
}
3237

3338
object TypedSplice {
34-
def apply(tree: tpd.Tree)(using Context): TypedSplice =
35-
new TypedSplice(tree)(ctx.owner) {}
39+
def apply(tree: tpd.Tree, isExtensionReceiver: Boolean = false)(using Context): TypedSplice =
40+
new TypedSplice(tree)(ctx.owner, isExtensionReceiver) {}
3641
}
3742

3843
/** mods object name impl */

compiler/src/dotty/tools/dotc/core/Denotations.scala

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,11 @@ object Denotations {
257257
def checkUnique(using Context): SingleDenotation = suchThat(alwaysTrue)
258258

259259
/** Does this denotation have an alternative that satisfies the predicate `p`? */
260-
def hasAltWith(p: SingleDenotation => Boolean): Boolean
260+
inline def hasAltWith(inline p: SingleDenotation => Boolean): Boolean =
261+
def p1(x: SingleDenotation) = p(x)
262+
this match
263+
case d: SingleDenotation => exists && p1(d)
264+
case _ => filterWithPredicate(p1).exists
261265

262266
/** The denotation made up from the alternatives of this denotation that
263267
* are accessible from prefix `pre`, or NoDenotation if no accessible alternative exists.
@@ -604,9 +608,6 @@ object Denotations {
604608
def suchThat(p: Symbol => Boolean)(using Context): SingleDenotation =
605609
if (exists && p(symbol)) this else NoDenotation
606610

607-
def hasAltWith(p: SingleDenotation => Boolean): Boolean =
608-
exists && p(this)
609-
610611
def accessibleFrom(pre: Type, superAccess: Boolean)(using Context): Denotation =
611612
if (!symbol.exists || symbol.isAccessibleFrom(pre, superAccess)) this else NoDenotation
612613

@@ -1185,8 +1186,6 @@ object Denotations {
11851186
}
11861187
override def filterWithPredicate(p: SingleDenotation => Boolean): Denotation =
11871188
derivedUnionDenotation(denot1.filterWithPredicate(p), denot2.filterWithPredicate(p))
1188-
def hasAltWith(p: SingleDenotation => Boolean): Boolean =
1189-
denot1.hasAltWith(p) || denot2.hasAltWith(p)
11901189
def accessibleFrom(pre: Type, superAccess: Boolean)(using Context): Denotation = {
11911190
val d1 = denot1 accessibleFrom (pre, superAccess)
11921191
val d2 = denot2 accessibleFrom (pre, superAccess)

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2162,7 +2162,9 @@ trait Applications extends Compatibility {
21622162

21632163
val (core, pt1) = normalizePt(methodRef, pt)
21642164
val app = withMode(Mode.SynthesizeExtMethodReceiver) {
2165-
typed(untpd.Apply(core, untpd.TypedSplice(receiver) :: Nil), pt1, ctx.typerState.ownedVars)
2165+
typed(
2166+
untpd.Apply(core, untpd.TypedSplice(receiver, isExtensionReceiver = true) :: Nil),
2167+
pt1, ctx.typerState.ownedVars)
21662168
}
21672169
def isExtension(tree: Tree): Boolean = methPart(tree) match {
21682170
case Inlined(call, _, _) => isExtension(call)

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,12 @@ object ProtoTypes {
517517
/** A prototype for type constructors that are followed by a type application */
518518
@sharable object AnyTypeConstructorProto extends UncachedGroundType with MatchAlways
519519

520+
extension (pt: Type)
521+
def isExtensionApplyProto: Boolean = pt match
522+
case PolyProto(targs, res) => res.isExtensionApplyProto
523+
case FunProto((arg: untpd.TypedSplice) :: Nil, _) => arg.isExtensionReceiver
524+
case _ => false
525+
520526
/** Add all parameters of given type lambda `tl` to the constraint's domain.
521527
* If the constraint contains already some of these parameters in its domain,
522528
* make a copy of the type lambda and add the copy's type parameters instead.

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

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -450,27 +450,30 @@ class Typer extends Namer
450450
if (name == nme.ROOTPKG)
451451
return tree.withType(defn.RootPackage.termRef)
452452

453-
val rawType = {
453+
val rawType =
454454
val saved1 = unimported
455455
val saved2 = foundUnderScala2
456456
unimported = Set.empty
457457
foundUnderScala2 = NoType
458-
try {
459-
var found = findRef(name, pt, EmptyFlags, tree.srcPos)
460-
if (foundUnderScala2.exists && !(foundUnderScala2 =:= found)) {
458+
try
459+
val found = findRef(name, pt, EmptyFlags, tree.srcPos)
460+
if foundUnderScala2.exists && !(foundUnderScala2 =:= found) then
461461
report.migrationWarning(
462462
ex"""Name resolution will change.
463463
| currently selected : $foundUnderScala2
464464
| in the future, without -source 3.0-migration: $found""", tree.srcPos)
465-
found = foundUnderScala2
466-
}
467-
found
468-
}
469-
finally {
465+
foundUnderScala2
466+
else
467+
found match
468+
case found: TermRef
469+
if !found.denot.hasAltWith(!_.symbol.is(ExtensionMethod))
470+
&& !pt.isExtensionApplyProto =>
471+
NoType // direct calls to extension methods need a prefix
472+
case _ =>
473+
found
474+
finally
470475
unimported = saved1
471476
foundUnderScala2 = saved2
472-
}
473-
}
474477

475478
def setType(ownType: Type): Tree =
476479
val tree1 = ownType match
@@ -842,7 +845,9 @@ class Typer extends Namer
842845
case fn @ TypeApply(fn1, targs) =>
843846
untpd.cpy.TypeApply(fn)(toSetter(fn1), targs.map(untpd.TypedSplice(_)))
844847
case fn @ Apply(fn1, args) =>
845-
val result = untpd.cpy.Apply(fn)(toSetter(fn1), args.map(untpd.TypedSplice(_)))
848+
val result = untpd.cpy.Apply(fn)(
849+
toSetter(fn1),
850+
args.map(untpd.TypedSplice(_, isExtensionReceiver = true)))
846851
fn1 match
847852
case Apply(_, _) => // current apply is to implicit arguments
848853
result.setApplyKind(ApplyKind.Using)
@@ -2939,7 +2944,15 @@ class Typer extends Namer
29392944
}
29402945

29412946
def adaptOverloaded(ref: TermRef) = {
2942-
val altDenots = ref.denot.alternatives
2947+
val altDenots =
2948+
val allDenots = ref.denot.alternatives
2949+
def isIdent = tree match
2950+
case _: Ident => true
2951+
case Select(qual, name) => qual.span.isZeroExtent
2952+
case _ => false
2953+
if pt.isExtensionApplyProto then allDenots.filter(_.symbol.is(ExtensionMethod))
2954+
else if isIdent then allDenots.filterNot(_.symbol.is(ExtensionMethod))
2955+
else allDenots
29432956
typr.println(i"adapt overloaded $ref with alternatives ${altDenots map (_.info)}%\n\n %")
29442957
def altRef(alt: SingleDenotation) = TermRef(ref.prefix, ref.name, alt)
29452958
val alts = altDenots.map(altRef)

tests/neg/i6183.check

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
-- [E008] Not Found Error: tests/neg/i6183.scala:5:5 -------------------------------------------------------------------
2-
5 | 42.render // error
3-
| ^^^^^^^^^
4-
| value render is not a member of Int.
5-
| An extension method was tried, but could not be fully constructed:
1+
-- [E008] Not Found Error: tests/neg/i6183.scala:6:7 -------------------------------------------------------------------
2+
6 | 42.render // error
3+
| ^^^^^^^^^
4+
| value render is not a member of Int.
5+
| An extension method was tried, but could not be fully constructed:
66
|
7-
| extension_render(42)
8-
-- [E051] Reference Error: tests/neg/i6183.scala:6:2 -------------------------------------------------------------------
9-
6 | extension_render(42) // error
10-
| ^^^^^^^^^^^^^^^^
11-
| Ambiguous overload. The overloaded alternatives of method extension_render with types
12-
| [B](b: B)(using x$1: DummyImplicit): Char
13-
| [A](a: A): String
14-
| both match arguments ((42 : Int))
7+
| extension_render(42)
8+
-- [E051] Reference Error: tests/neg/i6183.scala:7:9 -------------------------------------------------------------------
9+
7 | Test.extension_render(42) // error
10+
| ^^^^^^^^^^^^^^^^^^^^^
11+
| Ambiguous overload. The overloaded alternatives of method extension_render in object Test with types
12+
| [B](b: B)(using x$1: DummyImplicit): Char
13+
| [A](a: A): String
14+
| both match arguments ((42 : Int))
1515

1616
longer explanation available when compiling with `-explain`

tests/neg/i6183.scala

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
extension [A](a: A) def render: String = "Hi"
2-
extension [B](b: B) def render(using DummyImplicit): Char = 'x'
1+
object Test:
2+
extension [A](a: A) def render: String = "Hi"
3+
extension [B](b: B) def render(using DummyImplicit): Char = 'x'
34

4-
val test = {
5-
42.render // error
6-
extension_render(42) // error
7-
}
5+
val test = {
6+
42.render // error
7+
Test.extension_render(42) // error
8+
}

tests/neg/i6779.check

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
-- [E007] Type Mismatch Error: tests/neg/i6779.scala:9:30 --------------------------------------------------------------
2-
9 |def g1[T](x: T): F[G[T]] = x.f(using summon[Stuff]) // error
3-
| ^^^^^^^^^^^^^^^^^^^^^^^^
4-
| Found: F[T]
5-
| Required: F[G[T]]
6-
-- [E007] Type Mismatch Error: tests/neg/i6779.scala:11:29 -------------------------------------------------------------
7-
11 |def g2[T](x: T): F[G[T]] = x.f // error
8-
| ^^^
9-
| Found: F[T]
10-
| Required: F[G[T]]
11-
-- [E007] Type Mismatch Error: tests/neg/i6779.scala:13:41 -------------------------------------------------------------
12-
13 |def g3[T](x: T): F[G[T]] = extension_f(x)(using summon[Stuff]) // error
13-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14-
| Found: F[T]
15-
| Required: F[G[T]]
1+
-- [E007] Type Mismatch Error: tests/neg/i6779.scala:10:32 -------------------------------------------------------------
2+
10 | def g1[T](x: T): F[G[T]] = x.f(using summon[Stuff]) // error
3+
| ^^^^^^^^^^^^^^^^^^^^^^^^
4+
| Found: F[T]
5+
| Required: F[G[T]]
6+
-- [E007] Type Mismatch Error: tests/neg/i6779.scala:12:31 -------------------------------------------------------------
7+
12 | def g2[T](x: T): F[G[T]] = x.f // error
8+
| ^^^
9+
| Found: F[T]
10+
| Required: F[G[T]]
11+
-- [E007] Type Mismatch Error: tests/neg/i6779.scala:14:48 -------------------------------------------------------------
12+
14 | def g3[T](x: T): F[G[T]] = this.extension_f(x)(using summon[Stuff]) // error
13+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14+
| Found: F[T]
15+
| Required: F[G[T]]

tests/neg/i6779.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ type G[T]
33
type Stuff
44
given Stuff = ???
55

6-
extension [T](x: T) def f(using Stuff): F[T] = ???
6+
object Test:
7+
extension [T](x: T) def f(using Stuff): F[T] = ???
78

89

9-
def g1[T](x: T): F[G[T]] = x.f(using summon[Stuff]) // error
10+
def g1[T](x: T): F[G[T]] = x.f(using summon[Stuff]) // error
1011

11-
def g2[T](x: T): F[G[T]] = x.f // error
12+
def g2[T](x: T): F[G[T]] = x.f // error
1213

13-
def g3[T](x: T): F[G[T]] = extension_f(x)(using summon[Stuff]) // error
14+
def g3[T](x: T): F[G[T]] = this.extension_f(x)(using summon[Stuff]) // error

tests/neg/i9185.check

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
| M.extension_pure[A, F]("ola")(
88
| /* ambiguous: both object listMonad in object M and object optionMonad in object M match type M[F] */summon[M[F]]
99
| )
10-
-- Error: tests/neg/i9185.scala:8:36 -----------------------------------------------------------------------------------
11-
8 | val value3 = extension_pure("ola") // error
12-
| ^
10+
-- Error: tests/neg/i9185.scala:8:38 -----------------------------------------------------------------------------------
11+
8 | val value3 = M.extension_pure("ola") // error
12+
| ^
1313
|ambiguous implicit arguments: both object listMonad in object M and object optionMonad in object M match type M[F] of parameter m of method extension_pure in object M
1414
-- [E008] Not Found Error: tests/neg/i9185.scala:11:16 -----------------------------------------------------------------
1515
11 | val l = "abc".len // error

tests/neg/i9185.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ object M {
55
given optionMonad as M[Option] { def pure[A](x: A): Option[A] = Some(x) }
66
val value1: List[String] = "ola".pure
77
val value2 = "ola".pure // error
8-
val value3 = extension_pure("ola") // error
8+
val value3 = M.extension_pure("ola") // error
99

1010
extension (x: Int) def len: Int = x
1111
val l = "abc".len // error

tests/pos/i7401.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ object Test {
77
x.foo(4)
88
x.foo
99
Test.extension_foo(x)(4)
10-
extension_foo(x)
10+
this.extension_foo(x)
1111
}

tests/pos/reference/extension-methods.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ object ExtMethods:
99

1010
val circle = Circle(0, 0, 1)
1111
circle.circumference
12-
assert(circle.circumference == extension_circumference(circle))
12+
assert(circle.circumference == this.extension_circumference(circle))
1313

1414
extension (x: String) def < (y: String) = x.compareTo(y) < 0
1515
extension [Elem](x: Elem) def #: (xs: Seq[Elem]) = x +: xs
@@ -118,6 +118,6 @@ object ExtMethods:
118118
if exponent == 0 then 1 else x * (x ** (exponent - 1))
119119

120120
import DoubleOps.{**, extension_**}
121-
assert(2.0 ** 3 == extension_**(2.0)(3))
121+
assert(2.0 ** 3 == DoubleOps.extension_**(2.0)(3))
122122

123123
end ExtMethods

tests/run-macros/i6201/macro_1.scala

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import scala.quoted._
22

3-
extension (inline x: String) inline def strip: String =
4-
${ stripImpl('x) }
3+
object M:
54

6-
def stripImpl(x: Expr[String])(using qctx: QuoteContext) : Expr[String] =
7-
Expr(x.unliftOrError.stripMargin)
5+
extension (inline x: String) inline def strip: String =
6+
${ stripImpl('x) }
87

9-
inline def isHello(inline x: String): Boolean =
10-
${ isHelloImpl('x) }
8+
def stripImpl(x: Expr[String])(using qctx: QuoteContext) : Expr[String] =
9+
Expr(x.unliftOrError.stripMargin)
1110

12-
def isHelloImpl(x: Expr[String])(using qctx: QuoteContext) : Expr[Boolean] =
13-
if (x.unliftOrError == "hello") Expr(true) else Expr(false)
11+
inline def isHello(inline x: String): Boolean =
12+
${ isHelloImpl('x) }
13+
14+
def isHelloImpl(x: Expr[String])(using qctx: QuoteContext) : Expr[Boolean] =
15+
if (x.unliftOrError == "hello") Expr(true) else Expr(false)

tests/run-macros/i6201/test_2.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
object Test {
2+
import M._
23
def main(args: Array[String]): Unit = {
3-
assert(isHello(extension_strip("hello")))
4-
assert(!isHello(extension_strip("bonjour")))
4+
assert(isHello(M.extension_strip("hello")))
5+
assert(!isHello(M.extension_strip("bonjour")))
56
}
67
}

tests/run/extension-methods.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ object Test extends App {
22

33
extension (x: Int) def em: Boolean = x > 0
44

5-
assert(1.em == extension_em(1))
5+
assert(1.em == this.extension_em(1))
66

77
case class Circle(x: Double, y: Double, radius: Double)
88

99
extension (c: Circle) def circumference: Double = c.radius * math.Pi * 2
1010

1111
val circle = new Circle(1, 1, 2.0)
1212

13-
assert(circle.circumference == extension_circumference(circle))
13+
assert(circle.circumference == this.extension_circumference(circle))
1414

1515
extension (xs: Seq[String]) def longestStrings: Seq[String] = {
1616
val maxLength = xs.map(_.length).max

0 commit comments

Comments
 (0)