From e11cac6ecc3c8791be3a37fc3b9f6837c9d46d23 Mon Sep 17 00:00:00 2001 From: Adriaan Moors Date: Fri, 20 Aug 2010 14:48:12 +0000 Subject: 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 --- .../scala/tools/nsc/symtab/AnnotationInfos.scala | 12 +++++++ .../scala/tools/nsc/symtab/Definitions.scala | 1 + src/compiler/scala/tools/nsc/symtab/Symbols.scala | 18 +++------- .../scala/tools/nsc/typechecker/Implicits.scala | 41 ++++++++++++++++++++++ .../scala/tools/nsc/typechecker/RefChecks.scala | 8 ++++- .../scala/tools/nsc/typechecker/Typers.scala | 14 +++++--- .../scala/annotation/implicitNotFound.scala | 18 ++++++++++ .../scala/collection/generic/CanBuildFrom.scala | 3 +- 8 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 src/library/scala/annotation/implicitNotFound.scala (limited to 'src') 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. -- cgit v1.2.3