Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Translate java error positions into lines+columns #14

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import java.util.Optional
import java.io.File
import javax.tools.{ Diagnostic, JavaFileObject, DiagnosticListener }
import sbt.io.IO
import sbt.util.InterfaceUtil.{ o2jo, jo2o }
import sbt.util.InterfaceUtil.o2jo
import xsbti.{ Severity, Reporter }
import javax.tools.Diagnostic.NOPOS

Expand Down Expand Up @@ -45,7 +45,7 @@ final class DiagnosticsReporter(reporter: Reporter) extends DiagnosticListener[J
val msg = d.getMessage(null)
val pos: xsbti.Position = PositionImpl(d)
if (severity == Severity.Error) errorEncountered = true
reporter.log(problem("", pos, msg, severity))
reporter.log(problem("", pos, msg, severity, rendered = None))
}
}

Expand All @@ -60,23 +60,84 @@ object DiagnosticsReporter {
override val lineContent: String,
override val offset: Optional[Integer],
override val startOffset: Optional[Integer],
override val endOffset: Optional[Integer]
override val endOffset: Optional[Integer],
override val startLine: Optional[Integer],
override val startColumn: Optional[Integer],
override val endLine: Optional[Integer],
override val endColumn: Optional[Integer]
) extends xsbti.Position {
override val sourcePath: Optional[String] = o2jo(sourceUri)
override val sourceFile: Optional[File] = o2jo(sourceUri.map(new File(_)))
override val pointer: Optional[Integer] = o2jo(Option.empty[Integer])
override val pointerSpace: Optional[String] = o2jo(Option.empty[String])

override val startLine: Optional[Integer] = o2jo(Option.empty)
override val startColumn: Optional[Integer] = o2jo(Option.empty)
override val endLine: Optional[Integer] = o2jo(Option.empty)
override val endColumn: Optional[Integer] = o2jo(Option.empty)

override def toString: String =
if (sourceUri.isDefined) s"${sourceUri.get}:${if (line.isPresent) line.get else -1}"
else ""
}

/**
* VSCode documentation...
* A range in a text document expressed as (zero-based) start and end positions.
* A range is comparable to a selection in an editor.
* Therefore the end position is exclusive.
* If you want to specify a range that contains a line including the line ending character(s) then use an end position denoting the start of the next line.
* Here the lines are 1-based as ZincInternals subtracts 1 later
*/
def contentAndRanges(cc: CharSequence,
start: Long,
end: Long): (Integer, Integer, Integer, Integer, String) = {
var startPos = start.toInt
var endPos = end.toInt
val lineContent = cc.subSequence(startPos, endPos).toString
// ignore CR or LF - depending on which one isn't found
var checkForN = true
var checkForR = true
// find startLine and startColumn
var startLine = 1
var startColumn = 0
startPos = startPos - 1
while (startPos >= 0) {
val ch = cc.charAt(startPos)
if (checkForR && ch == '\r') {
startLine = startLine + 1
checkForN = false
} else if (checkForN && ch == '\n') {
startLine = startLine + 1
checkForR = false
} else if (startLine == 1)
startColumn = startColumn + 1

startPos = startPos - 1
}
// find endLine and endColumn
var endLine = startLine
var endColumn = 0
endPos = endPos - 1
while (endPos >= start) {
// mimic linefeed if at the end of the file
if (endPos == cc.length)
endLine = endLine + 1
else {
val ch = cc.charAt(endPos)
if (checkForR && ch == '\r') {
endLine = endLine + 1
checkForN = false
} else if (checkForN && ch == '\n') {
endLine = endLine + 1
checkForR = false
} else if (endLine == startLine)
endColumn = endColumn + 1
}

endPos = endPos - 1
}
if (startLine == endLine)
endColumn = endColumn + startColumn

(startLine, startColumn, endLine, endColumn, lineContent)
}

