aboutsummaryrefslogtreecommitdiff
path: root/src/dotty/tools/dotc/printing/Formatting.scala
blob: b39d5683e0c389f721063c1798c70929e9cabd7d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package dotty.tools.dotc
package printing

import core._
import Texts._, Types._, Flags._, Names._, Symbols._, NameOps._, Contexts._
import collection.mutable
import collection.Map
import Decorators._
import scala.annotation.switch
import scala.util.control.NonFatal
import reporting.diagnostic.MessageContainer

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(ctx.addMode(Mode.FutureDefsOK))
        catch {
          case NonFatal(ex) => s"[cannot display due to $ex, raw string = $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 `<nonsensical>...</nonsensical>` 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 = {
      import MessageContainer._
      def isSensical(arg: Any): Boolean = arg match {
        case tpe: Type =>
          tpe.exists && !tpe.isErroneous
        case sym: Symbol if sym.isCompleted =>
          sym.info != ErrorType && sym.info != TypeAlias(ErrorType) && sym.info.exists
        case _ => true
      }
      val str = super.showArg(arg)
      if (isSensical(arg)) str
      else nonSensicalStartTag + str + nonSensicalEndTag
    }
  }

  private type Recorded = AnyRef /*Symbol | PolyParam*/

  private class Seen extends mutable.HashMap[String, List[Recorded]] {

    override def default(key: String) = Nil

    def record(str: String, entry: Recorded)(implicit ctx: Context): String = {
      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
      }
      lazy val dealiased = followAlias(entry)
      var alts = apply(str).dropWhile(alt => dealiased ne followAlias(alt))
      if (alts.isEmpty) {
        alts = entry :: apply(str)
        update(str, alts)
      }
      str + "'" * (alts.length - 1)
    }
  }

  private class ExplainingPrinter(seen: Seen)(_ctx: Context) extends RefinedPrinter(_ctx) {
    override def simpleNameString(sym: Symbol): String =
      if ((sym is ModuleClass) && sym.sourceModule.exists) simpleNameString(sym.sourceModule)
      else seen.record(super.simpleNameString(sym), sym)

    override def polyParamNameString(param: PolyParam): String =
      seen.record(super.polyParamNameString(param), param)
  }

  def explained2(op: Context => String)(implicit ctx: Context): String = {
    val seen = new Seen
    val explainCtx = 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))
    }

    def explanation(entry: Recorded): 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)(implicit ctx: Context): 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: PolyParam =>
          s"is a type variable${addendum("constraint", ctx.typeComparer.bounds(param))}"
        case sym: Symbol =>
          s"is a ${ctx.printer.kindString(sym)}${sym.showExtendedLocation}${addendum("bounds", sym.info)}"
      }
    }

    def explanations(seen: Seen)(implicit ctx: Context): String = {
      def needsExplanation(entry: Recorded) = entry match {
        case param: PolyParam => ctx.typerState.constraint.contains(param)
        case _ => false
      }
      val toExplain: List[(String, Recorded)] = seen.toList.flatMap {
        case (str, entry :: Nil) =>
          if (needsExplanation(entry)) (str, entry) :: Nil else Nil
        case (str, entries) =>
          entries.map(alt => (seen.record(str, alt), alt))
      }.sortBy(_._1)
      val explainParts = toExplain.map { case (str, entry) => (str, explanation(entry)) }
      val explainLines = columnar(explainParts, "  ")
      if (explainLines.isEmpty) "" else i"\n\nwhere  $explainLines%\n       %\n"
    }

    op(explainCtx) ++ explanations(seen)
  }

  def columnar(parts: List[(String, String)], sep: String): List[String] = {
    lazy val maxLen = parts.map(_._1.length).max
    parts.map {
      case (leader, trailer) =>
        s"$leader${" " * (maxLen - leader.length)}$sep$trailer"
    }
  }
}