From 58fb6611bd120c96c8df2ff7f5ec4036a9d03889 Mon Sep 17 00:00:00 2001 From: wpitula Date: Fri, 7 Jul 2017 14:36:23 +0200 Subject: [PATCH 1/2] ApiDiff implementation not relying on diffutils. --- .../main/scala/sbt/internal/inc/APIDiff.scala | 268 ++++++++++++++---- .../sbt/internal/inc/IncrementalCommon.scala | 6 - project/Dependencies.scala | 4 +- 3 files changed, 221 insertions(+), 57 deletions(-) diff --git a/internal/zinc-core/src/main/scala/sbt/internal/inc/APIDiff.scala b/internal/zinc-core/src/main/scala/sbt/internal/inc/APIDiff.scala index e4c8bf73ee..96e39b207c 100644 --- a/internal/zinc-core/src/main/scala/sbt/internal/inc/APIDiff.scala +++ b/internal/zinc-core/src/main/scala/sbt/internal/inc/APIDiff.scala @@ -18,36 +18,9 @@ import xsbti.api.Companions /** * A class which computes diffs (unified diffs) between two textual representations of an API. * - * Internally, it uses java-diff-utils library but it calls it through reflection so there's - * no hard dependency on java-diff-utils. - * - * The reflective lookup of java-diff-utils library is performed in the constructor. Exceptions - * thrown by reflection are passed as-is to the caller of the constructor. - * - * @throws ClassNotFoundException if difflib.DiffUtils class cannot be located - * @throws LinkageError - * @throws ExceptionInInitializerError */ private[inc] class APIDiff { - import APIDiff._ - - private val diffUtilsClass = Class.forName(diffUtilsClassName) - // method signature: diff(List, List) - private val diffMethod: Method = - diffUtilsClass.getMethod(diffMethodName, classOf[JList[_]], classOf[JList[_]]) - - private val generateUnifiedDiffMethod: Method = { - val patchClass = Class.forName(patchClassName) - // method signature: generateUnifiedDiff(String, String, List, Patch, int) - diffUtilsClass.getMethod(generateUnifiedDiffMethodName, - classOf[String], - classOf[String], - classOf[JList[String]], - patchClass, - classOf[Int]) - } - /** * Generates an unified diff between textual representations of `api1` and `api2`. */ @@ -57,29 +30,228 @@ private[inc] class APIDiff { contextSize: Int): String = { val api1Str = DefaultShowAPI(api1.classApi) + "\n" + DefaultShowAPI(api1.objectApi) val api2Str = DefaultShowAPI(api2.classApi) + "\n" + DefaultShowAPI(api2.objectApi) - generateApiDiff(fileName, api1Str, api2Str, contextSize) + DiffUtil.mkColoredCodeDiff(api1Str, api2Str, printDiffDel = true) } - private def generateApiDiff(fileName: String, f1: String, f2: String, contextSize: Int): String = { - assert((diffMethod != null) && (generateUnifiedDiffMethod != null), - "APIDiff isn't properly initialized.") - import scala.collection.JavaConverters._ - def asJavaList[T](it: Iterator[T]): java.util.List[T] = it.toSeq.asJava - val f1Lines = asJavaList(f1.lines) - val f2Lines = asJavaList(f2.lines) - //val diff = DiffUtils.diff(f1Lines, f2Lines) - val diff /*: Patch*/ = diffMethod.invoke(null, f1Lines, f2Lines) - val unifiedPatch: JList[String] = generateUnifiedDiffMethod - .invoke(null, fileName, fileName, f1Lines, diff, (contextSize: java.lang.Integer)) - .asInstanceOf[JList[String]] - unifiedPatch.asScala.mkString("\n") - } + /** + * This class was directly copied from Dotty + * [[https://github.com/lampepfl/dotty/blob/0.1.2-RC1/compiler/src/dotty/tools/dotc/util/DiffUtil.scala]] + * + * Copyright (c) 2014, EPFL + * + * All rights reserved. + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + private object DiffUtil { -} + import scala.annotation.tailrec + import scala.collection.mutable + + private final val ANSI_DEFAULT = "\u001B[0m" + private final val ANSI_RED = "\u001B[31m" + private final val ANSI_GREEN = "\u001B[32m" + + private final val DELETION_COLOR = ANSI_RED + private final val ADDITION_COLOR = ANSI_GREEN + + @tailrec private def splitTokens(str: String, acc: List[String] = Nil): List[String] = { + if (str == "") { + acc.reverse + } else { + val head = str.charAt(0) + val (token, rest) = if (Character.isAlphabetic(head) || Character.isDigit(head)) { + str.span(c => Character.isAlphabetic(c) || Character.isDigit(c)) + } else if (Character.isMirrored(head) || Character.isWhitespace(head)) { + str.splitAt(1) + } else { + str.span { c => + !Character.isAlphabetic(c) && !Character.isDigit(c) && + !Character.isMirrored(c) && !Character.isWhitespace(c) + } + } + splitTokens(rest, token :: acc) + } + } + + /** @return a tuple of the (found, expected, changedPercentage) diffs as strings */ + def mkColoredTypeDiff(found: String, expected: String): (String, String, Double) = { + var totalChange = 0 + val foundTokens = splitTokens(found, Nil).toArray + val expectedTokens = splitTokens(expected, Nil).toArray + + val diffExp = hirschberg(foundTokens, expectedTokens) + val diffAct = hirschberg(expectedTokens, foundTokens) + + val exp = diffExp.collect { + case Unmodified(str) => str + case Inserted(str) => + totalChange += str.length + ADDITION_COLOR + str + ANSI_DEFAULT + }.mkString + + val fnd = diffAct.collect { + case Unmodified(str) => str + case Inserted(str) => + totalChange += str.length + DELETION_COLOR + str + ANSI_DEFAULT + }.mkString + + (fnd, exp, totalChange.toDouble / (expected.length + found.length)) + } + + def mkColoredLineDiff(expected: String, actual: String): String = { + val tokens = splitTokens(expected, Nil).toArray + val lastTokens = splitTokens(actual, Nil).toArray + + val diff = hirschberg(lastTokens, tokens) + + " |SOF\n" + diff.collect { + case Unmodified(str) => + " |" + str + case Inserted(str) => + ADDITION_COLOR + "e |" + str + ANSI_DEFAULT + case Modified(old, str) => + DELETION_COLOR + "a |" + old + "\ne |" + ADDITION_COLOR + str + ANSI_DEFAULT + case Deleted(str) => + DELETION_COLOR + "\na |" + str + ANSI_DEFAULT + }.mkString + "\n |EOF" + } + + def mkColoredCodeDiff(code: String, lastCode: String, printDiffDel: Boolean): String = { + val tokens = splitTokens(code, Nil).toArray + val lastTokens = splitTokens(lastCode, Nil).toArray + + val diff = hirschberg(lastTokens, tokens) + + diff.collect { + case Unmodified(str) => str + case Inserted(str) => ADDITION_COLOR + str + ANSI_DEFAULT + case Modified(old, str) if printDiffDel => + DELETION_COLOR + old + ADDITION_COLOR + str + ANSI_DEFAULT + case Modified(_, str) => ADDITION_COLOR + str + ANSI_DEFAULT + case Deleted(str) if printDiffDel => DELETION_COLOR + str + ANSI_DEFAULT + }.mkString + } + + private sealed trait Patch + private final case class Unmodified(str: String) extends Patch + private final case class Modified(original: String, str: String) extends Patch + private final case class Deleted(str: String) extends Patch + private final case class Inserted(str: String) extends Patch + + private def hirschberg(a: Array[String], b: Array[String]): Array[Patch] = { + def build(x: Array[String], y: Array[String], builder: mutable.ArrayBuilder[Patch]): Unit = { + if (x.isEmpty) { + builder += Inserted(y.mkString) + } else if (y.isEmpty) { + builder += Deleted(x.mkString) + } else if (x.length == 1 || y.length == 1) { + needlemanWunsch(x, y, builder) + } else { + val xlen = x.length + val xmid = xlen / 2 + val ylen = y.length + + val (x1, x2) = x.splitAt(xmid) + val leftScore = nwScore(x1, y) + val rightScore = nwScore(x2.reverse, y.reverse) + val scoreSum = (leftScore zip rightScore.reverse).map { + case (left, right) => left + right + } + val max = scoreSum.max + val ymid = scoreSum.indexOf(max) + + val (y1, y2) = y.splitAt(ymid) + build(x1, y1, builder) + build(x2, y2, builder) + } + } + val builder = Array.newBuilder[Patch] + build(a, b, builder) + builder.result() + } + + private def nwScore(x: Array[String], y: Array[String]): Array[Int] = { + def ins(s: String) = -2 + def del(s: String) = -2 + def sub(s1: String, s2: String) = if (s1 == s2) 2 else -1 + + val score = Array.fill(x.length + 1, y.length + 1)(0) + for (j <- 1 to y.length) + score(0)(j) = score(0)(j - 1) + ins(y(j - 1)) + for (i <- 1 to x.length) { + score(i)(0) = score(i - 1)(0) + del(x(i - 1)) + for (j <- 1 to y.length) { + val scoreSub = score(i - 1)(j - 1) + sub(x(i - 1), y(j - 1)) + val scoreDel = score(i - 1)(j) + del(x(i - 1)) + val scoreIns = score(i)(j - 1) + ins(y(j - 1)) + score(i)(j) = scoreSub max scoreDel max scoreIns + } + } + Array.tabulate(y.length + 1)(j => score(x.length)(j)) + } + + private def needlemanWunsch(x: Array[String], + y: Array[String], + builder: mutable.ArrayBuilder[Patch]): Unit = { + def similarity(a: String, b: String) = if (a == b) 2 else -1 + val d = 1 + val score = Array.tabulate(x.length + 1, y.length + 1) { (i, j) => + if (i == 0) d * j + else if (j == 0) d * i + else 0 + } + for (i <- 1 to x.length) { + for (j <- 1 to y.length) { + val mtch = score(i - 1)(j - 1) + similarity(x(i - 1), y(j - 1)) + val delete = score(i - 1)(j) + d + val insert = score(i)(j - 1) + d + score(i)(j) = mtch max insert max delete + } + } + + var alignment = List.empty[Patch] + var i = x.length + var j = y.length + while (i > 0 || j > 0) { + if (i > 0 && j > 0 && score(i)(j) == score(i - 1)(j - 1) + similarity(x(i - 1), y(j - 1))) { + val newHead = + if (x(i - 1) == y(j - 1)) Unmodified(x(i - 1)) + else Modified(x(i - 1), y(j - 1)) + alignment = newHead :: alignment + i = i - 1 + j = j - 1 + } else if (i > 0 && score(i)(j) == score(i - 1)(j) + d) { + alignment = Deleted(x(i - 1)) :: alignment + i = i - 1 + } else { + alignment = Inserted(y(j - 1)) :: alignment + j = j - 1 + } + } + builder ++= alignment + } + + } -private[inc] object APIDiff { - private val diffUtilsClassName = "difflib.DiffUtils" - private val patchClassName = "difflib.Patch" - private val diffMethodName = "diff" - private val generateUnifiedDiffMethodName = "generateUnifiedDiff" } diff --git a/internal/zinc-core/src/main/scala/sbt/internal/inc/IncrementalCommon.scala b/internal/zinc-core/src/main/scala/sbt/internal/inc/IncrementalCommon.scala index 89635a865f..42a9127268 100644 --- a/internal/zinc-core/src/main/scala/sbt/internal/inc/IncrementalCommon.scala +++ b/internal/zinc-core/src/main/scala/sbt/internal/inc/IncrementalCommon.scala @@ -168,12 +168,6 @@ private[inc] abstract class IncrementalCommon(val log: sbt.util.Logger, options: wrappedLog.debug(s"Detected a change in a public API ($src):\n$apiUnifiedPatch") } } catch { - case e: ClassNotFoundException => - log.error( - "You have api debugging enabled but DiffUtils library cannot be found on sbt's classpath") - case e: LinkageError => - log.error("Encountered linkage error while trying to load DiffUtils library.") - log.trace(e) case e: Exception => log.error("An exception has been thrown while trying to dump an api diff.") log.trace(e) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 05bbc3f70d..cf82253956 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -77,7 +77,6 @@ object Dependencies { val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.13.4" val scalatest = "org.scalatest" %% "scalatest" % "3.0.1" val junit = "junit" % "junit" % "4.11" - val diffUtils = "com.googlecode.java-diff-utils" % "diffutils" % "1.3.0" val sjsonnew = Def.setting { "com.eed3si9n" %% "sjson-new-core" % contrabandSjsonNewVersion.value } val sjsonnewScalaJson = Def.setting { "com.eed3si9n" %% "sjson-new-scalajson" % contrabandSjsonNewVersion.value } @@ -87,8 +86,7 @@ object Dependencies { Seq( scalaCheck % Test, scalatest % Test, - junit % Test, - diffUtils % Test + junit % Test )) .configure(addSbtUtilTesting) } From 7ded74a9712add81850a6a174c8b1cbd0615766a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Indykiewicz?= Date: Fri, 7 Jul 2017 14:19:04 +0200 Subject: [PATCH 2/2] Remove unused 'resident' occurrence --- .../src/main/scala/xsbt/CompilerInterface.scala | 10 +++------- .../scala/xsbt/ScalaCompilerForUnitTesting.scala | 2 +- .../java/xsbti/compile/CachedCompilerProvider.java | 4 ++-- .../src/main/scala/xsbt/ZincBenchmark.scala | 2 +- .../scala/sbt/internal/inc/AnalyzingCompiler.scala | 14 +++++--------- .../scala/sbt/internal/inc/CompilerCache.scala | 4 ++-- 6 files changed, 14 insertions(+), 22 deletions(-) diff --git a/internal/compiler-bridge/src/main/scala/xsbt/CompilerInterface.scala b/internal/compiler-bridge/src/main/scala/xsbt/CompilerInterface.scala index 1fd218e609..59bcb2682d 100644 --- a/internal/compiler-bridge/src/main/scala/xsbt/CompilerInterface.scala +++ b/internal/compiler-bridge/src/main/scala/xsbt/CompilerInterface.scala @@ -19,9 +19,8 @@ final class CompilerInterface { def newCompiler(options: Array[String], output: Output, initialLog: Logger, - initialDelegate: Reporter, - resident: Boolean): CachedCompiler = - new CachedCompiler0(options, output, new WeakLog(initialLog, initialDelegate), resident) + initialDelegate: Reporter): CachedCompiler = + new CachedCompiler0(options, output, new WeakLog(initialLog, initialDelegate)) def run(sources: Array[File], changes: DependencyChanges, @@ -54,10 +53,7 @@ private final class WeakLog(private[this] var log: Logger, private[this] var del } } -private final class CachedCompiler0(args: Array[String], - output: Output, - initialLog: WeakLog, - resident: Boolean) +private final class CachedCompiler0(args: Array[String], output: Output, initialLog: WeakLog) extends CachedCompiler with CachedCompilerCompat { diff --git a/internal/compiler-bridge/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala b/internal/compiler-bridge/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala index bfc638ccb9..55b71199bc 100644 --- a/internal/compiler-bridge/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala +++ b/internal/compiler-bridge/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala @@ -189,7 +189,7 @@ class ScalaCompilerForUnitTesting { override def toString = s"SingleOutput($getOutputDirectory)" } val weakLog = new WeakLog(ConsoleLogger(), ConsoleReporter) - val cachedCompiler = new CachedCompiler0(args, output, weakLog, false) + val cachedCompiler = new CachedCompiler0(args, output, weakLog) val settings = cachedCompiler.settings settings.classpath.value = classpath settings.usejavacp.value = true diff --git a/internal/compiler-interface/src/main/java/xsbti/compile/CachedCompilerProvider.java b/internal/compiler-interface/src/main/java/xsbti/compile/CachedCompilerProvider.java index fa86efc3f7..f5cfed26d8 100644 --- a/internal/compiler-interface/src/main/java/xsbti/compile/CachedCompilerProvider.java +++ b/internal/compiler-interface/src/main/java/xsbti/compile/CachedCompilerProvider.java @@ -18,5 +18,5 @@ public interface CachedCompilerProvider { ScalaInstance scalaInstance(); /** Return a new cached compiler from the usual compiler input. */ - CachedCompiler newCachedCompiler(String[] arguments, Output output, Logger log, Reporter reporter, boolean resident); -} \ No newline at end of file + CachedCompiler newCachedCompiler(String[] arguments, Output output, Logger log, Reporter reporter); +} diff --git a/internal/zinc-benchmarks/src/main/scala/xsbt/ZincBenchmark.scala b/internal/zinc-benchmarks/src/main/scala/xsbt/ZincBenchmark.scala index ab58e6b6e0..5a414547c9 100644 --- a/internal/zinc-benchmarks/src/main/scala/xsbt/ZincBenchmark.scala +++ b/internal/zinc-benchmarks/src/main/scala/xsbt/ZincBenchmark.scala @@ -133,7 +133,7 @@ private[xsbt] object ZincBenchmark { val args = compilationInfo.scalacOptions val classpath = compilationInfo.classpath val weakLog = new WeakLog(ConsoleLogger(), ConsoleReporter) - val cachedCompiler = new CachedCompiler0(args, output, weakLog, false) + val cachedCompiler = new CachedCompiler0(args, output, weakLog) val settings = cachedCompiler.settings settings.classpath.value = classpath val delegatingReporter = DelegatingReporter(settings, ConsoleReporter) diff --git a/internal/zinc-compile-core/src/main/scala/sbt/internal/inc/AnalyzingCompiler.scala b/internal/zinc-compile-core/src/main/scala/sbt/internal/inc/AnalyzingCompiler.scala index c76a601f0f..266267d9e7 100644 --- a/internal/zinc-compile-core/src/main/scala/sbt/internal/inc/AnalyzingCompiler.scala +++ b/internal/zinc-compile-core/src/main/scala/sbt/internal/inc/AnalyzingCompiler.scala @@ -114,26 +114,22 @@ final class AnalyzingCompiler( arguments: Array[String], output: Output, log: xLogger, - reporter: Reporter, - resident: Boolean + reporter: Reporter ): CachedCompiler = - newCachedCompiler(arguments: Seq[String], output, log, reporter, resident) + newCachedCompiler(arguments: Seq[String], output, log, reporter) def newCachedCompiler( arguments: Seq[String], output: Output, log: xLogger, - reporter: Reporter, - resident: Boolean + reporter: Reporter ): CachedCompiler = { - val javaResident: java.lang.Boolean = resident val compiler = call("xsbt.CompilerInterface", "newCompiler", log)( classOf[Array[String]], classOf[Output], classOf[xLogger], - classOf[Reporter], - classOf[Boolean] - )(arguments.toArray[String], output, log, reporter, javaResident) + classOf[Reporter] + )(arguments.toArray[String], output, log, reporter) compiler.asInstanceOf[CachedCompiler] } diff --git a/internal/zinc-compile-core/src/main/scala/sbt/internal/inc/CompilerCache.scala b/internal/zinc-compile-core/src/main/scala/sbt/internal/inc/CompilerCache.scala index 5e3d5ee8b6..a339522f58 100644 --- a/internal/zinc-compile-core/src/main/scala/sbt/internal/inc/CompilerCache.scala +++ b/internal/zinc-compile-core/src/main/scala/sbt/internal/inc/CompilerCache.scala @@ -45,7 +45,7 @@ final class CompilerCache(val maxInstances: Int) extends GlobalsCache { case null => log.debug(f0(s"Compiler cache miss. $key ")) val newCompiler: CachedCompiler = - c.newCachedCompiler(args, output, log, reporter, !forceNew) + c.newCachedCompiler(args, output, log, reporter) cache.put(key, newCompiler) newCompiler case cachedCompiler => @@ -75,5 +75,5 @@ final class FreshCompilerCache extends GlobalsCache { c: CachedCompilerProvider, log: xLogger, reporter: Reporter - ): CachedCompiler = c.newCachedCompiler(args, output, log, reporter, false) + ): CachedCompiler = c.newCachedCompiler(args, output, log, reporter) }