Fixes for windows.

* Move error parser into its own file.
* Add the ability to parse Windows filenames.
* Remove existence check for the file as a mandatory.
* Add specific test for the parser.
This commit is contained in:
Josh Suereth 2014-10-31 10:58:51 -04:00
parent 8d158e5ab6
commit a2e7b324f3
4 changed files with 266 additions and 188 deletions

View File

@ -0,0 +1,199 @@
package sbt.compiler.javac
import java.io.File
import sbt.Logger.o2m
import xsbti.{ Problem, Severity, Maybe, Position }
/** A wrapper around xsbti.Position so we can pass in Java input. */
final case class JavaPosition(_sourceFilePath: String, _line: Int, _contents: String) extends Position {
def line: Maybe[Integer] = o2m(Option(Integer.valueOf(_line)))
def lineContent: String = _contents
def offset: Maybe[Integer] = o2m(None)
def pointer: Maybe[Integer] = o2m(None)
def pointerSpace: Maybe[String] = o2m(None)
def sourcePath: Maybe[String] = o2m(Option(_sourceFilePath))
def sourceFile: Maybe[File] = o2m(Option(new File(_sourceFilePath)))
override def toString = s"${_sourceFilePath}:${_line}"
}
/** A position which has no information, because there is none. */
object JavaNoPosition extends Position {
def line: Maybe[Integer] = o2m(None)
def lineContent: String = ""
def offset: Maybe[Integer] = o2m(None)
def pointer: Maybe[Integer] = o2m(None)
def pointerSpace: Maybe[String] = o2m(None)
def sourcePath: Maybe[String] = o2m(None)
def sourceFile: Maybe[File] = o2m(None)
override def toString = "NoPosition"
}
/** A wrapper around xsbti.Problem with java-specific options. */
final case class JavaProblem(val position: Position, val severity: Severity, val message: String) extends xsbti.Problem {
override def category: String = "javac" // TODO - what is this even supposed to be? For now it appears unused.
override def toString = s"$severity @ $position - $message"
}
/** A parser that is able to parse java's error output successfully. */
class JavaErrorParser(relativeDir: File = new File(new File(".").getAbsolutePath).getCanonicalFile) extends util.parsing.combinator.RegexParsers {
// Here we track special handlers to catch "Note:" and "Warning:" lines.
private val NOTE_LINE_PREFIXES = Array("Note: ", "\u6ce8: ", "\u6ce8\u610f\uff1a ")
private val WARNING_PREFIXES = Array("warning", "\u8b66\u544a", "\u8b66\u544a\uff1a")
private val END_OF_LINE = System.getProperty("line.separator")
override val skipWhitespace = false
val CHARAT: Parser[String] = literal("^")
val SEMICOLON: Parser[String] = literal(":") | literal("\uff1a")
val SYMBOL: Parser[String] = allUntilChar(':') // We ignore whether it actually says "symbol" for i18n
val LOCATION: Parser[String] = allUntilChar(':') // We ignore whether it actually says "location" for i18n.
val WARNING: Parser[String] = allUntilChar(':') ^? {
case x if WARNING_PREFIXES.exists(x.trim.startsWith) => x
}
// Parses the rest of an input line.
val restOfLine: Parser[String] =
// TODO - Can we use END_OF_LINE here without issues?
allUntilChars(Array('\n', '\r')) ~ "[\r]?[\n]?".r ^^ {
case msg ~ _ => msg
}
val NOTE: Parser[String] = restOfLine ^? {
case x if NOTE_LINE_PREFIXES exists x.startsWith => x
}
// Parses ALL characters until an expected character is met.
def allUntilChar(c: Char): Parser[String] = allUntilChars(Array(c))
def allUntilChars(chars: Array[Char]): Parser[String] = new Parser[String] {
def isStopChar(c: Char): Boolean = {
var i = 0
while (i < chars.length) {
if (c == chars(i)) return true
i += 1
}
false
}
def apply(in: Input) = {
val source = in.source
val offset = in.offset
val start = handleWhiteSpace(source, offset)
var i = start
while (i < source.length && !isStopChar(source.charAt(i))) {
i += 1
}
Success(source.subSequence(start, i).toString, in.drop(i - offset))
}
}
// Helper to extract an integer from a string
private object ParsedInteger {
def unapply(s: String): Option[Int] = try Some(Integer.parseInt(s)) catch { case e: NumberFormatException => None }
}
// Parses a line number
val line: Parser[Int] = allUntilChar(':') ^? {
case ParsedInteger(x) => x
}
// Parses the file + lineno output of javac.
val fileAndLineNo: Parser[(String, Int)] = {
val linuxFile = allUntilChar(':') ^^ { _.trim() }
val windowsRootFile = linuxFile ~ SEMICOLON ~ linuxFile ^^ { case root ~ _ ~ path => s"$root:$path" }
val linuxOption = linuxFile ~ SEMICOLON ~ line ^^ { case f ~ _ ~ l => (f, l) }
val windowsOption = windowsRootFile ~ SEMICOLON ~ line ^^ { case f ~ _ ~ l => (f, l) }
(linuxOption | windowsOption)
}
val allUntilCharat: Parser[String] = allUntilChar('^')
// Helper method to try to handle relative vs. absolute file pathing....
// NOTE - this is probably wrong...
private def findFileSource(f: String): String = {
// If a file looks like an absolute path, leave it as is.
def isAbsolute(f: String) =
(f startsWith "/") || (f matches """[^\\]+:\\.*""")
// TODO - we used to use existence checks, that may be the right way to go
if (isAbsolute(f)) f
else (new File(relativeDir, f)).getAbsolutePath
}
/** Parses an error message (not this WILL parse warning messages as error messages if used incorrectly. */
val errorMessage: Parser[Problem] = {
val fileLineMessage = fileAndLineNo ~ SEMICOLON ~ restOfLine ^^ {
case (file, line) ~ _ ~ msg => (file, line, msg)
}
fileLineMessage ~ allUntilCharat ~ restOfLine ^^ {
case (file, line, msg) ~ contents ~ _ =>
new JavaProblem(
new JavaPosition(
findFileSource(file),
line,
contents + '^' // TODO - Actually parse charat position out of here.
),
Severity.Error,
msg
)
}
}
/** Parses javac warning messages. */
val warningMessage: Parser[Problem] = {
val fileLineMessage = fileAndLineNo ~ SEMICOLON ~ WARNING ~ SEMICOLON ~ restOfLine ^^ {
case (file, line) ~ _ ~ _ ~ _ ~ msg => (file, line, msg)
}
fileLineMessage ~ allUntilCharat ~ restOfLine ^^ {
case (file, line, msg) ~ contents ~ _ =>
new JavaProblem(
new JavaPosition(
findFileSource(file),
line,
contents + "^"
),
Severity.Warn,
msg
)
}
}
val noteMessage: Parser[Problem] =
NOTE ^^ { msg =>
new JavaProblem(
JavaNoPosition,
Severity.Info,
msg
)
}
val potentialProblem: Parser[Problem] = warningMessage | errorMessage | noteMessage
val javacOutput: Parser[Seq[Problem]] = rep(potentialProblem)
/**
* Example:
*
* Test.java:4: cannot find symbol
* symbol : method baz()
* location: class Foo
* return baz();
* ^
*
* Test.java:8: warning: [deprecation] RMISecurityException(java.lang.String) in java.rmi.RMISecurityException has been deprecated
* throw new java.rmi.RMISecurityException("O NOES");
* ^
*/
final def parseProblems(in: String, logger: sbt.Logger): Seq[Problem] =
parse(javacOutput, in) match {
case Success(result, _) => result
case Failure(msg, n) =>
logger.warn("Unexpected javac output at:${n.pos.longString}. Please report to sbt-dev@googlegroups.com.")
Seq.empty
case Error(msg, n) =>
logger.warn("Unexpected javac output at:${n.pos.longString}. Please report to sbt-dev@googlegroups.com.")
Seq.empty
}
}
object JavaErrorParser {
def main(args: Array[String]): Unit = {
}
}

