diff --git a/compile/src/main/scala/sbt/compiler/javac/JavaErrorParser.scala b/compile/src/main/scala/sbt/compiler/javac/JavaErrorParser.scala new file mode 100644 index 000000000..5b5bcacd0 --- /dev/null +++ b/compile/src/main/scala/sbt/compiler/javac/JavaErrorParser.scala @@ -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 = { + + } +} \ No newline at end of file diff --git a/compile/src/main/scala/sbt/compiler/javac/JavacProcessLogger.scala b/compile/src/main/scala/sbt/compiler/javac/JavacProcessLogger.scala index b16fbd813..86c7920b1 100644 --- a/compile/src/main/scala/sbt/compiler/javac/JavacProcessLogger.scala +++ b/compile/src/main/scala/sbt/compiler/javac/JavacProcessLogger.scala @@ -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 - } - } \ No newline at end of file diff --git a/compile/src/test/scala/sbt/compiler/javac/NewJavaCompilerSpec.scala b/compile/src/test/scala/sbt/compiler/javac/JavaCompilerSpec.scala similarity index 99% rename from compile/src/test/scala/sbt/compiler/javac/NewJavaCompilerSpec.scala rename to compile/src/test/scala/sbt/compiler/javac/JavaCompilerSpec.scala index 9357cf201..5093b02d0 100644 --- a/compile/src/test/scala/sbt/compiler/javac/NewJavaCompilerSpec.scala +++ b/compile/src/test/scala/sbt/compiler/javac/JavaCompilerSpec.scala @@ -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 diff --git a/compile/src/test/scala/sbt/compiler/javac/javaErrorParserSpec.scala b/compile/src/test/scala/sbt/compiler/javac/javaErrorParserSpec.scala new file mode 100644 index 000000000..aa3fa24c4 --- /dev/null +++ b/compile/src/test/scala/sbt/compiler/javac/javaErrorParserSpec.scala @@ -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""" +}