private[sbt] object PositionImpl {

/**
Expand All @@ -100,50 +161,63 @@ object DiagnosticsReporter {
}

val source: Option[JavaFileObject] = Option(d.getSource)
val sourcePath: Option[String] = source map (obj => IO.toFile(obj.toUri).getAbsolutePath)
val sourcePath: Option[String] = source match {
case Some(obj) =>
val uri = obj.toUri
if (uri.getScheme == "file") Some(IO.toFile(uri).getAbsolutePath)
else Some(uri.toString)
case _ => None
}

def startPosition: Option[Long] = checkNoPos(d.getStartPosition)
def endPosition: Option[Long] = checkNoPos(d.getEndPosition)

val line: Optional[Integer] = o2jo(checkNoPos(d.getLineNumber) map (_.toInt))
val offset: Optional[Integer] = o2jo(checkNoPos(d.getPosition) map (_.toInt))
val startOffset: Optional[Integer] = o2jo(checkNoPos(d.getStartPosition) map (_.toInt))
val endOffset: Optional[Integer] = o2jo(checkNoPos(d.getEndPosition) map (_.toInt))

def lineContent: String = {
def getDiagnosticLine: Option[String] =
try {
def invoke(obj: Any, m: java.lang.reflect.Method, args: AnyRef*) =
Option(m.invoke(obj, args: _*))
// See com.sun.tools.javac.api.ClientCodeWrapper.DiagnosticSourceUnwrapper
val diagnostic = d.getClass.getField("d").get(d)
// See com.sun.tools.javac.util.JCDiagnostic#getDiagnosticSource
val getDiagnosticSource = diagnostic.getClass.getDeclaredMethod("getDiagnosticSource")
val getPosition = diagnostic.getClass.getDeclaredMethod("getPosition")
(invoke(diagnostic, getDiagnosticSource), invoke(diagnostic, getPosition)) match {
case (Some(diagnosticSource), Some(position: java.lang.Long)) =>
// See com.sun.tools.javac.util.DiagnosticSource
val getLineMethod = diagnosticSource.getClass.getMethod("getLine", Integer.TYPE)
invoke(diagnosticSource, getLineMethod, new Integer(position.intValue()))
.map(_.toString)
case _ => None
val startOffset: Optional[Integer] = o2jo(startPosition map (_.toInt))
val endOffset: Optional[Integer] = o2jo(endPosition map (_.toInt))

def noPositionInfo
: (Optional[Integer], Optional[Integer], Optional[Integer], Optional[Integer], String) =
if (line.isPresent)
(line, o2jo(Some(0)), Optional.of(line.get() + 1), o2jo(Some(0)), "")
else
(o2jo(Option.empty[Integer]),
o2jo(Option.empty[Integer]),
o2jo(Option.empty[Integer]),
o2jo(Option.empty[Integer]),
"")

// TODO - Is this pulling contents of the line correctly?
// Would be ok to just return null if this version of the JDK doesn't support grabbing
// source lines?
val (startLine, startColumn, endLine, endColumn, lineContent) =
source match {
case Some(source: JavaFileObject) =>
(Option(source.getCharContent(true)), startPosition, endPosition) match {
case (Some(cc), Some(start), Some(end)) =>
// can't optimise using line as it's not always the same as startLine
val range = contentAndRanges(cc, start, end)
(o2jo(Option(range._1)),
o2jo(Option(range._2)),
o2jo(Option(range._3)),
o2jo(Option(range._4)),
range._5)
case _ => noPositionInfo
}
} catch {
case _: ReflectiveOperationException => None
}

def getExpression: String =
source match {
case None => ""
case Some(source) =>
(Option(source.getCharContent(true)), jo2o(startOffset), jo2o(endOffset)) match {
case (Some(cc), Some(start), Some(end)) =>
cc.subSequence(start, end).toString
case _ => ""
}
}

getDiagnosticLine.getOrElse(getExpression)
}
case _ => noPositionInfo
}

new PositionImpl(sourcePath, line, lineContent, offset, startOffset, endOffset)
new PositionImpl(sourcePath,
line,
lineContent,
offset,
startOffset,
endOffset,
startLine,
startColumn,
endLine,
endColumn)
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Zinc - The incremental compiler for Scala.
* Copyright Lightbend, Inc. and Mark Harrah
*
* 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 sbt
package internal
package inc
package javac

class JavaErrorPositionTranslator extends UnitSpec {

"The JavaErrorPositionTranslator" should "be able to translate file positions to line+column positions 1" in translate1()
it should "be able to translate file positions to line+column positions 2" in translate2()
it should "be able to translate file positions to line+column positions 3" in translate3()
it should "be able to translate file positions to line+column positions 4" in translate4()
it should "be able to translate file positions to line+column positions 5" in translate5()
it should "be able to translate file positions to line+column positions 6" in translate6()
it should "be able to translate file positions to line+column positions 7" in translate7()
it should "be able to translate file positions to line+column positions 8" in translate8()
it should "be able to translate file positions to line+column positions 9" in translate9()
it should "be able to translate file positions to line+column positions 10" in translate10()
it should "be able to translate file positions to line+column positions 11" in translate11()
it should "be able to translate file positions to line+column positions 12" in translate12()
it should "be able to translate file positions to line+column positions 13" in translate13()
it should "be able to translate file positions to line+column positions 14" in translate14()

private def testSingleHighlight(code: String, startPos: Long, endPos: Long)(
startLine: Int,
startCol: Int,
endLine: Int,
endCol: Int,
text: String): Unit = {
val position = DiagnosticsReporter.contentAndRanges(code, startPos, endPos)
position._5 shouldBe text
position._1 shouldBe startLine
position._2 shouldBe startCol
position._3 shouldBe endLine
position._4 shouldBe endCol
()
}

private def testHighlight(code: String, startPos: Long, endPos: Long)(startLine: Int,
endLine: Int,
startCol: Int,
endCol: Int,
text: String): Unit = {
// test with /n /r and /r/n variations
val codeN = code.replace('\r', '\n')
val textN = text.replace('\r', '\n')
testSingleHighlight(codeN, startPos, endPos)(startLine, endLine, startCol, endCol, textN)
testSingleHighlight(codeN.replace('\n', '\r'), startPos, endPos)(startLine,
endLine,
startCol,
endCol,
textN.replace('\n', '\r'))
val rnStartPos = startPos + codeN
.substring(0, startPos.toInt)
.map(f => if (f == '\n') 1 else 0)
.sum
val rnEndPos = endPos + codeN.substring(0, endPos.toInt).map(f => if (f == '\n') 1 else 0).sum
testSingleHighlight(codeN.replace("\n", "\r\n"), rnStartPos, rnEndPos)(
startLine,
endLine,
startCol,
endCol,
textN.replace("\n", "\r\n"))
}

private def translate1(): Unit = testHighlight("hello", 0, 5)(1, 0, 1, 5, "hello")
private def translate2(): Unit = {
// on Java an "; expected" error message has no text to return
// should the first char of the next line be specified?
testHighlight("foo", 3, 3)(1, 3, 1, 3, "")
}
private def translate3(): Unit = testHighlight("\nhello", 1, 1)(2, 0, 2, 0, "")
private def translate4(): Unit = testHighlight("\nhello", 0, 0)(1, 0, 1, 0, "")
private def translate5(): Unit = testHighlight("\nhello", 1, 5)(2, 0, 2, 4, "hell")
private def translate6(): Unit = testHighlight("hello", 0, 1)(1, 0, 1, 1, "h")
private def translate7(): Unit = testHighlight("h\n", 0, 1)(1, 0, 1, 1, "h")
private def translate8(): Unit =
testHighlight(
"\npublic class Hello {\n UnknownClass someVar;\n public static void main(String[] args) {\n System.out.println(\"Hello World\");\n }\n}",
26,
38
)(3, 4, 3, 16, "UnknownClass")
private def translate9(): Unit =
testHighlight(
"\npublic class Hello {\n UnknownClass someVar;\n public static void main(String[] args) {\n System.out.println(\"Hello World\");\n }\n}",
0,
38
)(1, 0, 3, 16, "\npublic class Hello {\n UnknownClass")
private def translate10(): Unit = testHighlight("\n\n\n", 0, 0)(1, 0, 1, 0, "")
private def translate11(): Unit = testHighlight("\n\n\n", 1, 1)(2, 0, 2, 0, "")
private def translate12(): Unit = testHighlight("", 0, 0)(1, 0, 1, 0, "")
private def translate13(): Unit = testHighlight("foo", 0, 0)(1, 0, 1, 0, "")
private def translate14(): Unit =
testHighlight(
"\n\n protected void finalize() throws Throwable\n {\n try\n {\n dispose();\n }\n finally\n {\n super.finalize();\n }\n }\n ",
3,
118)(
3,
1,
13,
2,
"protected void finalize() throws Throwable\n {\n try\n {\n dispose();\n }\n finally\n {\n super.finalize();\n }\n }")
}