diff --git a/.travis.yml b/.travis.yml index 80eaa60a..deadaf32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,7 +66,6 @@ cache: - "$HOME/.sbt/boot/scala*" - "$HOME/.sbt/launchers" - "$HOME/.ivy2/cache" - - "$HOME/.coursier" before_cache: - du -h -d 1 $HOME/.ivy2/cache diff --git a/compat/src/main/scala-2.11_2.12/scala/collection/compat/package.scala b/compat/src/main/scala-2.11_2.12/scala/collection/compat/package.scala index 7a6f9e26..dc99f4b0 100644 --- a/compat/src/main/scala-2.11_2.12/scala/collection/compat/package.scala +++ b/compat/src/main/scala-2.11_2.12/scala/collection/compat/package.scala @@ -54,6 +54,12 @@ package object compat { def rangeUntil(until: K): T = fact.until(until) } + implicit class IteratorExtensionMethods[A](private val self: Iterator[A]) extends AnyVal { + def sameElements[B >: A](that: IterableOnce[B]): Boolean = { + self.sameElements(that.iterator) + } + } + implicit class TraversableOnceExtensionMethods[A](private val self: TraversableOnce[A]) extends AnyVal { def iterator: Iterator[A] = self.toIterator } diff --git a/compat/src/test/scala/test/scala/collection/CollectionTest.scala b/compat/src/test/scala/test/scala/collection/CollectionTest.scala index da499f14..7b23243c 100644 --- a/compat/src/test/scala/test/scala/collection/CollectionTest.scala +++ b/compat/src/test/scala/test/scala/collection/CollectionTest.scala @@ -52,4 +52,15 @@ class CollectionTest { val xs = Iterator(1, 2, 3).iterator.toList assertEquals(List(1, 2, 3), xs) } + + @Test + def testSameElements: Unit = { + val it1: Iterable[Int] = List(1) + val it2: Iterable[Int] = List(1, 2, 3) + val it3: Iterable[Int] = List(1, 2, 3) + + assertTrue(it1.iterator.sameElements(it1)) + assertFalse(it1.iterator.sameElements(it2)) + assertTrue(it2.iterator.sameElements(it3)) + } } diff --git a/scalafix/input/src/main/scala/fix/IterableSrc.scala b/scalafix/input/src/main/scala/fix/IterableSrc.scala index 8735a55b..4467a658 100644 --- a/scalafix/input/src/main/scala/fix/IterableSrc.scala +++ b/scalafix/input/src/main/scala/fix/IterableSrc.scala @@ -1,5 +1,5 @@ /* -rule = "scala:fix.NewCollections" +rule = "scala:fix.CrossCompat" */ package fix diff --git a/scalafix/input/src/main/scala/fix/TraversableSrc.scala b/scalafix/input/src/main/scala/fix/TraversableSrc.scala index 8560160e..05b17f06 100644 --- a/scalafix/input/src/main/scala/fix/TraversableSrc.scala +++ b/scalafix/input/src/main/scala/fix/TraversableSrc.scala @@ -1,5 +1,5 @@ /* -rule = "scala:fix.NewCollections" +rule = "scala:fix.CrossCompat" */ package fix diff --git a/scalafix/output213/src/main/scala/fix/IterableSrc.scala b/scalafix/output212/src/main/scala/fix/IterableSrc.scala similarity index 72% rename from scalafix/output213/src/main/scala/fix/IterableSrc.scala rename to scalafix/output212/src/main/scala/fix/IterableSrc.scala index cda2811d..8730e421 100644 --- a/scalafix/output213/src/main/scala/fix/IterableSrc.scala +++ b/scalafix/output212/src/main/scala/fix/IterableSrc.scala @@ -3,6 +3,7 @@ package fix +import scala.collection.compat._ class IterableSrc(it: Iterable[Int]) { it.iterator.sameElements(it) } diff --git a/scalafix/output213/src/main/scala/fix/TraversableSrc.scala b/scalafix/output212/src/main/scala/fix/TraversableSrc.scala similarity index 83% rename from scalafix/output213/src/main/scala/fix/TraversableSrc.scala rename to scalafix/output212/src/main/scala/fix/TraversableSrc.scala index 096f01d8..450c049f 100644 --- a/scalafix/output213/src/main/scala/fix/TraversableSrc.scala +++ b/scalafix/output212/src/main/scala/fix/TraversableSrc.scala @@ -3,6 +3,7 @@ package fix +import scala.collection.compat._ object TraversableSrc { def foo(xs: Iterable[(Int, String)], ys: List[Int]): Unit = { xs.to(List) diff --git a/scalafix/rules/src/main/scala/fix/CanBuildFrom.scala b/scalafix/rules/src/main/scala/fix/CanBuildFrom.scala index 86a09837..bc85ce63 100644 --- a/scalafix/rules/src/main/scala/fix/CanBuildFrom.scala +++ b/scalafix/rules/src/main/scala/fix/CanBuildFrom.scala @@ -4,6 +4,8 @@ import scalafix._ import scalafix.util._ import scala.meta._ +import scala.collection.mutable + object CanBuildFrom { def apply(paramss: List[List[Term.Param]], body: Term, @@ -77,7 +79,8 @@ object CanBuildFromNothing { ctx: RuleCtx, collectionCanBuildFrom: SymbolMatcher, nothing: SymbolMatcher, - toTpe: SymbolMatcher)(implicit index: SemanticdbIndex): Patch = { + toTpe: SymbolMatcher, + handledTo: mutable.Set[Tree])(implicit index: SemanticdbIndex): Patch = { paramss.flatten.collect{ case Term.Param( @@ -97,7 +100,7 @@ object CanBuildFromNothing { ) ), _ - ) => new CanBuildFromNothing(param, tpe, t, cct, cc, body, ctx, toTpe) + ) => new CanBuildFromNothing(param, tpe, t, cct, cc, body, ctx, toTpe, handledTo) }.map(_.toFactory).asPatch } } @@ -118,7 +121,8 @@ case class CanBuildFromNothing(param: Name, cc: Type, body: Term, ctx: RuleCtx, - toTpe: SymbolMatcher) { + toTpe: SymbolMatcher, + handledTo: mutable.Set[Tree]) { def toFactory(implicit index: SemanticdbIndex): Patch = { val matchCbf = SymbolMatcher.exact(ctx.index.symbol(param).get) @@ -127,7 +131,7 @@ case class CanBuildFromNothing(param: Name, ctx.replaceTree(tree, Term.Select(cbf2, Term.Name("newBuilder")).syntax) // don't patch cbf.apply twice (cbf.apply and cbf.apply()) - val visitedCbfCalls = scala.collection.mutable.Set[Tree]() + val visitedCbfCalls = mutable.Set[Tree]() val cbfCalls = body.collect { @@ -153,6 +157,8 @@ case class CanBuildFromNothing(param: Name, body.collect { case ap @ Term.ApplyType(Term.Select(e, to @ toTpe(_)), List(cc2 @ matchCC(_))) => + handledTo += to + // e.to[CC](*cbf*) extract implicit parameter val synth = ctx.index.synthetics.find(_.position.end == ap.pos.end).get val Term.Apply(_, List(implicitCbf)) = synth.text.parse[Term].get diff --git a/scalafix/rules/src/main/scala/fix/CrossCompat.scala b/scalafix/rules/src/main/scala/fix/CrossCompat.scala index d6d4cd78..9a08f593 100644 --- a/scalafix/rules/src/main/scala/fix/CrossCompat.scala +++ b/scalafix/rules/src/main/scala/fix/CrossCompat.scala @@ -2,4 +2,10 @@ package fix import scalafix._ -case class CrossCompat(index: SemanticdbIndex) extends SemanticRule(index, "CrossCompat") with Stable212Base +case class CrossCompat(index: SemanticdbIndex) + extends SemanticRule(index, "CrossCompat") + with CrossCompatibility + with Stable212Base { + + def isCrossCompatible: Boolean = true +} diff --git a/scalafix/rules/src/main/scala/fix/NewCollections.scala b/scalafix/rules/src/main/scala/fix/NewCollections.scala index 9cf99a0d..a5033c9f 100644 --- a/scalafix/rules/src/main/scala/fix/NewCollections.scala +++ b/scalafix/rules/src/main/scala/fix/NewCollections.scala @@ -5,10 +5,15 @@ import scalafix.util._ import scala.meta._ // Not 2.12 Cross-Compatible -case class NewCollections(index: SemanticdbIndex) extends SemanticRule(index, "NewCollections") with Stable212Base { +case class NewCollections(index: SemanticdbIndex) + extends SemanticRule(index, "NewCollections") + with Stable212Base { + + + def isCrossCompatible: Boolean = false + // == Symbols == - val iterableSameElement = exact("_root_.scala.collection.IterableLike#sameElements(Lscala/collection/GenIterable;)Z.") - val iterator = normalized("_root_.scala.collection.TraversableLike.toIterator.") + val tupleZipped = normalized( "_root_.scala.runtime.Tuple2Zipped.Ops.zipped.", "_root_.scala.runtime.Tuple3Zipped.Ops.zipped." @@ -100,18 +105,6 @@ case class NewCollections(index: SemanticdbIndex) extends SemanticRule(index, "N }.asPatch } - def replaceToList(ctx: RuleCtx): Patch = { - ctx.tree.collect { - case iterator(t: Name) => - ctx.replaceTree(t, "iterator") - - case t @ toTpe(n: Name) => - trailingBrackets(n, ctx).map { case (open, close) => - ctx.replaceToken(open, "(") + ctx.replaceToken(close, ")") - }.asPatch - }.asPatch - } - def replaceTupleZipped(ctx: RuleCtx): Patch = { ctx.tree.collect { case tupleZipped(Term.Select(Term.Tuple(args), name)) => @@ -153,13 +146,6 @@ case class NewCollections(index: SemanticdbIndex) extends SemanticRule(index, "N }.asPatch } - def replaceIterableSameElements(ctx: RuleCtx): Patch = { - ctx.tree.collect { - case Term.Apply(Term.Select(lhs, iterableSameElement(_)), List(_)) => - ctx.addRight(lhs, ".iterator") - }.asPatch - } - def replaceBreakout(ctx: RuleCtx): Patch = { import Breakout._ @@ -216,12 +202,10 @@ case class NewCollections(index: SemanticdbIndex) extends SemanticRule(index, "N override def fix(ctx: RuleCtx): Patch = { super.fix(ctx) + - replaceToList(ctx) + replaceSymbols(ctx) + replaceTupleZipped(ctx) + replaceMutableMap(ctx) + replaceMutableSet(ctx) + - replaceBreakout(ctx) + - replaceIterableSameElements(ctx) + replaceBreakout(ctx) } } diff --git a/scalafix/rules/src/main/scala/fix/Stable212Base.scala b/scalafix/rules/src/main/scala/fix/Stable212Base.scala index 530fc83a..a2993c41 100644 --- a/scalafix/rules/src/main/scala/fix/Stable212Base.scala +++ b/scalafix/rules/src/main/scala/fix/Stable212Base.scala @@ -4,10 +4,20 @@ import scalafix._ import scalafix.util._ import scala.meta._ +import scala.collection.mutable + +trait CrossCompatibility { + def isCrossCompatible: Boolean +} + // 2.12 Cross-Compatible -trait Stable212Base { self: SemanticRule => +trait Stable212Base extends CrossCompatibility { self: SemanticRule => + + // Two rules triggers the same rewrite TraversableLike.to and CanBuildFrom + // we keep track of what is handled in CanBuildFrom and guard against TraversableLike.to + val handledTo = mutable.Set[Tree]() - // == Symbols == + // == Symbols == def foldSymbol(isLeft: Boolean): SymbolMatcher = { val op = if (isLeft) "/:" @@ -16,9 +26,11 @@ trait Stable212Base { self: SemanticRule => normalized(s"_root_.scala.collection.TraversableOnce.`$op`.") } + val iterator = normalized("_root_.scala.collection.TraversableLike.toIterator.") val toTpe = normalized("_root_.scala.collection.TraversableLike.to.") val copyToBuffer = normalized("_root_.scala.collection.TraversableOnce.copyToBuffer.") val arrayBuilderMake = normalized("_root_.scala.collection.mutable.ArrayBuilder.make(Lscala/reflect/ClassTag;)Lscala/collection/mutable/ArrayBuilder;.") + val iterableSameElement = exact("_root_.scala.collection.IterableLike#sameElements(Lscala/collection/GenIterable;)Z.") val collectionCanBuildFrom = exact("_root_.scala.collection.generic.CanBuildFrom#") val collectionCanBuildFromImport = exact("_root_.scala.collection.generic.CanBuildFrom.;_root_.scala.collection.generic.CanBuildFrom#") val nothing = exact("_root_.scala.Nothing#") @@ -30,14 +42,48 @@ trait Stable212Base { self: SemanticRule => val foldLeftSymbol = foldSymbol(isLeft = true) val foldRightSymbol = foldSymbol(isLeft = false) + val traversable = exact( + "_root_.scala.package.Traversable#", + "_root_.scala.collection.Traversable#", + "_root_.scala.package.Iterable#", + "_root_.scala.collection.Iterable#" + ) + // == Rules == + def replaceIterableSameElements(ctx: RuleCtx): Patch = { + ctx.tree.collect { + case Term.Apply(Term.Select(lhs, iterableSameElement(_)), List(_)) => + ctx.addRight(lhs, ".iterator") + }.asPatch + } + + def replaceSymbols0(ctx: RuleCtx): Patch = { - ctx.replaceSymbols( - "scala.collection.LinearSeq" -> "scala.collection.immutable.List", - "scala.Traversable" -> "scala.Iterable", - "scala.collection.Traversable" -> "scala.collection.Iterable" - ) + val traversableToIterable = + ctx.replaceSymbols( + "scala.Traversable" -> "scala.Iterable", + "scala.collection.Traversable" -> "scala.collection.Iterable" + ) + + val linearSeqToList = + ctx.replaceSymbols( + "scala.collection.LinearSeq" -> "scala.collection.immutable.List", + ) + + import scala.meta.contrib._ + val hasTraversable = + ctx.tree.exists { + case traversable(_) => true + case _ => false + + } + + val compatImport = + if (hasTraversable) addCompatImport(ctx) + else Patch.empty + + traversableToIterable + linearSeqToList + compatImport } def replaceSymbolicFold(ctx: RuleCtx): Patch = { @@ -123,7 +169,7 @@ trait Stable212Base { self: SemanticRule => val useSites = ctx.tree.collect { case Defn.Def(_, _, _, paramss, _, body) => - CanBuildFromNothing(paramss, body, ctx, collectionCanBuildFrom, nothing, toTpe) + + CanBuildFromNothing(paramss, body, ctx, collectionCanBuildFrom, nothing, toTpe, handledTo) + CanBuildFrom(paramss, body, ctx, collectionCanBuildFrom, nothing) }.asPatch @@ -133,22 +179,41 @@ trait Stable212Base { self: SemanticRule => ctx.removeImportee(i) }.asPatch - val compatImport = - ctx.addGlobalImport(importer"scala.collection.compat._") + val compatImport = addCompatImport(ctx) if (useSites.nonEmpty) useSites + imports + compatImport else Patch.empty } + def replaceToList(ctx: RuleCtx): Patch = { + ctx.tree.collect { + case iterator(t: Name) => + ctx.replaceTree(t, "iterator") + + case t @ toTpe(n: Name) if !handledTo.contains(n) => + trailingBrackets(n, ctx).map { case (open, close) => + ctx.replaceToken(open, "(") + ctx.replaceToken(close, ")") + }.asPatch + }.asPatch + } + + + def addCompatImport(ctx: RuleCtx): Patch = { + if (isCrossCompatible) ctx.addGlobalImport(importer"scala.collection.compat._") + else Patch.empty + } + override def fix(ctx: RuleCtx): Patch = { replaceSymbols0(ctx) + replaceCanBuildFrom(ctx) + + replaceToList(ctx) + replaceCopyToBuffer(ctx) + replaceSymbolicFold(ctx) + replaceSetMapPlus2(ctx) + replaceMutSetMapPlus(ctx) + replaceMutMapUpdated(ctx) + - replaceArrayBuilderMake(ctx) + replaceArrayBuilderMake(ctx) + + replaceIterableSameElements(ctx) } } diff --git a/scalafix/tests/src/test/scala/fix/ScalafixTests.scala b/scalafix/tests/src/test/scala/fix/ScalafixTests.scala index 2ab26e4d..0cd50cc4 100644 --- a/scalafix/tests/src/test/scala/fix/ScalafixTests.scala +++ b/scalafix/tests/src/test/scala/fix/ScalafixTests.scala @@ -14,5 +14,8 @@ class ScalafixTests AbsolutePath(BuildInfo.output213FailureSourceroot) ) ) { + runAllTests() + // to run only one test: + // testsToRun.filter(_.filename.toNIO.getFileName.toString == "IterableSrc.scala" ).foreach(runOn) }