dotty.tools.dotc.printing.Formatting.scala Maven / Gradle / Ivy
The newest version!
package dotty.tools.dotc
package printing
import core._
import Texts._, Types._, Flags._, Symbols._, Contexts._
import collection.mutable
import Decorators._
import scala.util.control.NonFatal
import reporting.diagnostic.MessageContainer
import util.DiffUtil
import Highlighting._
object Formatting {
/** General purpose string formatter, with the following features:
*
* 1) On all Showables, `show` is called instead of `toString`
* 2) Exceptions raised by a `show` are handled by falling back to `toString`.
* 3) Sequences can be formatted using the desired separator between two `%` signs,
* eg `i"myList = (${myList}%, %)"`
* 4) Safe handling of multi-line margins. Left margins are skipped om the parts
* of the string context *before* inserting the arguments. That way, we guard
* against accidentally treating an interpolated value as a margin.
*/
class StringFormatter(protected val sc: StringContext) {
protected def showArg(arg: Any)(implicit ctx: Context): String = arg match {
case arg: Showable =>
try arg.show
catch {
case ex: CyclicReference => "... (caught cyclic reference) ..."
case NonFatal(ex)
if !ctx.mode.is(Mode.PrintShowExceptions) &&
!ctx.settings.YshowPrintErrors.value =>
val msg = ex match { case te: TypeError => te.toMessage case _ => ex.getMessage }
s"[cannot display due to $msg, raw string = ${arg.toString}]"
}
case _ => arg.toString
}
private def treatArg(arg: Any, suffix: String)(implicit ctx: Context): (Any, String) = arg match {
case arg: Seq[_] if suffix.nonEmpty && suffix.head == '%' =>
val (rawsep, rest) = suffix.tail.span(_ != '%')
val sep = StringContext.treatEscapes(rawsep)
if (rest.nonEmpty) (arg.map(showArg).mkString(sep), rest.tail)
else (arg, suffix)
case _ =>
(showArg(arg), suffix)
}
def assemble(args: Seq[Any])(implicit ctx: Context): String = {
def isLineBreak(c: Char) = c == '\n' || c == '\f' // compatible with StringLike#isLineBreak
def stripTrailingPart(s: String) = {
val (pre, post) = s.span(c => !isLineBreak(c))
pre ++ post.stripMargin
}
val (prefix, suffixes) = sc.parts.toList match {
case head :: tail => (head.stripMargin, tail map stripTrailingPart)
case Nil => ("", Nil)
}
val (args1, suffixes1) = (args, suffixes).zipped.map(treatArg(_, _)).unzip
new StringContext(prefix :: suffixes1.toList: _*).s(args1: _*)
}
}
/** The `em` string interpolator works like the `i` string interpolator, but marks nonsensical errors
* using `... ` tags.
* Note: Instead of these tags, it would be nicer to return a data structure containing the message string
* and a boolean indicating whether the message is sensical, but then we cannot use string operations
* like concatenation, stripMargin etc on the values returned by em"...", and in the current error
* message composition methods, this is crucial.
*/
class ErrorMessageFormatter(sc: StringContext) extends StringFormatter(sc) {
override protected def showArg(arg: Any)(implicit ctx: Context): String =
wrapNonSensical(arg, super.showArg(arg))
}
class SyntaxFormatter(sc: StringContext) extends StringFormatter(sc) {
override protected def showArg(arg: Any)(implicit ctx: Context): String =
arg match {
case hl: Highlight =>
hl.show
case hb: HighlightBuffer =>
hb.toString
case _ =>
SyntaxHighlighting.highlight(super.showArg(arg))
}
}
private def wrapNonSensical(arg: Any, str: String)(implicit ctx: Context): String = {
import MessageContainer._
def isSensical(arg: Any): Boolean = arg match {
case tpe: Type =>
tpe.exists && !tpe.isErroneous
case sym: Symbol if sym.isCompleted =>
sym.info match {
case _: ErrorType | TypeAlias(_: ErrorType) | NoType => false
case _ => true
}
case _ => true
}
if (isSensical(arg)) str
else nonSensicalStartTag + str + nonSensicalEndTag
}
private type Recorded = Symbol | ParamRef | SkolemType
private case class SeenKey(str: String, isType: Boolean)
private class Seen extends mutable.HashMap[SeenKey, List[Recorded]] {
override def default(key: SeenKey) = Nil
def record(str: String, isType: Boolean, entry: Recorded)(implicit ctx: Context): String = {
/** If `e1` is an alias of another class of the same name, return the other
* class symbol instead. This normalization avoids recording e.g. scala.List
* and scala.collection.immutable.List as two different types
*/
def followAlias(e1: Recorded): Recorded = e1 match {
case e1: Symbol if e1.isAliasType =>
val underlying = e1.typeRef.underlyingClassRef(refinementOK = false).typeSymbol
if (underlying.name == e1.name) underlying else e1
case _ => e1
}
val key = SeenKey(str, isType)
val existing = apply(key)
lazy val dealiased = followAlias(entry)
// alts: The alternatives in `existing` that are equal, or follow (an alias of) `entry`
var alts = existing.dropWhile(alt => dealiased ne followAlias(alt))
if (alts.isEmpty) {
alts = entry :: existing
update(key, alts)
}
str + "'" * (alts.length - 1)
}
}
private class ExplainingPrinter(seen: Seen)(_ctx: Context) extends RefinedPrinter(_ctx) {
/** True if printer should a source module instead of its module class */
private def useSourceModule(sym: Symbol): Boolean =
sym.is(ModuleClass, butNot = Package) && sym.sourceModule.exists && !_ctx.settings.YdebugNames.value
override def simpleNameString(sym: Symbol): String =
if (useSourceModule(sym)) simpleNameString(sym.sourceModule)
else seen.record(super.simpleNameString(sym), sym.isType, sym)
override def ParamRefNameString(param: ParamRef): String =
seen.record(super.ParamRefNameString(param), param.isInstanceOf[TypeParamRef], param)
override def toTextRef(tp: SingletonType): Text = tp match {
case tp: SkolemType => seen.record(tp.repr.toString, isType = true, tp)
case _ => super.toTextRef(tp)
}
override def toText(tp: Type): Text = tp match {
case tp: TypeRef if useSourceModule(tp.symbol) => Str("object ") ~ super.toText(tp)
case _ => super.toText(tp)
}
}
/** Create explanation for single `Recorded` type or symbol */
def explanation(entry: AnyRef)(implicit ctx: Context): String = {
def boundStr(bound: Type, default: ClassSymbol, cmp: String) =
if (bound.isRef(default)) "" else i"$cmp $bound"
def boundsStr(bounds: TypeBounds): String = {
val lo = boundStr(bounds.lo, defn.NothingClass, ">:")
val hi = boundStr(bounds.hi, defn.AnyClass, "<:")
if (lo.isEmpty) hi
else if (hi.isEmpty) lo
else s"$lo and $hi"
}
def addendum(cat: String, info: Type): String = info match {
case bounds @ TypeBounds(lo, hi) if bounds ne TypeBounds.empty =>
if (lo eq hi) i" which is an alias of $lo"
else i" with $cat ${boundsStr(bounds)}"
case _ =>
""
}
entry match {
case param: TypeParamRef =>
s"is a type variable${addendum("constraint", ctx.typeComparer.bounds(param))}"
case param: TermParamRef =>
s"is a reference to a value parameter"
case sym: Symbol =>
val info =
if (ctx.gadt.contains(sym))
sym.info & ctx.gadt.fullBounds(sym)
else
sym.info
s"is a ${ctx.printer.kindString(sym)}${sym.showExtendedLocation}${addendum("bounds", info)}"
case tp: SkolemType =>
s"is an unknown value of type ${tp.widen.show}"
}
}
/** Turns a `Seen` into a `String` to produce an explanation for types on the
* form `where: T is...`
*
* @return string disambiguating types
*/
private def explanations(seen: Seen)(implicit ctx: Context): String = {
def needsExplanation(entry: Recorded) = entry match {
case param: TypeParamRef => ctx.typerState.constraint.contains(param)
case param: ParamRef => false
case skolem: SkolemType => true
case sym: Symbol =>
ctx.gadt.contains(sym) && ctx.gadt.fullBounds(sym) != TypeBounds.empty
}
val toExplain: List[(String, Recorded)] = seen.toList.flatMap { kvs =>
val res: List[(String, Recorded)] = kvs match {
case (key, entry :: Nil) =>
if (needsExplanation(entry)) (key.str, entry) :: Nil else Nil
case (key, entries) =>
for (alt <- entries) yield {
val tickedString = seen.record(key.str, key.isType, alt)
(tickedString, alt)
}
}
res // help the inferrencer out
}.sortBy(_._1)
def columnar(parts: List[(String, String)]): List[String] = {
lazy val maxLen = parts.map(_._1.length).max
parts.map {
case (leader, trailer) =>
val variable = hl(leader)
s"""$variable${" " * (maxLen - leader.length)} $trailer"""
}
}
val explainParts = toExplain.map { case (str, entry) => (str, explanation(entry)) }
val explainLines = columnar(explainParts)
if (explainLines.isEmpty) "" else i"where: $explainLines%\n %\n"
}
/** Context with correct printer set for explanations */
private def explainCtx(seen: Seen)(implicit ctx: Context): Context = ctx.printer match {
case dp: ExplainingPrinter =>
ctx // re-use outer printer and defer explanation to it
case _ => ctx.fresh.setPrinterFn(ctx => new ExplainingPrinter(seen)(ctx))
}
/** Entrypoint for explanation string interpolator:
*
* ```
* ex"disambiguate $tpe1 and $tpe2"
* ```
*/
def explained(op: Context => String)(implicit ctx: Context): String = {
val seen = new Seen
val msg = op(explainCtx(seen))
val addendum = explanations(seen)
if (addendum.isEmpty) msg else msg ++ "\n\n" ++ addendum
}
/** When getting a type mismatch it is useful to disambiguate placeholders like:
*
* ```
* found: List[Int]
* required: List[T]
* where: T is a type in the initializer of value s which is an alias of
* String
* ```
*
* @return the `where` section as well as the printing context for the
* placeholders - `("T is a...", printCtx)`
*/
def disambiguateTypes(args: Type*)(implicit ctx: Context): (String, Context) = {
val seen = new Seen
val printCtx = explainCtx(seen)
args.foreach(_.show(printCtx)) // showing each member will put it into `seen`
(explanations(seen), printCtx)
}
/** This method will produce a colored type diff from the given arguments.
* The idea is to do this for known cases that are useful and then fall back
* on regular syntax highlighting for the cases which are unhandled.
*
* Please not that if used in combination with `disambiguateTypes` the
* correct `Context` for printing should also be passed when calling the
* method.
*
* @return the (found, expected, changePercentage) with coloring to
* highlight the difference
*/
def typeDiff(found: Type, expected: Type)(implicit ctx: Context): (String, String) = {
val fnd = wrapNonSensical(found, found.show)
val exp = wrapNonSensical(expected, expected.show)
DiffUtil.mkColoredTypeDiff(fnd, exp) match {
case _ if ctx.settings.color.value == "never" => (fnd, exp)
case (fnd, exp, change) if change < 0.5 => (fnd, exp)
case _ => (fnd, exp)
}
}
/** Explicit syntax highlighting */
def hl(s: String)(implicit ctx: Context): String =
SyntaxHighlighting.highlight(s)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy