Skip to content

Commit eb44b34

Browse files
authored
Merge pull request #10902 from dotty-staging/fix-#10810
Better error diagnostics and other fixes for extension methods
2 parents 665a268 + 8324d4b commit eb44b34

21 files changed

+268
-80
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ object Decorators {
5656
termName(chars, 0, len)
5757
case name: TypeName => s.concat(name.toTermName)
5858
case _ => termName(s.concat(name.toString))
59+
60+
def indented(width: Int): String =
61+
val padding = " " * width
62+
padding + s.replace("\n", "\n" + padding)
5963
end extension
6064

6165
/** Implements a findSymbol method on iterators of Symbols that

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

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -137,21 +137,6 @@ object NameOps {
137137
else name.toTermName
138138
}
139139

140-
/** Does the name match `extension`? */
141-
def isExtension: Boolean = name match
142-
case name: SimpleName =>
143-
name.length == "extension".length && name.startsWith("extension")
144-
case _ => false
145-
146-
/** Does this name start with `extension_`? */
147-
def isExtensionName: Boolean = name match
148-
case name: SimpleName => name.startsWith("extension_")
149-
case _ => false
150-
151-
// TODO: Drop next 3 methods once extension names have stabilized
152-
/** Add an `extension_` in front of this name */
153-
def toExtensionName(using Context): SimpleName = "extension_".concat(name)
154-
155140
/** The expanded name.
156141
* This is the fully qualified name of `base` with `ExpandPrefixName` as separator,
157142
* followed by `kind` and the name.

compiler/src/dotty/tools/dotc/transform/PostTyper.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,8 +383,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
383383

384384
def checkIdent(sel: untpd.ImportSelector): Unit =
385385
if !exprTpe.member(sel.name).exists
386-
&& !exprTpe.member(sel.name.toTypeName).exists
387-
&& !exprTpe.member(sel.name.toExtensionName).exists then
386+
&& !exprTpe.member(sel.name.toTypeName).exists then
388387
report.error(NotAMember(exprTpe, sel.name, "value"), sel.imported.srcPos)
389388
if seen.contains(sel.name) then
390389
report.error(ImportRenamedTwice(sel.imported), sel.imported.srcPos)

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2153,19 +2153,11 @@ trait Applications extends Compatibility {
21532153
(tree, currentPt)
21542154

21552155
val (core, pt1) = normalizePt(methodRef, pt)
2156-
val app = withMode(Mode.SynthesizeExtMethodReceiver) {
2156+
withMode(Mode.SynthesizeExtMethodReceiver) {
21572157
typed(
21582158
untpd.Apply(core, untpd.TypedSplice(receiver, isExtensionReceiver = true) :: Nil),
21592159
pt1, ctx.typerState.ownedVars)
21602160
}
2161-
def isExtension(tree: Tree): Boolean = methPart(tree) match {
2162-
case Inlined(call, _, _) => isExtension(call)
2163-
case tree @ Select(qual, nme.apply) => tree.symbol.is(ExtensionMethod) || isExtension(qual)
2164-
case tree => tree.symbol.is(ExtensionMethod)
2165-
}
2166-
if (!isExtension(app))
2167-
report.error(em"not an extension method: $methodRef", receiver.srcPos)
2168-
app
21692161
}
21702162

21712163
def isApplicableExtensionMethod(ref: TermRef, receiver: Type)(using Context) =

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

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import util.SrcPos
1313
import config.Feature
1414
import java.util.regex.Matcher.quoteReplacement
1515
import reporting._
16+
import collection.mutable
1617

1718
import scala.util.matching.Regex
1819

@@ -133,26 +134,47 @@ object ErrorReporting {
133134
if Feature.migrateTo3 then "\nThis patch can be inserted automatically under -rewrite."
134135
else ""
135136

137+
def whyFailedStr(fail: FailedExtension) =
138+
i""" failed with
139+
|
140+
|${fail.whyFailed.message.indented(8)}"""
141+
136142
def selectErrorAddendum
137143
(tree: untpd.RefTree, qual1: Tree, qualType: Type, suggestImports: Type => String)
138144
(using Context): String =
139-
val attempts: List[Tree] = qual1.getAttachment(Typer.HiddenSearchFailure) match
140-
case Some(failures) =>
141-
for failure <- failures
142-
if !failure.reason.isInstanceOf[Implicits.NoMatchingImplicits]
143-
yield failure.tree
144-
case _ => Nil
145+
146+
val attempts = mutable.ListBuffer[(Tree, String)]()
147+
val nested = mutable.ListBuffer[NestedFailure]()
148+
for
149+
failures <- qual1.getAttachment(Typer.HiddenSearchFailure)
150+
failure <- failures
151+
do
152+
failure.reason match
153+
case fail: NestedFailure => nested += fail
154+
case fail: FailedExtension => attempts += ((failure.tree, whyFailedStr(fail)))
155+
case fail: Implicits.NoMatchingImplicits => // do nothing
156+
case _ => attempts += ((failure.tree, ""))
145157
if qualType.derivesFrom(defn.DynamicClass) then
146158
"\npossible cause: maybe a wrong Dynamic method signature?"
147159
else if attempts.nonEmpty then
148-
val attemptStrings = attempts.map(_.showIndented(4)).distinct
160+
val attemptStrings =
161+
attempts.toList
162+
.map((tree, whyFailed) => (tree.showIndented(4), whyFailed))
163+
.distinctBy(_._1)
164+
.map((treeStr, whyFailed) =>
165+
i"""
166+
| $treeStr$whyFailed""")
149167
val extMethods =
150168
if attemptStrings.length > 1 then "Extension methods were"
151169
else "An extension method was"
152170
i""".
153171
|$extMethods tried, but could not be fully constructed:
172+
|$attemptStrings%\n%"""
173+
else if nested.nonEmpty then
174+
i""".
175+
|Extension methods were tried, but the search failed with:
154176
|
155-
| $attemptStrings%\nor\n %"""
177+
| ${nested.head.explanation}"""
156178
else if tree.hasAttachment(desugar.MultiLineInfix) then
157179
i""".
158180
|Note that `${tree.name}` is treated as an infix operator in Scala 3.

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

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ object Implicits:
7676
*/
7777
def hasExtMethod(tp: Type, expected: Type)(using Context) = expected match
7878
case selProto @ SelectionProto(selName: TermName, _, _, _) =>
79-
tp.memberBasedOnFlags(selName, required = ExtensionMethod).exists
80-
|| tp.memberBasedOnFlags(selProto.extensionName, required = ExtensionMethod).exists
81-
case _ => false
79+
tp.memberBasedOnFlags(selName, required = ExtensionMethod).exists
80+
case _ =>
81+
false
8282

8383
def strictEquality(using Context): Boolean =
8484
ctx.mode.is(Mode.StrictEquality) || Feature.enabled(nme.strictEquality)
@@ -513,9 +513,19 @@ object Implicits:
513513
em"${err.refStr(ref)} produces a diverging implicit search when trying to $qualify"
514514
}
515515

