mirror of https://github.com/sbt/sbt.git
Merge pull request #1702 from jsuereth/wip/incremental-compiler-javac-cleanup
Create a new API for calling Java toolchains.
This commit is contained in:
commit
b9964d5153
|
|
@ -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] = {
|
||||
|
|
@ -195,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._
|
||||
|
|
@ -220,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 }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
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] {
|
||||
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(END_OF_LINE_MATCHER) match {
|
||||
case Array(head, tail @ _*) =>
|
||||
val newHead = head.split(":").last
|
||||
newHead +: tail
|
||||
case Array(head) =>
|
||||
head.split(":").last :: Nil
|
||||
case Array() => Seq.empty[String]
|
||||
}
|
||||
lines.mkString(EOL)
|
||||
}
|
||||
d.getKind match {
|
||||
case Diagnostic.Kind.ERROR | Diagnostic.Kind.WARNING | Diagnostic.Kind.MANDATORY_WARNING => fixWarnOrErrorMessage
|
||||
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. 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)
|
||||
}
|
||||
}
|
||||
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 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).
|
||||
getOrElse("")
|
||||
}
|
||||
override val offset = Logger.o2m(Option(Integer.valueOf(d.getPosition.toInt)))
|
||||
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 =
|
||||
if (sourceUri.isDefined) s"${sourceUri.get}:${if (line.isDefined) line.get else -1}"
|
||||
else ""
|
||||
}
|
||||
reporter.log(pos, msg, severity)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 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 Javadoc {
|
||||
def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean =
|
||||
ForkedJava.launch(javaHome, "javadoc", sources, options, log, reporter)
|
||||
}
|
||||
|
|
@ -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: JavaTool
|
||||
/**
|
||||
* 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: JavaCompiler, docgen: Javadoc): 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(_) => (JavaCompiler.fork(javaHome), Javadoc.fork(javaHome))
|
||||
case _ =>
|
||||
val c = JavaCompiler.local.getOrElse(JavaCompiler.fork(None))
|
||||
val d = Javadoc.local.getOrElse(Javadoc.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 JavaTool {
|
||||
/**
|
||||
* 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 JavaCompiler extends JavaTool {}
|
||||
/** Factory methods for constructing a java compiler. */
|
||||
object JavaCompiler {
|
||||
/** Returns a local compiler, if the current runtime supports it. */
|
||||
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): JavaCompiler =
|
||||
new ForkedJavaCompiler(javaHome)
|
||||
|
||||
}
|
||||
|
||||
/** Interface we use to document java code. This is a tag over the raw JavaTool interface. */
|
||||
trait Javadoc extends JavaTool {}
|
||||
/** Factory methods for constructing a javadoc. */
|
||||
object Javadoc {
|
||||
/** Returns a local compiler, if the current runtime supports it. */
|
||||
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): Javadoc =
|
||||
new ForkedJavadoc(javaHome)
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
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: 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.
|
||||
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)
|
||||
// We sort the sources for deterministic results.
|
||||
val success = delegate.run(sources.sortBy(_.getAbsolutePath), args)(log, reporter)
|
||||
if (!success) {
|
||||
// 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())
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
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 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"))
|
||||
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 {
|
||||
warnOrError.close()
|
||||
infoWriter.close()
|
||||
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 JavaCompiler {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
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
|
||||
override def close() = flush()
|
||||
override def flush(): Unit =
|
||||
synchronized {
|
||||
if (buffer.length > 0) {
|
||||
log(buffer.toString)
|
||||
buffer.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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
|
||||
public class good {
|
||||
public static String test() {
|
||||
return "Hello";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import java.rmi.RMISecurityException;
|
||||
|
||||
public class Test {
|
||||
public NotFound foo() { return 5; }
|
||||
|
||||
public String warning() {
|
||||
throw new RMISecurityException("O NOES");
|
||||
}
|
||||
}
|
||||
|
|
@ -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 JavaCompilerSpec 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(JavaCompiler.fork(), Javadoc.fork())
|
||||
|
||||
def local =
|
||||
JavaTools(
|
||||
JavaCompiler.local.getOrElse(sys.error("This test cannot be run on a JRE, but only a JDK.")),
|
||||
Javadoc.local.getOrElse(Javadoc.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)
|
||||
}
|
||||
|
|
@ -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"""
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
package sbt
|
||||
|
||||
import sbt.compiler.javac.{ IncrementalCompilerJavaTools, JavaCompiler, 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,14 +48,25 @@ 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 =
|
||||
@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)
|
||||
|
|
@ -70,7 +92,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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
|
||||
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"
|
||||
val file = new File(first.position.sourcePath.get)
|
||||
assert(file == 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"
|
||||
val file = new File(first.position.sourcePath.get)
|
||||
assert(file == scalaFile, s"First failure file location is not $scalaFile, $first")
|
||||
}
|
||||
|
||||
compileOrder := CompileOrder.Mixed
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
class bad {
|
||||
public bad foo() { return 1; }
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
trait badScala {
|
||||
def foo: Int = false
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
> checkScalaFailures
|
||||
> set compileOrder := CompileOrder.JavaThenScala
|
||||
> checkJavaFailures
|
||||
|
|
@ -104,6 +104,7 @@ object Logger {
|
|||
val position = pos
|
||||
val message = msg
|
||||
val severity = sev
|
||||
override def toString = s"[$severity] $pos: $message"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue