From 70cdce0830cc59357775647485e59ee4f53e1e0a Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 28 Oct 2014 16:44:23 -0400 Subject: [PATCH 1/6] Create a new API for calling Java toolchains. * Create a new sbt.compiler.javac package * Create new interfaces to control running `javac` and `javadoc` whether forked or local. * Ensure new interfaces make use of `xsbti.Reporter`. * Create new method on `xsbti.compiler.JavaCompiler` which takes a `xsbti.Reporter` * Create a new mechanism to parse (more accurately) Warnings + Errors, to distinguish the two. * Ensure older xsbti.Compiler implementations still succeed via catcing NoSuchMethodError. * Feed new toolchain through sbt.actions.Compiler API via dirty hackery until we can break things in sbt 1.0 * Added a set of unit tests for parsing errors from Javac/Javadoc * Added a new integration test for hidden compilerReporter key, including testing threading of javac reports. Fixes #875, Fixes #1542, Related #1178 could be looked into/cleaned up. --- .../sbt/compiler/AggressiveCompile.scala | 7 +- .../main/scala/xsbt/DelegatingReporter.scala | 6 + .../scala/sbt/compiler/JavaCompiler.scala | 39 ++- .../compiler/javac/DiagnosticsReporter.scala | 67 +++++ .../scala/sbt/compiler/javac/ForkedJava.scala | 73 ++++++ .../compiler/javac/JavaCompilerAdapter.scala | 43 ++++ .../compiler/javac/JavacProcessLogger.scala | 242 ++++++++++++++++++ .../scala/sbt/compiler/javac/LocalJava.scala | 69 +++++ .../sbt/compiler/javac/NewJavaCompiler.scala | 151 +++++++++++ .../compiler/javac/ProcessLoggerWriter.scala | 43 ++++ .../resources/sbt/compiler/javac/good.java | 7 + .../resources/sbt/compiler/javac/test1.java | 9 + .../compiler/javac/NewJavaCompilerSpec.scala | 134 ++++++++++ .../main/java/xsbti/compile/JavaCompiler.java | 16 +- .../actions/src/main/scala/sbt/Compiler.scala | 33 ++- .../semantic-errors/build.sbt | 27 ++ .../src/main/scala/sbt/TestPlugin.scala | 51 ++++ .../semantic-errors/src/main/java/bad.java | 4 + .../semantic-errors/src/main/scala/bad.scala | 3 + .../compiler-project/semantic-errors/test | 3 + util/log/src/main/scala/sbt/Logger.scala | 1 + 21 files changed, 1019 insertions(+), 9 deletions(-) create mode 100644 compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala create mode 100644 compile/src/main/scala/sbt/compiler/javac/ForkedJava.scala create mode 100644 compile/src/main/scala/sbt/compiler/javac/JavaCompilerAdapter.scala create mode 100644 compile/src/main/scala/sbt/compiler/javac/JavacProcessLogger.scala create mode 100644 compile/src/main/scala/sbt/compiler/javac/LocalJava.scala create mode 100644 compile/src/main/scala/sbt/compiler/javac/NewJavaCompiler.scala create mode 100644 compile/src/main/scala/sbt/compiler/javac/ProcessLoggerWriter.scala create mode 100644 compile/src/test/resources/sbt/compiler/javac/good.java create mode 100644 compile/src/test/resources/sbt/compiler/javac/test1.java create mode 100644 compile/src/test/scala/sbt/compiler/javac/NewJavaCompilerSpec.scala create mode 100644 sbt/src/sbt-test/compiler-project/semantic-errors/build.sbt create mode 100644 sbt/src/sbt-test/compiler-project/semantic-errors/project/src/main/scala/sbt/TestPlugin.scala create mode 100644 sbt/src/sbt-test/compiler-project/semantic-errors/src/main/java/bad.java create mode 100644 sbt/src/sbt-test/compiler-project/semantic-errors/src/main/scala/bad.scala create mode 100644 sbt/src/sbt-test/compiler-project/semantic-errors/test diff --git a/compile/integration/src/main/scala/sbt/compiler/AggressiveCompile.scala b/compile/integration/src/main/scala/sbt/compiler/AggressiveCompile.scala index 960c6e6be..38ccc7da7 100644 --- a/compile/integration/src/main/scala/sbt/compiler/AggressiveCompile.scala +++ b/compile/integration/src/main/scala/sbt/compiler/AggressiveCompile.scala @@ -120,7 +120,12 @@ class AggressiveCompile(cacheFile: File) { val loader = ClasspathUtilities.toLoader(searchClasspath) timed("Java compilation", log) { - javac.compile(javaSrcs.toArray, absClasspath.toArray, output, options.javacOptions.toArray, log) + try javac.compileWithReporter(javaSrcs.toArray, absClasspath.toArray, output, options.javacOptions.toArray, reporter, log) + catch { + // Handle older APIs + case _: NoSuchMethodError => + javac.compile(javaSrcs.toArray, absClasspath.toArray, output, options.javacOptions.toArray, log) + } } def readAPI(source: File, classes: Seq[Class[_]]): Set[String] = { diff --git a/compile/interface/src/main/scala/xsbt/DelegatingReporter.scala b/compile/interface/src/main/scala/xsbt/DelegatingReporter.scala index 732fafbb7..b0513c8a5 100644 --- a/compile/interface/src/main/scala/xsbt/DelegatingReporter.scala +++ b/compile/interface/src/main/scala/xsbt/DelegatingReporter.scala @@ -81,6 +81,12 @@ private final class DelegatingReporter(warnFatal: Boolean, noWarn: Boolean, priv val sourceFile = o2m(sourceFile0) val pointer = o2mi(pointer0) val pointerSpace = o2m(pointerSpace0) + override def toString = + (sourcePath0, line0) match { + case (Some(s), Some(l)) => s + ":" + l + case (Some(s), _) => s + ":" + case _ => "" + } } import xsbti.Severity.{ Info, Warn, Error } diff --git a/compile/src/main/scala/sbt/compiler/JavaCompiler.scala b/compile/src/main/scala/sbt/compiler/JavaCompiler.scala index 60e9a48d5..6534363e0 100644 --- a/compile/src/main/scala/sbt/compiler/JavaCompiler.scala +++ b/compile/src/main/scala/sbt/compiler/JavaCompiler.scala @@ -6,11 +6,26 @@ package compiler import java.io.{ File, PrintWriter } +import xsbti.{ Severity, Reporter } +import xsbti.compile.Output + +@deprecated("0.13.8", "Please use the new set of compilers in sbt.compilers.javac") abstract class JavacContract(val name: String, val clazz: String) { def exec(args: Array[String], writer: PrintWriter): Int } +/** An interface we use to call the Java compiler. */ +@deprecated("0.13.8", "Please use the new set of compilers in sbt.compilers.javac") trait JavaCompiler extends xsbti.compile.JavaCompiler { - def apply(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger) + /** + * Runs the java compiler + * + * @param sources The source files to compile + * @param classpath The classpath for the compiler + * @param outputDirectory The output directory for class files + * @param options The arguments to pass into Javac + * @param log A log in which we write all the output from Javac. + */ + def apply(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger): Unit def compile(sources: Array[File], classpath: Array[File], output: xsbti.compile.Output, options: Array[String], log: xsbti.Logger): Unit = { val outputDirectory = output match { @@ -20,13 +35,20 @@ trait JavaCompiler extends xsbti.compile.JavaCompiler { apply(sources, classpath, outputDirectory, options)(log) } + // TODO - Fix this so that the reporter is actually used. + def compileWithReporter(sources: Array[File], classpath: Array[File], output: Output, options: Array[String], reporter: Reporter, log: xsbti.Logger): Unit = { + compile(sources, classpath, output, options, log) + } + def onArgs(f: Seq[String] => Unit): JavaCompiler } +@deprecated("0.13.8", "Please use the new set of compilers in sbt.compilers.javac") trait Javadoc { def doc(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], maximumErrors: Int, log: Logger) def onArgs(f: Seq[String] => Unit): Javadoc } +@deprecated("0.13.8", "Please use the new set of compilers in sbt.compilers.javac") trait JavaTool extends Javadoc with JavaCompiler { def apply(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger) = compile(JavaCompiler.javac, sources, classpath, outputDirectory, options)(log) @@ -38,7 +60,9 @@ trait JavaTool extends Javadoc with JavaCompiler { def onArgs(f: Seq[String] => Unit): JavaTool } +@deprecated("0.13.8", "Please use the new set of compilers in sbt.compilers.javac") object JavaCompiler { + @deprecated("0.13.8", "Please use the new set of compilers in sbt.compilers.javac") type Fork = (JavacContract, Seq[String], Logger) => Int val javac = new JavacContract("javac", "com.sun.tools.javac.Main") { @@ -56,6 +80,7 @@ object JavaCompiler { def construct(f: Fork, cp: ClasspathOptions, scalaInstance: ScalaInstance): JavaTool = new JavaTool0(f, cp, scalaInstance, _ => ()) + /** The actual implementation of a JavaTool (javadoc + javac). */ private[this] class JavaTool0(f: Fork, cp: ClasspathOptions, scalaInstance: ScalaInstance, onArgsF: Seq[String] => Unit) extends JavaTool { def onArgs(g: Seq[String] => Unit): JavaTool = new JavaTool0(f, cp, scalaInstance, g) def commandArguments(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger): Seq[String] = @@ -93,7 +118,8 @@ object JavaCompiler { def forkJavac(implicit doFork: Fork) = (contract: JavacContract, arguments: Seq[String], log: Logger) => { val (jArgs, nonJArgs) = arguments.partition(_.startsWith("-J")) - def externalJavac(argFile: File) = doFork(contract, jArgs :+ ("@" + normalizeSlash(argFile.getAbsolutePath)), log) + def externalJavac(argFile: File) = + doFork(contract, jArgs :+ ("@" + normalizeSlash(argFile.getAbsolutePath)), log) withArgumentFile(nonJArgs)(externalJavac) } val directJavac = (contract: JavacContract, arguments: Seq[String], log: Logger) => @@ -108,6 +134,15 @@ object JavaCompiler { finally { logger.flushLines(if (exitCode == 0) Level.Warn else Level.Error) } exitCode } + + /** + * Helper method to create an argument file that we pass to Javac. Gets over the windows + * command line length limitation. + * @param args The string arguments to pass to Javac. + * @param f A function which is passed the arg file. + * @tparam T The return type. + * @return The result of using the argument file. + */ def withArgumentFile[T](args: Seq[String])(f: File => T): T = { import IO.{ Newline, withTemporaryDirectory, write } diff --git a/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala b/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala new file mode 100644 index 000000000..2fdb63790 --- /dev/null +++ b/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala @@ -0,0 +1,67 @@ +package sbt.compiler.javac + +import java.io.File +import javax.tools.{ Diagnostic, JavaFileObject, DiagnosticListener } + +import sbt.Logger +import xsbti.{ Severity, Reporter } + +/** + * A diagnostics listener that feeds all messages into the given reporter. + * @param reporter + */ +final class DiagnosticsReporter(reporter: Reporter) extends DiagnosticListener[JavaFileObject] { + + private def fixedDiagnosticMessage(d: Diagnostic[_ <: JavaFileObject]): String = { + def getRawMessage = d.getMessage(null) + def fixWarnOrErrorMessage = { + val tmp = getRawMessage + // we fragment off the line/source/type report from the message. + val lines: Seq[String] = + tmp.split("[\r\n]") match { + case Array(head, tail @ _*) => + val newHead = head.split(":").last + newHead +: tail + case Array(head) => + head.split(":").last :: Nil + case Array() => Seq.empty[String] + } + // TODO - Real EOL + lines.mkString("\n") + } + d.getKind match { + case Diagnostic.Kind.ERROR | Diagnostic.Kind.WARNING | Diagnostic.Kind.MANDATORY_WARNING => fixWarnOrErrorMessage + case _ => getRawMessage + } + } + override def report(d: Diagnostic[_ <: JavaFileObject]) { + val severity = + d.getKind match { + case Diagnostic.Kind.ERROR => Severity.Error + case Diagnostic.Kind.WARNING | Diagnostic.Kind.MANDATORY_WARNING => Severity.Warn + case _ => Severity.Info + } + val msg = fixedDiagnosticMessage(d) + val pos: xsbti.Position = + new xsbti.Position { + override val line = + Logger.o2m(if (d.getLineNumber == -1) None + else Option(new Integer(d.getLineNumber.toInt))) + override def lineContent = { + // TODO - Is this pulling error correctly? Is null an ok return value? + Option(d.getSource). + flatMap(s => Option(s.getCharContent(true))). + map(_.subSequence(d.getStartPosition.intValue, d.getEndPosition.intValue).toString). + getOrElse("") + } + override val offset = Logger.o2m(Option(Integer.valueOf(d.getPosition.toInt))) + private val sourceUri = Option(d.getSource).map(_.toUri.toString) + override val sourcePath = Logger.o2m(sourceUri) + override val sourceFile = Logger.o2m(sourceUri.map(new File(_))) + override val pointer = Logger.o2m(Option.empty[Integer]) + override val pointerSpace = Logger.o2m(Option.empty[String]) + override def toString = s"${d.getSource}:${d.getLineNumber}" + } + reporter.log(pos, msg, severity) + } +} diff --git a/compile/src/main/scala/sbt/compiler/javac/ForkedJava.scala b/compile/src/main/scala/sbt/compiler/javac/ForkedJava.scala new file mode 100644 index 000000000..a329b3c98 --- /dev/null +++ b/compile/src/main/scala/sbt/compiler/javac/ForkedJava.scala @@ -0,0 +1,73 @@ +package sbt.compiler.javac + +import java.io.File + +import sbt.IO._ +import sbt.{ IO, Process, Logger } +import xsbti.Reporter +import xsbti.compile.{ ClasspathOptions, ScalaInstance } + +/** Helper methods for running the java toolchain by forking. */ +object ForkedJava { + /** Helper method to launch programs. */ + private[javac] def launch(javaHome: Option[File], program: String, sources: Seq[File], options: Seq[String], log: Logger, reporter: Reporter): Boolean = { + val (jArgs, nonJArgs) = options.partition(_.startsWith("-J")) + val allArguments = nonJArgs ++ sources.map(_.getAbsolutePath) + + withArgumentFile(allArguments) { argsFile => + val forkArgs = jArgs :+ s"@${normalizeSlash(argsFile.getAbsolutePath)}" + val exe = getJavaExecutable(javaHome, program) + val cwd = new File(new File(".").getAbsolutePath).getCanonicalFile + val javacLogger = new JavacLogger(log, reporter, cwd) + var exitCode = -1 + try { + exitCode = Process(exe +: forkArgs, cwd) ! javacLogger + } finally { + javacLogger.flush(exitCode) + } + // We return true or false, depending on success. + exitCode == 0 + } + } + + /** + * Helper method to create an argument file that we pass to Javac. Gets over the windows + * command line length limitation. + * @param args The string arguments to pass to Javac. + * @param f A function which is passed the arg file. + * @tparam T The return type. + * @return The result of using the argument file. + */ + def withArgumentFile[T](args: Seq[String])(f: File => T): T = + { + import IO.{ Newline, withTemporaryDirectory, write } + withTemporaryDirectory { tmp => + val argFile = new File(tmp, "argfile") + write(argFile, args.map(escapeSpaces).mkString(Newline)) + f(argFile) + } + } + // javac's argument file seems to allow naive space escaping with quotes. escaping a quote with a backslash does not work + private def escapeSpaces(s: String): String = '\"' + normalizeSlash(s) + '\"' + private def normalizeSlash(s: String) = s.replace(File.separatorChar, '/') + + import sbt.Path._ + /** create the executable name for java */ + private[javac] def getJavaExecutable(javaHome: Option[File], name: String): String = + javaHome match { + case None => name + case Some(jh) => + // TODO - Was there any hackery for windows before? + (jh / "bin" / name).getAbsolutePath + } +} + +/** An implementation of compiling java which forks a Javac instance. */ +final class ForkedJavaCompiler(javaHome: Option[File]) extends NewJavaCompiler { + def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = + ForkedJava.launch(javaHome, "javac", sources, options, log, reporter) +} +final class ForkedJavadoc(javaHome: Option[File]) extends NewJavadoc { + def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = + ForkedJava.launch(javaHome, "javadoc", sources, options, log, reporter) +} diff --git a/compile/src/main/scala/sbt/compiler/javac/JavaCompilerAdapter.scala b/compile/src/main/scala/sbt/compiler/javac/JavaCompilerAdapter.scala new file mode 100644 index 000000000..fb090770e --- /dev/null +++ b/compile/src/main/scala/sbt/compiler/javac/JavaCompilerAdapter.scala @@ -0,0 +1,43 @@ +package sbt.compiler.javac + +import java.io.File + +import sbt.compiler.{ CompileFailed, CompilerArguments } +import sbt.{ ClasspathOptions, Logger, LoggerReporter } +import xsbti.Reporter +import xsbti.compile.{ MultipleOutput, SingleOutput, Output } + +/** + * This class adapts the new java compiler with the classpath/argument option hackery needed to handle scala. + * + * The xsbti.Compiler interface is used by the IncrementalCompiler classes, so this lets us adapt a more generic + * wrapper around running Javac (forked or direct) into the interfaces used by incremental compiler. + * + */ +class JavaCompilerAdapter(delegate: NewJavaTool, scalaInstance: xsbti.compile.ScalaInstance, cpOptions: xsbti.compile.ClasspathOptions) extends xsbti.compile.JavaCompiler { + override final def compile(sources: Array[File], classpath: Array[File], output: Output, options: Array[String], log: xsbti.Logger): Unit = { + // TODO - 5 max errors ok? + val reporter = new LoggerReporter(5, log) + compileWithReporter(sources, classpath, output, options, reporter, log) + } + override final def compileWithReporter(sources: Array[File], classpath: Array[File], output: Output, options: Array[String], reporter: Reporter, log: xsbti.Logger): Unit = { + val target = output match { + case so: SingleOutput => so.outputDirectory + case mo: MultipleOutput => throw new RuntimeException("Javac doesn't support multiple output directories") + } + val args = commandArguments(Seq(), classpath, target, options, log) + // TODO - is sorting the sources correct here? + val success = delegate.run(sources.sortBy(_.getAbsolutePath), args)(log, reporter) + // TODO - What should this error message be, and how do we ensure things are correct here? + if (!success) { + // TODO - Should we track only the problems from this javac? + throw new CompileFailed(args.toArray, "javac returned nonzero exit code", reporter.problems()) + } + } + private[this] def commandArguments(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger): Seq[String] = + { + val augmentedClasspath = if (cpOptions.autoBoot) classpath ++ Seq(scalaInstance.libraryJar) else classpath + val javaCp = ClasspathOptions.javac(cpOptions.compiler) + (new CompilerArguments(scalaInstance, javaCp))(sources, augmentedClasspath, Some(outputDirectory), options) + } +} diff --git a/compile/src/main/scala/sbt/compiler/javac/JavacProcessLogger.scala b/compile/src/main/scala/sbt/compiler/javac/JavacProcessLogger.scala new file mode 100644 index 000000000..ea4a179dc --- /dev/null +++ b/compile/src/main/scala/sbt/compiler/javac/JavacProcessLogger.scala @@ -0,0 +1,242 @@ +package sbt +package compiler +package javac + +import java.util.StringTokenizer + +import xsbti._ +import java.io.File + +/** + * An adapted process logger which can feed semantic error events from Javac as well as just + * dump logs. + * + * + * @param log The logger where all input will go. + * @param reporter A reporter for semantic Javac error messages. + * @param cwd The current working directory of the Javac process, used when parsing Filenames. + */ +final class JavacLogger(log: sbt.Logger, reporter: Reporter, cwd: File) extends ProcessLogger { + import scala.collection.mutable.ListBuffer + import Level.{ Info, Warn, Error, Value => LogLevel } + + private val msgs: ListBuffer[(LogLevel, String)] = new ListBuffer() + + def info(s: => String): Unit = + synchronized { msgs += ((Info, s)) } + + def error(s: => String): Unit = + synchronized { msgs += ((Error, s)) } + + def buffer[T](f: => T): T = f + + private def print(desiredLevel: LogLevel)(t: (LogLevel, String)) = t match { + case (Info, msg) => log.info(msg) + case (Error, msg) => log.log(desiredLevel, msg) + } + + // Helper method to dump all semantic errors. + private def parseAndDumpSemanticErrors(): Unit = { + val input = + msgs collect { + case (Error, msg) => msg + } mkString "\n" + val parser = new JavaErrorParser(cwd) + parser.parseProblems(input, log) foreach { e => + reporter.log(e.position, e.message, e.severity) + } + } + + def flush(exitCode: Int): Unit = { + parseAndDumpSemanticErrors() + val level = if (exitCode == 0) Warn else Error + // Here we only display things that wouldn't otherwise be output by the error reporter. + // TODO - NOTES may not be displayed correctly! + msgs collect { + case (Info, msg) => msg + } foreach { msg => + log.info(msg) + } + 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 = null // TODO - what is this even supposed to be? + 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") + + 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] = + 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 + } + // Parses a line number + val line: Parser[Int] = allUntilChar(':') ^? { + case x if isInteger(x) => Integer.parseInt(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/main/scala/sbt/compiler/javac/LocalJava.scala b/compile/src/main/scala/sbt/compiler/javac/LocalJava.scala new file mode 100644 index 000000000..2db9ca63a --- /dev/null +++ b/compile/src/main/scala/sbt/compiler/javac/LocalJava.scala @@ -0,0 +1,69 @@ +package sbt.compiler.javac + +import java.io.{ File, PrintWriter } + +import sbt.{ LoggerWriter, Level, Logger } +import xsbti.Reporter +import xsbti.compile.{ ScalaInstance, ClasspathOptions } + +/** + * Helper methods for trying to run the java toolchain out of our own classloaders. + */ +object LocalJava { + private[this] val javadocClass = "com.sun.tools.javadoc.Main" + + private[this] def javadocMethod = + try { + Option(Class.forName(javadocClass).getDeclaredMethod("execute", classOf[String], classOf[PrintWriter], classOf[PrintWriter], classOf[PrintWriter], classOf[String], classOf[Array[String]])) + } catch { + case e @ (_: ClassNotFoundException | _: NoSuchMethodException) => None + } + + /** True if we can call a forked Javadoc. */ + def hasLocalJavadoc: Boolean = javadocMethod.isDefined + + /** A mechanism to call the javadoc tool via reflection. */ + private[javac] def unsafeJavadoc(args: Array[String], err: PrintWriter, warn: PrintWriter, notice: PrintWriter): Int = { + javadocMethod match { + case Some(m) => + System.err.println("Running javadoc tool!") + m.invoke(null, "javadoc", err, warn, notice, "com.sun.tools.doclets.standard.Standard", args).asInstanceOf[java.lang.Integer].intValue + case _ => + System.err.println("Unable to reflectively invoke javadoc, cannot find it on the current classloader!") + -1 + } + } +} +/** Implementation of javadoc tool which attempts to run it locally (in-class). */ +final class LocalJavadoc() extends NewJavadoc { + override def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = { + val cwd = new File(new File(".").getAbsolutePath).getCanonicalFile + val (jArgs, nonJArgs) = options.partition(_.startsWith("-J")) + val allArguments = nonJArgs ++ sources.map(_.getAbsolutePath) + val javacLogger = new JavacLogger(log, reporter, cwd) + val warnOrError = new PrintWriter(new ProcessLoggerWriter(javacLogger, Level.Error)) + val infoWriter = new PrintWriter(new ProcessLoggerWriter(javacLogger, Level.Info)) + var exitCode = -1 + try { + exitCode = LocalJava.unsafeJavadoc(allArguments.toArray, warnOrError, warnOrError, infoWriter) + } finally { + javacLogger.flush(exitCode) + } + // We return true or false, depending on success. + exitCode == 0 + } +} + +/** An implementation of compiling java which delegates to the JVM resident java compiler. */ +final class LocalJavaCompiler(compiler: javax.tools.JavaCompiler) extends NewJavaCompiler { + override def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = { + import collection.JavaConverters._ + val logger = new LoggerWriter(log) + val logWriter = new PrintWriter(logger) + log.debug("Attempting to call " + compiler + " directly...") + val diagnostics = new DiagnosticsReporter(reporter) + val fileManager = compiler.getStandardFileManager(diagnostics, null, null) + val jfiles = fileManager.getJavaFileObjectsFromFiles(sources.asJava) + compiler.getTask(logWriter, fileManager, diagnostics, options.asJava, null, jfiles).call() + } +} \ No newline at end of file diff --git a/compile/src/main/scala/sbt/compiler/javac/NewJavaCompiler.scala b/compile/src/main/scala/sbt/compiler/javac/NewJavaCompiler.scala new file mode 100644 index 000000000..03643abeb --- /dev/null +++ b/compile/src/main/scala/sbt/compiler/javac/NewJavaCompiler.scala @@ -0,0 +1,151 @@ +package sbt.compiler.javac + +import sbt.ClasspathOptions +import sbt.{ ClasspathOptions => _, _ } +import sbt.compiler._ +import java.io.{ PrintWriter, File } + +import javax.tools.{ DiagnosticListener, Diagnostic, JavaFileObject, DiagnosticCollector } +import xsbti.compile.ScalaInstance +import xsbti.compile._ +import xsbti.{ Severity, Reporter } + +/** + * An interface to the toolchain of Java. + * + * Specifically, access to run javadoc + javac. + */ +sealed trait JavaTools { + /** The raw interface of the java compiler for direct access. */ + def compiler: NewJavaTool + /** + * This will run a java compiler. + * + * + * @param sources The list of java source files to compile. + * @param options The set of options to pass to the java compiler (includes the classpath). + * @param log The logger to dump output into. + * @param reporter The reporter for semantic error messages. + * @return true if no errors, false otherwise. + */ + def compile(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean + /** + * This will run a java compiler. + * + * + * @param sources The list of java source files to compile. + * @param options The set of options to pass to the java compiler (includes the classpath). + * @param log The logger to dump output into. + * @param reporter The reporter for semantic error messages. + * @return true if no errors, false otherwise. + */ + def doc(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean +} + +/** + * An extension of the JavaTools trait that also includes interfaces specific to running + * the java compiler inside of the incremental comppiler. + */ +sealed trait IncrementalCompilerJavaTools extends JavaTools { + /** An instance of the java Compiler for use with incremental compilation. */ + def xsbtiCompiler: xsbti.compile.JavaCompiler +} +/** Factory methods for getting a java toolchain. */ +object JavaTools { + /** Create a new aggregate tool from existing tools. */ + def apply(c: NewJavaCompiler, docgen: NewJavadoc): JavaTools = + new JavaTools { + override def compiler = c + def compile(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = + c.run(sources, options) + def doc(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = + docgen.run(sources, options) + } + + /** + * Constructs a new set of java toolchain for incremental compilation. + * + * @param instance + * The scalaInstance being used in this incremental compile. Used if we need to append + * scala to the classpath (yeah.... the classpath doesn't already have it). + * @param cpOptions + * Classpath options configured for this incremental compiler. Basically, should we append scala or not. + * @param javaHome + * If this is defined, the location where we should look for javac when we run. + * @return + * A new set of the Java toolchain that also includes and instance of xsbti.compile.JavaCompiler + */ + def directOrFork(instance: xsbti.compile.ScalaInstance, cpOptions: xsbti.compile.ClasspathOptions, javaHome: Option[File]): IncrementalCompilerJavaTools = { + val (compiler, doc) = javaHome match { + case Some(_) => (NewJavaCompiler.fork(javaHome), NewJavadoc.fork(javaHome)) + case _ => + val c = NewJavaCompiler.local.getOrElse(NewJavaCompiler.fork(None)) + val d = NewJavadoc.local.getOrElse(NewJavadoc.fork()) + (c, d) + } + val delegate = apply(compiler, doc) + new IncrementalCompilerJavaTools { + val xsbtiCompiler = new JavaCompilerAdapter(delegate.compiler, instance, cpOptions) + def compiler = delegate.compiler + def compile(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = + delegate.compile(sources, options) + def doc(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = + delegate.doc(sources, options) + } + } +} + +/** + * An interface for on of the tools in the java tool chain. + * + * We assume the following is true of tools: + * - The all take sources and options and log error messages + * - They return success or failure. + */ +sealed trait NewJavaTool { + /** + * This will run a java compiler / or other like tool (e.g. javadoc). + * + * + * @param sources The list of java source files to compile. + * @param options The set of options to pass to the java compiler (includes the classpath). + * @param log The logger to dump output into. + * @param reporter The reporter for semantic error messages. + * @return true if no errors, false otherwise. + */ + def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean +} + +/** Interface we use to compile java code. This is mostly a tag over the raw JavaTool interface. */ +trait NewJavaCompiler extends NewJavaTool {} +/** Factory methods for constructing a java compiler. */ +object NewJavaCompiler { + /** Returns a local compiler, if the current runtime supports it. */ + def local: Option[NewJavaCompiler] = + for { + compiler <- Option(javax.tools.ToolProvider.getSystemJavaCompiler) + } yield new LocalJavaCompiler(compiler) + + /** Returns a local compiler that will fork javac when needed. */ + def fork(javaHome: Option[File] = None): NewJavaCompiler = + new ForkedJavaCompiler(javaHome) + +} + +/** Interface we use to document java code. This is a tag over the raw JavaTool interface. */ +trait NewJavadoc extends NewJavaTool {} +/** Factory methods for constructing a javadoc. */ +object NewJavadoc { + /** Returns a local compiler, if the current runtime supports it. */ + def local: Option[NewJavadoc] = + // TODO - javax doc tool not supported in JDK6 + //Option(javax.tools.ToolProvider.getSystemDocumentationTool) + if (LocalJava.hasLocalJavadoc) Some(new LocalJavadoc) + else None + + /** Returns a local compiler that will fork javac when needed. */ + def fork(javaHome: Option[File] = None): NewJavadoc = + new ForkedJavadoc(javaHome) + +} + diff --git a/compile/src/main/scala/sbt/compiler/javac/ProcessLoggerWriter.scala b/compile/src/main/scala/sbt/compiler/javac/ProcessLoggerWriter.scala new file mode 100644 index 000000000..c058155f9 --- /dev/null +++ b/compile/src/main/scala/sbt/compiler/javac/ProcessLoggerWriter.scala @@ -0,0 +1,43 @@ +package sbt.compiler.javac + +import sbt.{ Level, ProcessLogger } + +/** Delegates a stream into a process logger. Mimics LoggerWriter, but for the ProcessLogger interface which differs. */ +private class ProcessLoggerWriter(delegate: ProcessLogger, level: Level.Value, nl: String = System.getProperty("line.separator")) extends java.io.Writer { + + private[this] val buffer = new StringBuilder + private[this] val lines = new collection.mutable.ListBuffer[String] + + override def close() = flush() + override def flush(): Unit = + synchronized { + if (buffer.length > 0) { + log(buffer.toString) + buffer.clear() + } + } + def flushLines(level: Level.Value): Unit = + synchronized { + for (line <- lines) + log(line) + lines.clear() + } + override def write(content: Array[Char], offset: Int, length: Int): Unit = + synchronized { + buffer.appendAll(content, offset, length) + process() + } + + private[this] def process() { + val i = buffer.indexOf(nl) + if (i >= 0) { + log(buffer.substring(0, i)) + buffer.delete(0, i + nl.length) + process() + } + } + private[this] def log(s: String): Unit = level match { + case Level.Warn | Level.Error => delegate.error(s) + case Level.Info => delegate.info(s) + } +} diff --git a/compile/src/test/resources/sbt/compiler/javac/good.java b/compile/src/test/resources/sbt/compiler/javac/good.java new file mode 100644 index 000000000..d4004211a --- /dev/null +++ b/compile/src/test/resources/sbt/compiler/javac/good.java @@ -0,0 +1,7 @@ + + +public class good { + public static String test() { + return "Hello"; + } +} \ No newline at end of file diff --git a/compile/src/test/resources/sbt/compiler/javac/test1.java b/compile/src/test/resources/sbt/compiler/javac/test1.java new file mode 100644 index 000000000..66da263b5 --- /dev/null +++ b/compile/src/test/resources/sbt/compiler/javac/test1.java @@ -0,0 +1,9 @@ +import java.rmi.RMISecurityException; + +public class Test { + public NotFound foo() { return 5; } + + public String warning() { + throw new RMISecurityException("O NOES"); + } +} \ 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/NewJavaCompilerSpec.scala new file mode 100644 index 000000000..9357cf201 --- /dev/null +++ b/compile/src/test/scala/sbt/compiler/javac/NewJavaCompilerSpec.scala @@ -0,0 +1,134 @@ +package sbt.compiler.javac + +import java.io.File +import java.net.URLClassLoader + +import sbt._ +import org.specs2.Specification +import xsbti.{ Severity, Problem } + +object NewJavaCompilerSpec extends Specification { + def is = s2""" + + This is a specification for forking + inline-running of the java compiler, and catching Error messages + + + Compiling a java file with local javac should + compile a java file ${works(local)} + issue errors and warnings ${findsErrors(local)} + + Compiling a file with forked javac should + compile a java file ${works(forked)} + issue errors and warnings ${findsErrors(forked)} + yield the same errors as local javac $forkSameAsLocal + + Documenting a file with forked javadoc should + document a java file ${docWorks(forked)} + find errors in a java file ${findsDocErrors(forked)} + """ + + // TODO - write a test to ensure that java .class files wind up in the right spot, and we can call the compiled java code. + def docWorks(compiler: JavaTools) = IO.withTemporaryDirectory { out => + val (result, problems) = doc(compiler, Seq(knownSampleGoodFile), Seq("-d", out.getAbsolutePath)) + val compiled = result must beTrue + val indexExists = (new File(out, "index.html")).exists must beTrue setMessage ("index.html does not exist!") + val classExists = (new File(out, "good.html")).exists must beTrue setMessage ("good.html does not exist!") + compiled and classExists and indexExists + } + + def works(compiler: JavaTools) = IO.withTemporaryDirectory { out => + val (result, problems) = compile(compiler, Seq(knownSampleGoodFile), Seq("-deprecation", "-d", out.getAbsolutePath)) + val compiled = result must beTrue + val classExists = (new File(out, "good.class")).exists must beTrue + val cl = new URLClassLoader(Array(out.toURI.toURL)) + val clazzz = cl.loadClass("good") + val mthd = clazzz.getDeclaredMethod("test") + val testResult = mthd.invoke(null) + val canRun = mthd.invoke(null) must equalTo("Hello") + compiled and classExists and canRun + } + + def findsErrors(compiler: JavaTools) = { + val (result, problems) = compile(compiler, Seq(knownSampleErrorFile), Seq("-deprecation")) + val errored = result must beFalse + val foundErrorAndWarning = problems must haveSize(5) + val hasKnownErrors = problems.toSeq must contain(errorOnLine(1), warnOnLine(7)) + errored and foundErrorAndWarning and hasKnownErrors + } + + def findsDocErrors(compiler: JavaTools) = IO.withTemporaryDirectory { out => + val (result, problems) = doc(compiler, Seq(knownSampleErrorFile), Seq("-d", out.getAbsolutePath)) + val errored = result must beTrue + val foundErrorAndWarning = problems must haveSize(2) + val hasKnownErrors = problems.toSeq must contain(errorOnLine(3), errorOnLine(4)) + errored and foundErrorAndWarning and hasKnownErrors + } + + def lineMatches(p: Problem, lineno: Int): Boolean = + p.position.line.isDefined && (p.position.line.get == lineno) + def isError(p: Problem): Boolean = p.severity == Severity.Error + def isWarn(p: Problem): Boolean = p.severity == Severity.Warn + + def errorOnLine(lineno: Int) = + beLike[Problem]({ + case p if lineMatches(p, lineno) && isError(p) => ok + case _ => ko + }) + def warnOnLine(lineno: Int) = + beLike[Problem]({ + case p if lineMatches(p, lineno) && isWarn(p) => ok + case _ => ko + }) + + def forkSameAsLocal = { + val (fresult, fproblems) = compile(forked, Seq(knownSampleErrorFile), Seq("-deprecation")) + val (lresult, lproblems) = compile(local, Seq(knownSampleErrorFile), Seq("-deprecation")) + val sameResult = fresult must beEqualTo(lresult) + + val pResults = for ((f, l) <- fproblems zip lproblems) yield { + val sourceIsSame = + if (f.position.sourcePath.isDefined) (f.position.sourcePath.get must beEqualTo(l.position.sourcePath.get)).setMessage(s"${f.position} != ${l.position}") + else l.position.sourcePath.isDefined must beFalse + val lineIsSame = + if (f.position.line.isDefined) f.position.line.get must beEqualTo(l.position.line.get) + else l.position.line.isDefined must beFalse + val severityIsSame = f.severity must beEqualTo(l.severity) + // TODO - We should check to see if the levenshtein distance of the messages is close... + sourceIsSame and lineIsSame and severityIsSame + } + val errorsAreTheSame = pResults.reduce(_ and _) + sameResult and errorsAreTheSame + } + + def compile(c: JavaTools, sources: Seq[File], args: Seq[String]): (Boolean, Array[Problem]) = { + val log = Logger.Null + val reporter = new LoggerReporter(10, log) + val result = c.compile(sources, args)(log, reporter) + (result, reporter.problems) + } + + def doc(c: JavaTools, sources: Seq[File], args: Seq[String]): (Boolean, Array[Problem]) = { + val log = Logger.Null + val reporter = new LoggerReporter(10, log) + val result = c.doc(sources, args)(log, reporter) + (result, reporter.problems) + } + + // TODO - Create one with known JAVA HOME. + def forked = JavaTools(NewJavaCompiler.fork(), NewJavadoc.fork()) + + def local = + JavaTools( + NewJavaCompiler.local.getOrElse(sys.error("This test cannot be run on a JRE, but only a JDK.")), + NewJavadoc.local.getOrElse(NewJavadoc.fork()) + ) + + def cwd = + (new File(new File(".").getAbsolutePath)).getCanonicalFile + + def knownSampleErrorFile = + new java.io.File(getClass.getResource("test1.java").toURI) + + def knownSampleGoodFile = + new java.io.File(getClass.getResource("good.java").toURI) +} \ No newline at end of file diff --git a/interface/src/main/java/xsbti/compile/JavaCompiler.java b/interface/src/main/java/xsbti/compile/JavaCompiler.java index ff6b83cc3..95f9fb992 100644 --- a/interface/src/main/java/xsbti/compile/JavaCompiler.java +++ b/interface/src/main/java/xsbti/compile/JavaCompiler.java @@ -2,6 +2,7 @@ package xsbti.compile; import java.io.File; import xsbti.Logger; +import xsbti.Reporter; /** * Interface to a Java compiler. @@ -9,6 +10,17 @@ import xsbti.Logger; public interface JavaCompiler { /** Compiles Java sources using the provided classpath, output directory, and additional options. - * Output should be sent to the provided logger.*/ + * Output should be sent to the provided logger. + * + * @deprecated 0.13.8 - Use compileWithReporter instead + */ void compile(File[] sources, File[] classpath, Output output, String[] options, Logger log); -} + + /** + * Compiles java sources using the provided classpath, output directory and additional options. + * + * Output should be sent to the provided logger. + * Failures should be passed to the provided Reporter. + */ + void compileWithReporter(File[] sources, File[] classpath, Output output, String[] options, Reporter reporter, Logger log); +} \ No newline at end of file diff --git a/main/actions/src/main/scala/sbt/Compiler.scala b/main/actions/src/main/scala/sbt/Compiler.scala index cb075553c..9cd51f72f 100644 --- a/main/actions/src/main/scala/sbt/Compiler.scala +++ b/main/actions/src/main/scala/sbt/Compiler.scala @@ -3,6 +3,7 @@ */ package sbt +import sbt.compiler.javac.{ IncrementalCompilerJavaTools, NewJavaCompiler, JavaTools } import xsbti.{ Logger => _, _ } import xsbti.compile.{ CompileOrder, GlobalsCache } import CompileOrder.{ JavaThenScala, Mixed, ScalaThenJava } @@ -17,7 +18,17 @@ object Compiler { final case class Inputs(compilers: Compilers, config: Options, incSetup: IncSetup) final case class Options(classpath: Seq[File], sources: Seq[File], classesDirectory: File, options: Seq[String], javacOptions: Seq[String], maxErrors: Int, sourcePositionMapper: Position => Position, order: CompileOrder) final case class IncSetup(analysisMap: File => Option[Analysis], definesClass: DefinesClass, skip: Boolean, cacheFile: File, cache: GlobalsCache, incOptions: IncOptions) - final case class Compilers(scalac: AnalyzingCompiler, javac: JavaTool) + private[sbt] trait JavaToolWithNewInterface extends JavaTool { + def newJavac: IncrementalCompilerJavaTools + } + final case class Compilers(scalac: AnalyzingCompiler, javac: JavaTool) { + final def newJavac: Option[IncrementalCompilerJavaTools] = + javac match { + case x: JavaToolWithNewInterface => Some(x.newJavac) + case _ => None + } + } + final case class NewCompilers(scalac: AnalyzingCompiler, javac: JavaTools) def inputs(classpath: Seq[File], sources: Seq[File], classesDirectory: File, options: Seq[String], javacOptions: Seq[String], maxErrors: Int, sourcePositionMappers: Seq[Position => Option[Position]], order: CompileOrder)(implicit compilers: Compilers, incSetup: IncSetup, log: Logger): Inputs = new Inputs( @@ -37,8 +48,17 @@ object Compiler { def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javaHome: Option[File])(implicit app: AppConfiguration, log: Logger): Compilers = { - val javac = AggressiveCompile.directOrFork(instance, cpOptions, javaHome) - compilers(instance, cpOptions, javac) + val javac = + AggressiveCompile.directOrFork(instance, cpOptions, javaHome) + val javac2 = + JavaTools.directOrFork(instance, cpOptions, javaHome) + // Hackery to enable both the new and deprecated APIs to coexist peacefully. + case class CheaterJavaTool(newJavac: IncrementalCompilerJavaTools, delegate: JavaTool) extends JavaTool with JavaToolWithNewInterface { + def compile(contract: JavacContract, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger): Unit = + javac.compile(contract, sources, classpath, outputDirectory, options)(log) + def onArgs(f: Seq[String] => Unit): JavaTool = CheaterJavaTool(newJavac, delegate.onArgs(f)) + } + compilers(instance, cpOptions, CheaterJavaTool(javac2, javac)) } def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javac: JavaCompiler.Fork)(implicit app: AppConfiguration, log: Logger): Compilers = { @@ -70,7 +90,12 @@ object Compiler { import in.config._ import in.incSetup._ val agg = new AggressiveCompile(cacheFile) - agg(scalac, javac, sources, classpath, CompileOutput(classesDirectory), cache, None, options, javacOptions, + // Here is some trickery to choose the more recent (reporter-using) java compiler rather + // than the previously defined versions. + // TODO - Remove this hackery in sbt 1.0. + val javacChosen: xsbti.compile.JavaCompiler = + in.compilers.newJavac.map(_.xsbtiCompiler).getOrElse(in.compilers.javac) + agg(scalac, javacChosen, sources, classpath, CompileOutput(classesDirectory), cache, None, options, javacOptions, analysisMap, definesClass, reporter, order, skip, incOptions)(log) } diff --git a/sbt/src/sbt-test/compiler-project/semantic-errors/build.sbt b/sbt/src/sbt-test/compiler-project/semantic-errors/build.sbt new file mode 100644 index 000000000..94233c1a9 --- /dev/null +++ b/sbt/src/sbt-test/compiler-project/semantic-errors/build.sbt @@ -0,0 +1,27 @@ + + +TaskKey[Unit]("checkJavaFailures") := { + val reporter = savedReporter.value + val ignore = (compile in Compile).failure.value + val ps = reporter.problems + assert(!ps.isEmpty, "Failed to report any problems!") + // First error should be on a specific line/file + val first = ps(0) + assert(first.position.line.get == 3, s"First failure position is not line 3, failure = $first") + val javaFile = baseDirectory.value / "src/main/java/bad.java" + assert(first.position.sourceFile.get == javaFile, s"First failure file location is not $javaFile, $first") +} + +TaskKey[Unit]("checkScalaFailures") := { + val reporter = savedReporter.value + val ignore = (compile in Compile).failure.value + val ps = reporter.problems + assert(!ps.isEmpty, "Failed to report any problems!") + // First error should be on a specific line/file + val first = ps(0) + assert(first.position.line.get == 2, s"First failure position is not line 2, failure = $first") + val scalaFile = baseDirectory.value / "src/main/scala/bad.scala" + assert(first.position.sourceFile.get == scalaFile, s"First failure file location is not $scalaFile, $first") +} + +compileOrder := CompileOrder.Mixed \ No newline at end of file diff --git a/sbt/src/sbt-test/compiler-project/semantic-errors/project/src/main/scala/sbt/TestPlugin.scala b/sbt/src/sbt-test/compiler-project/semantic-errors/project/src/main/scala/sbt/TestPlugin.scala new file mode 100644 index 000000000..caa4e7006 --- /dev/null +++ b/sbt/src/sbt-test/compiler-project/semantic-errors/project/src/main/scala/sbt/TestPlugin.scala @@ -0,0 +1,51 @@ +package sbt + +import Keys._ +import xsbti.{Position, Severity} + +object TestPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + object autoImport { + val savedReporter = settingKey[xsbti.Reporter]("Saved reporter that collects compilation failures.") + val problems = taskKey[Array[xsbti.Problem]]("Problems reported during compilation.") + } + import autoImport._ + override def projectSettings = Seq( + savedReporter := new CollectingReporter, + compilerReporter in (Compile, compile) := Some(savedReporter.value), + problems := savedReporter.value.problems + ) +} + +class CollectingReporter extends xsbti.Reporter { + val buffer = collection.mutable.ArrayBuffer.empty[xsbti.Problem] + + def reset(): Unit = { + System.err.println(s"DEBUGME: Clearing errors: $buffer") + buffer.clear() + } + def hasErrors: Boolean = buffer.exists(_.severity == Severity.Error) + def hasWarnings: Boolean = buffer.exists(_.severity == Severity.Warn) + def printSummary(): Unit = () + def problems: Array[xsbti.Problem] = buffer.toArray + + /** Logs a message. */ + def log(pos: xsbti.Position, msg: String, sev: xsbti.Severity): Unit = { + object MyProblem extends xsbti.Problem { + def category: String = null + def severity: Severity = sev + def message: String = msg + def position: Position = pos + override def toString = s"$position:$severity: $message" + } + System.err.println(s"DEBUGME: Logging: $MyProblem") + buffer.append(MyProblem) + } + + /** Reports a comment. */ + def comment(pos: xsbti.Position, msg: String): Unit = () + + override def toString = "CollectingReporter" +} \ No newline at end of file diff --git a/sbt/src/sbt-test/compiler-project/semantic-errors/src/main/java/bad.java b/sbt/src/sbt-test/compiler-project/semantic-errors/src/main/java/bad.java new file mode 100644 index 000000000..4bc138b9e --- /dev/null +++ b/sbt/src/sbt-test/compiler-project/semantic-errors/src/main/java/bad.java @@ -0,0 +1,4 @@ + +class bad { + public bad foo() { return 1; } +} \ No newline at end of file diff --git a/sbt/src/sbt-test/compiler-project/semantic-errors/src/main/scala/bad.scala b/sbt/src/sbt-test/compiler-project/semantic-errors/src/main/scala/bad.scala new file mode 100644 index 000000000..67c4a5da5 --- /dev/null +++ b/sbt/src/sbt-test/compiler-project/semantic-errors/src/main/scala/bad.scala @@ -0,0 +1,3 @@ +trait badScala { + def foo: Int = false +} \ No newline at end of file diff --git a/sbt/src/sbt-test/compiler-project/semantic-errors/test b/sbt/src/sbt-test/compiler-project/semantic-errors/test new file mode 100644 index 000000000..dc1c5592e --- /dev/null +++ b/sbt/src/sbt-test/compiler-project/semantic-errors/test @@ -0,0 +1,3 @@ +> checkScalaFailures +> set compileOrder := CompileOrder.JavaThenScala +> checkJavaFailures \ No newline at end of file diff --git a/util/log/src/main/scala/sbt/Logger.scala b/util/log/src/main/scala/sbt/Logger.scala index c507484ce..3c3dd92e1 100644 --- a/util/log/src/main/scala/sbt/Logger.scala +++ b/util/log/src/main/scala/sbt/Logger.scala @@ -104,6 +104,7 @@ object Logger { val position = pos val message = msg val severity = sev + override def toString = s"[$severity] $pos: $message" } } From 624a74dcd617377b22b0009e77c7608386385c78 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Wed, 29 Oct 2014 21:15:10 -0400 Subject: [PATCH 2/6] Attempt to fix unit test for TravisCI. --- sbt/src/sbt-test/compiler-project/semantic-errors/build.sbt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sbt/src/sbt-test/compiler-project/semantic-errors/build.sbt b/sbt/src/sbt-test/compiler-project/semantic-errors/build.sbt index 94233c1a9..f20622620 100644 --- a/sbt/src/sbt-test/compiler-project/semantic-errors/build.sbt +++ b/sbt/src/sbt-test/compiler-project/semantic-errors/build.sbt @@ -9,7 +9,8 @@ TaskKey[Unit]("checkJavaFailures") := { val first = ps(0) assert(first.position.line.get == 3, s"First failure position is not line 3, failure = $first") val javaFile = baseDirectory.value / "src/main/java/bad.java" - assert(first.position.sourceFile.get == javaFile, s"First failure file location is not $javaFile, $first") + val file = new File(first.position.sourcePath.get) + assert(file == javaFile, s"First failure file location is not $javaFile, $first") } TaskKey[Unit]("checkScalaFailures") := { @@ -21,7 +22,8 @@ TaskKey[Unit]("checkScalaFailures") := { val first = ps(0) assert(first.position.line.get == 2, s"First failure position is not line 2, failure = $first") val scalaFile = baseDirectory.value / "src/main/scala/bad.scala" - assert(first.position.sourceFile.get == scalaFile, s"First failure file location is not $scalaFile, $first") + val file = new File(first.position.sourcePath.get) + assert(file == scalaFile, s"First failure file location is not $scalaFile, $first") } compileOrder := CompileOrder.Mixed \ No newline at end of file From 5f9f38f300d8651840ea7c8ed88b6675fea6e8c8 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Wed, 29 Oct 2014 21:55:14 -0400 Subject: [PATCH 3/6] Fix cross-JDK issues around invalid URIs in JDK6 --- .../sbt/compiler/javac/DiagnosticsReporter.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala b/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala index 2fdb63790..cd908bc63 100644 --- a/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala +++ b/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala @@ -34,6 +34,14 @@ final class DiagnosticsReporter(reporter: Reporter) extends DiagnosticListener[J case _ => getRawMessage } } + private def fixSource[T <: JavaFileObject](source: T): Option[String] = { + try Option(source).map(_.toUri.normalize).map(new File(_)).map(_.getAbsolutePath) + catch { + case t: IllegalArgumentException => + // Oracle JDK6 has a super dumb notion of what a URI is + Option(source).map(_.toUri.toString) + } + } override def report(d: Diagnostic[_ <: JavaFileObject]) { val severity = d.getKind match { @@ -55,12 +63,14 @@ final class DiagnosticsReporter(reporter: Reporter) extends DiagnosticListener[J getOrElse("") } override val offset = Logger.o2m(Option(Integer.valueOf(d.getPosition.toInt))) - private val sourceUri = Option(d.getSource).map(_.toUri.toString) + private val sourceUri = fixSource(d.getSource) override val sourcePath = Logger.o2m(sourceUri) override val sourceFile = Logger.o2m(sourceUri.map(new File(_))) override val pointer = Logger.o2m(Option.empty[Integer]) override val pointerSpace = Logger.o2m(Option.empty[String]) - override def toString = s"${d.getSource}:${d.getLineNumber}" + override def toString = + if (sourceUri.isDefined) s"${sourceUri.get}:${if (line.isDefined) line.get else -1}" + else "" } reporter.log(pos, msg, severity) } From 8d158e5ab6bc391357d2a451c13793f48ad83568 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 30 Oct 2014 13:28:41 -0400 Subject: [PATCH 4/6] TODO cleanups based on @havocp's comments. --- .../compiler/javac/DiagnosticsReporter.scala | 17 +++++++++++------ .../compiler/javac/JavaCompilerAdapter.scala | 9 +++++---- .../sbt/compiler/javac/JavacProcessLogger.scala | 11 +++++++++-- .../scala/sbt/compiler/javac/LocalJava.scala | 2 ++ .../compiler/javac/ProcessLoggerWriter.scala | 9 --------- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala b/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala index cd908bc63..d898fbac2 100644 --- a/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala +++ b/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala @@ -11,14 +11,16 @@ import xsbti.{ Severity, Reporter } * @param reporter */ final class DiagnosticsReporter(reporter: Reporter) extends DiagnosticListener[JavaFileObject] { - + val END_OF_LINE_MATCHER = "[\r\n]|[\r]|[\n]" + val EOL = System.getProperty("line.separator") private def fixedDiagnosticMessage(d: Diagnostic[_ <: JavaFileObject]): String = { def getRawMessage = d.getMessage(null) def fixWarnOrErrorMessage = { val tmp = getRawMessage // we fragment off the line/source/type report from the message. + // NOTE - End of line handling may be off. val lines: Seq[String] = - tmp.split("[\r\n]") match { + tmp.split(END_OF_LINE_MATCHER) match { case Array(head, tail @ _*) => val newHead = head.split(":").last newHead +: tail @@ -26,8 +28,7 @@ final class DiagnosticsReporter(reporter: Reporter) extends DiagnosticListener[J head.split(":").last :: Nil case Array() => Seq.empty[String] } - // TODO - Real EOL - lines.mkString("\n") + lines.mkString(EOL) } d.getKind match { case Diagnostic.Kind.ERROR | Diagnostic.Kind.WARNING | Diagnostic.Kind.MANDATORY_WARNING => fixWarnOrErrorMessage @@ -38,7 +39,9 @@ final class DiagnosticsReporter(reporter: Reporter) extends DiagnosticListener[J try Option(source).map(_.toUri.normalize).map(new File(_)).map(_.getAbsolutePath) catch { case t: IllegalArgumentException => - // Oracle JDK6 has a super dumb notion of what a URI is + // Oracle JDK6 has a super dumb notion of what a URI is. In fact, it's not even a legimitate URL, but a dump + // of the filename in a "I hope this works to toString it" kind of way. This appears to work in practice + // but we may need to re-evaluate. Option(source).map(_.toUri.toString) } } @@ -56,7 +59,9 @@ final class DiagnosticsReporter(reporter: Reporter) extends DiagnosticListener[J Logger.o2m(if (d.getLineNumber == -1) None else Option(new Integer(d.getLineNumber.toInt))) override def lineContent = { - // TODO - Is this pulling error correctly? Is null an ok return value? + // 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? Option(d.getSource). flatMap(s => Option(s.getCharContent(true))). map(_.subSequence(d.getStartPosition.intValue, d.getEndPosition.intValue).toString). diff --git a/compile/src/main/scala/sbt/compiler/javac/JavaCompilerAdapter.scala b/compile/src/main/scala/sbt/compiler/javac/JavaCompilerAdapter.scala index fb090770e..cf581679f 100644 --- a/compile/src/main/scala/sbt/compiler/javac/JavaCompilerAdapter.scala +++ b/compile/src/main/scala/sbt/compiler/javac/JavaCompilerAdapter.scala @@ -16,7 +16,8 @@ import xsbti.compile.{ MultipleOutput, SingleOutput, Output } */ class JavaCompilerAdapter(delegate: NewJavaTool, scalaInstance: xsbti.compile.ScalaInstance, cpOptions: xsbti.compile.ClasspathOptions) extends xsbti.compile.JavaCompiler { override final def compile(sources: Array[File], classpath: Array[File], output: Output, options: Array[String], log: xsbti.Logger): Unit = { - // TODO - 5 max errors ok? + // TODO - 5 max errors ok? We're not expecting this code path to be called, ever. This is only for clients who try to use the xsbti.compile.JavaCompiler interface + // outside of the incremental compiler, for some reason. val reporter = new LoggerReporter(5, log) compileWithReporter(sources, classpath, output, options, reporter, log) } @@ -26,11 +27,11 @@ class JavaCompilerAdapter(delegate: NewJavaTool, scalaInstance: xsbti.compile.Sc case mo: MultipleOutput => throw new RuntimeException("Javac doesn't support multiple output directories") } val args = commandArguments(Seq(), classpath, target, options, log) - // TODO - is sorting the sources correct here? + // We sort the sources for deterministic results. val success = delegate.run(sources.sortBy(_.getAbsolutePath), args)(log, reporter) - // TODO - What should this error message be, and how do we ensure things are correct here? if (!success) { - // TODO - Should we track only the problems from this javac? + // TODO - Will the reporter have problems from Scalac? It appears like it does not, only from the most recent run. + // This is because the incremental compiler will not run javac if scalac fails. throw new CompileFailed(args.toArray, "javac returned nonzero exit code", reporter.problems()) } } diff --git a/compile/src/main/scala/sbt/compiler/javac/JavacProcessLogger.scala b/compile/src/main/scala/sbt/compiler/javac/JavacProcessLogger.scala index ea4a179dc..b16fbd813 100644 --- a/compile/src/main/scala/sbt/compiler/javac/JavacProcessLogger.scala +++ b/compile/src/main/scala/sbt/compiler/javac/JavacProcessLogger.scala @@ -89,7 +89,7 @@ object JavaNoPosition extends Position { /** 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 = null // TODO - what is this even supposed to be? + 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" } @@ -98,6 +98,7 @@ class JavaErrorParser(relativeDir: File) extends util.parsing.combinator.RegexPa // 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 @@ -110,6 +111,7 @@ class JavaErrorParser(relativeDir: File) extends util.parsing.combinator.RegexPa } // 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 } @@ -151,9 +153,14 @@ class JavaErrorParser(relativeDir: File) extends util.parsing.combinator.RegexPa } 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 x if isInteger(x) => Integer.parseInt(x) + case ParsedInteger(x) => x } val allUntilCharat: Parser[String] = allUntilChar('^') diff --git a/compile/src/main/scala/sbt/compiler/javac/LocalJava.scala b/compile/src/main/scala/sbt/compiler/javac/LocalJava.scala index 2db9ca63a..406c49c3a 100644 --- a/compile/src/main/scala/sbt/compiler/javac/LocalJava.scala +++ b/compile/src/main/scala/sbt/compiler/javac/LocalJava.scala @@ -47,6 +47,8 @@ final class LocalJavadoc() extends NewJavadoc { try { exitCode = LocalJava.unsafeJavadoc(allArguments.toArray, warnOrError, warnOrError, infoWriter) } finally { + warnOrError.close() + infoWriter.close() javacLogger.flush(exitCode) } // We return true or false, depending on success. diff --git a/compile/src/main/scala/sbt/compiler/javac/ProcessLoggerWriter.scala b/compile/src/main/scala/sbt/compiler/javac/ProcessLoggerWriter.scala index c058155f9..eb64bfdae 100644 --- a/compile/src/main/scala/sbt/compiler/javac/ProcessLoggerWriter.scala +++ b/compile/src/main/scala/sbt/compiler/javac/ProcessLoggerWriter.scala @@ -4,10 +4,7 @@ import sbt.{ Level, ProcessLogger } /** Delegates a stream into a process logger. Mimics LoggerWriter, but for the ProcessLogger interface which differs. */ private class ProcessLoggerWriter(delegate: ProcessLogger, level: Level.Value, nl: String = System.getProperty("line.separator")) extends java.io.Writer { - private[this] val buffer = new StringBuilder - private[this] val lines = new collection.mutable.ListBuffer[String] - override def close() = flush() override def flush(): Unit = synchronized { @@ -16,12 +13,6 @@ private class ProcessLoggerWriter(delegate: ProcessLogger, level: Level.Value, n buffer.clear() } } - def flushLines(level: Level.Value): Unit = - synchronized { - for (line <- lines) - log(line) - lines.clear() - } override def write(content: Array[Char], offset: Int, length: Int): Unit = synchronized { buffer.appendAll(content, offset, length) From 75402b26f23e7c5b6edbb57b4085d7c9b3731609 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 30 Oct 2014 19:37:22 -0400 Subject: [PATCH 5/6] More fixes from review. * Fix regex for end of line. * Rename NewJavaTool and friends to remove the New. --- .../sbt/compiler/AggressiveCompile.scala | 4 ++- .../compiler/javac/DiagnosticsReporter.scala | 2 +- .../scala/sbt/compiler/javac/ForkedJava.scala | 4 +-- ...wJavaCompiler.scala => JavaCompiler.scala} | 28 +++++++++---------- .../compiler/javac/JavaCompilerAdapter.scala | 2 +- .../scala/sbt/compiler/javac/LocalJava.scala | 4 +-- .../compiler/javac/NewJavaCompilerSpec.scala | 6 ++-- .../actions/src/main/scala/sbt/Compiler.scala | 8 ++++-- 8 files changed, 31 insertions(+), 27 deletions(-) rename compile/src/main/scala/sbt/compiler/javac/{NewJavaCompiler.scala => JavaCompiler.scala} (89%) diff --git a/compile/integration/src/main/scala/sbt/compiler/AggressiveCompile.scala b/compile/integration/src/main/scala/sbt/compiler/AggressiveCompile.scala index 38ccc7da7..c4ce04804 100644 --- a/compile/integration/src/main/scala/sbt/compiler/AggressiveCompile.scala +++ b/compile/integration/src/main/scala/sbt/compiler/AggressiveCompile.scala @@ -200,13 +200,14 @@ object AggressiveCompile { b } } - + @deprecated("0.13.8", "Deprecated in favor of new sbt.compiler.javac package.") def directOrFork(instance: ScalaInstance, cpOptions: ClasspathOptions, javaHome: Option[File]): JavaTool = if (javaHome.isDefined) JavaCompiler.fork(cpOptions, instance)(forkJavac(javaHome)) else JavaCompiler.directOrFork(cpOptions, instance)(forkJavac(None)) + @deprecated("0.13.8", "Deprecated in favor of new sbt.compiler.javac package.") def forkJavac(javaHome: Option[File]): JavaCompiler.Fork = { import Path._ @@ -225,6 +226,7 @@ object AggressiveCompile { } } +@deprecated("0.13.8", "Deprecated in favor of new sbt.compiler.javac package.") private[sbt] class JavacLogger(log: Logger) extends ProcessLogger { import scala.collection.mutable.ListBuffer import Level.{ Info, Warn, Error, Value => LogLevel } diff --git a/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala b/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala index d898fbac2..3efcdfabf 100644 --- a/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala +++ b/compile/src/main/scala/sbt/compiler/javac/DiagnosticsReporter.scala @@ -11,7 +11,7 @@ import xsbti.{ Severity, Reporter } * @param reporter */ final class DiagnosticsReporter(reporter: Reporter) extends DiagnosticListener[JavaFileObject] { - val END_OF_LINE_MATCHER = "[\r\n]|[\r]|[\n]" + val END_OF_LINE_MATCHER = "(\r\n)|[\r]|[\n]" val EOL = System.getProperty("line.separator") private def fixedDiagnosticMessage(d: Diagnostic[_ <: JavaFileObject]): String = { def getRawMessage = d.getMessage(null) diff --git a/compile/src/main/scala/sbt/compiler/javac/ForkedJava.scala b/compile/src/main/scala/sbt/compiler/javac/ForkedJava.scala index a329b3c98..4797d08e8 100644 --- a/compile/src/main/scala/sbt/compiler/javac/ForkedJava.scala +++ b/compile/src/main/scala/sbt/compiler/javac/ForkedJava.scala @@ -63,11 +63,11 @@ object ForkedJava { } /** An implementation of compiling java which forks a Javac instance. */ -final class ForkedJavaCompiler(javaHome: Option[File]) extends NewJavaCompiler { +final class ForkedJavaCompiler(javaHome: Option[File]) extends JavaCompiler { def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = ForkedJava.launch(javaHome, "javac", sources, options, log, reporter) } -final class ForkedJavadoc(javaHome: Option[File]) extends NewJavadoc { +final class ForkedJavadoc(javaHome: Option[File]) extends Javadoc { def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = ForkedJava.launch(javaHome, "javadoc", sources, options, log, reporter) } diff --git a/compile/src/main/scala/sbt/compiler/javac/NewJavaCompiler.scala b/compile/src/main/scala/sbt/compiler/javac/JavaCompiler.scala similarity index 89% rename from compile/src/main/scala/sbt/compiler/javac/NewJavaCompiler.scala rename to compile/src/main/scala/sbt/compiler/javac/JavaCompiler.scala index 03643abeb..8ea1be070 100644 --- a/compile/src/main/scala/sbt/compiler/javac/NewJavaCompiler.scala +++ b/compile/src/main/scala/sbt/compiler/javac/JavaCompiler.scala @@ -17,7 +17,7 @@ import xsbti.{ Severity, Reporter } */ sealed trait JavaTools { /** The raw interface of the java compiler for direct access. */ - def compiler: NewJavaTool + def compiler: JavaTool /** * This will run a java compiler. * @@ -53,7 +53,7 @@ sealed trait IncrementalCompilerJavaTools extends JavaTools { /** Factory methods for getting a java toolchain. */ object JavaTools { /** Create a new aggregate tool from existing tools. */ - def apply(c: NewJavaCompiler, docgen: NewJavadoc): JavaTools = + def apply(c: JavaCompiler, docgen: Javadoc): JavaTools = new JavaTools { override def compiler = c def compile(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = @@ -77,10 +77,10 @@ object JavaTools { */ def directOrFork(instance: xsbti.compile.ScalaInstance, cpOptions: xsbti.compile.ClasspathOptions, javaHome: Option[File]): IncrementalCompilerJavaTools = { val (compiler, doc) = javaHome match { - case Some(_) => (NewJavaCompiler.fork(javaHome), NewJavadoc.fork(javaHome)) + case Some(_) => (JavaCompiler.fork(javaHome), Javadoc.fork(javaHome)) case _ => - val c = NewJavaCompiler.local.getOrElse(NewJavaCompiler.fork(None)) - val d = NewJavadoc.local.getOrElse(NewJavadoc.fork()) + val c = JavaCompiler.local.getOrElse(JavaCompiler.fork(None)) + val d = Javadoc.local.getOrElse(Javadoc.fork()) (c, d) } val delegate = apply(compiler, doc) @@ -102,7 +102,7 @@ object JavaTools { * - The all take sources and options and log error messages * - They return success or failure. */ -sealed trait NewJavaTool { +sealed trait JavaTool { /** * This will run a java compiler / or other like tool (e.g. javadoc). * @@ -117,34 +117,34 @@ sealed trait NewJavaTool { } /** Interface we use to compile java code. This is mostly a tag over the raw JavaTool interface. */ -trait NewJavaCompiler extends NewJavaTool {} +trait JavaCompiler extends JavaTool {} /** Factory methods for constructing a java compiler. */ -object NewJavaCompiler { +object JavaCompiler { /** Returns a local compiler, if the current runtime supports it. */ - def local: Option[NewJavaCompiler] = + def local: Option[JavaCompiler] = for { compiler <- Option(javax.tools.ToolProvider.getSystemJavaCompiler) } yield new LocalJavaCompiler(compiler) /** Returns a local compiler that will fork javac when needed. */ - def fork(javaHome: Option[File] = None): NewJavaCompiler = + def fork(javaHome: Option[File] = None): JavaCompiler = new ForkedJavaCompiler(javaHome) } /** Interface we use to document java code. This is a tag over the raw JavaTool interface. */ -trait NewJavadoc extends NewJavaTool {} +trait Javadoc extends JavaTool {} /** Factory methods for constructing a javadoc. */ -object NewJavadoc { +object Javadoc { /** Returns a local compiler, if the current runtime supports it. */ - def local: Option[NewJavadoc] = + def local: Option[Javadoc] = // TODO - javax doc tool not supported in JDK6 //Option(javax.tools.ToolProvider.getSystemDocumentationTool) if (LocalJava.hasLocalJavadoc) Some(new LocalJavadoc) else None /** Returns a local compiler that will fork javac when needed. */ - def fork(javaHome: Option[File] = None): NewJavadoc = + def fork(javaHome: Option[File] = None): Javadoc = new ForkedJavadoc(javaHome) } diff --git a/compile/src/main/scala/sbt/compiler/javac/JavaCompilerAdapter.scala b/compile/src/main/scala/sbt/compiler/javac/JavaCompilerAdapter.scala index cf581679f..7ee67543a 100644 --- a/compile/src/main/scala/sbt/compiler/javac/JavaCompilerAdapter.scala +++ b/compile/src/main/scala/sbt/compiler/javac/JavaCompilerAdapter.scala @@ -14,7 +14,7 @@ import xsbti.compile.{ MultipleOutput, SingleOutput, Output } * wrapper around running Javac (forked or direct) into the interfaces used by incremental compiler. * */ -class JavaCompilerAdapter(delegate: NewJavaTool, scalaInstance: xsbti.compile.ScalaInstance, cpOptions: xsbti.compile.ClasspathOptions) extends xsbti.compile.JavaCompiler { +class JavaCompilerAdapter(delegate: JavaTool, scalaInstance: xsbti.compile.ScalaInstance, cpOptions: xsbti.compile.ClasspathOptions) extends xsbti.compile.JavaCompiler { override final def compile(sources: Array[File], classpath: Array[File], output: Output, options: Array[String], log: xsbti.Logger): Unit = { // TODO - 5 max errors ok? We're not expecting this code path to be called, ever. This is only for clients who try to use the xsbti.compile.JavaCompiler interface // outside of the incremental compiler, for some reason. diff --git a/compile/src/main/scala/sbt/compiler/javac/LocalJava.scala b/compile/src/main/scala/sbt/compiler/javac/LocalJava.scala index 406c49c3a..81f4e1293 100644 --- a/compile/src/main/scala/sbt/compiler/javac/LocalJava.scala +++ b/compile/src/main/scala/sbt/compiler/javac/LocalJava.scala @@ -35,7 +35,7 @@ object LocalJava { } } /** Implementation of javadoc tool which attempts to run it locally (in-class). */ -final class LocalJavadoc() extends NewJavadoc { +final class LocalJavadoc() extends Javadoc { override def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = { val cwd = new File(new File(".").getAbsolutePath).getCanonicalFile val (jArgs, nonJArgs) = options.partition(_.startsWith("-J")) @@ -57,7 +57,7 @@ final class LocalJavadoc() extends NewJavadoc { } /** An implementation of compiling java which delegates to the JVM resident java compiler. */ -final class LocalJavaCompiler(compiler: javax.tools.JavaCompiler) extends NewJavaCompiler { +final class LocalJavaCompiler(compiler: javax.tools.JavaCompiler) extends JavaCompiler { override def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = { import collection.JavaConverters._ val logger = new LoggerWriter(log) diff --git a/compile/src/test/scala/sbt/compiler/javac/NewJavaCompilerSpec.scala b/compile/src/test/scala/sbt/compiler/javac/NewJavaCompilerSpec.scala index 9357cf201..d0333d797 100644 --- a/compile/src/test/scala/sbt/compiler/javac/NewJavaCompilerSpec.scala +++ b/compile/src/test/scala/sbt/compiler/javac/NewJavaCompilerSpec.scala @@ -115,12 +115,12 @@ object NewJavaCompilerSpec extends Specification { } // TODO - Create one with known JAVA HOME. - def forked = JavaTools(NewJavaCompiler.fork(), NewJavadoc.fork()) + def forked = JavaTools(JavaCompiler.fork(), Javadoc.fork()) def local = JavaTools( - NewJavaCompiler.local.getOrElse(sys.error("This test cannot be run on a JRE, but only a JDK.")), - NewJavadoc.local.getOrElse(NewJavadoc.fork()) + JavaCompiler.local.getOrElse(sys.error("This test cannot be run on a JRE, but only a JDK.")), + Javadoc.local.getOrElse(Javadoc.fork()) ) def cwd = diff --git a/main/actions/src/main/scala/sbt/Compiler.scala b/main/actions/src/main/scala/sbt/Compiler.scala index 9cd51f72f..672e4105e 100644 --- a/main/actions/src/main/scala/sbt/Compiler.scala +++ b/main/actions/src/main/scala/sbt/Compiler.scala @@ -3,7 +3,7 @@ */ package sbt -import sbt.compiler.javac.{ IncrementalCompilerJavaTools, NewJavaCompiler, JavaTools } +import sbt.compiler.javac.{ IncrementalCompilerJavaTools, JavaCompiler, JavaTools } import xsbti.{ Logger => _, _ } import xsbti.compile.{ CompileOrder, GlobalsCache } import CompileOrder.{ JavaThenScala, Mixed, ScalaThenJava } @@ -60,11 +60,13 @@ object Compiler { } compilers(instance, cpOptions, CheaterJavaTool(javac2, javac)) } - def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javac: JavaCompiler.Fork)(implicit app: AppConfiguration, log: Logger): Compilers = + @deprecated("0.13.8", "Deprecated in favor of new sbt.compiler.javac package.") + def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javac: sbt.compiler.JavaCompiler.Fork)(implicit app: AppConfiguration, log: Logger): Compilers = { - val javaCompiler = JavaCompiler.fork(cpOptions, instance)(javac) + val javaCompiler = sbt.compiler.JavaCompiler.fork(cpOptions, instance)(javac) compilers(instance, cpOptions, javaCompiler) } + @deprecated("0.13.8", "Deprecated in favor of new sbt.compiler.javac package.") def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javac: JavaTool)(implicit app: AppConfiguration, log: Logger): Compilers = { val scalac = scalaCompiler(instance, cpOptions) From a2e7b324f3522f60709e8869328330cd456d767b Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Fri, 31 Oct 2014 10:58:51 -0400 Subject: [PATCH 6/6] 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. --- .../sbt/compiler/javac/JavaErrorParser.scala | 199 ++++++++++++++++++ .../compiler/javac/JavacProcessLogger.scala | 187 ---------------- ...pilerSpec.scala => JavaCompilerSpec.scala} | 2 +- .../compiler/javac/javaErrorParserSpec.scala | 66 ++++++ 4 files changed, 266 insertions(+), 188 deletions(-) create mode 100644 compile/src/main/scala/sbt/compiler/javac/JavaErrorParser.scala rename compile/src/test/scala/sbt/compiler/javac/{NewJavaCompilerSpec.scala => JavaCompilerSpec.scala} (99%) create mode 100644 compile/src/test/scala/sbt/compiler/javac/javaErrorParserSpec.scala 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""" +}