516-
class FailedExtension(extApp: Tree, val expectedType: Type) extends SearchFailureType:
516+
/** A search failure type for attempted ill-typed extension method calls */
517+
class FailedExtension(extApp: Tree, val expectedType: Type, val whyFailed: Message) extends SearchFailureType:
517518
def argument = EmptyTree
518519
def explanation(using Context) = em"$extApp does not $qualify"
520+
521+
/** A search failure type for aborted searches of extension methods, typically
522+
* because of a cyclic reference or similar.
523+
*/
524+
class NestedFailure(_msg: Message, val expectedType: Type) extends SearchFailureType:
525+
def argument = EmptyTree
526+
override def msg(using Context) = _msg
527+
def explanation(using Context) = msg.toString
528+
519529
end Implicits
520530

521531
import Implicits._
@@ -1013,12 +1023,7 @@ trait Implicits:
10131023
pt match
10141024
case selProto @ SelectionProto(selName: TermName, mbrType, _, _) if cand.isExtension =>
10151025
def tryExtension(using Context) =
1016-
val xname =
1017-
if ref.memberBasedOnFlags(selProto.extensionName, required = ExtensionMethod).exists then
1018-
selProto.extensionName
1019-
else
1020-
selName
1021-
extMethodApply(untpd.Select(untpdGenerated, xname), argument, mbrType)
1026+
extMethodApply(untpd.Select(untpdGenerated, selName), argument, mbrType)
10221027
if cand.isConversion then
10231028
val extensionCtx, conversionCtx = ctx.fresh.setNewTyperState()
10241029
val extensionResult = tryExtension(using extensionCtx)

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,6 @@ object ProtoTypes {
147147
abstract case class SelectionProto(name: Name, memberProto: Type, compat: Compatibility, privateOK: Boolean)
148148
extends CachedProxyType with ProtoType with ValueTypeOrProto {
149149

150-
private var myExtensionName: TermName = null
151-
def extensionName(using Context): TermName =
152-
if myExtensionName == null then myExtensionName = name.toExtensionName
153-
myExtensionName
154-
155150
/** Is the set of members of this type unknown? This is the case if:
156151
* 1. The type has Nothing or Wildcard as a prefix or underlying type
157152
* 2. The type has an uninstantiated TypeVar as a prefix or underlying type,
@@ -447,8 +442,7 @@ object ProtoTypes {
447442
ctx.typer.isApplicableType(tp, argType :: Nil, resultType) || {
448443
resType match {
449444
case selProto @ SelectionProto(selName: TermName, mbrType, _, _) =>
450-
ctx.typer.hasExtensionMethodNamed(tp, selName, argType, mbrType)
451-
|| ctx.typer.hasExtensionMethodNamed(tp, selProto.extensionName, argType, mbrType)
445+
ctx.typer.hasExtensionMethodNamed(tp, selName, argType, mbrType)
452446
//.reporting(i"has ext $tp $name $argType $mbrType: $result")
453447
case _ =>
454448
false

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

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3348,11 +3348,8 @@ class Typer extends Namer
33483348
// implicit conversion to the receiver type.
33493349
def sharpenedPt = pt match
33503350
case pt: SelectionProto
3351-
if pt.name.isExtensionName
3352-
|| pt.memberProto.revealIgnored.isExtensionApplyProto =>
3353-
pt.deepenProto
3354-
case _ =>
3355-
pt
3351+
if pt.memberProto.revealIgnored.isExtensionApplyProto => pt.deepenProto
3352+
case _ => pt
33563353

33573354
def adaptNoArgs(wtp: Type): Tree = {
33583355
val ptNorm = underlyingApplied(pt)
@@ -3499,24 +3496,27 @@ class Typer extends Namer
34993496
// try an extension method in scope
35003497
pt match {
35013498
case selProto @ SelectionProto(selName: TermName, mbrType, _, _) =>
3499+
35023500
def tryExtension(using Context): Tree =
3503-
try
3504-
findRef(selName, WildcardType, ExtensionMethod, EmptyFlags, tree.srcPos) match
3505-
case ref: TermRef =>
3506-
extMethodApply(untpd.ref(ref).withSpan(tree.span), tree, mbrType)
3507-
case _ => findRef(selProto.extensionName, WildcardType, ExtensionMethod, EmptyFlags, tree.srcPos) match
3508-
case ref: TermRef =>
3509-
extMethodApply(untpd.ref(ref).withSpan(tree.span), tree, mbrType)
3510-
case _ => EmptyTree
3511-
catch case ex: TypeError => errorTree(tree, ex, tree.srcPos)
3512-
val nestedCtx = ctx.fresh.setNewTyperState()
3513-
val app = tryExtension(using nestedCtx)
3514-
if (!app.isEmpty && !nestedCtx.reporter.hasErrors) {
3515-
nestedCtx.typerState.commit()
3516-
return ExtMethodApply(app)
3517-
}
3518-
else if !app.isEmpty then
3519-
rememberSearchFailure(tree, SearchFailure(app.withType(FailedExtension(app, pt))))
3501+
findRef(selName, WildcardType, ExtensionMethod, EmptyFlags, tree.srcPos) match
3502+
case ref: TermRef =>
3503+
extMethodApply(untpd.ref(ref).withSpan(tree.span), tree, mbrType)
3504+
case _ =>
3505+
EmptyTree
3506+
3507+
try
3508+
val nestedCtx = ctx.fresh.setNewTyperState()
3509+
val app = tryExtension(using nestedCtx)
3510+
if !app.isEmpty && !nestedCtx.reporter.hasErrors then
3511+
nestedCtx.typerState.commit()
3512+
return ExtMethodApply(app)
3513+
else
3514+
for err <- nestedCtx.reporter.allErrors.take(1) do
3515+
rememberSearchFailure(tree,
3516+
SearchFailure(app.withType(FailedExtension(app, pt, err.msg))))
3517+
catch case ex: TypeError =>
3518+
rememberSearchFailure(tree,
3519+
SearchFailure(tree.withType(NestedFailure(ex.toMessage, pt))))
35203520
case _ =>
35213521
}
35223522

tests/neg/enum-values.check

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
| meaning a values array is not defined.
77
| An extension method was tried, but could not be fully constructed:
88
|
9-
| example.Extensions.values(Tag)
9+
| example.Extensions.values(Tag) failed with
10+
|
11+
| Found: example.Tag.type
12+
| Required: Nothing
1013
-- [E008] Not Found Error: tests/neg/enum-values.scala:33:50 -----------------------------------------------------------
1114
33 | val listlikes: Array[ListLike[?]] = ListLike.values // error
1215
| ^^^^^^^^^^^^^^^
@@ -15,7 +18,10 @@
1518
| meaning a values array is not defined.
1619
| An extension method was tried, but could not be fully constructed:
1720
|
18-
| example.Extensions.values(ListLike)
21+
| example.Extensions.values(ListLike) failed with
22+
|
23+
| Found: example.ListLike.type
24+
| Required: Nothing
1925
-- [E008] Not Found Error: tests/neg/enum-values.scala:34:52 -----------------------------------------------------------
2026
34 | val typeCtorsK: Array[TypeCtorsK[?]] = TypeCtorsK.values // error
2127
| ^^^^^^^^^^^^^^^^^
@@ -24,7 +30,10 @@
2430
| meaning a values array is not defined.
2531
| An extension method was tried, but could not be fully constructed:
2632
|
27-
| example.Extensions.values(TypeCtorsK)
33+
| example.Extensions.values(TypeCtorsK) failed with
34+
|
35+
| Found: example.TypeCtorsK.type
36+
| Required: Nothing
2837
-- [E008] Not Found Error: tests/neg/enum-values.scala:36:6 ------------------------------------------------------------
2938
36 | Tag.valueOf("Int") // error
3039
| ^^^^^^^^^^^
@@ -54,7 +63,10 @@
5463
| value values is not a member of object example.NotAnEnum.
5564
| An extension method was tried, but could not be fully constructed:
5665
|
57-
| example.Extensions.values(NotAnEnum)
66+
| example.Extensions.values(NotAnEnum) failed with
67+
|
68+
| Found: example.NotAnEnum.type
69+
| Required: Nothing
5870
-- [E008] Not Found Error: tests/neg/enum-values.scala:41:12 -----------------------------------------------------------
5971
41 | NotAnEnum.valueOf("Foo") // error
6072
| ^^^^^^^^^^^^^^^^^

tests/neg/i10870.check

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- [E008] Not Found Error: tests/neg/i10870.scala:10:16 ----------------------------------------------------------------
2+
10 | def x = b.a.x // error
3+
| ^^^^^
4+
| value x is not a member of A.
5+
| Extension methods were tried, but the search failed with:
6+
|
7+
| Overloaded or recursive method x needs return type

tests/neg/i10870.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
final case class A()
2+
final case class B(a:A)
3+
4+
object Test:
5+
6+
extension(a:A)
7+
def x = 5
8+
9+
extension(b:B)
10+
def x = b.a.x // error

tests/neg/i10901.check

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
-- [E008] Not Found Error: tests/neg/i10901.scala:45:38 ----------------------------------------------------------------
2+
45 | val pos1: Point2D[Int,Double] = x º y // error
3+
| ^^^
4+
| value º is not a member of object BugExp4Point2D.IntT.
5+
| An extension method was tried, but could not be fully constructed:
6+
|
7+
| º(x) failed with
8+
|
9+
| Ambiguous overload. The overloaded alternatives of method º in object dsl with types
10+
| [T1, T2]
11+
| (x: BugExp4Point2D.ColumnType[T1])
12+
| (y: BugExp4Point2D.ColumnType[T2])
13+
| (implicit evidence$7: Numeric[T1], evidence$8: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
14+
| [T1, T2]
15+
| (x: T1)
16+
| (y: BugExp4Point2D.ColumnType[T2])
17+
| (implicit evidence$5: Numeric[T1], evidence$6: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
18+
| both match arguments ((x : BugExp4Point2D.IntT.type))
19+
-- [E008] Not Found Error: tests/neg/i10901.scala:48:38 ----------------------------------------------------------------
20+
48 | val pos4: Point2D[Int,Double] = x º 201.1 // error
21+
| ^^^
22+
|value º is not a member of object BugExp4Point2D.IntT.
23+
|An extension method was tried, but could not be fully constructed:
24+
|
25+
| º(x) failed with
26+
|
27+
| Ambiguous overload. The overloaded alternatives of method º in object dsl with types
28+
| [T1, T2]
29+
| (x: BugExp4Point2D.ColumnType[T1])
30+
| (y: T2)(implicit evidence$9: Numeric[T1], evidence$10: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
31+
| [T1, T2](x: T1)(y: T2)(implicit evidence$3: Numeric[T1], evidence$4: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
32+
| both match arguments ((x : BugExp4Point2D.IntT.type))
33+
-- [E008] Not Found Error: tests/neg/i10901.scala:62:16 ----------------------------------------------------------------
34+
62 | val y = "abc".foo // error
35+
| ^^^^^^^^^
36+
| value foo is not a member of String.
37+
| An extension method was tried, but could not be fully constructed:
38+
|
39+
| Test.foo("abc")(/* missing */summon[C]) failed with
40+
|
41+
| no implicit argument of type C was found for parameter x$1 of method foo in object Test

0 commit comments

Comments
 (0)