View File

@ -59,191 +59,4 @@ final class JavacLogger(log: sbt.Logger, reporter: Reporter, cwd: File) extends
}
msgs.clear()
}
}
import sbt.Logger.o2m
/** A wrapper around xsbti.Position so we can pass in Java input. */
final case class JavaPosition(_sourceFile: File, _line: Int, _contents: String) extends Position {
def line: Maybe[Integer] = o2m(Option(Integer.valueOf(_line)))
def lineContent: String = _contents
def offset: Maybe[Integer] = o2m(None)
def pointer: Maybe[Integer] = o2m(None)
def pointerSpace: Maybe[String] = o2m(None)
def sourcePath: Maybe[String] = o2m(Option(_sourceFile.getCanonicalPath))
def sourceFile: Maybe[File] = o2m(Option(_sourceFile))
override def toString = s"${_sourceFile}:${_line}"
}
/** A position which has no information, because there is none. */
object JavaNoPosition extends Position {
def line: Maybe[Integer] = o2m(None)
def lineContent: String = ""
def offset: Maybe[Integer] = o2m(None)
def pointer: Maybe[Integer] = o2m(None)
def pointerSpace: Maybe[String] = o2m(None)
def sourcePath: Maybe[String] = o2m(None)
def sourceFile: Maybe[File] = o2m(None)
override def toString = "NoPosition"
}
/** A wrapper around xsbti.Problem with java-specific options. */
final case class JavaProblem(val position: Position, val severity: Severity, val message: String) extends xsbti.Problem {
override def category: String = "javac" // TODO - what is this even supposed to be? For now it appears unused.
override def toString = s"$severity @ $position - $message"
}
/** A parser that is able to parse java's error output successfully. */
class JavaErrorParser(relativeDir: File) extends util.parsing.combinator.RegexParsers {
// Here we track special handlers to catch "Note:" and "Warning:" lines.
private val NOTE_LINE_PREFIXES = Array("Note: ", "\u6ce8: ", "\u6ce8\u610f\uff1a ")
private val WARNING_PREFIXES = Array("warning", "\u8b66\u544a", "\u8b66\u544a\uff1a")
private val END_OF_LINE = System.getProperty("line.separator")
override val skipWhitespace = false
val CHARAT: Parser[String] = literal("^")
val SEMICOLON: Parser[String] = literal(":") | literal("\uff1a")
val SYMBOL: Parser[String] = allUntilChar(':') // We ignore whether it actually says "symbol" for i18n
val LOCATION: Parser[String] = allUntilChar(':') // We ignore whether it actually says "location" for i18n.
val WARNING: Parser[String] = allUntilChar(':') ^? {
case x if WARNING_PREFIXES.exists(x.trim.startsWith) => x
}
// Parses the rest of an input line.
val restOfLine: Parser[String] =
// TODO - Can we use END_OF_LINE here without issues?
allUntilChars(Array('\n', '\r')) ~ "[\r]?[\n]?".r ^^ {
case msg ~ _ => msg
}
val NOTE: Parser[String] = restOfLine ^? {
case x if NOTE_LINE_PREFIXES exists x.startsWith => x
}
// Parses ALL characters until an expected character is met.
def allUntilChar(c: Char): Parser[String] = allUntilChars(Array(c))
def allUntilChars(chars: Array[Char]): Parser[String] = new Parser[String] {
def isStopChar(c: Char): Boolean = {
var i = 0
while (i < chars.length) {
if (c == chars(i)) return true
i += 1
}
false
}
def apply(in: Input) = {
val source = in.source
val offset = in.offset
val start = handleWhiteSpace(source, offset)
var i = start
while (i < source.length && !isStopChar(source.charAt(i))) {
i += 1
}
Success(source.subSequence(start, i).toString, in.drop(i - offset))
}
}
//parses a file name (no checks)
val file: Parser[String] = allUntilChar(':') ^^ { _.trim() }
// Checks if a string is an integer
def isInteger(s: String): Boolean =
try {
Integer.parseInt(s)
true
} catch {
case e: NumberFormatException => false
}
// Helper to extract an integer from a string
private object ParsedInteger {
def unapply(s: String): Option[Int] = try Some(Integer.parseInt(s)) catch { case e: NumberFormatException => None }
}
// Parses a line number
val line: Parser[Int] = allUntilChar(':') ^? {
case ParsedInteger(x) => x
}
val allUntilCharat: Parser[String] = allUntilChar('^')
// Helper method to try to handle relative vs. absolute file pathing....
// NOTE - this is probably wrong...
private def findFileSource(f: String): File = {
val tmp = new File(f)
if (tmp.exists) tmp
else new File(relativeDir, f)
}
/** Parses an error message (not this WILL parse warning messages as error messages if used incorrectly. */
val errorMessage: Parser[Problem] = {
val fileLineMessage = file ~ SEMICOLON ~ line ~ SEMICOLON ~ restOfLine ^^ {
case file ~ _ ~ line ~ _ ~ msg => (file, line, msg)
}
fileLineMessage ~ allUntilCharat ~ restOfLine ^^ {
case (file, line, msg) ~ contents ~ _ =>
new JavaProblem(
new JavaPosition(
findFileSource(file),
line,
contents + '^' // TODO - Actually parse charat position out of here.
),
Severity.Error,
msg
)
}
}
/** Parses javac warning messages. */
val warningMessage: Parser[Problem] = {
val fileLineMessage = file ~ SEMICOLON ~ line ~ SEMICOLON ~ WARNING ~ SEMICOLON ~ restOfLine ^^ {
case file ~ _ ~ line ~ _ ~ _ ~ _ ~ msg => (file, line, msg)
}
fileLineMessage ~ allUntilCharat ~ restOfLine ^^ {
case (file, line, msg) ~ contents ~ _ =>
new JavaProblem(
new JavaPosition(
findFileSource(file),
line,
contents + "^"
),
Severity.Warn,
msg
)
}
}
val noteMessage: Parser[Problem] =
NOTE ^^ { msg =>
new JavaProblem(
JavaNoPosition,
Severity.Info,
msg
)
}
val potentialProblem: Parser[Problem] = warningMessage | errorMessage | noteMessage
val javacOutput: Parser[Seq[Problem]] = rep(potentialProblem)
/**
* Example:
*
* Test.java:4: cannot find symbol
* symbol : method baz()
* location: class Foo
* return baz();
* ^
*
* Test.java:8: warning: [deprecation] RMISecurityException(java.lang.String) in java.rmi.RMISecurityException has been deprecated
* throw new java.rmi.RMISecurityException("O NOES");
* ^
*/
final def parseProblems(in: String, logger: sbt.Logger): Seq[Problem] =
parse(javacOutput, in) match {
case Success(result, _) => result
case Failure(msg, n) =>
logger.warn("Unexpected javac output at:${n.pos.longString}. Please report to sbt-dev@googlegroups.com.")
Seq.empty
case Error(msg, n) =>
logger.warn("Unexpected javac output at:${n.pos.longString}. Please report to sbt-dev@googlegroups.com.")
Seq.empty
}
}

