Skip to content

Better error diagnostics and other fixes for extension methods #10902

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 9 commits into from
Dec 31, 2020
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Decorators.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ object Decorators {
termName(chars, 0, len)
case name: TypeName => s.concat(name.toTermName)
case _ => termName(s.concat(name.toString))

def indented(width: Int): String =
val padding = " " * width
padding + s.replace("\n", "\n" + padding)
end extension

/** Implements a findSymbol method on iterators of Symbols that
Expand Down
15 changes: 0 additions & 15 deletions compiler/src/dotty/tools/dotc/core/NameOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,21 +137,6 @@ object NameOps {
else name.toTermName
}

/** Does the name match `extension`? */
def isExtension: Boolean = name match
case name: SimpleName =>
name.length == "extension".length && name.startsWith("extension")
case _ => false

/** Does this name start with `extension_`? */
def isExtensionName: Boolean = name match
case name: SimpleName => name.startsWith("extension_")
case _ => false

// TODO: Drop next 3 methods once extension names have stabilized
/** Add an `extension_` in front of this name */
def toExtensionName(using Context): SimpleName = "extension_".concat(name)

/** The expanded name.
* This is the fully qualified name of `base` with `ExpandPrefixName` as separator,
* followed by `kind` and the name.
Expand Down
3 changes: 1 addition & 2 deletions compiler/src/dotty/tools/dotc/transform/PostTyper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase

def checkIdent(sel: untpd.ImportSelector): Unit =
if !exprTpe.member(sel.name).exists
&& !exprTpe.member(sel.name.toTypeName).exists
&& !exprTpe.member(sel.name.toExtensionName).exists then
&& !exprTpe.member(sel.name.toTypeName).exists then
report.error(NotAMember(exprTpe, sel.name, "value"), sel.imported.srcPos)
if seen.contains(sel.name) then
report.error(ImportRenamedTwice(sel.imported), sel.imported.srcPos)
Expand Down
10 changes: 1 addition & 9 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2153,19 +2153,11 @@ trait Applications extends Compatibility {
(tree, currentPt)

val (core, pt1) = normalizePt(methodRef, pt)
val app = withMode(Mode.SynthesizeExtMethodReceiver) {
withMode(Mode.SynthesizeExtMethodReceiver) {
typed(
untpd.Apply(core, untpd.TypedSplice(receiver, isExtensionReceiver = true) :: Nil),
pt1, ctx.typerState.ownedVars)
}
def isExtension(tree: Tree): Boolean = methPart(tree) match {
case Inlined(call, _, _) => isExtension(call)
case tree @ Select(qual, nme.apply) => tree.symbol.is(ExtensionMethod) || isExtension(qual)
case tree => tree.symbol.is(ExtensionMethod)
}
if (!isExtension(app))
report.error(em"not an extension method: $methodRef", receiver.srcPos)
app
}

def isApplicableExtensionMethod(ref: TermRef, receiver: Type)(using Context) =
Expand Down
38 changes: 30 additions & 8 deletions compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import util.SrcPos
import config.Feature
import java.util.regex.Matcher.quoteReplacement
import reporting._
import collection.mutable

import scala.util.matching.Regex

Expand Down Expand Up @@ -140,26 +141,47 @@ object ErrorReporting {
if Feature.migrateTo3 then "\nThis patch can be inserted automatically under -rewrite."
else ""

def whyFailedStr(fail: FailedExtension) =
i""" failed with
|
|${fail.whyFailed.message.indented(8)}"""

def selectErrorAddendum
(tree: untpd.RefTree, qual1: Tree, qualType: Type, suggestImports: Type => String)
(using Context): String =
val attempts: List[Tree] = qual1.getAttachment(Typer.HiddenSearchFailure) match
case Some(failures) =>
for failure <- failures
if !failure.reason.isInstanceOf[Implicits.NoMatchingImplicits]
yield failure.tree
case _ => Nil

val attempts = mutable.ListBuffer[(Tree, String)]()
val nested = mutable.ListBuffer[NestedFailure]()
for
failures <- qual1.getAttachment(Typer.HiddenSearchFailure)
failure <- failures
do
failure.reason match
case fail: NestedFailure => nested += fail
case fail: FailedExtension => attempts += ((failure.tree, whyFailedStr(fail)))
case fail: Implicits.NoMatchingImplicits => // do nothing
case _ => attempts += ((failure.tree, ""))
if qualType.derivesFrom(defn.DynamicClass) then
"\npossible cause: maybe a wrong Dynamic method signature?"
else if attempts.nonEmpty then
val attemptStrings = attempts.map(_.showIndented(4)).distinct
val attemptStrings =
attempts.toList
.map((tree, whyFailed) => (tree.showIndented(4), whyFailed))
.distinctBy(_._1)
.map((treeStr, whyFailed) =>
i"""
| $treeStr$whyFailed""")
val extMethods =
if attemptStrings.length > 1 then "Extension methods were"
else "An extension method was"
i""".
|$extMethods tried, but could not be fully constructed:
|$attemptStrings%\n%"""
else if nested.nonEmpty then
i""".
|Extension methods were tried, but the search failed with:
|
| $attemptStrings%\nor\n %"""
| ${nested.head.explanation}"""
else if tree.hasAttachment(desugar.MultiLineInfix) then
i""".
|Note that `${tree.name}` is treated as an infix operator in Scala 3.
Expand Down
25 changes: 15 additions & 10 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ object Implicits:
*/
def hasExtMethod(tp: Type, expected: Type)(using Context) = expected match
case selProto @ SelectionProto(selName: TermName, _, _, _) =>
tp.memberBasedOnFlags(selName, required = ExtensionMethod).exists
|| tp.memberBasedOnFlags(selProto.extensionName, required = ExtensionMethod).exists
case _ => false
tp.memberBasedOnFlags(selName, required = ExtensionMethod).exists
case _ =>
false

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

class FailedExtension(extApp: Tree, val expectedType: Type) extends SearchFailureType:
/** A search failure type for attempted ill-typed extension method calls */
class FailedExtension(extApp: Tree, val expectedType: Type, val whyFailed: Message) extends SearchFailureType:
def argument = EmptyTree
def explanation(using Context) = em"$extApp does not $qualify"

/** A search failure type for aborted searches of extension methods, typically
* because of a cyclic reference or similar.
*/
class NestedFailure(_msg: Message, val expectedType: Type) extends SearchFailureType:
def argument = EmptyTree
override def msg(using Context) = _msg
def explanation(using Context) = msg.toString

end Implicits

import Implicits._
Expand Down Expand Up @@ -1013,12 +1023,7 @@ trait Implicits:
pt match
case selProto @ SelectionProto(selName: TermName, mbrType, _, _) if cand.isExtension =>
def tryExtension(using Context) =
val xname =
if ref.memberBasedOnFlags(selProto.extensionName, required = ExtensionMethod).exists then
selProto.extensionName
else
selName
extMethodApply(untpd.Select(untpdGenerated, xname), argument, mbrType)
extMethodApply(untpd.Select(untpdGenerated, selName), argument, mbrType)
if cand.isConversion then
val extensionCtx, conversionCtx = ctx.fresh.setNewTyperState()
val extensionResult = tryExtension(using extensionCtx)
Expand Down
8 changes: 1 addition & 7 deletions compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,6 @@ object ProtoTypes {
abstract case class SelectionProto(name: Name, memberProto: Type, compat: Compatibility, privateOK: Boolean)
extends CachedProxyType with ProtoType with ValueTypeOrProto {

private var myExtensionName: TermName = null
def extensionName(using Context): TermName =
if myExtensionName == null then myExtensionName = name.toExtensionName
myExtensionName

/** Is the set of members of this type unknown? This is the case if:
* 1. The type has Nothing or Wildcard as a prefix or underlying type
* 2. The type has an uninstantiated TypeVar as a prefix or underlying type,
Expand Down Expand Up @@ -447,8 +442,7 @@ object ProtoTypes {
ctx.typer.isApplicableType(tp, argType :: Nil, resultType) || {
resType match {
case selProto @ SelectionProto(selName: TermName, mbrType, _, _) =>
ctx.typer.hasExtensionMethodNamed(tp, selName, argType, mbrType)
|| ctx.typer.hasExtensionMethodNamed(tp, selProto.extensionName, argType, mbrType)
ctx.typer.hasExtensionMethodNamed(tp, selName, argType, mbrType)
//.reporting(i"has ext $tp $name $argType $mbrType: $result")
case _ =>
false
Expand Down
44 changes: 22 additions & 22 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3348,11 +3348,8 @@ class Typer extends Namer
// implicit conversion to the receiver type.
def sharpenedPt = pt match
case pt: SelectionProto
if pt.name.isExtensionName
|| pt.memberProto.revealIgnored.isExtensionApplyProto =>
pt.deepenProto
case _ =>
pt
if pt.memberProto.revealIgnored.isExtensionApplyProto => pt.deepenProto
case _ => pt

def adaptNoArgs(wtp: Type): Tree = {
val ptNorm = underlyingApplied(pt)
Expand Down Expand Up @@ -3499,24 +3496,27 @@ class Typer extends Namer
// try an extension method in scope
pt match {
case selProto @ SelectionProto(selName: TermName, mbrType, _, _) =>

def tryExtension(using Context): Tree =
try
findRef(selName, WildcardType, ExtensionMethod, EmptyFlags, tree.srcPos) match
case ref: TermRef =>
extMethodApply(untpd.ref(ref).withSpan(tree.span), tree, mbrType)
case _ => findRef(selProto.extensionName, WildcardType, ExtensionMethod, EmptyFlags, tree.srcPos) match
case ref: TermRef =>
extMethodApply(untpd.ref(ref).withSpan(tree.span), tree, mbrType)
case _ => EmptyTree
catch case ex: TypeError => errorTree(tree, ex, tree.srcPos)
val nestedCtx = ctx.fresh.setNewTyperState()
val app = tryExtension(using nestedCtx)
if (!app.isEmpty && !nestedCtx.reporter.hasErrors) {
nestedCtx.typerState.commit()
return ExtMethodApply(app)
}
else if !app.isEmpty then
rememberSearchFailure(tree, SearchFailure(app.withType(FailedExtension(app, pt))))
findRef(selName, WildcardType, ExtensionMethod, EmptyFlags, tree.srcPos) match
case ref: TermRef =>
extMethodApply(untpd.ref(ref).withSpan(tree.span), tree, mbrType)
case _ =>
EmptyTree

try
val nestedCtx = ctx.fresh.setNewTyperState()
val app = tryExtension(using nestedCtx)
if !app.isEmpty && !nestedCtx.reporter.hasErrors then
nestedCtx.typerState.commit()
return ExtMethodApply(app)
else
for err <- nestedCtx.reporter.allErrors.take(1) do
rememberSearchFailure(tree,
SearchFailure(app.withType(FailedExtension(app, pt, err.msg))))
catch case ex: TypeError =>
rememberSearchFailure(tree,
SearchFailure(tree.withType(NestedFailure(ex.toMessage, pt))))
case _ =>
}

Expand Down
20 changes: 16 additions & 4 deletions tests/neg/enum-values.check
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
| meaning a values array is not defined.
| An extension method was tried, but could not be fully constructed:
|
| example.Extensions.values(Tag)
| example.Extensions.values(Tag) failed with
|
| Found: example.Tag.type
| Required: Nothing
-- [E008] Not Found Error: tests/neg/enum-values.scala:33:50 -----------------------------------------------------------
33 | val listlikes: Array[ListLike[?]] = ListLike.values // error
| ^^^^^^^^^^^^^^^
Expand All @@ -15,7 +18,10 @@
| meaning a values array is not defined.
| An extension method was tried, but could not be fully constructed:
|
| example.Extensions.values(ListLike)
| example.Extensions.values(ListLike) failed with
|
| Found: example.ListLike.type
| Required: Nothing
-- [E008] Not Found Error: tests/neg/enum-values.scala:34:52 -----------------------------------------------------------
34 | val typeCtorsK: Array[TypeCtorsK[?]] = TypeCtorsK.values // error
| ^^^^^^^^^^^^^^^^^
Expand All @@ -24,7 +30,10 @@
| meaning a values array is not defined.
| An extension method was tried, but could not be fully constructed:
|
| example.Extensions.values(TypeCtorsK)
| example.Extensions.values(TypeCtorsK) failed with
|
| Found: example.TypeCtorsK.type
| Required: Nothing
-- [E008] Not Found Error: tests/neg/enum-values.scala:36:6 ------------------------------------------------------------
36 | Tag.valueOf("Int") // error
| ^^^^^^^^^^^
Expand Down Expand Up @@ -54,7 +63,10 @@
| value values is not a member of object example.NotAnEnum.
| An extension method was tried, but could not be fully constructed:
|
| example.Extensions.values(NotAnEnum)
| example.Extensions.values(NotAnEnum) failed with
|
| Found: example.NotAnEnum.type
| Required: Nothing
-- [E008] Not Found Error: tests/neg/enum-values.scala:41:12 -----------------------------------------------------------
41 | NotAnEnum.valueOf("Foo") // error
| ^^^^^^^^^^^^^^^^^
Expand Down
7 changes: 7 additions & 0 deletions tests/neg/i10870.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- [E008] Not Found Error: tests/neg/i10870.scala:10:16 ----------------------------------------------------------------
10 | def x = b.a.x // error
| ^^^^^
| value x is not a member of A.
| Extension methods were tried, but the search failed with:
|
| Overloaded or recursive method x needs return type
10 changes: 10 additions & 0 deletions tests/neg/i10870.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
final case class A()
final case class B(a:A)

object Test:

extension(a:A)
def x = 5

extension(b:B)
def x = b.a.x // error
41 changes: 41 additions & 0 deletions tests/neg/i10901.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- [E008] Not Found Error: tests/neg/i10901.scala:45:38 ----------------------------------------------------------------
45 | val pos1: Point2D[Int,Double] = x º y // error
| ^^^
| value º is not a member of object BugExp4Point2D.IntT.
| An extension method was tried, but could not be fully constructed:
|
| º(x) failed with
|
| Ambiguous overload. The overloaded alternatives of method º in object dsl with types
| [T1, T2]
| (x: BugExp4Point2D.ColumnType[T1])
| (y: BugExp4Point2D.ColumnType[T2])
| (implicit evidence$7: Numeric[T1], evidence$8: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
| [T1, T2]
| (x: T1)
| (y: BugExp4Point2D.ColumnType[T2])
| (implicit evidence$5: Numeric[T1], evidence$6: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
| both match arguments ((x : BugExp4Point2D.IntT.type))
-- [E008] Not Found Error: tests/neg/i10901.scala:48:38 ----------------------------------------------------------------
48 | val pos4: Point2D[Int,Double] = x º 201.1 // error
| ^^^
|value º is not a member of object BugExp4Point2D.IntT.
|An extension method was tried, but could not be fully constructed:
|
| º(x) failed with
|
| Ambiguous overload. The overloaded alternatives of method º in object dsl with types
| [T1, T2]
| (x: BugExp4Point2D.ColumnType[T1])
| (y: T2)(implicit evidence$9: Numeric[T1], evidence$10: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
| [T1, T2](x: T1)(y: T2)(implicit evidence$3: Numeric[T1], evidence$4: Numeric[T2]): BugExp4Point2D.Point2D[T1, T2]
| both match arguments ((x : BugExp4Point2D.IntT.type))
-- [E008] Not Found Error: tests/neg/i10901.scala:62:16 ----------------------------------------------------------------
62 | val y = "abc".foo // error
| ^^^^^^^^^
| value foo is not a member of String.
| An extension method was tried, but could not be fully constructed:
|
| Test.foo("abc")(/* missing */summon[C]) failed with
|
| no implicit argument of type C was found for parameter x$1 of method foo in object Test
Loading