aboutsummaryrefslogtreecommitdiff
path: root/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala
blob: 91e65ab66d302cbcbc54af47df96ab4351e9e9f6 (plain)
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
package dotty.tools
package dotc
package reporting

import core.Contexts.Context
import core.Decorators._
import printing.Highlighting.{Blue, Red}
import printing.SyntaxHighlighting
import diagnostic.{ErrorMessageID, Message, MessageContainer, NoExplanation}
import diagnostic.messages._
import util.SourcePosition
import util.Chars.{ LF, CR, FF, SU }
import scala.annotation.switch

import scala.collection.mutable

trait MessageRendering {
  /** Remove ANSI coloring from `str`, useful for getting real length of
    * strings
    *
    * @return string stripped of ANSI escape codes
    */
  def stripColor(str: String): String =
    str.replaceAll("\u001b\\[.*?m", "")

  /** When inlining a method call, if there's an error we'd like to get the
    * outer context and the `pos` at which the call was inlined.
    *
    * @return a list of strings with inline locations
    */
  def outer(pos: SourcePosition, prefix: String)(implicit ctx: Context): List[String] =
    if (pos.outer.exists) {
       s"$prefix| This location is in code that was inlined at ${pos.outer}" ::
       outer(pos.outer, prefix)
    } else Nil

  /** Get the sourcelines before and after the position, as well as the offset
    * for rendering line numbers
    *
    * @return (lines before error, lines after error, line numbers offset)
    */
  def sourceLines(pos: SourcePosition)(implicit ctx: Context): (List[String], List[String], Int) = {
    var maxLen = Int.MinValue
    def render(offsetAndLine: (Int, String)): String = {
      val (offset, line) = offsetAndLine
      val lineNbr = pos.source.offsetToLine(offset)
      val prefix = s"${lineNbr + 1} |"
      maxLen = math.max(maxLen, prefix.length)
      val lnum = Red(" " * math.max(0, maxLen - prefix.length) + prefix).show
      lnum + line.stripLineEnd
    }

    def linesFrom(arr: Array[Char]): List[String] = {
      def pred(c: Char) = (c: @switch) match {
        case LF | CR | FF | SU => true
        case _ => false
      }
      val (line, rest0) = arr.span(!pred(_))
      val (_, rest) = rest0.span(pred)
      new String(line) :: { if (rest.isEmpty) Nil else linesFrom(rest) }
    }

    val syntax =
      if (ctx.settings.color.value != "never")
        SyntaxHighlighting(pos.linesSlice).toArray
      else pos.linesSlice
    val lines = linesFrom(syntax)
    val (before, after) = pos.beforeAndAfterPoint

    (
      before.zip(lines).map(render),
      after.zip(lines.drop(before.length)).map(render),
      maxLen
    )
  }

  /** The column markers aligned under the error */
  def columnMarker(pos: SourcePosition, offset: Int)(implicit ctx: Context): String = {
    val prefix = " " * (offset - 1)
    val whitespace = " " * pos.startColumn
    val carets = Red {
      if (pos.startLine == pos.endLine)
        "^" * math.max(1, pos.endColumn - pos.startColumn)
      else "^"
    }

    s"$prefix|$whitespace${carets.show}"
  }

  /** The error message (`msg`) aligned under `pos`
    *
    * @return aligned error message
    */
  def errorMsg(pos: SourcePosition, msg: String, offset: Int)(implicit ctx: Context): String = {
    val leastWhitespace = msg.lines.foldLeft(Int.MaxValue) { (minPad, line) =>
      val lineLength = stripColor(line).length
      val currPad = math.min(
        math.max(0, ctx.settings.pageWidth.value - offset - lineLength),
        offset + pos.startColumn
      )

      math.min(currPad, minPad)
    }

    msg.lines
      .map { line => " " * (offset - 1) + "|" + (" " * (leastWhitespace - offset)) + line}
      .mkString(sys.props("line.separator"))
  }

  /** The separator between errors containing the source file and error type
    *
    * @return separator containing error location and kind
    */
  def posStr(pos: SourcePosition, diagnosticLevel: String, message: Message)(implicit ctx: Context): String =
    if (pos.exists) Blue({
      val file = pos.source.file.toString
      val errId =
        if (message.errorId ne ErrorMessageID.NoExplanationID) {
          val errorNumber = message.errorId.errorNumber()
          s"[E${"0" * (3 - errorNumber.toString.length) + errorNumber}] "
        } else ""
      val kind =
        if (message.kind == "") diagnosticLevel
        else s"${message.kind} $diagnosticLevel"
      val prefix = s"-- ${errId}${kind}: $file "

      prefix +
        ("-" * math.max(ctx.settings.pageWidth.value - stripColor(prefix).length, 0))
    }).show else ""

  /** Explanation rendered under "Explanation" header */
  def explanation(m: Message)(implicit ctx: Context): String = {
    val sb = new StringBuilder(
      hl"""|
           |${Blue("Explanation")}
           |${Blue("===========")}"""
    )
    sb.append('\n').append(m.explanation)
    if (m.explanation.lastOption != Some('\n')) sb.append('\n')
    sb.toString
  }

  /** The whole message rendered from `msg` */
  def messageAndPos(msg: Message, pos: SourcePosition, diagnosticLevel: String)(implicit ctx: Context): String = {
    val sb = mutable.StringBuilder.newBuilder
    sb.append(posStr(pos, diagnosticLevel, msg)).append('\n')
    if (pos.exists) {
      val (srcBefore, srcAfter, offset) = sourceLines(pos)
      val marker = columnMarker(pos, offset)
      val err = errorMsg(pos, msg.msg, offset)
      sb.append((srcBefore ::: marker :: err :: outer(pos, " " * (offset - 1)) ::: srcAfter).mkString("\n"))
    } else sb.append(msg.msg)
    sb.toString
  }

  def diagnosticLevel(cont: MessageContainer): String =
    cont match {
      case m: Error => "Error"
      case m: FeatureWarning => "Feature Warning"
      case m: DeprecationWarning => "Deprecation Warning"
      case m: UncheckedWarning => "Unchecked Warning"
      case m: MigrationWarning => "Migration Warning"
      case m: Warning => "Warning"
      case m: Info => "Info"
    }
}