View File

@ -7,7 +7,7 @@ import sbt._
import org.specs2.Specification
import xsbti.{ Severity, Problem }
object NewJavaCompilerSpec extends Specification {
object JavaCompilerSpec extends Specification {
def is = s2"""
This is a specification for forking + inline-running of the java compiler, and catching Error messages

View File

@ -0,0 +1,66 @@
package sbt.compiler.javac
import java.io.File
import org.specs2.matcher.MatchResult
import sbt.Logger
import org.specs2.Specification
object JavaErrorParserSpec extends Specification {
def is = s2"""
This is a specification for parsing of java error messages.
The JavaErrorParser should
be able to parse linux errors $parseSampleLinux
be able to parse windows file names $parseWindowsFile
be able to parse windows errors $parseSampleWindows
"""
def parseSampleLinux = {
val parser = new JavaErrorParser()
val logger = Logger.Null
val problems = parser.parseProblems(sampleLinuxMessage, logger)
def rightSize = problems must haveSize(1)
def rightFile = problems(0).position.sourcePath.get must beEqualTo("/home/me/projects/sample/src/main/Test.java")
rightSize and rightFile
}
def parseSampleWindows = {
val parser = new JavaErrorParser()
val logger = Logger.Null
val problems = parser.parseProblems(sampleWindowsMessage, logger)
def rightSize = problems must haveSize(1)
def rightFile = problems(0).position.sourcePath.get must beEqualTo(windowsFile)
rightSize and rightFile
}
def parseWindowsFile: MatchResult[_] = {
val parser = new JavaErrorParser()
def failure = false must beTrue
parser.parse(parser.fileAndLineNo, sampleWindowsMessage) match {
case parser.Success((file, line), rest) => file must beEqualTo(windowsFile)
case parser.Error(msg, next) => failure.setMessage(s"Error to parse: $msg, ${next.pos.longString}")
case parser.Failure(msg, next) => failure.setMessage(s"Failed to parse: $msg, ${next.pos.longString}")
}
}
def sampleLinuxMessage =
"""
|/home/me/projects/sample/src/main/Test.java:4: cannot find symbol
|symbol : method baz()
|location: class Foo
|return baz();
""".stripMargin
def sampleWindowsMessage =
s"""
|$windowsFile:4: cannot find symbol
|symbol : method baz()
|location: class Foo
|return baz();
""".stripMargin
def windowsFile = """C:\Projects\sample\src\main\java\Test.java"""
def windowsFileAndLine = s"""$windowsFile:4"""
}