diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index 0cc91b84f95a..6cc05249a38e 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -548,4 +548,49 @@ trait ScalaSettings extends StandardScalaSettings with Warnings { */ None } + + object YsplainChoices extends MultiChoiceEnumeration { + val enable = Choice("enable", "activate the splain error formatting engine") + val noColor = Choice("no-color", "don't colorize type errors formatted by splain") + val noBoundsImplicit = Choice("no-bounds-implicit", "suppress any implicit bounds errors") + val noTree = Choice("no-tree", "don't display implicit chains in tree layout") + val verboseTree = Choice("verbose-tree", "display all intermediate implicits in a chain") + val noInfix = Choice("no-infix", "don't format infix types") + val noFoundReq = Choice("no-found-req", "don't format found/required type errors") + } + + val Ysplain: MultiChoiceSetting[YsplainChoices.type] = + MultiChoiceSetting( + name = "-Yexplain-implicits", + helpArg = "feature", + descr = "activate the splain error formatter engine", + domain = YsplainChoices, + default = None, + ) + + val YsplainTruncRefined: IntSetting = + IntSetting( + "-Yexplain-implicits-trunc-refined", + "truncate refined types as F {...}", + 0, + Some((0, Int.MaxValue)), + str => Some(str.toInt), + ) + + val YsplainBreakInfix: IntSetting = + IntSetting( + "-Yexplain-implicits-break-infix", + "break infix types into multiple lines when exceeding this number of characters", + 0, + Some((0, Int.MaxValue)), + str => Some(str.toInt), + ) + + def splainSettingEnable: Boolean = Ysplain.contains(YsplainChoices.enable) + def splainSettingNoFoundReq: Boolean = Ysplain.contains(YsplainChoices.noFoundReq) + def splainSettingNoInfix: Boolean =Ysplain.contains(YsplainChoices.noInfix) + def splainSettingNoColor: Boolean = Ysplain.contains(YsplainChoices.noColor) + def splainSettingVerboseTree: Boolean = Ysplain.contains(YsplainChoices.verboseTree) + def splainSettingNoTree: Boolean = Ysplain.contains(YsplainChoices.noTree) + def splainSettingNoBoundsImplicits: Boolean = Ysplain.contains(YsplainChoices.noBoundsImplicit) } diff --git a/src/compiler/scala/tools/nsc/typechecker/AnalyzerPlugins.scala b/src/compiler/scala/tools/nsc/typechecker/AnalyzerPlugins.scala index a86f2c409151..e563f45d5160 100644 --- a/src/compiler/scala/tools/nsc/typechecker/AnalyzerPlugins.scala +++ b/src/compiler/scala/tools/nsc/typechecker/AnalyzerPlugins.scala @@ -16,7 +16,7 @@ package typechecker /** * @author Lukas Rytz */ -trait AnalyzerPlugins { self: Analyzer => +trait AnalyzerPlugins { self: Analyzer with splain.SplainData => import global._ trait AnalyzerPlugin { @@ -179,6 +179,15 @@ trait AnalyzerPlugins { self: Analyzer => * @param result The result to a given implicit search. */ def pluginsNotifyImplicitSearchResult(result: SearchResult): Unit = () + + /** + * Construct a custom error message for implicit parameters that could not be resolved. + * + * @param tree The tree that requested the implicit + * @param param The implicit parameter that was resolved + */ + def noImplicitFoundError(param: Symbol, errors: List[ImplicitError], previous: Option[String]): Option[String] = + previous } /** @@ -390,6 +399,13 @@ trait AnalyzerPlugins { self: Analyzer => def accumulate = (_, p) => p.pluginsNotifyImplicitSearchResult(result) }) + /** @see AnalyzerPlugin.noImplicitFoundError */ + def pluginsNoImplicitFoundError(param: Symbol, errors: List[ImplicitError], initial: String): Option[String] = + invoke(new CumulativeOp[Option[String]] { + def default = Some(initial) + def accumulate = (previous, p) => p.noImplicitFoundError(param, errors, previous) + }) + /** A list of registered macro plugins */ private var macroPlugins: List[MacroPlugin] = Nil diff --git a/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala b/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala index 471a0f3c1f4b..0c61a737fb70 100644 --- a/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala +++ b/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala @@ -23,7 +23,9 @@ import scala.tools.nsc.util.stackTraceString import scala.reflect.io.NoAbstractFile import scala.reflect.internal.util.NoSourceFile -trait ContextErrors { +trait ContextErrors +extends splain.SplainErrors +{ self: Analyzer => import global._ @@ -151,7 +153,7 @@ trait ContextErrors { MacroIncompatibleEngineError("macro cannot be expanded, because it was compiled by an incompatible macro engine", internalMessage) def NoImplicitFoundError(tree: Tree, param: Symbol)(implicit context: Context): Unit = { - def errMsg = { + def defaultErrMsg = { val paramName = param.name val paramTp = param.tpe def evOrParam = @@ -173,7 +175,8 @@ trait ContextErrors { } } } - issueNormalTypeError(tree, errMsg) + val errMsg = splainPushOrReportNotFound(tree, param) + issueNormalTypeError(tree, errMsg.getOrElse(defaultErrMsg)) } trait TyperContextErrors { diff --git a/src/compiler/scala/tools/nsc/typechecker/Implicits.scala b/src/compiler/scala/tools/nsc/typechecker/Implicits.scala index c278dd54ffe0..0f79fe18665a 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Implicits.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Implicits.scala @@ -32,7 +32,7 @@ import scala.language.implicitConversions * * @author Martin Odersky */ -trait Implicits { +trait Implicits extends splain.SplainData { self: Analyzer => import global._ @@ -105,12 +105,14 @@ trait Implicits { if (shouldPrint) typingStack.printTyping(tree, "typing implicit: %s %s".format(tree, context.undetparamsString)) val implicitSearchContext = context.makeImplicit(reportAmbiguous) + ImplicitErrors.startSearch(pt) val dpt = if (isView) pt else dropByName(pt) val isByName = dpt ne pt val search = new ImplicitSearch(tree, dpt, isView, implicitSearchContext, pos, isByName) pluginsNotifyImplicitSearch(search) val result = search.bestImplicit pluginsNotifyImplicitSearchResult(result) + ImplicitErrors.finishSearch(result.isSuccess, pt) if (result.isFailure && saveAmbiguousDivergent && implicitSearchContext.reporter.hasErrors) implicitSearchContext.reporter.propagateImplicitTypeErrorsTo(context.reporter) @@ -909,7 +911,8 @@ trait Implicits { // bounds check on the expandee tree itree3.attachments.get[MacroExpansionAttachment] match { case Some(MacroExpansionAttachment(exp @ TypeApply(fun, targs), _)) => - checkBounds(exp, NoPrefix, NoSymbol, fun.symbol.typeParams, targs.map(_.tpe), "inferred ") + val withinBounds = checkBounds(exp, NoPrefix, NoSymbol, fun.symbol.typeParams, targs.map(_.tpe), "inferred ") + if (!withinBounds) splainPushNonconformantBonds(pt, tree, targs.map(_.tpe), undetParams, None) case _ => () } @@ -956,6 +959,7 @@ trait Implicits { context.reporter.firstError match { case Some(err) => + splainPushImplicitSearchFailure(itree3, pt, err) fail("typing TypeApply reported errors for the implicit tree: " + err.errMsg) case None => val result = new SearchResult(unsuppressMacroExpansion(itree3), subst, context.undetparams) diff --git a/src/compiler/scala/tools/nsc/typechecker/TypeDiagnostics.scala b/src/compiler/scala/tools/nsc/typechecker/TypeDiagnostics.scala index 4c6c2c628cbf..cb2d25861dbe 100644 --- a/src/compiler/scala/tools/nsc/typechecker/TypeDiagnostics.scala +++ b/src/compiler/scala/tools/nsc/typechecker/TypeDiagnostics.scala @@ -39,7 +39,9 @@ import scala.annotation.tailrec * * @author Paul Phillips */ -trait TypeDiagnostics { +trait TypeDiagnostics +extends splain.SplainDiagnostics +{ self: Analyzer with StdAttachments => import global._ @@ -308,7 +310,7 @@ trait TypeDiagnostics { // when the message will never be seen. I though context.reportErrors // being false would do that, but if I return "" under // that condition, I see it. - def foundReqMsg(found: Type, req: Type): String = { + def builtinFoundReqMsg(found: Type, req: Type): String = { val foundWiden = found.widen val reqWiden = req.widen val sameNamesDifferentPrefixes = @@ -338,6 +340,9 @@ trait TypeDiagnostics { } } + def foundReqMsg(found: Type, req: Type): String = + splainFoundReqMsg(found, req).getOrElse(builtinFoundReqMsg(found, req)) + def typePatternAdvice(sym: Symbol, ptSym: Symbol) = { val clazz = if (sym.isModuleClass) sym.companionClass else sym val caseString = diff --git a/src/compiler/scala/tools/nsc/typechecker/splain/Colors.scala b/src/compiler/scala/tools/nsc/typechecker/splain/Colors.scala new file mode 100644 index 000000000000..67bea85500db --- /dev/null +++ b/src/compiler/scala/tools/nsc/typechecker/splain/Colors.scala @@ -0,0 +1,47 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.tools.nsc +package typechecker +package splain + +trait StringColor +{ + def color(s: String, col: String): String +} + +object StringColors +{ + implicit val noColor = + new StringColor { + def color(s: String, col: String) = s + } + + implicit val color = + new StringColor { + import Console.RESET + + def color(s: String, col: String) = col + s + RESET + } +} + +object StringColor +{ + implicit class StringColorOps(s: String)(implicit sc: StringColor) + { + import Console._ + def red = sc.color(s, RED) + def green = sc.color(s, GREEN) + def yellow = sc.color(s, YELLOW) + def blue = sc.color(s, BLUE) + } +} diff --git a/src/compiler/scala/tools/nsc/typechecker/splain/SplainData.scala b/src/compiler/scala/tools/nsc/typechecker/splain/SplainData.scala new file mode 100644 index 000000000000..ef8ac58b003f --- /dev/null +++ b/src/compiler/scala/tools/nsc/typechecker/splain/SplainData.scala @@ -0,0 +1,107 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.tools.nsc +package typechecker +package splain + +import scala.util.matching.Regex + +trait SplainData { self: Analyzer => + + import global._ + + sealed trait ImplicitErrorSpecifics + + object ImplicitErrorSpecifics + { + case class NotFound(param: Symbol) + extends ImplicitErrorSpecifics + + case class NonconformantBounds(targs: List[Type], tparams: List[Symbol], originalError: Option[AbsTypeError]) + extends ImplicitErrorSpecifics + } + + object ImplicitErrors + { + var stack: List[Type] = Nil + + var errors: List[ImplicitError] = Nil + + def push(error: ImplicitError): Unit = errors = error :: errors + + def nesting: Int = stack.length - 1 + + def nested: Boolean = stack.nonEmpty + + def removeErrorsFor(tpe: Type): Unit = errors = errors.dropWhile(_.tpe == tpe) + + def startSearch(expectedType: Type): Unit = { + if (!nested) errors = List() + stack = expectedType :: stack + } + + def finishSearch(success: Boolean, expectedType: Type): Unit = { + if (success) removeErrorsFor(expectedType) + stack = stack.drop(1) + } + } + + case class ImplicitError(tpe: Type, candidate: Tree, nesting: Int, specifics: ImplicitErrorSpecifics) + { + override def equals(other: Any) = other match { + case o: ImplicitError => + o.tpe.toString == tpe.toString && ImplicitError.candidateName(this) == ImplicitError.candidateName(o) + case _ => false + } + + override def hashCode = (tpe.toString.hashCode, ImplicitError.candidateName(this).hashCode).hashCode + + override def toString: String = + s"NotFound(${ImplicitError.shortName(tpe.toString)}, ${ImplicitError.shortName(candidate.toString)}), $nesting, $specifics)" + } + + object ImplicitError + { + def notFound(tpe: Type, candidate: Tree, nesting: Int)(param: Symbol): ImplicitError = + ImplicitError(tpe, candidate, nesting, ImplicitErrorSpecifics.NotFound(param)) + + def nonconformantBounds + (tpe: Type, candidate: Tree, nesting: Int) + (targs: List[Type], tparams: List[Symbol], originalError: Option[AbsTypeError]) + : ImplicitError = + ImplicitError(tpe, candidate, nesting, ImplicitErrorSpecifics.NonconformantBounds(targs, tparams, originalError)) + + def unapplyCandidate(e: ImplicitError): Tree = + e.candidate match { + case TypeApply(name, _) => name + case a => a + } + + def candidateName(e: ImplicitError): String = + unapplyCandidate(e) match { + case Select(_, name) => name.toString + case Ident(name) => name.toString + case a => a.toString + } + + val candidateRegex: Regex = """.*\.this\.(.*)""".r + + def cleanCandidate(e: ImplicitError): String = + unapplyCandidate(e).toString match { + case candidateRegex(suf) => suf + case a => a + } + + def shortName(ident: String): String = ident.split('.').toList.lastOption.getOrElse(ident) + } +} diff --git a/src/compiler/scala/tools/nsc/typechecker/splain/SplainDiagnostics.scala b/src/compiler/scala/tools/nsc/typechecker/splain/SplainDiagnostics.scala new file mode 100644 index 000000000000..11fc9dce8d65 --- /dev/null +++ b/src/compiler/scala/tools/nsc/typechecker/splain/SplainDiagnostics.scala @@ -0,0 +1,42 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.tools.nsc +package typechecker +package splain + +import scala.util.control.NonFatal + +trait SplainDiagnostics +extends SplainFormatting +{ self: Analyzer with SplainData => + import global._ + + def showStats[A](desc: String, run: => A): A = { + val ret = run + if (sys.env.contains("SPLAIN_CACHE_STATS")) + reporter.echo(s"$desc entries/hits: $cacheStats") + ret + } + + def foundReqMsgShort(found: Type, req: Type): Option[TypeRepr] = + try { + Some(showStats("foundreq", showFormattedL(formatDiff(found, req, true), true))) + } catch { + case NonFatal(e) => + None + } + + def splainFoundReqMsg(found: Type, req: Type): Option[String] = + if (!settings.splainSettingEnable || settings.splainSettingNoFoundReq) None + else foundReqMsgShort(found, req).map(a => ";\n" + a.indent.joinLines) +} diff --git a/src/compiler/scala/tools/nsc/typechecker/splain/SplainErrors.scala b/src/compiler/scala/tools/nsc/typechecker/splain/SplainErrors.scala new file mode 100644 index 000000000000..7070d563b772 --- /dev/null +++ b/src/compiler/scala/tools/nsc/typechecker/splain/SplainErrors.scala @@ -0,0 +1,60 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.tools.nsc +package typechecker +package splain + +trait SplainErrors { self: Analyzer with SplainFormatting => + import global._ + + def splainPushNotFound(tree: Tree, param: Symbol): Unit = + ImplicitErrors.stack + .headOption + .map(ImplicitError.notFound(_, tree, ImplicitErrors.nesting)(param)) + .foreach(err => ImplicitErrors.push(err)) + + def splainPushOrReportNotFound(tree: Tree, param: Symbol): Option[String] = + if (settings.splainSettingEnable) + if (ImplicitErrors.nested) { + splainPushNotFound(tree, param) + None + } + else pluginsNoImplicitFoundError(param, ImplicitErrors.errors, formatImplicitError(param, ImplicitErrors.errors)) + else None + + def splainPushNonconformantBonds( + tpe: Type, + candidate: Tree, + targs: List[Type], + tparams: List[Symbol], + originalError: Option[AbsTypeError], + ): Unit = { + val err = ImplicitError.nonconformantBounds(tpe, candidate, ImplicitErrors.nesting)(targs, tparams, originalError) + ImplicitErrors.push(err) + } + + def splainPushImplicitSearchFailure(implicitTree: Tree, expectedType: Type, originalError: AbsTypeError): Unit = { + def pushImpFailure(fun: Tree, args: List[Tree]): Unit = { + fun.tpe match { + case PolyType(tparams, restpe) if tparams.nonEmpty && sameLength(tparams, args) => + val targs = mapList(args)(treeTpe) + splainPushNonconformantBonds(expectedType, implicitTree, targs, tparams, Some(originalError)) + case _ => () + } + } + implicitTree match { + case TypeApply(fun, args) => pushImpFailure(fun, args) + case Apply(TypeApply(fun, args), _) => pushImpFailure(fun, args) + } + } +} diff --git a/src/compiler/scala/tools/nsc/typechecker/splain/SplainFormatData.scala b/src/compiler/scala/tools/nsc/typechecker/splain/SplainFormatData.scala new file mode 100644 index 000000000000..5b5ac057129f --- /dev/null +++ b/src/compiler/scala/tools/nsc/typechecker/splain/SplainFormatData.scala @@ -0,0 +1,104 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.tools.nsc +package typechecker +package splain + +sealed trait Formatted +{ + def length: Int +} + +case class Infix(infix: Formatted, left: Formatted, right: Formatted, + top: Boolean) +extends Formatted +{ + def length = List(infix, left, right).map(_.length).sum + 2 +} + +case class Simple(tpe: String) +extends Formatted +{ + def length = tpe.length +} + +case object UnitForm +extends Formatted +{ + def length = 4 +} + +case class Applied(cons: Formatted, args: List[Formatted]) +extends Formatted +{ + def length = args.map(_.length).sum + (args.length - 1) * 2 + cons.length + 2 +} + +case class TupleForm(elems: List[Formatted]) +extends Formatted +{ + def length = elems.map(_.length).sum + (elems.length - 1) + 2 +} + +case class FunctionForm(args: List[Formatted], ret: Formatted, top: Boolean) +extends Formatted +{ + def length = args.map(_.length).sum + (args.length - 1) + 2 + ret.length + 4 +} + +object FunctionForm +{ + def fromArgs(args: List[Formatted], top: Boolean) = { + val (params, returnt) = args.splitAt(args.length - 1) + FunctionForm(params, returnt.headOption.getOrElse(UnitForm), top) + } +} + +case class SLRecordItem(key: Formatted, value: Formatted) +extends Formatted +{ + def length = key.length + value.length + 5 +} + +case class Diff(left: Formatted, right: Formatted) +extends Formatted +{ + def length = left.length + right.length + 1 +} + +trait TypeRepr +{ + def broken: Boolean + def flat: String + def lines: List[String] + def tokenize = lines mkString " " + def joinLines = lines mkString "\n" + def indent: TypeRepr +} + +case class BrokenType(lines: List[String]) +extends TypeRepr +{ + def broken = true + def flat = lines mkString " " + def indent = BrokenType(lines map (" " + _)) +} + +case class FlatType(flat: String) +extends TypeRepr +{ + def broken = false + def length = flat.length + def lines = List(flat) + def indent = FlatType(" " + flat) +} diff --git a/src/compiler/scala/tools/nsc/typechecker/splain/SplainFormatting.scala b/src/compiler/scala/tools/nsc/typechecker/splain/SplainFormatting.scala new file mode 100644 index 000000000000..2ad42a327c26 --- /dev/null +++ b/src/compiler/scala/tools/nsc/typechecker/splain/SplainFormatting.scala @@ -0,0 +1,656 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.tools.nsc +package typechecker +package splain + +import collection.mutable + +import StringColor._ + +object Messages +{ + val hasMatching = "hasMatchingSymbol reported error: " + + val typingTypeApply = + "typing TypeApply reported errors for the implicit tree: " + + val lazyderiv = "could not find Lazy implicit" +} + +class FormatCache[K, V](cache: mutable.Map[K, V], var hits: Long) +{ + def apply(k: K, orElse: => V) = { + if (cache.contains(k)) hits += 1 + cache.getOrElseUpdate(k, orElse) + } + + def stats = s"${cache.size}/$hits" +} + +object FormatCache +{ + def apply[K, V] = new FormatCache[K, V](mutable.Map(), 0) +} + +trait SplainFormatters +{ self: Analyzer => + import global._ + + def formatType(tpe: Type, top: Boolean): Formatted + + def formatInfix[A](simple: String, left: A, right: A, top: Boolean, rec: A => Boolean => Formatted): Formatted + + trait SpecialFormatter + { + def apply[A](tpe: Type, simple: String, args: List[A], + formattedArgs: => List[Formatted], top: Boolean, + rec: A => Boolean => Formatted): Option[Formatted] + + def diff(left: Type, right: Type, top: Boolean): Option[Formatted] + } + + object FunctionFormatter + extends SpecialFormatter + { + def apply[A](tpe: Type, simple: String, args: List[A], + formattedArgs: => List[Formatted], top: Boolean, + rec: A => Boolean => Formatted) = { + if (simple.startsWith("Function")) + Some(FunctionForm.fromArgs(formattedArgs, top)) + else None + } + + def diff(left: Type, right: Type, top: Boolean) = None + } + + object TupleFormatter + extends SpecialFormatter + { + def apply[A](tpe: Type, simple: String, args: List[A], + formattedArgs: => List[Formatted], top: Boolean, + rec: A => Boolean => Formatted) = { + if (simple.startsWith("Tuple")) + Some(TupleForm(formattedArgs)) + else None + } + + def diff(left: Type, right: Type, top: Boolean) = None + } + + object SLRecordItemFormatter + extends SpecialFormatter + { + def keyTagName = "shapeless.labelled.KeyTag" + + def taggedName = "shapeless.tag.Tagged" + + def isKeyTag(tpe: Type) = tpe.typeSymbol.fullName == keyTagName + + def isTagged(tpe: Type) = tpe.typeSymbol.fullName == taggedName + + object extractRecord + { + def unapply(tpe: Type) = tpe match { + case RefinedType(actual :: key :: Nil, _) if isKeyTag(key) => + Some((actual, key)) + case _ => None + } + } + + object extractStringConstant + { + def unapply(tpe: Type) = tpe match { + case ConstantType(Constant(a: String)) => Some(a) + case _ => None + } + } + + def formatConstant(tag: String): PartialFunction[Type, String] = { + case a if a == typeOf[scala.Symbol] => + s"'$tag" + } + + def formatKeyArg: PartialFunction[List[Type], Option[Formatted]] = { + case RefinedType(parents, _) :: _ :: Nil => + for { + main <- parents.headOption + tagged <- parents.find(isTagged) + headArg <- tagged.typeArgs.headOption + tag <- extractStringConstant.unapply(headArg) + repr <- formatConstant(tag).lift(main) + } yield Simple(repr) + case extractStringConstant(tag) :: _ :: Nil => + Some(Simple(s""""$tag"""")) + case tag :: _ :: Nil => + Some(formatType(tag, true)) + } + + def formatKey(tpe: Type): Formatted = { + formatKeyArg.lift(tpe.typeArgs).flatten getOrElse formatType(tpe, true) + } + + def recordItem(actual: Type, key: Type) = + SLRecordItem(formatKey(key), formatType(actual, true)) + + def apply[A](tpe: Type, simple: String, args: List[A], + formattedArgs: => List[Formatted], top: Boolean, + rec: A => Boolean => Formatted) = { + tpe match { + case extractRecord(actual, key) => + Some(recordItem(actual, key)) + case _ => + None + } + } + + def diff(left: Type, right: Type, top: Boolean) = { + (left -> right) match { + case (extractRecord(a1, k1), extractRecord(a2, k2)) => + val rec = (l: Formatted, r: Formatted) => (t: Boolean) => + if (l == r) l else Diff(l, r) + val recT = rec.tupled + val left = formatKey(k1) -> formatKey(k2) + val right = formatType(a1, true) -> formatType(a2, true) + Some(formatInfix("->>", left, right, top, recT)) + case _ => None + } + } + } +} + +trait SplainFormatting +extends SplainFormatters +{ self: Analyzer => + import global._ + + def splainSettingBreakInfix: Option[Int] = { + val value = settings.YsplainBreakInfix.value + if (value == 0) None else Some(value) + } + + def splainSettingTruncRefined: Option[Int] = { + val value = settings.YsplainTruncRefined.value + if (value == 0) None else Some(value) + } + + def formatMsg(msg: Message, tpe: Type): String = + msg.formatDefSiteMessage(tpe) + + implicit def colors = + if(settings.splainSettingNoColor) StringColors.noColor + else StringColors.color + + def dealias(tpe: Type) = + if (isAux(tpe)) tpe + else { + val actual = tpe match { + case ExistentialType(_, t) => t + case _ => tpe + } + actual.dealias + } + + def extractArgs(tpe: Type) = { + tpe match { + case PolyType(params, result) => + result.typeArgs.map { + case t if params.contains(t.typeSymbol) => WildcardType + case a => a + } + case t: AliasTypeRef if !isAux(tpe) => + t.betaReduce.typeArgs.map(a => if (a.typeSymbolDirect.isTypeParameter) WildcardType else a) + case _ => tpe.typeArgs + } + } + + def isRefined(tpe: Type) = tpe.dealias match { + case RefinedType(_, _) => true + case _ => false + } + + def isSymbolic(tpe: Type) = { + val n = tpe.typeConstructor.typeSymbol.name + !isRefined(tpe) && (n.encodedName.toString != n.decodedName.toString) + } + + def ctorNames(tpe: Type): List[String] = + scala.util.Try(tpe.typeConstructor.toString) + .map(_.split('.').toList) + .getOrElse(List(tpe.toString)) + + def isAux(tpe: Type) = ctorNames(tpe).lastOption.contains("Aux") + + def formatRefinement(sym: Symbol) = { + if (sym.hasRawInfo) { + val rhs = showType(sym.rawInfo) + s"$sym = $rhs" + } + else sym.toString + } + + def formatAuxSimple(tpe: Type) = { + val names = ctorNames(tpe) + val num = if (names.lift(names.length - 2).contains("Case")) 3 else 2 + ctorNames(tpe) takeRight num mkString "." + } + + def formatNormalSimple(tpe: Type) = + tpe match { + case RefinedType(parents, decls) => + val simple = parents map showType mkString " with " + val refine = decls map formatRefinement mkString "; " + val refine1 = splainSettingTruncRefined.collect { case len if (refine.length > len) => "..." }.getOrElse(refine) + s"$simple {$refine1}" + case a @ WildcardType => a.toString + case a => + val sym = if (a.takesTypeArgs) a.typeSymbolDirect else a.typeSymbol + val name = sym.name.decodedName.toString + if (sym.isModuleClass) s"$name.type" + else name + } + + def formatSimpleType(tpe: Type): String = { + if (isAux(tpe)) formatAuxSimple(tpe) + else formatNormalSimple(tpe) + } + + def indentLine(line: String, n: Int = 1, prefix: String = " ") = (prefix * n) + line + + def indent(lines: List[String], n: Int = 1, prefix: String = " ") = lines map (indentLine(_, n, prefix)) + + /** + * If the args of an applied type constructor are multiline, create separate + * lines for the constructor name and the closing bracket; else return a + * single line. + */ + def showTypeApply + (cons: String, args: List[TypeRepr], break: Boolean) + : TypeRepr = { + val flatArgs = bracket(args map (_.flat)) + val flat = FlatType(s"$cons$flatArgs") + def brokenArgs = args match { + case head :: tail => + tail.foldLeft(head.lines)((z, a) => z ::: "," :: a.lines) + case _ => Nil + } + def broken = BrokenType(s"$cons[" :: indent(brokenArgs) ::: List("]")) + if (break) decideBreak(flat, broken) else flat + } + + def showTuple(args: List[String]) = + args match { + case head :: Nil => head + case _ => args.mkString("(", ",", ")") + } + + def showSLRecordItem(key: Formatted, value: Formatted) = { + FlatType( + s"(${showFormattedNoBreak(key)} ->> ${showFormattedNoBreak(value)})") + } + + def bracket[A](params: List[A]) = params.mkString("[", ", ", "]") + + def formatFunction(args: List[String]) = { + val (params, returnt) = args.splitAt(args.length - 1) + s"${showTuple(params)} => ${showTuple(returnt)}" + } + + def decideBreak(flat: FlatType, broken: => BrokenType) + : TypeRepr = { + splainSettingBreakInfix + .flatMap { maxlen => + if (flat.length > maxlen) Some(broken) + else None + } + .getOrElse(flat) + } + + /** + * Turn a nested infix type structure into a flat list + * ::[A, ::[B, C]]] => List(A, ::, B, ::, C) + */ + def flattenInfix(tpe: Infix): List[Formatted] = { + def step(tpe: Formatted): List[Formatted] = tpe match { + case Infix(infix, left, right, top) => + left :: infix :: step(right) + case a => List(a) + } + step(tpe) + } + + /** + * Break a list produced by [[flattenInfix]] into lines by taking two + * elements at a time, then appending the terminal. + * If the expression's length is smaller than the threshold specified via + * plugin parameter, return a single line. + */ + def breakInfix(types: List[Formatted]): TypeRepr = { + val form = types map showFormattedLBreak + def broken: List[String] = form + .sliding(2, 2) + .toList + .flatMap { + case left :: right :: Nil => + (left, right) match { + case (FlatType(tpe), FlatType(infix)) => + List(s"$tpe $infix") + case _ => left.lines ++ right.lines + } + case last :: Nil => last.lines + // for exhaustiveness, cannot be reached + case l => l.flatMap(_.lines) + } + val flat = FlatType(form.flatMap(_.lines) mkString " ") + decideBreak(flat, BrokenType(broken)) + } + + val showFormattedLCache = FormatCache[(Formatted, Boolean), TypeRepr] + + def showFormattedLImpl(tpe: Formatted, break: Boolean): TypeRepr = + tpe match { + case Simple(a) => FlatType(a) + case Applied(cons, args) => + val reprs = args map (showFormattedL(_, break)) + showTypeApply(showFormattedNoBreak(cons), reprs, break) + case tpe @ Infix(infix, left, right, top) => + val flat = flattenInfix(tpe) + val broken: TypeRepr = + if (break) breakInfix(flat) + else FlatType(flat map showFormattedNoBreak mkString " ") + wrapParensRepr(broken, top) + case UnitForm => FlatType("Unit") + case FunctionForm(args, ret, top) => + val a = showTuple(args map showFormattedNoBreak) + val r = showFormattedNoBreak(ret) + FlatType(wrapParens(s"$a => $r", top)) + case TupleForm(elems) => + FlatType(showTuple(elems map showFormattedNoBreak)) + case SLRecordItem(key, value) => + showSLRecordItem(key, value) + case Diff(left, right) => + val l = showFormattedNoBreak(left) + val r = showFormattedNoBreak(right) + FlatType(s"${l.red}|${r.green}") + } + + def showFormattedL(tpe: Formatted, break: Boolean): TypeRepr = { + val key = (tpe, break) + showFormattedLCache(key, showFormattedLImpl(tpe, break)) + } + + def showFormattedLBreak(tpe: Formatted) = showFormattedL(tpe, true) + + def showFormattedLNoBreak(tpe: Formatted) = showFormattedL(tpe, false) + + def showFormatted(tpe: Formatted, break: Boolean): String = + showFormattedL(tpe, break).joinLines + + def showFormattedNoBreak(tpe: Formatted) = + showFormattedLNoBreak(tpe).tokenize + + def showType(tpe: Type): String = showFormatted(formatType(tpe, true), false) + + def showTypeBreak(tpe: Type): String = showFormatted(formatType(tpe, true), true) + + def showTypeBreakL(tpe: Type): List[String] = showFormattedL(formatType(tpe, true), true).lines + + def wrapParens(expr: String, top: Boolean) = + if (top) expr else s"($expr)" + + def wrapParensRepr(tpe: TypeRepr, top: Boolean): TypeRepr = { + tpe match { + case FlatType(tpe) => FlatType(wrapParens(tpe, top)) + case BrokenType(lines) => + if (top) tpe else BrokenType("(" :: indent(lines) ::: List(")")) + } + } + + val specialFormatters: List[SpecialFormatter] = + List( + FunctionFormatter, + TupleFormatter, + SLRecordItemFormatter + ) + + def formatSpecial[A](tpe: Type, simple: String, args: List[A], formattedArgs: => List[Formatted], top: Boolean, + rec: A => Boolean => Formatted) + : Option[Formatted] = { + specialFormatters + .map(_.apply(tpe, simple, args, formattedArgs, top, rec)) + .collectFirst { case Some(a) => a } + .headOption + } + + def formatInfix[A](simple: String, left: A, right: A, top: Boolean, rec: A => Boolean => Formatted) = { + val l = rec(left)(false) + val r = rec(right)(false) + Infix(Simple(simple), l, r, top) + } + + def formatWithInfix[A](tpe: Type, args: List[A], top: Boolean, rec: A => Boolean => Formatted): Formatted = { + val simple = formatSimpleType(tpe) + lazy val formattedArgs = args.map(rec(_)(true)) + formatSpecial(tpe, simple, args, formattedArgs, top, rec) getOrElse { + args match { + case left :: right :: Nil if isSymbolic(tpe) => + formatInfix(simple, left, right, top, rec) + case _ :: _ => + Applied(Simple(simple), formattedArgs) + case _ => + Simple(simple) + } + } + } + + def formatTypeImpl(tpe: Type, top: Boolean): Formatted = { + val dtpe = dealias(tpe) + val rec = (tp: Type) => (t: Boolean) => formatType(tp, t) + if (settings.splainSettingNoInfix) Simple(dtpe.toLongString) + else formatWithInfix(dtpe, extractArgs(dtpe), top, rec) + } + + val formatTypeCache = FormatCache[(Type, Boolean), Formatted] + + def formatType(tpe: Type, top: Boolean): Formatted = { + val key = (tpe, top) + formatTypeCache(key, formatTypeImpl(tpe, top)) + } + + def formatDiffInfix(left: Type, right: Type, top: Boolean) = { + val rec = (l: Type, r: Type) => (t: Boolean) => formatDiff(l, r, t) + val recT = rec.tupled + val args = extractArgs(left) zip extractArgs(right) + formatWithInfix(left, args, top, recT) + } + + def formatDiffSpecial(left: Type, right: Type, top: Boolean) = { + specialFormatters.map(_.diff(left, right, top)) + .collectFirst { case Some(a) => a } + .headOption + } + + def formatDiffSimple(left: Type, right: Type) = { + val l = formatType(left, true) + val r = formatType(right, true) + Diff(l, r) + } + + def formatDiffImpl(found: Type, req: Type, top: Boolean): Formatted = { + val (left, right) = dealias(found) -> dealias(req) + if (left =:= right) + formatType(left, top) + else if (left.typeSymbol == right.typeSymbol) + formatDiffInfix(left, right, top) + else + formatDiffSpecial(left, right, top) getOrElse + formatDiffSimple(left, right) + } + + val formatDiffCache = FormatCache[(Type, Type, Boolean), Formatted] + + def formatDiff(left: Type, right: Type, top: Boolean) = { + val key = (left, right, top) + formatDiffCache(key, formatDiffImpl(left, right, top)) + } + + // TODO split non conf bounds + def formatNonConfBounds(err: ImplicitErrorSpecifics.NonconformantBounds): List[String] = { + val params = bracket(err.tparams.map(_.defString)) + val tpes = bracket(err.targs map showType) + List("nonconformant bounds;", tpes.red, params.green) + } + + def formatNestedImplicit(err: ImplicitError): (String, List[String], Int) = { + val candidate = ImplicitError.cleanCandidate(err) + val problem = s"${candidate.red} invalid because" + val reason = err.specifics match { + case e: ImplicitErrorSpecifics.NotFound => implicitMessage(e.param) + case e: ImplicitErrorSpecifics.NonconformantBounds => formatNonConfBounds(e) + } + (problem, reason, err.nesting) + } + + def hideImpError(error: ImplicitError): Boolean = + (ImplicitError.candidateName(error).toString == "mkLazy") || (settings.splainSettingNoBoundsImplicits && ( + error.specifics match { + case ImplicitErrorSpecifics.NonconformantBounds(_, _, _) => true + case ImplicitErrorSpecifics.NotFound(_) => false + } + )) + + def indentTree(tree: List[(String, List[String], Int)], baseIndent: Int): List[String] = { + val nestings = tree.map(_._3).distinct.sorted + tree + .flatMap { + case (head, tail, nesting) => + val ind = baseIndent + nestings.indexOf(nesting).abs + indentLine(head, ind, "――") :: indent(tail, ind) + } + } + + def formatIndentTree(chain: List[ImplicitError], baseIndent: Int) = { + val formatted = chain map formatNestedImplicit + indentTree(formatted, baseIndent) + } + + def deepestLevel(chain: List[ImplicitError]) = { + chain.foldLeft(0)((z, a) => if (a.nesting > z) a.nesting else z) + } + + def formatImplicitChainTreeCompact(chain: List[ImplicitError]): Option[List[String]] = { + chain + .headOption + .map { head => + val max = deepestLevel(chain) + val leaves = chain.drop(1).dropWhile(_.nesting < max) + val base = if (head.nesting == 0) 0 else 1 + val (fhh, fht, fhn) = formatNestedImplicit(head) + val spacer = if (leaves.nonEmpty && leaves.length < chain.length) List("⋮".blue) else Nil + val fh = (fhh, fht ++ spacer, fhn) + val ft = leaves map formatNestedImplicit + indentTree(fh :: ft, base) + } + } + + def formatImplicitChainTreeFull(chain: List[ImplicitError]): List[String] = { + val baseIndent = chain.headOption.map(_.nesting).getOrElse(0) + formatIndentTree(chain, baseIndent) + } + + def formatImplicitChainFlat(chain: List[ImplicitError]): List[String] = { + chain map formatNestedImplicit flatMap { case (h, t, _) => h :: t } + } + + def formatImplicitChainTree(chain: List[ImplicitError]): List[String] = { + val compact = if (settings.splainSettingVerboseTree) None else formatImplicitChainTreeCompact(chain) + compact getOrElse formatImplicitChainTreeFull(chain) + } + + def formatImplicitChain(chain: List[ImplicitError]): List[String] = { + if (settings.splainSettingNoTree) formatImplicitChainFlat(chain) + else formatImplicitChainTree(chain) + } + + /** + * Remove duplicates and special cases that should not be shown. + * In some cases, candidates are reported twice, once as `Foo.f` and once as + * `f`. `ImplicitError.equals` checks the simple names for identity, which + * is suboptimal, but works for 99% of cases. + * Special cases are handled in [[hideImpError]] + */ + def formatNestedImplicits(errors: List[ImplicitError]) = { + val visible = errors filterNot hideImpError + val chains = splitChains(visible).map(_.distinct).distinct + chains map formatImplicitChain flatMap ("" :: _) drop 1 + } + + def formatImplicitParam(sym: Symbol) = sym.name.toString + + def overrideMessage(msg: String): Option[String] = { + if (msg.startsWith(Messages.lazyderiv)) None + else Some(msg) + } + + def effectiveImplicitType(tpe: Type) = { + if (tpe.typeSymbol.name.toString == "Lazy") + tpe.typeArgs.headOption.getOrElse(tpe) + else tpe + } + + def implicitMessage(param: Symbol): List[String] = { + val tpe = param.tpe + val msg = tpe.typeSymbolDirect match { + case ImplicitNotFoundMsg(msg) => + overrideMessage(formatMsg(msg, tpe)) + .map(a => s" ($a)") + .getOrElse("") + case _ => "" + } + val effTpe = effectiveImplicitType(tpe) + val paramName = formatImplicitParam(param) + val bang = "!" + val i = "I" + val head = s"${bang.red}${i.blue} ${paramName.yellow}$msg:" + showTypeBreakL(effTpe) match { + case single :: Nil => List(s"$head ${single.green}") + case l => head :: indent(l).map(_.green) + } + } + + def splitChains(errors: List[ImplicitError]): List[List[ImplicitError]] = { + errors.foldRight(Nil: List[List[ImplicitError]]) { + case (a, chains @ ((chain @ (prev :: _)) :: tail)) => + if (a.nesting > prev.nesting) List(a) :: chains + else (a :: chain) :: tail + case (a, _) => + List(List(a)) + } + } + + def formatImplicitError(param: Symbol, errors: List[ImplicitError]) = { + val stack = formatNestedImplicits(errors) + val nl = if (errors.nonEmpty) "\n" else "" + val ex = stack.mkString("\n") + val pre = "implicit error;\n" + val msg = implicitMessage(param).mkString("\n") + s"$pre$msg$nl$ex" + } + + def cacheStats = { + val sfl = showFormattedLCache.stats + val ft = formatTypeCache.stats + val df = formatDiffCache.stats + s"showFormatted -> $sfl, formatType -> $ft, formatDiff -> $df" + } +} diff --git a/test/files/run/splain.check b/test/files/run/splain.check new file mode 100644 index 000000000000..38bd9ff9299f --- /dev/null +++ b/test/files/run/splain.check @@ -0,0 +1,22 @@ +warning: 1 deprecation (since 2.13.0); re-run with -deprecation for details +newSource1.scala:13: error: implicit error; +!I e: II +ImplicitChain.g invalid because +!I impPar3: I1 +⋮ +――ImplicitChain.i1 invalid because + !I impPar7: I3 + implicitly[II] + ^ +newSource1.scala:6: error: type mismatch; + L|R + f(new L) + ^ +newSource1.scala:7: error: implicit error; +!I e: F[Arg] +Bounds.g invalid because +nonconformant bounds; +[Arg, Nothing] +[A <: Bounds.Base, B] + implicitly[F[Arg]] + ^ diff --git a/test/files/run/splain.scala b/test/files/run/splain.scala new file mode 100644 index 000000000000..7231ea31f492 --- /dev/null +++ b/test/files/run/splain.scala @@ -0,0 +1,53 @@ +import scala.tools.partest._ + +object Test +extends DirectTest +{ + override def extraSettings: String = "-usejavacp -Yexplain-implicits:enable -Yexplain-implicits:no-color" + + def code = """ +object ImplicitChain +{ + trait I1 + trait I2 + trait I3 + trait I4 + trait II + implicit def i1(implicit impPar7: I3): I1 = ??? + implicit def i2a(implicit impPar8: I3): I2 = ??? + implicit def i2b(implicit impPar8: I3): I2 = ??? + implicit def i4(implicit impPar9: I2): I4 = ??? + implicit def g(implicit impPar3: I1, impPar1: I4): II = ??? + implicitly[II] +} + """.trim + + def foundReq = """ +object FoundReq +{ + class L + type R + def f(r: R): Int = ??? + f(new L) +} + """.trim + + def bounds = """ +object Bounds +{ + trait Base + trait Arg + trait F[A] + implicit def g[A <: Base, B]: F[A] = ??? + implicitly[F[Arg]] +} + """.trim + + + def show() { + val global = newCompiler() + compileString(global)(code) + compileString(global)(foundReq) + compileString(global)(bounds) + } +}