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"
}
}