diff --git a/.circleci/config.yml b/.circleci/config.yml index 5caaa4a..85df58e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,26 +1,52 @@ -version: 2 +version: 2.1 + +commands: + collect_test_reports: + description: Collect test reports + steps: + - run: + name: Collect test report + command: | + mkdir test-reports + find . -path '*/test-reports/*' -name '*.xml' \ + -print -exec cp {} test-reports \; + when: always + + publish_snapshots: + description: Publish artifacts to Snapshots repository + steps: + - deploy: + name: Publish artifacts to Sonatype Snapshots + command: | + if [ ! "${CIRCLE_BRANCH}" = "master" ]; then + echo "Skipped on branch ${CIRCLE_BRANCH}" + exit 0 + fi + + export PUBLISH_REPO_NAME="Sonatype Nexus Repository Manager" + export PUBLISH_REPO_ID="oss.sonatype.org" + export PUBLISH_REPO_URL="https://oss.sonatype.org/content/repositories/snapshots" + + if [ "x$PUBLISH_USER" = "x" -o "x$PUBLISH_PASS" = "x" ]; then + echo "Missing publication credentials" + exit 1 + fi + + sbt +publish + jobs: - build: + build_n_tests: docker: - # specify the version you desire here - image: circleci/openjdk:8-jdk working_directory: ~/repo - environment: - # Customize the JVM maximum heap limit - JVM_OPTS: -Xmx3200m - TERM: dumb - steps: - checkout - # Download and cache dependencies - restore_cache: keys: - - v1-dependencies-{{ checksum "build.sbt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- + - &scala_cache_key scala-{{ checksum "version.sbt" }}-{{ checksum "compiler.sbt" }}-{{ checksum "build.sbt" }} - run: name: Tests @@ -29,7 +55,67 @@ jobs: - save_cache: paths: - ~/.ivy2 - key: v1-dependencies--{{ checksum "build.sbt" }} + - ~/.coursier/cache + - ~/.sbt + key: *scala_cache_key + - collect_test_reports - store_test_results: - path: . + path: test-reports + + publish_snapshots: + docker: + - image: circleci/openjdk:8-jdk + + working_directory: ~/repo + + steps: + - checkout + + - restore_cache: + keys: + - *scala_cache_key + + - publish_snapshots + + - save_cache: + paths: + - ~/.ivy2 + - ~/.coursier/cache + - ~/.sbt + key: *scala_cache_key + + trigger_dependent_builds: + docker: + - image: cimg/base:2020.01 + + working_directory: ~/repo + + steps: + - run: + name: Trigger build @ Reactivemongo-Site + command: | + curl -X POST "https://circleci.com/api/v1.1/project/github/ReactiveMongo/reactivemongo-site/build?circle-token=${REACTIVEMONGO_SITE_API_TOKEN}" + +workflows: + version: 2 + + main_suite: + jobs: + - build_n_tests + + - publish_snapshots: + filters: + branches: + only: + - master + requires: + - build_n_tests + + - trigger_dependent_builds: + filters: + branches: + only: + - master + requires: + - publish_snapshots \ No newline at end of file diff --git a/input/src/main/scala/fix/ApiUsage.scala b/input/src/main/scala/fix/ApiUsage.scala new file mode 100644 index 0000000..fa84df9 --- /dev/null +++ b/input/src/main/scala/fix/ApiUsage.scala @@ -0,0 +1,78 @@ +/* +rule = ReactiveMongoUpgrade +*/ +package fix + +import scala.concurrent.{ ExecutionContext, Future } + +import reactivemongo.api.{ MongoDriver, MongoConnection } +import reactivemongo.api.commands.{ CollStatsResult, WriteConcern } + +import reactivemongo.api.commands.MultiBulkWriteResult + +import reactivemongo.api.collections.GenericCollection +import reactivemongo.api.collections.bson.BSONCollection + +import com.github.ghik.silencer.silent + +object Commands { + def collStats(drv: MongoDriver, wc: WriteConcern): Future[CollStatsResult] = ??? + + def bulk: Future[MultiBulkWriteResult] = ??? +} + +object Drv { + def connect(d: MongoDriver) = d.connection("mongodb://...") + + def closeCon(con: MongoConnection) = con.askClose()(null) + + @silent + def conFromStr(uri: String)(implicit ec: ExecutionContext) = + MongoConnection.parseURI(uri) +} + +object Coll { + import reactivemongo.bson.BSONDocument + + @silent def rename1(coll: GenericCollection[_])( + implicit + ec: ExecutionContext) = coll.rename("foo") + + @silent def rename2(coll: BSONCollection)( + implicit + ec: ExecutionContext) = coll.rename("foo", false) + + def query1(coll: BSONCollection) = { + val qry = coll.find(BSONDocument.empty, BSONDocument("bar" -> 1)). + partial + + val b = qry.sortOption + val c = qry.projectionOption + val d = qry.hintOption + val e = qry.explainFlag + val f = qry.snapshotFlag + val g = qry.commentString + val h = qry.maxTimeMsOption + + if (System.currentTimeMillis() == -1) { + println(s"b=$b,c=$c,d=$d,e=$e,f=$f,g=$g,h=$h") + } + } + + def query2(coll: BSONCollection) = coll.find( + projection = BSONDocument("lorem" -> 1), + selector = BSONDocument.empty) + + @silent + def agg1(coll: BSONCollection)(implicit ec: ExecutionContext) = + coll.aggregateWith1[BSONDocument](explain = true, true) { f => + import f._ + + val m = Match(BSONDocument("foo" -> 1)) + val _ = m.makePipe + + m -> List(Out("bar")) + } + + def agg2(coll: BSONCollection) = coll.BatchCommands.AggregationFramework +} diff --git a/input/src/main/scala/fix/LinterUsage.scala b/input/src/main/scala/fix/LinterUsage.scala index 9578775..ecd98ad 100644 --- a/input/src/main/scala/fix/LinterUsage.scala +++ b/input/src/main/scala/fix/LinterUsage.scala @@ -5,12 +5,12 @@ package fix import scala.concurrent.Future -import reactivemongo.api.{ DB, DefaultDB } +import reactivemongo.api.DB object LinterUsage { val db1 = resolveDB // assert: ReactiveMongoLinter val db2: Future[DB] = db1 - private def resolveDB: Future[DefaultDB] = ??? + private def resolveDB: Future[DB] = ??? } diff --git a/output/src/main/scala/fix/ApiUsage.scala b/output/src/main/scala/fix/ApiUsage.scala new file mode 100644 index 0000000..70d94fd --- /dev/null +++ b/output/src/main/scala/fix/ApiUsage.scala @@ -0,0 +1,72 @@ +package fix + +import scala.concurrent.{ ExecutionContext, Future } + +import reactivemongo.api.MongoConnection + + +import reactivemongo.api.collections.GenericCollection +import reactivemongo.api.bson.collection.BSONCollection + +import com.github.ghik.silencer.silent +import reactivemongo.api.{ AsyncDriver, CollectionStats, WriteConcern } +import reactivemongo.api.bson.BSONDocument + +object Commands { + def collStats(drv: AsyncDriver, wc: WriteConcern): Future[CollectionStats] = ??? + + def bulk: Future[Any /* MultiBulkWriteResult ~> anyCollection.MultiBulkWriteResult */] = ??? +} + +object Drv { + def connect(d: AsyncDriver) = d.connect("mongodb://...") + + def closeCon(con: MongoConnection) = con.close()(null) + + @silent + def conFromStr(uri: String)(implicit ec: ExecutionContext) = + MongoConnection.fromString(uri) +} + +object Coll { + + @silent def rename1(coll: GenericCollection[_])( + implicit + ec: ExecutionContext) = coll.db.connection.database("admin").flatMap(_.renameCollection(coll.db.name, coll.name, "foo")) + + @silent def rename2(coll: BSONCollection)( + implicit + ec: ExecutionContext) = coll.db.connection.database("admin").flatMap(_.renameCollection(coll.db.name, coll.name, "foo", false)) + + def query1(coll: BSONCollection) = { + val qry = coll.find(BSONDocument.empty, Some(BSONDocument("bar" -> 1))). + allowPartialResults + + val b = qry.sort + val c = qry.projection + val d = qry.hint + val e = qry.explain + val f = qry.snapshot + val g = qry.comment + val h = qry.maxTimeMs + + if (System.currentTimeMillis() == -1) { + println(s"b=$b,c=$c,d=$d,e=$e,f=$f,g=$g,h=$h") + } + } + + def query2(coll: BSONCollection) = coll.find(selector = BSONDocument.empty, projection = Some(BSONDocument("lorem" -> 1))) + + @silent + def agg1(coll: BSONCollection)(implicit ec: ExecutionContext) = + coll.aggregateWith[BSONDocument](explain = true, true) { f => + import f._ + + val m = Match(BSONDocument("foo" -> 1)) + val _ = m + + m -> List(Out("bar")) + } + + def agg2(coll: BSONCollection) = coll.AggregationFramework +} diff --git a/output/src/main/scala/fix/Bson.scala b/output/src/main/scala/fix/Bson.scala index d059fdf..21a09af 100644 --- a/output/src/main/scala/fix/Bson.scala +++ b/output/src/main/scala/fix/Bson.scala @@ -1,11 +1,9 @@ package fix -import reactivemongo.api.bson._ -import reactivemongo.api.bson.BSONValue -import reactivemongo.api.bson.{ BSONDocument, BSONObjectID } -import reactivemongo.api.bson.collection.BSONSerializationPack import reactivemongo.api.bson.collection.BSONCollection +import reactivemongo.api.bson.{ BSONDocument, BSONObjectID, BSONValue, _ } +import reactivemongo.api.bson.collection.BSONSerializationPack object Bson { @com.github.ghik.silencer.silent diff --git a/output/src/main/scala/fix/LinterUsage.scala b/output/src/main/scala/fix/LinterUsage.scala index bb4346f..6d87eb5 100644 --- a/output/src/main/scala/fix/LinterUsage.scala +++ b/output/src/main/scala/fix/LinterUsage.scala @@ -2,12 +2,12 @@ package fix import scala.concurrent.Future -import reactivemongo.api.{ DB, DefaultDB } +import reactivemongo.api.DB object LinterUsage { val db1 = resolveDB val db2: Future[DB] = db1 - private def resolveDB: Future[DefaultDB] = ??? + private def resolveDB: Future[DB] = ??? } diff --git a/output/src/main/scala/fix/gridfs/Compatible.scala b/output/src/main/scala/fix/gridfs/Compatible.scala index 4643e78..ce1b144 100644 --- a/output/src/main/scala/fix/gridfs/Compatible.scala +++ b/output/src/main/scala/fix/gridfs/Compatible.scala @@ -2,17 +2,17 @@ package fix.gridfs import scala.concurrent.ExecutionContext -import reactivemongo.api.bson.BSONValue -import reactivemongo.api.bson.collection.BSONSerializationPack -import reactivemongo.api.DefaultDB import reactivemongo.api.gridfs.{ GridFS, ReadFile } +import reactivemongo.api.DB +import reactivemongo.api.bson.BSONValue +import reactivemongo.api.bson.collection.BSONSerializationPack object Compatible { - def resolve1(db: DefaultDB) = + def resolve1(db: DB) = db.gridfs - def resolve2(db: DefaultDB) = + def resolve2(db: DB) = db.gridfs("foo") // --- diff --git a/output/src/main/scala/fix/gridfs/RequireMigration.scala b/output/src/main/scala/fix/gridfs/RequireMigration.scala index 1db6a06..fa86b63 100644 --- a/output/src/main/scala/fix/gridfs/RequireMigration.scala +++ b/output/src/main/scala/fix/gridfs/RequireMigration.scala @@ -4,8 +4,8 @@ import com.github.ghik.silencer.silent import scala.concurrent.ExecutionContext -import reactivemongo.api.bson.collection.BSONSerializationPack import reactivemongo.api.gridfs.GridFS +import reactivemongo.api.bson.collection.BSONSerializationPack object RequireMigration { @silent diff --git a/output/src/main/scala/fix/play/Controller.scala b/output/src/main/scala/fix/play/Controller.scala index beaf6fd..d5d6efa 100644 --- a/output/src/main/scala/fix/play/Controller.scala +++ b/output/src/main/scala/fix/play/Controller.scala @@ -5,9 +5,10 @@ import scala.concurrent.Future import play.modules.reactivemongo.{ MongoController, ReactiveMongoComponents -}, MongoController.GridFS +} import com.github.ghik.silencer.silent +import MongoController.GridFS trait Controller extends MongoController { self: ReactiveMongoComponents => @silent diff --git a/rules/src/main/scala/Upgrade.scala b/rules/src/main/scala/Upgrade.scala index 88d9f5a..65f2639 100644 --- a/rules/src/main/scala/Upgrade.scala +++ b/rules/src/main/scala/Upgrade.scala @@ -3,10 +3,25 @@ package reactivemongo.scalafix import scalafix.v1._ import scala.meta._ -final class Upgrade extends SemanticRule("ReactiveMongoUpgrade") { +final class Upgrade extends SemanticRule("ReactiveMongoUpgrade") { self => override def fix(implicit doc: SemanticDocument): Patch = { + val fixes = Seq[Fix]( + coreUpgrade, + apiUpgrade, gridfsUpgrade, bsonUpgrade, streamingUpgrade, playUpgrade) + + val fixImport: PartialFunction[Importer, Patch] = + (fixes.foldLeft(PartialFunction.empty[Importer, Patch]) { + _ orElse _.`import` + }).orElse({ case _ => Patch.empty }) + + val pipeline = fixes.foldLeft[PartialFunction[Tree, Patch]]({ + case Import(importers) => Patch.fromIterable(importers.map(fixImport)) + }) { + _ orElse _.refactor + } + val transformer: PartialFunction[Tree, Patch] = - coreUpgrade orElse gridfsUpgrade orElse bsonUpgrade orElse streamingUpgrade orElse playUpgrade orElse { + pipeline orElse { case _ => //println(s"x = ${x.structure}") Patch.empty @@ -24,227 +39,400 @@ def insert[T](document: T, writeConcern: WriteConcern = writeConcern)(implicit w find[S, J](selector: S, projection: J)(implicit swriter: pack.Writer[S], pwriter: pack.Writer[J]) ~> Some(projection) -def aggregateWith1[T](explain: Boolean = false, allowDiskUse: Boolean = false, bypassDocumentValidation: Boolean = false, readConcern: Option[ReadConcern] = None, readPreference: ReadPreference = ReadPreference.primary, batchSize: Option[Int] = None)(f: AggregationFramework => AggregationPipeline)(implicit ec: ExecutionContext, reader: pack.Reader[T], cf: CursorFlattener[Cursor], cp: CursorProducer[T]) ~> aggregateWith + */ -aggregatorContext[T](firstOperator: PipelineOperator, otherOperators: List[PipelineOperator], explain: Boolean, allowDiskUse: Boolean, bypassDocumentValidation: Boolean, readConcern: Option[ReadConcern], readPreference: ReadPreference, batchSize: Option[Int])(implicit reader: pack.Reader[T]): AggregatorContext[T] + // --- -AsyncDriver: def connect(nodes: Seq[String], options: MongoConnectionOptions = MongoConnectionOptions.default, authentications: Seq[Authenticate] = Seq.empty, name: Option[String] = None): Future[MongoConnection] + private def streamingUpgrade(implicit doc: SemanticDocument) = Fix( + refactor = { + case t @ Term.Apply( + Term.Select(c @ Term.Name(_), Term.Name("responseSource")), _) if ( + c.symbol.info.exists( + _.signature.toString startsWith "AkkaStreamCursor")) => + Patch.replaceTree(t, s"??? /* ${t.syntax}: Use bulkSource */") + + case t @ Term.Apply( + Term.Select(c @ Term.Name(_), Term.Name("responsePublisher")), _) if ( + c.symbol.info.exists( + _.signature.toString startsWith "AkkaStreamCursor")) => + Patch.replaceTree(t, s"??? /* ${t.syntax}: Use bulkPublisher */") + + case t @ Term.Apply( + Term.Select(c @ Term.Name(_), Term.Name("responseEnumerator")), _) if ( + c.symbol.info.exists( + _.signature.toString startsWith "PlayIterateesCursor")) => + Patch.replaceTree(t, s"??? /* ${t.syntax}: Use bulkEnumerator */") + + }) + + private def coreUpgrade(implicit doc: SemanticDocument) = Fix( + `import` = { + case Importer( + s @ Term.Select( + Term.Select(Term.Name("reactivemongo"), Term.Name("core")), + Term.Name("errors") + ), + importees + ) => { + val patches = Seq.newBuilder[Patch] + + importees.foreach { + case i @ Importee.Name( + Name.Indeterminate("DetailedDatabaseException" | + "GenericDatabaseException")) => + patches += (Patch.removeImportee(i) + Patch.addGlobalImport( + Importer(s, List(importee"DatabaseException")))).atomic + + case i @ Importee.Name( + Name.Indeterminate("ConnectionException" | + "ConnectionNotInitialized" | + "DriverException" | "GenericDriverException")) => + patches += (Patch.removeImportee(i) + Patch.addGlobalImport( + Importer(s, List(importee"ReactiveMongoException")))).atomic + + case _ => + } + + Patch.fromIterable(patches.result()) + } + }, + refactor = { + case t @ Type.Name("DetailedDatabaseException" | + "GenericDatabaseException") if (t.symbol.info.exists( + _.toString startsWith "reactivemongo/core/errors/")) => + Patch.replaceTree(t, "DatabaseException") + + case t @ Type.Name("ConnectionException" | + "ConnectionNotInitialized" | + "DriverException" | + "GenericDriverException") if (t.symbol.info.exists( + _.toString startsWith "reactivemongo/core/errors/")) => + Patch.replaceTree(t, "ReactiveMongoException") + }) + + private object QueryBuilderNaming { + private val mapping = Map( + "partial" -> "allowPartialResults", + "sortOption" -> "sort", + "projectionOption" -> "projection", + "hintOption" -> "hint", + "explainFlag" -> "explain", + "snapshotFlag" -> "snapshot", + "commentString" -> "comment", + "maxTimeMsOption" -> "maxTimeMs") + + def unapply(name: String): Option[String] = mapping.get(name) + } -MongoDriver => AsyncDriver + private def apiUpgrade(implicit doc: SemanticDocument) = Fix( + `import` = { + case Importer( + Term.Select( + apiPkg @ Term.Select(Term.Name("reactivemongo"), Term.Name("api")), + Term.Name("commands") + ), + importees + ) => { + val patches = Seq.newBuilder[Patch] -MongoConnection.askClose => close + importees.foreach { + case i @ Importee.Name(Name.Indeterminate("CollStatsResult")) => + patches += (Patch.removeImportee(i) + Patch.addGlobalImport( + Importer(apiPkg, List(importee"CollectionStats")))).atomic -MongoConnection.parseURL => fromString + case i @ Importee.Name(Name.Indeterminate("WriteConcern")) => + patches += (Patch.removeImportee(i) + Patch.addGlobalImport( + Importer(apiPkg, List(importee"WriteConcern")))).atomic -MongoConnection.def authenticate(db: String, user: String, password: String): Future[SuccessfulAuthentication] + failoverStrategy + case i @ Importee.Name( + Name.Indeterminate("MultiBulkWriteResult")) => + patches += Patch.removeImportee(i) -Collection.rename => coll.db.renameCollection(coll.name, ...) + case _ => + } -DefaultDB => DB -GenericDB => DB -reactivemongo.api.commands.CollStatsResult => CollectionStats + Patch.fromIterable(patches.result()) + } -(QueryOps|GenericQueryBuilder).partial ~> GenericQueryBuilder.allowPartialResults + case Importer( + apiPkg @ Term.Select(Term.Name("reactivemongo"), Term.Name("api")), + importees + ) => { + val patches = Seq.newBuilder[Patch] + + importees.foreach { + case i @ Importee.Name(Name.Indeterminate("BSONSerializationPack")) => + patches += (Patch.removeImportee(i) + Patch.addGlobalImport( + Importer( + Term.Select(Term.Select( + apiPkg, + Term.Name("bson")), Term.Name("collection")), + List(importee"BSONSerializationPack")))).atomic + + case i @ Importee.Name(Name.Indeterminate( + "DefaultDB" | "GenericDB")) => + patches += (Patch.removeImportee(i) + Patch.addGlobalImport( + Importer(apiPkg, List(importee"DB")))).atomic + + case i @ Importee.Name(Name.Indeterminate("MongoDriver")) => + patches += (Patch.removeImportee(i) + Patch.addGlobalImport( + Importer(apiPkg, List(importee"AsyncDriver")))).atomic + + case _ => + } + + Patch.fromIterable(patches.result()) + } + }, + refactor = { + case t @ Term.Apply( + Term.ApplyType( + x @ Term.Select(Term.Name(c), Term.Name("aggregateWith1")), + typeArgs + ), + args + ) if (x.symbol.info.exists(_.toString startsWith "reactivemongo/api/collections/GenericCollection")) => { + val refactored = Term.Apply(Term.ApplyType( + Term.Select(Term.Name(c), Term.Name("aggregateWith")), + typeArgs), args) -collection.aggregationFramework => AggregationFramework - */ + Patch.replaceTree(t, refactored.syntax) + } - // --- + case t @ Term.Select(op @ Term.Name(_), Term.Name("makePipe")) if (t.symbol.info.exists(_.toString startsWith "reactivemongo/api/commands/AggregationFramework")) => + Patch.replaceTree(t, op.syntax) - private def streamingUpgrade(implicit doc: SemanticDocument): PartialFunction[Tree, Patch] = { - case t @ Term.Apply( - Term.Select(c @ Term.Name(_), Term.Name("responseSource")), _) if ( - c.symbol.info.exists( - _.signature.toString startsWith "AkkaStreamCursor")) => - Patch.replaceTree(t, s"??? /* ${t.syntax}: Use bulkSource */") - - case t @ Term.Apply( - Term.Select(c @ Term.Name(_), Term.Name("responsePublisher")), _) if ( - c.symbol.info.exists( - _.signature.toString startsWith "AkkaStreamCursor")) => - Patch.replaceTree(t, s"??? /* ${t.syntax}: Use bulkPublisher */") - - case t @ Term.Apply( - Term.Select(c @ Term.Name(_), Term.Name("responseEnumerator")), _) if ( - c.symbol.info.exists( - _.signature.toString startsWith "PlayIterateesCursor")) => - Patch.replaceTree(t, s"??? /* ${t.syntax}: Use bulkEnumerator */") + case t @ Term.Select( + Term.Select(Term.Name(c), Term.Name("BatchCommands")), + Term.Name("AggregationFramework") + ) if (t.symbol.info.exists(_.toString startsWith "reactivemongo/api/collections/bson/BSONBatchCommands")) => + Patch.replaceTree(t, s"${c}.AggregationFramework") - } + case t @ Term.Select(Term.Name(c), Term.Name("aggregationFramework")) => + Patch.replaceTree(t, s"${c}.AggregationFramework") - private def coreUpgrade(implicit doc: SemanticDocument): PartialFunction[Tree, Patch] = { - case im @ Importer( - Term.Select( - Term.Select(Term.Name("reactivemongo"), Term.Name("core")), - Term.Name("errors") - ), - importees - ) => { - val is = importees.map { - case Importee.Name( - Name.Indeterminate("DetailedDatabaseException" | - "GenericDatabaseException")) => - "DatabaseException" - - case Importee.Name( - Name.Indeterminate("ConnectionException" | - "ConnectionNotInitialized" | - "DriverException" | "GenericDriverException")) => - "ReactiveMongoException" - - case i => - i.syntax - }.distinct.sorted - - val upd = if (is.size > 1) { - is.mkString("{ ", ", ", " }") - } else is.mkString - - Patch.replaceTree(im, s"reactivemongo.core.errors.$upd") - } + // --- - case t @ Type.Name("DetailedDatabaseException" | - "GenericDatabaseException") if (t.symbol.info.exists( - _.toString startsWith "reactivemongo/core/errors/")) => { + case t @ Term.Name(QueryBuilderNaming(newName)) if (t.symbol.info.exists(_.toString startsWith "reactivemongo/api/collections/GenericQueryBuilder")) => + Patch.replaceTree(t, newName) + + /* + case t @ Term.Select(x, Term.Name(nme)) if (t.symbol.info.exists(_.toString startsWith "reactivemongo/api/collections/GenericQueryBuilder")) => + println(s"====+> $t") + queryBuilderRefactoring.get(nme) match { + case Some(refactoring) => + Patch.replaceTree(t, Term.Select( + refactorQueryBuilder(x), Term.Name(refactoring)).syntax) + + case _ => + Patch.empty + } + */ + + case t @ Term.Apply( + Term.Select(c @ Term.Name(_), Term.Name("find")), + List(a, b)) if (t.symbol.info.exists { i => + val sym = i.toString + + sym.startsWith("reactivemongo/api/collections/GenericCollection") && + sym.indexOf("projection: J") != -1 + }) => { + val (selector, projection): (Term, Term) = a match { + case Term.Assign(n @ Term.Name("projection"), arg) => + b -> Term.Assign(n, q"Some(${arg})") + + case _ => a -> (b match { + case Term.Assign(n @ Term.Name("projection"), arg) => + Term.Assign(n, q"Some(${arg})") + + case _ => + q"Some($b)" + }) + } + + Patch.replaceTree(t, s"${c}.find($selector, $projection)") + } - Patch.replaceTree(t, "DatabaseException") - } + // --- - case t @ Type.Name("ConnectionException" | - "ConnectionNotInitialized" | - "DriverException" | - "GenericDriverException") if (t.symbol.info.exists( - _.toString startsWith "reactivemongo/core/errors/")) => { + case t @ Term.Apply( + Term.Select(Term.Name(c), Term.Name("rename")), args) if ( + t.symbol.info.exists { + _.toString startsWith "reactivemongo/api/CollectionMetaCommands" + }) => + Patch.replaceTree(t, s"""${c}.db.connection.database("admin").flatMap(_.renameCollection(${c}.db.name, ${c}.name, ${args.map(_.syntax).mkString(", ")}))""") - Patch.replaceTree(t, "ReactiveMongoException") - } - } + // --- - private def playUpgrade(implicit doc: SemanticDocument): PartialFunction[Tree, Patch] = { - case i @ Importer( - Term.Name("MongoController"), - List(Importee.Name(Name.Indeterminate("JsGridFS"))) - ) => Patch.replaceTree(i, "MongoController.GridFS") - - case n @ Type.Name("JsGridFS") => - Patch.replaceTree(n, "GridFS") - - case p @ Term.Apply( - Term.Apply(Term.Name("gridFSBodyParser"), gfs :: _), - _ :: _ :: mat :: Nil) => { - if (gfs.symbol.info.map( - _.signature.toString).exists(_ startsWith "Future[")) { - Patch.replaceTree(p, s"""gridFSBodyParser($gfs)($mat)""") - } else { - Patch.replaceTree( - p, s"""gridFSBodyParser(Future.successful($gfs))($mat)""") - } - } + case t @ Term.Select(Term.Name(d), + Term.Name("connect" | "connection")) if (t.symbol.info.exists { x => + val sym = x.toString + sym.startsWith("reactivemongo/api/MongoDriver") || sym.startsWith( + "reactivemongo/api/AsyncDriver") + }) => + Patch.replaceTree(t, s"${d}.connect") - case p @ Term.Apply( - Term.Apply(Term.Name("gridFSBodyParser"), gfs :: _ :: _), - _ :: _ :: mat :: _ :: Nil) => - Patch.replaceTree( - p, s"""gridFSBodyParser(Future.successful($gfs))($mat)""") + // --- - } + case t @ Term.Select(Term.Name(c), Term.Name(m)) if ( + t.symbol.info.exists( + _.toString startsWith "reactivemongo/api/MongoConnection")) => + m match { + case "askClose" => + Patch.replaceTree(t, s"${c}.close") - def bsonUpgrade(implicit doc: SemanticDocument): PartialFunction[Tree, Patch] = { - case i @ Importer( - Term.Select(Term.Name("reactivemongo"), Term.Name("bson")), - _ - ) => Patch.replaceTree(i, i.syntax.replace( - "reactivemongo.bson", "reactivemongo.api.bson")) - - case i @ Import(List(Importer( - s @ Term.Select(Term.Name("reactivemongo"), Term.Name("api")), - importees - ))) => { - var changed = false - val is = importees.filterNot { i => - val n = i.toString - if (n == "BSONSerializationPack" || n == "_") { - changed = true - - true - } else false - } + case "parseURI" => + Patch.replaceTree(t, s"${c}.fromString") - val up = if (is.nonEmpty) { - s"""import reactivemongo.api.bson.collection.BSONSerializationPack -import ${Importer(s, is).syntax}""" - } else { - "import reactivemongo.api.bson.collection.BSONSerializationPack" - } + case _ => + Patch.empty + } - Patch.replaceTree(i, up) - } + // --- - case t @ Type.Select( - Term.Select(Term.Name("reactivemongo"), Term.Name("bson")), - Type.Name(n) - ) => - Patch.replaceTree(t, s"reactivemongo.api.bson.$n") + case t @ Type.Name("CollStatsResult") if (t.symbol.info.exists( + _.toString startsWith "reactivemongo/api/commands/")) => + Patch.replaceTree(t, "CollectionStats") - case t @ Term.Select( - Term.Select( - Term.Select(Term.Name("reactivemongo"), Term.Name("api")), - Term.Name("collections") - ), - Term.Name("bson") - ) => Patch.replaceTree(t, s"reactivemongo.api.bson.collection") + // --- - case v @ Term.Apply(Term.Select( - Term.Apply( - Term.ApplyType( - Term.Select(d @ Term.Name(n), Term.Name("getAs")), - List(t @ Type.Name("BSONNumberLike" | "BSONBooleanLike")) - ), - List(f) - ), - Term.Name("map") - ), - List(body) - ) if (d.symbol.info.exists { s => - val t = s.signature.toString - t == "BSONDocument" || t == "BSONArray" - }) => { - val b = body match { - case Term.Select(_: Term.Placeholder, expr) => - s"_.${expr.syntax}.toOption" - - case Term.Block(List(Term.Function(List(param), b))) => - s"${param.syntax} => (${b.syntax}).toOption" + case t @ Type.Name("DefaultDB" | "GenericDB") if ( + t.symbol.info.exists(_.toString startsWith "reactivemongo/api/")) => + Patch.replaceTree(t, "DB") - case _ => - body.syntax + case t @ Type.Name("MongoDriver") if (t.symbol.info.exists( + _.toString startsWith "reactivemongo/api/")) => + Patch.replaceTree(t, "AsyncDriver") + + case t @ Type.Name("MultiBulkWriteResult") if ( + t.symbol.toString startsWith "reactivemongo/api/commands/") => + Patch.replaceTree(t, "Any /* MultiBulkWriteResult ~> anyCollection.MultiBulkWriteResult */") + + }) + + private def playUpgrade(implicit doc: SemanticDocument) = Fix( + `import` = { + case Importer( + x @ Term.Name("MongoController"), importees + ) if (x.symbol.toString startsWith "play/modules/reactivemongo/") => { + val patches = Seq.newBuilder[Patch] + + importees.foreach { + case i @ Importee.Name(Name.Indeterminate("JsGridFS")) => + patches += (Patch.removeImportee(i) + Patch.addGlobalImport( + Importer(x, List(importee"GridFS")))).atomic + + case _ => + } + + Patch.fromIterable(patches.result()) + } + }, + refactor = { + case n @ Type.Name("JsGridFS") => + Patch.replaceTree(n, "GridFS") + + case p @ Term.Apply( + Term.Apply(Term.Name("gridFSBodyParser"), gfs :: _), + _ :: _ :: mat :: Nil) => { + if (gfs.symbol.info.map( + _.signature.toString).exists(_ startsWith "Future[")) { + Patch.replaceTree(p, s"""gridFSBodyParser($gfs)($mat)""") + } else { + Patch.replaceTree( + p, s"gridFSBodyParser(Future.successful($gfs))($mat)") + } } - Patch.replaceTree(v, s"${n}.getAsOpt[${t.syntax}](${f.syntax}).flatMap { $b }") - } + case p @ Term.Apply( + Term.Apply(Term.Name("gridFSBodyParser"), gfs :: _ :: _), + _ :: _ :: mat :: _ :: Nil) => + Patch.replaceTree( + p, s"gridFSBodyParser(Future.successful($gfs))($mat)") + + }) + + private def bsonUpgrade(implicit doc: SemanticDocument) = Fix( + `import` = { + case Importer( + Term.Select( + Term.Name("reactivemongo"), Term.Name("bson")), is) => { + val apiPkg = Term.Select(Term.Select( + Term.Name("reactivemongo"), + Term.Name("api")), Term.Name("bson")) + + Patch.fromIterable(is.flatMap { i => + Seq((Patch.removeImportee(i) + Patch.addGlobalImport( + Importer(apiPkg, List(i)))).atomic) + }) + } + }, + refactor = { + case t @ Type.Select( + Term.Select(Term.Name("reactivemongo"), Term.Name("bson")), + Type.Name(n) + ) => + Patch.replaceTree(t, s"reactivemongo.api.bson.$n") + + case t @ Term.Select( + Term.Select( + Term.Select(Term.Name("reactivemongo"), Term.Name("api")), + Term.Name("collections") + ), + Term.Name("bson") + ) => Patch.replaceTree(t, s"reactivemongo.api.bson.collection") - case getAs @ Term.Apply( - Term.ApplyType( - Term.Select(x @ Term.Name(a), Term.Name("getAs")), - List(Type.Name(t)) + case v @ Term.Apply(Term.Select( + Term.Apply( + Term.ApplyType( + Term.Select(d @ Term.Name(n), Term.Name("getAs")), + List(t @ Type.Name("BSONNumberLike" | "BSONBooleanLike")) + ), + List(f) + ), + Term.Name("map") ), - List(f) - ) if (t != "BSONNumberLike" && t != "BSONBooleanLike" && - x.symbol.info.exists { i => - val s = i.signature.toString + List(body) + ) if (d.symbol.info.exists { s => + val t = s.signature.toString + t == "BSONDocument" || t == "BSONArray" + }) => { + val b = body match { + case Term.Select(_: Term.Placeholder, expr) => + s"_.${expr.syntax}.toOption" + + case Term.Block(List(Term.Function(List(param), b))) => + s"${param.syntax} => (${b.syntax}).toOption" + + case _ => + body.syntax + } + + Patch.replaceTree(v, s"${n}.getAsOpt[${t.syntax}](${f.syntax}).flatMap { $b }") + } - s == "BSONDocument" || s == "BSONArray" - }) => - Patch.replaceTree(getAs, s"${a}.getAsOpt[${t}]($f)") + case getAs @ Term.Apply( + Term.ApplyType( + Term.Select(x @ Term.Name(a), Term.Name("getAs")), + List(Type.Name(t)) + ), + List(f) + ) if (t != "BSONNumberLike" && t != "BSONBooleanLike" && + x.symbol.info.exists { i => + val s = i.signature.toString - case u @ Term.Apply( - Term.Select(x @ Term.Name(a), Term.Name("getUnflattenedTry")), - List(f) - ) if (x.symbol.info.exists(_.signature.toString == "BSONDocument")) => - Patch.replaceTree(u, s"${a}.getAsUnflattenedTry[reactivemongo.api.bson.BSONValue]($f)") - } + s == "BSONDocument" || s == "BSONArray" + }) => + Patch.replaceTree(getAs, s"${a}.getAsOpt[${t}]($f)") + + case u @ Term.Apply( + Term.Select(x @ Term.Name(a), Term.Name("getUnflattenedTry")), + List(f) + ) if (x.symbol.info.exists(_.signature.toString == "BSONDocument")) => + Patch.replaceTree(u, s"${a}.getAsUnflattenedTry[reactivemongo.api.bson.BSONValue]($f)") + }) - private def gridfsUpgrade(implicit doc: SemanticDocument): PartialFunction[Tree, Patch] = { + private def gridfsUpgrade(implicit doc: SemanticDocument) = Fix({ // Extractors object GridFSTermName { @@ -329,5 +517,11 @@ import ${Importer(s, is).syntax}""" Patch.replaceTree(gridfsRm, s"${gt}.remove(${ref}.id)") } - } + }) + + // --- + + private case class Fix( + refactor: PartialFunction[Tree, Patch], + `import`: PartialFunction[Importer, Patch] = PartialFunction.empty[Importer, Patch]) }