summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAdriaan Moors <adriaan.moors@epfl.ch>2010-08-20 14:48:12 +0000
committerAdriaan Moors <adriaan.moors@epfl.ch>2010-08-20 14:48:12 +0000
commite11cac6ecc3c8791be3a37fc3b9f6837c9d46d23 (patch)
treedfa0ddc52703125288c6d7a85c4dc7befdb1508c /src
parentd4645f93728ee8cb40a17c23f92a272e27052889 (diff)
downloadscala-e11cac6ecc3c8791be3a37fc3b9f6837c9d46d23.tar.gz
scala-e11cac6ecc3c8791be3a37fc3b9f6837c9d46d23.tar.bz2
scala-e11cac6ecc3c8791be3a37fc3b9f6837c9d46d23.zip
closes 2462. better implicit error messages.
@implicitNotFound(msg="Custom error message that may refer to type parameters ${T} and ${U}") trait Constraint[T, U] whenever an implicit argument of type Constraint[A, B] cannot be found, the custom error message will be used, where the type arguments are interpolated in the obvious way note: if the msg in the annotation references non-existing type params, a warning is emitted the patch also cleans up annotation argument retrieval (moved it to AnnotationInfo from Symbol) review by odersky
Diffstat (limited to 'src')
-rw-r--r--src/compiler/scala/tools/nsc/symtab/AnnotationInfos.scala12
-rw-r--r--src/compiler/scala/tools/nsc/symtab/Definitions.scala1
-rw-r--r--src/compiler/scala/tools/nsc/symtab/Symbols.scala18
-rw-r--r--src/compiler/scala/tools/nsc/typechecker/Implicits.scala41
-rw-r--r--src/compiler/scala/tools/nsc/typechecker/RefChecks.scala8
-rw-r--r--src/compiler/scala/tools/nsc/typechecker/Typers.scala14
-rw-r--r--src/library/scala/annotation/implicitNotFound.scala18
-rw-r--r--src/library/scala/collection/generic/CanBuildFrom.scala3
8 files changed, 95 insertions, 20 deletions
diff --git a/src/compiler/scala/tools/nsc/symtab/AnnotationInfos.scala b/src/compiler/scala/tools/nsc/symtab/AnnotationInfos.scala
index 40177fad10..2429f53aa1 100644
--- a/src/compiler/scala/tools/nsc/symtab/AnnotationInfos.scala
+++ b/src/compiler/scala/tools/nsc/symtab/AnnotationInfos.scala
@@ -123,6 +123,18 @@ trait AnnotationInfos extends reflect.generic.AnnotationInfos { self: SymbolTabl
val subs = new TreeSymSubstituter(List(from), List(to))
AnnotationInfo(atp, args.map(subs(_)), assocs).setPos(pos)
}
+
+ // !!! when annotation arguments are not literal strings, but any sort of
+ // assembly of strings, there is a fair chance they will turn up here not as
+ // Literal(const) but some arbitrary AST.
+ def stringArg(index: Int): Option[String] = if(args.size > index) Some(args(index) match {
+ case Literal(const) => const.stringValue
+ case x => x.toString // should not be necessary, but better than silently ignoring an issue
+ }) else None
+
+ def intArg(index: Int): Option[Int] = if(args.size > index) Some(args(index)) collect {
+ case Literal(Constant(x: Int)) => x
+ } else None
}
object AnnotationInfo extends AnnotationInfoExtractor
diff --git a/src/compiler/scala/tools/nsc/symtab/Definitions.scala b/src/compiler/scala/tools/nsc/symtab/Definitions.scala
index d9e453291f..dbf95f9ac2 100644
--- a/src/compiler/scala/tools/nsc/symtab/Definitions.scala
+++ b/src/compiler/scala/tools/nsc/symtab/Definitions.scala
@@ -124,6 +124,7 @@ trait Definitions extends reflect.generic.StandardDefinitions {
lazy val TailrecClass = getClass("scala.annotation.tailrec")
lazy val SwitchClass = getClass("scala.annotation.switch")
lazy val ElidableMethodClass = getClass("scala.annotation.elidable")
+ lazy val ImplicitNotFoundClass = getClass("scala.annotation.implicitNotFound")
lazy val FieldTargetClass = getClass("scala.annotation.target.field")
lazy val GetterTargetClass = getClass("scala.annotation.target.getter")
lazy val SetterTargetClass = getClass("scala.annotation.target.setter")
diff --git a/src/compiler/scala/tools/nsc/symtab/Symbols.scala b/src/compiler/scala/tools/nsc/symtab/Symbols.scala
index eb4e56a535..609dcdc829 100644
--- a/src/compiler/scala/tools/nsc/symtab/Symbols.scala
+++ b/src/compiler/scala/tools/nsc/symtab/Symbols.scala
@@ -132,14 +132,6 @@ trait Symbols extends reflect.generic.Symbols { self: SymbolTable =>
def getAnnotation(cls: Symbol): Option[AnnotationInfo] =
annotations find (_.atp.typeSymbol == cls)
- /** Finds the requested annotation and returns Some(Tree) containing
- * the argument at position 'index', or None if either the annotation
- * or the index does not exist.
- */
- private def getAnnotationArg(cls: Symbol, index: Int) =
- for (AnnotationInfo(_, args, _) <- getAnnotation(cls) ; if args.size > index) yield
- args(index)
-
/** Remove all annotations matching the given class. */
def removeAnnotation(cls: Symbol): Unit =
setAnnotations(annotations filterNot (_.atp.typeSymbol == cls))
@@ -461,18 +453,16 @@ trait Symbols extends reflect.generic.Symbols { self: SymbolTable =>
}
def isDeprecated = hasAnnotation(DeprecatedAttr)
- def deprecationMessage = getAnnotationArg(DeprecatedAttr, 0) collect { case Literal(const) => const.stringValue }
+ def deprecationMessage = getAnnotation(DeprecatedAttr) flatMap { _.stringArg(0) }
// !!! when annotation arguments are not literal strings, but any sort of
// assembly of strings, there is a fair chance they will turn up here not as
// Literal(const) but some arbitrary AST. However nothing in the compiler
// prevents someone from writing a @migration annotation with a calculated
// string. So this needs attention. For now the fact that migration is
// private[scala] ought to provide enough protection.
- def migrationMessage = getAnnotationArg(MigrationAnnotationClass, 2) collect {
- case Literal(const) => const.stringValue
- case x => x.toString // should not be necessary, but better than silently ignoring an issue
- }
- def elisionLevel = getAnnotationArg(ElidableMethodClass, 0) collect { case Literal(Constant(x: Int)) => x }
+ def migrationMessage = getAnnotation(MigrationAnnotationClass) flatMap { _.stringArg(2) }
+ def elisionLevel = getAnnotation(ElidableMethodClass) flatMap { _.intArg(0) }
+ def implicitNotFoundMsg = getAnnotation(ImplicitNotFoundClass) flatMap { _.stringArg(0) }
/** Does this symbol denote a wrapper object of the interpreter or its class? */
final def isInterpreterWrapper =
diff --git a/src/compiler/scala/tools/nsc/typechecker/Implicits.scala b/src/compiler/scala/tools/nsc/typechecker/Implicits.scala
index a6549021c5..0056dcd917 100644
--- a/src/compiler/scala/tools/nsc/typechecker/Implicits.scala
+++ b/src/compiler/scala/tools/nsc/typechecker/Implicits.scala
@@ -931,5 +931,46 @@ self: Analyzer =>
}
}
+ object ImplicitNotFoundMsg {
+ def unapply(sym: Symbol): Option[(Message)] = sym.implicitNotFoundMsg map (m => (new Message(sym, m)))
+ // check the message's syntax: should be a string literal that may contain occurences of the string "${X}",
+ // where `X` refers to a type parameter of `sym`
+ def check(sym: Symbol): Option[String] =
+ sym.getAnnotation(ImplicitNotFoundClass).flatMap(_.stringArg(0) match {
+ case Some(m) => new Message(sym, m) validate
+ case None => Some("Missing argument `msg` on implicitNotFound annotation.")
+ })
+
+
+ class Message(sym: Symbol, msg: String) {
+ // http://dcsobral.blogspot.com/2010/01/string-interpolation-in-scala-with.html
+ private def interpolate(text: String, vars: Map[String, String]) = { import scala.util.matching.Regex
+ """\$\{([^}]+)\}""".r.replaceAllIn(text, (_: Regex.Match) match {
+ case Regex.Groups(v) => vars.getOrElse(v, "")
+ })}
+
+ private lazy val typeParamNames: List[String] = sym.typeParams.map(_.decodedName)
+
+ def format(paramName: Name, paramTp: Type): String = format(paramTp.typeArgs map (_.toString))
+ def format(typeArgs: List[String]): String =
+ interpolate(msg, Map((typeParamNames zip typeArgs): _*)) // TODO: give access to the name and type of the implicit argument, etc?
+
+ def validate: Option[String] = {
+ import scala.util.matching.Regex; import collection.breakOut
+ // is there a shorter way to avoid the intermediate toList?
+ val refs = Set("""\$\{([^}]+)\}""".r.findAllIn(msg).matchData.map(_.group(1)).toList : _*)
+ val decls = Set(typeParamNames : _*)
+ (refs &~ decls) match {
+ case s if s isEmpty => None
+ case unboundNames =>
+ val singular = unboundNames.size == 1
+ Some("The type parameter"+( if(singular) " " else "s " )+ unboundNames.mkString(", ") +
+ " referenced in the message of the @implicitNotFound annotation "+( if(singular) "is" else "are" )+
+ " not defined by "+ sym +".")
+ }
+ }
+ }
+ }
+
private val DivergentImplicit = new Exception()
}
diff --git a/src/compiler/scala/tools/nsc/typechecker/RefChecks.scala b/src/compiler/scala/tools/nsc/typechecker/RefChecks.scala
index b63f8b0add..1a1ae99d28 100644
--- a/src/compiler/scala/tools/nsc/typechecker/RefChecks.scala
+++ b/src/compiler/scala/tools/nsc/typechecker/RefChecks.scala
@@ -1081,7 +1081,13 @@ abstract class RefChecks extends InfoTransform {
}
tree match {
- case m: MemberDef => applyChecks(m.symbol.annotations)
+ case m: MemberDef =>
+ val sym = m.symbol
+ applyChecks(sym.annotations)
+ // validate implicitNotFoundMessage
+ analyzer.ImplicitNotFoundMsg.check(sym) foreach { warn =>
+ unit.warning(tree.pos, "Invalid implicitNotFound message for %s%s:\n%s".format(sym, sym.locationString, warn))
+ }
case tpt@TypeTree() =>
if(tpt.original != null) {
tpt.original foreach {
diff --git a/src/compiler/scala/tools/nsc/typechecker/Typers.scala b/src/compiler/scala/tools/nsc/typechecker/Typers.scala
index 6b8c2fe0b4..b389a4c033 100644
--- a/src/compiler/scala/tools/nsc/typechecker/Typers.scala
+++ b/src/compiler/scala/tools/nsc/typechecker/Typers.scala
@@ -191,6 +191,15 @@ trait Typers { self: Analyzer =>
argResultsBuff += inferImplicit(fun, paramTp, true, false, context)
}
+ def errorMessage(paramName: Name, paramTp: Type) =
+ paramTp.typeSymbol match {
+ case ImplicitNotFoundMsg(msg) => msg.format(paramName, paramTp)
+ case _ =>
+ "could not find implicit value for "+
+ (if (paramName startsWith nme.EVIDENCE_PARAM_PREFIX) "evidence parameter of type "
+ else "parameter "+paramName+": ")+paramTp
+ }
+
val argResults = argResultsBuff.toList
val args = argResults.zip(params) flatMap {
case (arg, param) =>
@@ -199,10 +208,7 @@ trait Typers { self: Analyzer =>
else List(atPos(arg.tree.pos)(new AssignOrNamedArg(Ident(param.name), (arg.tree))))
} else {
if (!param.hasFlag(DEFAULTPARAM))
- context.error(
- fun.pos, "could not find implicit value for "+
- (if (param.name startsWith nme.EVIDENCE_PARAM_PREFIX) "evidence parameter of type "
- else "parameter "+param.name+": ")+param.tpe)
+ context.error(fun.pos, errorMessage(param.name, param.tpe))
positional = false
Nil
}
diff --git a/src/library/scala/annotation/implicitNotFound.scala b/src/library/scala/annotation/implicitNotFound.scala
new file mode 100644
index 0000000000..5d9b29c5f8
--- /dev/null
+++ b/src/library/scala/annotation/implicitNotFound.scala
@@ -0,0 +1,18 @@
+/* __ *\
+** ________ ___ / / ___ Scala API **
+** / __/ __// _ | / / / _ | (c) 2002-2010, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ **
+** /____/\___/_/ |_/____/_/ | | **
+** |/ **
+\* */
+
+package scala.annotation
+
+/**
+ * An annotation that specifies the error message that is emitted when the compiler
+ * cannot find an implicit value of the annotated type.
+ *
+ * @author Adriaan Moors
+ * @since 2.8.1
+ */
+final class implicitNotFound(msg: String) extends StaticAnnotation {} \ No newline at end of file
diff --git a/src/library/scala/collection/generic/CanBuildFrom.scala b/src/library/scala/collection/generic/CanBuildFrom.scala
index 79e352690e..4c923dca44 100644
--- a/src/library/scala/collection/generic/CanBuildFrom.scala
+++ b/src/library/scala/collection/generic/CanBuildFrom.scala
@@ -11,7 +11,7 @@ package scala.collection
package generic
import mutable.Builder
-
+import scala.annotation.implicitNotFound
/** A base trait for builder factories.
*
@@ -25,6 +25,7 @@ import mutable.Builder
* @author Adriaan Moors
* @since 2.8
*/
+@implicitNotFound(msg = "Cannot construct a collection of type ${To} with elements of type ${Elem} based on a collection of type ${To}.")
trait CanBuildFrom[-From, -Elem, +To] {
/** Creates a new builder on request of a collection.