mirror of https://github.com/sbt/sbt.git
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:
parent
8d158e5ab6
commit
a2e7b324f3
|
|
@ -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 = {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"""
|
||||
}
|
||||
Loading…
Reference in New Issue