From 97952d1025b768615f69e6e161e190eb9f26e26f Mon Sep 17 00:00:00 2001 From: Wojciech Mazur Date: Fri, 26 Apr 2024 17:05:04 +0200 Subject: [PATCH] Initial commit --- .github/workflows/ci.yaml | 44 ++ .gitignore | 16 + .scalafmt.conf | 3 + LICENSE | 201 ++++++++ build.sbt | 87 ++++ .../rewrite/accessible/AllCapabilities.scala | 80 +++ .../main/scala/rewrite/accessible/Basic.scala | 51 ++ .../rewrite/accessible/CompanionExists.scala | 36 ++ .../rewrite/accessible/ImplicitsParams.scala | 23 + .../main/scala/rewrite/mockable/Basic.scala | 58 +++ .../scala/rewrite/mockable/Overloaded.scala | 43 ++ .../scala/rewrite/mockable/ZStreams.scala | 36 ++ .../rewrite/accessible/AllCapabilities.scala | 104 ++++ .../main/scala/rewrite/accessible/Basic.scala | 71 +++ .../rewrite/accessible/CompanionExists.scala | 49 ++ .../rewrite/accessible/ImplicitsParams.scala | 28 ++ .../main/scala/rewrite/mockable/Basic.scala | 99 ++++ .../scala/rewrite/mockable/Overloaded.scala | 74 +++ .../scala/rewrite/mockable/ZStreams.scala | 56 +++ project/build.properties | 1 + project/plugins.sbt | 5 + readme.md | 34 ++ .../META-INF/services/scalafix.v1.Rule | 2 + .../org/virtuslab/rewrites/ZIOCodeGen.scala | 202 ++++++++ .../rewrites/ZioAccessibleCodeGen.scala | 318 ++++++++++++ .../rewrites/ZioMockableCodeGen.scala | 467 ++++++++++++++++++ tests/src/test/scala/fix/RuleSuite.scala | 8 + 27 files changed, 2196 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 .scalafmt.conf create mode 100644 LICENSE create mode 100644 build.sbt create mode 100644 input/src/main/scala/rewrite/accessible/AllCapabilities.scala create mode 100644 input/src/main/scala/rewrite/accessible/Basic.scala create mode 100644 input/src/main/scala/rewrite/accessible/CompanionExists.scala create mode 100644 input/src/main/scala/rewrite/accessible/ImplicitsParams.scala create mode 100644 input/src/main/scala/rewrite/mockable/Basic.scala create mode 100644 input/src/main/scala/rewrite/mockable/Overloaded.scala create mode 100644 input/src/main/scala/rewrite/mockable/ZStreams.scala create mode 100644 output/src/main/scala/rewrite/accessible/AllCapabilities.scala create mode 100644 output/src/main/scala/rewrite/accessible/Basic.scala create mode 100644 output/src/main/scala/rewrite/accessible/CompanionExists.scala create mode 100644 output/src/main/scala/rewrite/accessible/ImplicitsParams.scala create mode 100644 output/src/main/scala/rewrite/mockable/Basic.scala create mode 100644 output/src/main/scala/rewrite/mockable/Overloaded.scala create mode 100644 output/src/main/scala/rewrite/mockable/ZStreams.scala create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 readme.md create mode 100644 rules/src/main/resources/META-INF/services/scalafix.v1.Rule create mode 100644 rules/src/main/scala/org/virtuslab/rewrites/ZIOCodeGen.scala create mode 100644 rules/src/main/scala/org/virtuslab/rewrites/ZioAccessibleCodeGen.scala create mode 100644 rules/src/main/scala/org/virtuslab/rewrites/ZioMockableCodeGen.scala create mode 100644 tests/src/test/scala/fix/RuleSuite.scala diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..c2b7918 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,44 @@ +name: Tests +on: + pull_request: + push: + branches: + - main + tags: + - '*' + +jobs: + test: + name: Test scalafix rules + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: coursier/cache-action@v6 + - uses: coursier/setup-action@v1 + with: + jvm: temurin:17 + + - name: Test rules + run: sbt "+test" + + publish_release: + runs-on: [ubuntu-22.04] + needs: [test] + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') + + steps: + - uses: coursier/setup-action@v1 + with: + jvm: temurin:8 + - uses: actions/checkout@v4 + - name: Setup PGP Key + run: | + echo -n "$PGP_SECRET" | base64 --decode | gpg --batch --import + env: + PGP_SECRET: ${{ secrets.PGP_SECRET }} + - name: Publish release + env: + PGP_PW: ${{ secrets.PGP_PW }} + SONATYPE_PW: ${{ secrets.MAVEN_PASSWORD }} + SONATYPE_USER: ${{ secrets.MAVEN_USER }} + run: sbt "+rules/publishSigned; sonatypeBundleRelease" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8094f3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# sbt specific +**/target/ +project/plugins/project/ + +# VS Code +.vscode/ +# Metals +**/.bloop/ +**/.metals/ +**/metals.sbt + +# Bloop +**/.bsp + +# scala-cli +**/.scala-build diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..3a645e1 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,3 @@ +version = "3.7.15" +runner.dialect = scala3 +maxColumn = 140 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..1ea4b5f --- /dev/null +++ b/build.sbt @@ -0,0 +1,87 @@ +import xerial.sbt.Sonatype._ + +lazy val V = _root_.scalafix.sbt.BuildInfo + +val Scala212Version = "2.12.19" +val Scala213Version = "2.13.13" +val Scala3Version = "3.3.3" + +inThisBuild( + List( + organization := "org.virtuslab", + homepage := Some(url("https://github.com/VirtusLabRnD/scalafix-migrate-zio-macros")), + licenses := List( + "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") + ), + developers := List( + Developer("WojciechMazur", "Wojciech Mazur", "wmazur@virtuslab.com", url("https://github.com/WojciechMazur")) + ), + version := "0.1.0", + scalaVersion := Scala213Version, + semanticdbEnabled := true, + semanticdbIncludeInJar := true, + semanticdbVersion := scalafixSemanticdb.revision, + versionScheme := Some("early-semver") + ) +) + +Global / PgpKeys.pgpPassphrase := sys.env.get("PGP_PW").map(_.toCharArray()) +// Global / PgpKeys.pgpSigningKey := Some("BCE7DB09E1B2687C9C9C3AB2D8DF100359D36CBF") +Global / publishTo := sonatypePublishToBundle.value +Global / credentials ++= ( + for { + username <- sys.env.get("SONATYPE_USER") + password <- sys.env.get("SONATYPE_PW") + } yield Credentials("Sonatype Nexus Repository Manager", "oss.sonatype.org", username, password) +).toList +Global / scmInfo := Some( + ScmInfo( + url("https://github.com/VirtusLabRnD/scalafix-migrate-zio-macros"), + "scm:git@github.com:VirtusLabRnD/scalafix-migrate-zio-macros.git" + ) +) + +lazy val rules = project.settings( + moduleName := "scalafix-migrate-zio-macros", + libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % V.scalafixVersion, + crossScalaVersions := Seq(Scala212Version, Scala213Version) +) + +val ZIOVersion = "2.0.21" +val ZIOMockVersion = "1.0.0-RC11" + +lazy val commonTestDependencies = List( + "dev.zio" %% "zio" % ZIOVersion, + "dev.zio" %% "zio-managed" % ZIOVersion, + "dev.zio" %% "zio-mock" % ZIOMockVersion, + +) + +lazy val input = project.settings( + (publish / skip) := true, + scalaVersion := Scala213Version, + scalacOptions ++= Seq( + "-Ymacro-annotations" + ), + libraryDependencies ++= commonTestDependencies ++ Seq( + "dev.zio" %% "zio-macros" % ZIOVersion, + ) +) + +lazy val output = project.settings( + (publish / skip) := true, + scalaVersion := Scala3Version, + libraryDependencies ++= commonTestDependencies +) + +lazy val tests = project + .settings( + crossScalaVersions := Seq(Scala212Version, Scala213Version), + (publish / skip) := true, + libraryDependencies += "ch.epfl.scala" % "scalafix-testkit" % V.scalafixVersion % Test cross CrossVersion.full, + scalafixTestkitOutputSourceDirectories := (output / Compile / unmanagedSourceDirectories).value, + scalafixTestkitInputSourceDirectories := (input / Compile / unmanagedSourceDirectories).value, + scalafixTestkitInputClasspath := (input / Compile / fullClasspath).value + ) + .dependsOn(rules) + .enablePlugins(ScalafixTestkitPlugin) diff --git a/input/src/main/scala/rewrite/accessible/AllCapabilities.scala b/input/src/main/scala/rewrite/accessible/AllCapabilities.scala new file mode 100644 index 0000000..12adfa5 --- /dev/null +++ b/input/src/main/scala/rewrite/accessible/AllCapabilities.scala @@ -0,0 +1,80 @@ +/* +rule = ZIOAccessibleCodeGen + */ +package rewrite.accessible + +import zio._ +import zio.managed._ +import zio.stream._ +import zio.macros.accessible + +@accessible +object AllCapabilities { + trait Service { + val static: UIO[String] + def zeroArgs: UIO[Int] + def zeroArgsWithParens(): UIO[Long] + def singleArg(arg1: Int): UIO[String] + def multiArgs(arg1: Int, arg2: Long): UIO[String] + def multiParamLists(arg1: Int)(arg2: Long): UIO[String] + def typedVarargs[T](arg1: Int, arg2: T*): UIO[T] + def command(arg1: Int): UIO[Unit] + def overloaded(arg1: Int): UIO[String] + def overloaded(arg1: Long): UIO[String] + + val staticManaged: UManaged[String] + def zeroArgsManaged: UManaged[Int] + def zeroArgsTypedManaged[T]: UManaged[T] + def zeroArgsWithParensManaged(): UManaged[Long] + def singleArgManaged(arg1: Int): UManaged[String] + def multiArgsManaged(arg1: Int, arg2: Long): UManaged[String] + def multiParamListsManaged(arg1: Int)(arg2: Long): UManaged[String] + def typedVarargsManaged[T](arg1: Int, arg2: T*): UManaged[T] + def commandManaged(arg1: Int): UManaged[Unit] + def overloadedManaged(arg1: Int): UManaged[String] + def overloadedManaged(arg1: Long): UManaged[String] + + def function(arg1: Int): String + def sink(arg1: Int): ZSink[Any, Nothing, Int, Int, List[Int]] + def stream(arg1: Int): ZStream[Any, Nothing, Int] + } +} + +object AllCapabilitiesTest { + val static: ZIO[AllCapabilities.Service, Nothing, String] = AllCapabilities.static + def zeroArgs: ZIO[AllCapabilities.Service, Nothing, Int] = AllCapabilities.zeroArgs + def zeroArgsWithParens(): ZIO[AllCapabilities.Service, Nothing, Long] = AllCapabilities.zeroArgsWithParens() + def singleArg(arg1: Int): ZIO[AllCapabilities.Service, Nothing, String] = AllCapabilities.singleArg(arg1) + def multiArgs(arg1: Int, arg2: Long): ZIO[AllCapabilities.Service, Nothing, String] = + AllCapabilities.multiArgs(arg1, arg2) + def multiParamLists(arg1: Int)(arg2: Long): ZIO[AllCapabilities.Service, Nothing, String] = + AllCapabilities.multiParamLists(arg1)(arg2) + def typedVarargs[T](arg1: Int, arg2: T*): ZIO[AllCapabilities.Service, Nothing, T] = + AllCapabilities.typedVarargs[T](arg1, arg2: _*) + def command(arg1: Int): ZIO[AllCapabilities.Service, Nothing, Unit] = AllCapabilities.command(arg1) + def overloaded(arg1: Int): ZIO[AllCapabilities.Service, Nothing, String] = AllCapabilities.overloaded(arg1) + def overloaded(arg1: Long): ZIO[AllCapabilities.Service, Nothing, String] = AllCapabilities.overloaded(arg1) + + val staticManaged: ZManaged[AllCapabilities.Service, Nothing, String] = AllCapabilities.staticManaged + def zeroArgsManaged: ZManaged[AllCapabilities.Service, Nothing, Int] = AllCapabilities.zeroArgsManaged + def zeroArgsTypedManaged[T]: ZManaged[AllCapabilities.Service, Nothing, T] = AllCapabilities.zeroArgsTypedManaged[T] + def zeroArgsWithParensManaged(): ZManaged[AllCapabilities.Service, Nothing, Long] = + AllCapabilities.zeroArgsWithParensManaged() + def singleArgManaged(arg1: Int): ZManaged[AllCapabilities.Service, Nothing, String] = + AllCapabilities.singleArgManaged(arg1) + def multiArgsManaged(arg1: Int, arg2: Long): ZManaged[AllCapabilities.Service, Nothing, String] = + AllCapabilities.multiArgsManaged(arg1, arg2) + def multiParamListsManaged(arg1: Int)(arg2: Long): ZManaged[AllCapabilities.Service, Nothing, String] = + AllCapabilities.multiParamListsManaged(arg1)(arg2) + def typedVarargsManaged[T](arg1: Int, arg2: T*): ZManaged[AllCapabilities.Service, Nothing, T] = + AllCapabilities.typedVarargsManaged[T](arg1, arg2: _*) + def commandManaged(arg1: Int): ZManaged[AllCapabilities.Service, Nothing, Unit] = AllCapabilities.commandManaged(arg1) + def overloadedManaged(arg1: Int): ZManaged[AllCapabilities.Service, Nothing, String] = + AllCapabilities.overloadedManaged(arg1) + def overloadedManaged(arg1: Long): ZManaged[AllCapabilities.Service, Nothing, String] = + AllCapabilities.overloadedManaged(arg1) + + def function(arg1: Int): ZIO[AllCapabilities.Service, Throwable, String] = AllCapabilities.function(arg1) + def sink(arg1: Int): ZSink[AllCapabilities.Service, Nothing, Int, Int, List[Int]] = AllCapabilities.sink(arg1) + def stream(arg1: Int): ZStream[AllCapabilities.Service, Nothing, Int] = AllCapabilities.stream(arg1) +} diff --git a/input/src/main/scala/rewrite/accessible/Basic.scala b/input/src/main/scala/rewrite/accessible/Basic.scala new file mode 100644 index 0000000..5818369 --- /dev/null +++ b/input/src/main/scala/rewrite/accessible/Basic.scala @@ -0,0 +1,51 @@ +/* +rule = ZIOAccessibleCodeGen + */ +package rewrite.accessible + +import zio._ +import zio.macros.{accessible => accessibleAlias} + +trait Foo { val value: String } +case class Bar(value: String) extends Foo +case class Wrapped[T](value: T) + +@accessibleAlias +trait Service { + def get(key: String): UIO[Int] + def set(key: String, value: Int): UIO[Unit] + def reset: UIO[Unit] + def io: IO[String, Long] + def task: Task[Long] + def uio: UIO[Long] + def urio: URIO[String, Long] + def poly1[A: EnvironmentTag](a: A): UIO[Unit] + def poly2[A: EnvironmentTag]: IO[A, Unit] + def poly3[A: EnvironmentTag]: UIO[A] + def poly4[A: EnvironmentTag, B: EnvironmentTag](a: A): IO[B, Unit] + def poly5[A: EnvironmentTag, B: EnvironmentTag](a: A): IO[Unit, B] + def poly6[A: EnvironmentTag, B: EnvironmentTag]: IO[A, B] + def poly7[A: EnvironmentTag, B: EnvironmentTag, C: EnvironmentTag](a: A): IO[B, C] + def poly8[A: EnvironmentTag]: UIO[(A, String)] + def poly9[A <: Foo: EnvironmentTag]: UIO[A] + def poly10[A: EnvironmentTag](a: Wrapped[A]): UIO[A] +} + +object TestBasic { + val get: ZIO[Service with Any, Nothing, Int] = Service.get("foo") + val set: ZIO[Service with Any, Nothing, Unit] = Service.set("foo", 42) + val reset: ZIO[Service with Any, Nothing, Unit] = Service.reset + val io: ZIO[Service with Any, Throwable, Long] = Service.task + val uio: ZIO[Service with Any, Nothing, Long] = Service.uio + val urio: ZIO[Service with String, Nothing, Long] = Service.urio + val poly1: ZIO[Service with Any, Nothing, Unit] = Service.poly1("foo") + val poly2: ZIO[Service with Any, String, Unit] = Service.poly2[String] + val poly3: ZIO[Service with Any, Nothing, String] = Service.poly3[String] + val poly4: ZIO[Service with Any, Long, Unit] = Service.poly4[Int, Long](42) + val poly5: ZIO[Service with Any, Unit, Long] = Service.poly5[Int, Long](42) + val poly6: ZIO[Service with Any, String, Int] = Service.poly6[String, Int] + val poly7: ZIO[Service with Any, Long, Int] = Service.poly7[String, Long, Int]("foo") + val poly8: ZIO[Service with Any, Nothing, (Int, String)] = Service.poly8[Int] + val poly9: ZIO[Service with Any, Nothing, Bar] = Service.poly9[Bar] + val poly10: ZIO[Service with Any, Nothing, String] = Service.poly10(Wrapped("string")) +} diff --git a/input/src/main/scala/rewrite/accessible/CompanionExists.scala b/input/src/main/scala/rewrite/accessible/CompanionExists.scala new file mode 100644 index 0000000..fa484b9 --- /dev/null +++ b/input/src/main/scala/rewrite/accessible/CompanionExists.scala @@ -0,0 +1,36 @@ +/* +rule = ZIOAccessibleCodeGen + */ +package rewrite.accessible + +import zio._ +import zio.macros.accessible + +@accessible +trait ServiceCompanionExists { + def get(key: String): UIO[Int] +} + +object ServiceCompanionExists { + def default: ServiceCompanionExists = ??? +} + +@accessible +trait ServiceEmptyCompanion { + def get(key: String): UIO[Int] +} + +object ServiceEmptyCompanion + +@accessible +trait ServiceEmptyCompanion2 { + def get(key: String): UIO[Int] +} + +object ServiceEmptyCompanion2 {} + +object TestCompanionExists { + val get1: ZIO[ServiceCompanionExists with Any, Nothing, Int] = ServiceCompanionExists.get("foo") + val get2: ZIO[ServiceEmptyCompanion with Any, Nothing, Int] = ServiceEmptyCompanion.get("foo") + val get3: ZIO[ServiceEmptyCompanion2 with Any, Nothing, Int] = ServiceEmptyCompanion2.get("foo") +} diff --git a/input/src/main/scala/rewrite/accessible/ImplicitsParams.scala b/input/src/main/scala/rewrite/accessible/ImplicitsParams.scala new file mode 100644 index 0000000..7763b47 --- /dev/null +++ b/input/src/main/scala/rewrite/accessible/ImplicitsParams.scala @@ -0,0 +1,23 @@ +/* +rule = ZIOAccessibleCodeGen + */ +package rewrite.accessible + +import zio._ +import zio.stream.ZSink +import zio.macros.accessible + +case class GroupId(v: Int) +trait TransactionEvent + +@accessible +trait ImplicitsParamsService { + def reduceTransactions[R: Tag, E: Tag, A: Tag](groupId: GroupId, sink: ZSink[R, E, TransactionEvent, Nothing, A]): ZIO[R, E, A] + def eventsForGroup(id: GroupId)(implicit tag: Tag[GroupId]): List[TransactionEvent] +} + +object ImplicitsParamsServiceTest { + def sink: ZSink[Any, Nothing, TransactionEvent, Nothing, Unit] = ??? + val reduceTransactions: ZIO[ImplicitsParamsService, Nothing, Unit] = ImplicitsParamsService.reduceTransactions(GroupId(42), sink) + val eventsForGroup: ZIO[ImplicitsParamsService, Nothing, List[TransactionEvent]] = ImplicitsParamsService.eventsForGroup(GroupId(42)) +} diff --git a/input/src/main/scala/rewrite/mockable/Basic.scala b/input/src/main/scala/rewrite/mockable/Basic.scala new file mode 100644 index 0000000..387a4fb --- /dev/null +++ b/input/src/main/scala/rewrite/mockable/Basic.scala @@ -0,0 +1,58 @@ +/* +rule = ZIOMockableCodeGen + */ +package rewrite.mockable + +import zio._ +import zio.mock.{mockable, Mock} + +trait Foo { val value: String } +case class Bar(value: String) extends Foo +case class Wrapped[T](value: T) + +trait Service { + def get(key: String): UIO[Int] + def set(key: String, value: Int): UIO[Unit] + def reset: UIO[Unit] + def io: IO[String, Long] + def task: Task[Long] + def uio: UIO[Long] + def urio: URIO[String, Long] + def poly1[A: EnvironmentTag](a: A): UIO[Unit] + def poly2[A: EnvironmentTag]: IO[A, Unit] + def poly3[A: EnvironmentTag]: UIO[A] + def poly4[A: EnvironmentTag, B: EnvironmentTag](a: A): IO[B, Unit] + def poly5[A: EnvironmentTag, B: EnvironmentTag](a: A): IO[Unit, B] + def poly6[A: EnvironmentTag, B: EnvironmentTag]: IO[A, B] + def poly7[A: EnvironmentTag, B: EnvironmentTag, C: EnvironmentTag](a: A): IO[B, C] + def poly8[A: EnvironmentTag]: UIO[(A, String)] + def poly9[A <: Foo: EnvironmentTag]: UIO[A] + def poly10[A: EnvironmentTag](a: Wrapped[A]): UIO[A] +} + +trait ServiceCompanion +@mockable[Service] +object ServiceMock extends ServiceCompanion { + val foo = 42 +} + +object TestBasic { + implicitly[ServiceMock.type <:< Mock[Service]] + implicitly[ServiceMock.Get.type <:< ServiceMock.Effect[String, Nothing, Int]] + implicitly[ServiceMock.Set.type <:< ServiceMock.Effect[(String, Int), Nothing, Unit]] + implicitly[ServiceMock.Reset.type <:< ServiceMock.Effect[Unit, Nothing, Unit]] + implicitly[ServiceMock.Io.type <:< ServiceMock.Effect[Unit, String, Long]] + implicitly[ServiceMock.Task.type <:< ServiceMock.Effect[Unit, Throwable, Long]] + implicitly[ServiceMock.Uio.type <:< ServiceMock.Effect[Unit, Nothing, Long]] + implicitly[ServiceMock.Urio.type <:< ServiceMock.Effect[Unit, Nothing, Long]] + implicitly[ServiceMock.Poly1.type <:< ServiceMock.Poly.Effect.Input[Nothing, Unit]] + implicitly[ServiceMock.Poly2.type <:< ServiceMock.Poly.Effect.Error[Unit, Unit]] + implicitly[ServiceMock.Poly3.type <:< ServiceMock.Poly.Effect.Output[Unit, Nothing]] + implicitly[ServiceMock.Poly4.type <:< ServiceMock.Poly.Effect.InputError[Unit]] + implicitly[ServiceMock.Poly5.type <:< ServiceMock.Poly.Effect.InputOutput[Unit]] + implicitly[ServiceMock.Poly6.type <:< ServiceMock.Poly.Effect.ErrorOutput[Unit]] + implicitly[ServiceMock.Poly7.type <:< ServiceMock.Poly.Effect.InputErrorOutput] + implicitly[ServiceMock.Poly8.type <:< ServiceMock.Poly.Effect.Output[Unit, Nothing]] + implicitly[ServiceMock.Poly9.type <:< ServiceMock.Poly.Effect.Output[Unit, Nothing]] + implicitly[ServiceMock.Poly10.type <:< ServiceMock.Poly.Effect.InputOutput[Nothing]] +} diff --git a/input/src/main/scala/rewrite/mockable/Overloaded.scala b/input/src/main/scala/rewrite/mockable/Overloaded.scala new file mode 100644 index 0000000..34c2110 --- /dev/null +++ b/input/src/main/scala/rewrite/mockable/Overloaded.scala @@ -0,0 +1,43 @@ +/* +rule = ZIOMockableCodeGen + */ +package rewrite.mockable + +import zio._ +import zio.mock.{mockable, Mock} + +object Overloaded { + type OverloadedPureDefsModule = OverloadedPureDefsModule.Service + object OverloadedPureDefsModule { + trait Service { + def overloaded(n: Int): IO[String, String] + def overloaded(n: Long): IO[String, String] + } + } + + type OverloadedImpureDefsModule = OverloadedImpureDefsModule.Service + object OverloadedImpureDefsModule { + trait Service { + def overloaded(n: Int): String + def overloaded(n: Long): String + } + } +} +import Overloaded._ + +@mockable[OverloadedPureDefsModule] +object OverloadedPureDefsMocks + +@mockable[OverloadedImpureDefsModule] +object OverloadedImpureDefsMocks + +object OverloadedTest { + import Overloaded._ + implicitly[OverloadedPureDefsMocks.type <:< Mock[OverloadedPureDefsModule]] + implicitly[OverloadedPureDefsMocks.Overloaded._0.type <:< OverloadedPureDefsMocks.Effect[Int, String, String]] + implicitly[OverloadedPureDefsMocks.Overloaded._1.type <:< OverloadedPureDefsMocks.Effect[Long, String, String]] + + implicitly[OverloadedImpureDefsMocks.type <:< Mock[OverloadedImpureDefsModule]] + implicitly[OverloadedImpureDefsMocks.Overloaded._0.type <:< OverloadedImpureDefsMocks.Method[Int, Throwable, String]] + implicitly[OverloadedImpureDefsMocks.Overloaded._1.type <:< OverloadedImpureDefsMocks.Method[Long, Throwable, String]] +} diff --git a/input/src/main/scala/rewrite/mockable/ZStreams.scala b/input/src/main/scala/rewrite/mockable/ZStreams.scala new file mode 100644 index 0000000..aa8d9ae --- /dev/null +++ b/input/src/main/scala/rewrite/mockable/ZStreams.scala @@ -0,0 +1,36 @@ +/* +rule = ZIOMockableCodeGen + */ +package rewrite.mockable + +import zio._ +import zio.stream._ +import zio.mock.{mockable, Mock} + +object ZStreams { + type StreamDefsModule = StreamDefsModule.Service + object StreamDefsModule { + trait Service { + val static: ZStream[Any, String, String] + def zeroParams: ZStream[Any, String, String] + def zeroParamsWithParens(): ZStream[Any, String, String] + def singleParam(a: Int): ZStream[Any, String, String] + def manyParams(a: Int, b: String, c: Long): ZStream[Any, String, String] + def manyParamLists(a: Int)(b: String)(c: Long): ZStream[Any, String, String] + } + } +} + +@mockable[ZStreams.StreamDefsModule.Service] +object ZStreamsMock + +object TestZStreams { + import ZStreams.StreamDefsModule.Service + implicitly[ZStreamsMock.type <:< Mock[Service]] + implicitly[ZStreamsMock.Static.type <:< ZStreamsMock.Stream[Unit, String, String]] + implicitly[ZStreamsMock.ZeroParams.type <:< ZStreamsMock.Stream[Unit, String, String]] + implicitly[ZStreamsMock.ZeroParamsWithParens.type <:< ZStreamsMock.Stream[Unit, String, String]] + implicitly[ZStreamsMock.SingleParam.type <:< ZStreamsMock.Stream[Int, String, String]] + implicitly[ZStreamsMock.ManyParams.type <:< ZStreamsMock.Stream[(Int, String, Long), String, String]] + implicitly[ZStreamsMock.ManyParamLists.type <:< ZStreamsMock.Stream[(Int, String, Long), String, String]] +} diff --git a/output/src/main/scala/rewrite/accessible/AllCapabilities.scala b/output/src/main/scala/rewrite/accessible/AllCapabilities.scala new file mode 100644 index 0000000..0c1918c --- /dev/null +++ b/output/src/main/scala/rewrite/accessible/AllCapabilities.scala @@ -0,0 +1,104 @@ +package rewrite.accessible + +import zio._ +import zio.managed._ +import zio.stream._ + + +object AllCapabilities { + trait Service { + val static: UIO[String] + def zeroArgs: UIO[Int] + def zeroArgsWithParens(): UIO[Long] + def singleArg(arg1: Int): UIO[String] + def multiArgs(arg1: Int, arg2: Long): UIO[String] + def multiParamLists(arg1: Int)(arg2: Long): UIO[String] + def typedVarargs[T](arg1: Int, arg2: T*): UIO[T] + def command(arg1: Int): UIO[Unit] + def overloaded(arg1: Int): UIO[String] + def overloaded(arg1: Long): UIO[String] + + val staticManaged: UManaged[String] + def zeroArgsManaged: UManaged[Int] + def zeroArgsTypedManaged[T]: UManaged[T] + def zeroArgsWithParensManaged(): UManaged[Long] + def singleArgManaged(arg1: Int): UManaged[String] + def multiArgsManaged(arg1: Int, arg2: Long): UManaged[String] + def multiParamListsManaged(arg1: Int)(arg2: Long): UManaged[String] + def typedVarargsManaged[T](arg1: Int, arg2: T*): UManaged[T] + def commandManaged(arg1: Int): UManaged[Unit] + def overloadedManaged(arg1: Int): UManaged[String] + def overloadedManaged(arg1: Long): UManaged[String] + + def function(arg1: Int): String + def sink(arg1: Int): ZSink[Any, Nothing, Int, Int, List[Int]] + def stream(arg1: Int): ZStream[Any, Nothing, Int] + } + + // format: off + // Generated by ZIO Accessible CodeGen Scalafix Rule + val static: ZIO[AllCapabilities.Service, Nothing, String] = ZIO.serviceWithZIO[Service](_.static) + def zeroArgs(using izumi.reflect.Tag[AllCapabilities.Service]): ZIO[AllCapabilities.Service, Nothing, Int] = ZIO.serviceWithZIO[Service](_.zeroArgs) + def zeroArgsWithParens()(using izumi.reflect.Tag[AllCapabilities.Service]): ZIO[AllCapabilities.Service, Nothing, Long] = ZIO.serviceWithZIO[Service](_.zeroArgsWithParens()) + def singleArg(arg1: Int)(using izumi.reflect.Tag[AllCapabilities.Service]): ZIO[AllCapabilities.Service, Nothing, String] = ZIO.serviceWithZIO[Service](_.singleArg(arg1)) + def multiArgs(arg1: Int, arg2: Long)(using izumi.reflect.Tag[AllCapabilities.Service]): ZIO[AllCapabilities.Service, Nothing, String] = ZIO.serviceWithZIO[Service](_.multiArgs(arg1, arg2)) + def multiParamLists(arg1: Int)(arg2: Long)(using izumi.reflect.Tag[AllCapabilities.Service]): ZIO[AllCapabilities.Service, Nothing, String] = ZIO.serviceWithZIO[Service](_.multiParamLists(arg1)(arg2)) + def typedVarargs[T](arg1: Int, arg2: T*)(using izumi.reflect.Tag[AllCapabilities.Service]): ZIO[AllCapabilities.Service, Nothing, T] = ZIO.serviceWithZIO[Service](_.typedVarargs[T](arg1, arg2: _*)) + def command(arg1: Int)(using izumi.reflect.Tag[AllCapabilities.Service]): ZIO[AllCapabilities.Service, Nothing, Unit] = ZIO.serviceWithZIO[Service](_.command(arg1)) + def overloaded(arg1: Int)(using izumi.reflect.Tag[AllCapabilities.Service]): ZIO[AllCapabilities.Service, Nothing, String] = ZIO.serviceWithZIO[Service](_.overloaded(arg1)) + def overloaded(arg1: Long)(using izumi.reflect.Tag[AllCapabilities.Service]): ZIO[AllCapabilities.Service, Nothing, String] = ZIO.serviceWithZIO[Service](_.overloaded(arg1)) + val staticManaged: ZManaged[AllCapabilities.Service, Nothing, String] = ZManaged.serviceWithManaged[Service](_.staticManaged) + def zeroArgsManaged(using izumi.reflect.Tag[AllCapabilities.Service]): ZManaged[AllCapabilities.Service, Nothing, Int] = ZManaged.serviceWithManaged[Service](_.zeroArgsManaged) + def zeroArgsTypedManaged[T](using izumi.reflect.Tag[AllCapabilities.Service]): ZManaged[AllCapabilities.Service, Nothing, T] = ZManaged.serviceWithManaged[Service](_.zeroArgsTypedManaged[T]) + def zeroArgsWithParensManaged()(using izumi.reflect.Tag[AllCapabilities.Service]): ZManaged[AllCapabilities.Service, Nothing, Long] = ZManaged.serviceWithManaged[Service](_.zeroArgsWithParensManaged()) + def singleArgManaged(arg1: Int)(using izumi.reflect.Tag[AllCapabilities.Service]): ZManaged[AllCapabilities.Service, Nothing, String] = ZManaged.serviceWithManaged[Service](_.singleArgManaged(arg1)) + def multiArgsManaged(arg1: Int, arg2: Long)(using izumi.reflect.Tag[AllCapabilities.Service]): ZManaged[AllCapabilities.Service, Nothing, String] = ZManaged.serviceWithManaged[Service](_.multiArgsManaged(arg1, arg2)) + def multiParamListsManaged(arg1: Int)(arg2: Long)(using izumi.reflect.Tag[AllCapabilities.Service]): ZManaged[AllCapabilities.Service, Nothing, String] = ZManaged.serviceWithManaged[Service](_.multiParamListsManaged(arg1)(arg2)) + def typedVarargsManaged[T](arg1: Int, arg2: T*)(using izumi.reflect.Tag[AllCapabilities.Service]): ZManaged[AllCapabilities.Service, Nothing, T] = ZManaged.serviceWithManaged[Service](_.typedVarargsManaged[T](arg1, arg2: _*)) + def commandManaged(arg1: Int)(using izumi.reflect.Tag[AllCapabilities.Service]): ZManaged[AllCapabilities.Service, Nothing, Unit] = ZManaged.serviceWithManaged[Service](_.commandManaged(arg1)) + def overloadedManaged(arg1: Int)(using izumi.reflect.Tag[AllCapabilities.Service]): ZManaged[AllCapabilities.Service, Nothing, String] = ZManaged.serviceWithManaged[Service](_.overloadedManaged(arg1)) + def overloadedManaged(arg1: Long)(using izumi.reflect.Tag[AllCapabilities.Service]): ZManaged[AllCapabilities.Service, Nothing, String] = ZManaged.serviceWithManaged[Service](_.overloadedManaged(arg1)) + def function(arg1: Int)(using izumi.reflect.Tag[AllCapabilities.Service]): ZIO[AllCapabilities.Service, Nothing, String] = ZIO.serviceWith[Service](_.function(arg1)) + def sink(arg1: Int)(using izumi.reflect.Tag[AllCapabilities.Service]): ZSink[AllCapabilities.Service, Nothing, Int, Int, List[Int]] = ZSink.environmentWithSink[Service][AllCapabilities.Service & Any, Nothing, Int, Int, List[Int]](_.get[AllCapabilities.Service].sink(arg1)) + def stream(arg1: Int)(using izumi.reflect.Tag[AllCapabilities.Service]): ZStream[AllCapabilities.Service, Nothing, Int] = ZStream.serviceWithStream[Service](_.stream(arg1)) + // format: on +} + +object AllCapabilitiesTest { + val static: ZIO[AllCapabilities.Service, Nothing, String] = AllCapabilities.static + def zeroArgs: ZIO[AllCapabilities.Service, Nothing, Int] = AllCapabilities.zeroArgs + def zeroArgsWithParens(): ZIO[AllCapabilities.Service, Nothing, Long] = AllCapabilities.zeroArgsWithParens() + def singleArg(arg1: Int): ZIO[AllCapabilities.Service, Nothing, String] = AllCapabilities.singleArg(arg1) + def multiArgs(arg1: Int, arg2: Long): ZIO[AllCapabilities.Service, Nothing, String] = + AllCapabilities.multiArgs(arg1, arg2) + def multiParamLists(arg1: Int)(arg2: Long): ZIO[AllCapabilities.Service, Nothing, String] = + AllCapabilities.multiParamLists(arg1)(arg2) + def typedVarargs[T](arg1: Int, arg2: T*): ZIO[AllCapabilities.Service, Nothing, T] = + AllCapabilities.typedVarargs[T](arg1, arg2: _*) + def command(arg1: Int): ZIO[AllCapabilities.Service, Nothing, Unit] = AllCapabilities.command(arg1) + def overloaded(arg1: Int): ZIO[AllCapabilities.Service, Nothing, String] = AllCapabilities.overloaded(arg1) + def overloaded(arg1: Long): ZIO[AllCapabilities.Service, Nothing, String] = AllCapabilities.overloaded(arg1) + + val staticManaged: ZManaged[AllCapabilities.Service, Nothing, String] = AllCapabilities.staticManaged + def zeroArgsManaged: ZManaged[AllCapabilities.Service, Nothing, Int] = AllCapabilities.zeroArgsManaged + def zeroArgsTypedManaged[T]: ZManaged[AllCapabilities.Service, Nothing, T] = AllCapabilities.zeroArgsTypedManaged[T] + def zeroArgsWithParensManaged(): ZManaged[AllCapabilities.Service, Nothing, Long] = + AllCapabilities.zeroArgsWithParensManaged() + def singleArgManaged(arg1: Int): ZManaged[AllCapabilities.Service, Nothing, String] = + AllCapabilities.singleArgManaged(arg1) + def multiArgsManaged(arg1: Int, arg2: Long): ZManaged[AllCapabilities.Service, Nothing, String] = + AllCapabilities.multiArgsManaged(arg1, arg2) + def multiParamListsManaged(arg1: Int)(arg2: Long): ZManaged[AllCapabilities.Service, Nothing, String] = + AllCapabilities.multiParamListsManaged(arg1)(arg2) + def typedVarargsManaged[T](arg1: Int, arg2: T*): ZManaged[AllCapabilities.Service, Nothing, T] = + AllCapabilities.typedVarargsManaged[T](arg1, arg2: _*) + def commandManaged(arg1: Int): ZManaged[AllCapabilities.Service, Nothing, Unit] = AllCapabilities.commandManaged(arg1) + def overloadedManaged(arg1: Int): ZManaged[AllCapabilities.Service, Nothing, String] = + AllCapabilities.overloadedManaged(arg1) + def overloadedManaged(arg1: Long): ZManaged[AllCapabilities.Service, Nothing, String] = + AllCapabilities.overloadedManaged(arg1) + + def function(arg1: Int): ZIO[AllCapabilities.Service, Throwable, String] = AllCapabilities.function(arg1) + def sink(arg1: Int): ZSink[AllCapabilities.Service, Nothing, Int, Int, List[Int]] = AllCapabilities.sink(arg1) + def stream(arg1: Int): ZStream[AllCapabilities.Service, Nothing, Int] = AllCapabilities.stream(arg1) +} diff --git a/output/src/main/scala/rewrite/accessible/Basic.scala b/output/src/main/scala/rewrite/accessible/Basic.scala new file mode 100644 index 0000000..893a036 --- /dev/null +++ b/output/src/main/scala/rewrite/accessible/Basic.scala @@ -0,0 +1,71 @@ +package rewrite.accessible + +import zio._ + +trait Foo { val value: String } +case class Bar(value: String) extends Foo +case class Wrapped[T](value: T) + + +trait Service { + def get(key: String): UIO[Int] + def set(key: String, value: Int): UIO[Unit] + def reset: UIO[Unit] + def io: IO[String, Long] + def task: Task[Long] + def uio: UIO[Long] + def urio: URIO[String, Long] + def poly1[A: EnvironmentTag](a: A): UIO[Unit] + def poly2[A: EnvironmentTag]: IO[A, Unit] + def poly3[A: EnvironmentTag]: UIO[A] + def poly4[A: EnvironmentTag, B: EnvironmentTag](a: A): IO[B, Unit] + def poly5[A: EnvironmentTag, B: EnvironmentTag](a: A): IO[Unit, B] + def poly6[A: EnvironmentTag, B: EnvironmentTag]: IO[A, B] + def poly7[A: EnvironmentTag, B: EnvironmentTag, C: EnvironmentTag](a: A): IO[B, C] + def poly8[A: EnvironmentTag]: UIO[(A, String)] + def poly9[A <: Foo: EnvironmentTag]: UIO[A] + def poly10[A: EnvironmentTag](a: Wrapped[A]): UIO[A] +} + +object Service { + + // format: off + // Generated by ZIO Accessible CodeGen Scalafix Rule + def get(key: String)(using izumi.reflect.Tag[Service]): ZIO[Service, Nothing, Int] = ZIO.serviceWithZIO[Service](_.get(key)) + def set(key: String, value: Int)(using izumi.reflect.Tag[Service]): ZIO[Service, Nothing, Unit] = ZIO.serviceWithZIO[Service](_.set(key, value)) + def reset(using izumi.reflect.Tag[Service]): ZIO[Service, Nothing, Unit] = ZIO.serviceWithZIO[Service](_.reset) + def io(using izumi.reflect.Tag[Service]): ZIO[Service, String, Long] = ZIO.serviceWithZIO[Service](_.io) + def task(using izumi.reflect.Tag[Service]): ZIO[Service, Throwable, Long] = ZIO.serviceWithZIO[Service](_.task) + def uio(using izumi.reflect.Tag[Service]): ZIO[Service, Nothing, Long] = ZIO.serviceWithZIO[Service](_.uio) + def urio(using izumi.reflect.Tag[Service]): ZIO[Service & String, Nothing, Long] = ZIO.serviceWithZIO[Service](_.urio) + def poly1[A: EnvironmentTag](a: A)(using izumi.reflect.Tag[Service]): ZIO[Service, Nothing, Unit] = ZIO.serviceWithZIO[Service](_.poly1[A](a)) + def poly2[A: EnvironmentTag](using izumi.reflect.Tag[Service]): ZIO[Service, A, Unit] = ZIO.serviceWithZIO[Service](_.poly2[A]) + def poly3[A: EnvironmentTag](using izumi.reflect.Tag[Service]): ZIO[Service, Nothing, A] = ZIO.serviceWithZIO[Service](_.poly3[A]) + def poly4[A: EnvironmentTag, B: EnvironmentTag](a: A)(using izumi.reflect.Tag[Service]): ZIO[Service, B, Unit] = ZIO.serviceWithZIO[Service](_.poly4[A, B](a)) + def poly5[A: EnvironmentTag, B: EnvironmentTag](a: A)(using izumi.reflect.Tag[Service]): ZIO[Service, Unit, B] = ZIO.serviceWithZIO[Service](_.poly5[A, B](a)) + def poly6[A: EnvironmentTag, B: EnvironmentTag](using izumi.reflect.Tag[Service]): ZIO[Service, A, B] = ZIO.serviceWithZIO[Service](_.poly6[A, B]) + def poly7[A: EnvironmentTag, B: EnvironmentTag, C: EnvironmentTag](a: A)(using izumi.reflect.Tag[Service]): ZIO[Service, B, C] = ZIO.serviceWithZIO[Service](_.poly7[A, B, C](a)) + def poly8[A: EnvironmentTag](using izumi.reflect.Tag[Service]): ZIO[Service, Nothing, Tuple2[A, String]] = ZIO.serviceWithZIO[Service](_.poly8[A]) + def poly9[A <: Foo: EnvironmentTag](using izumi.reflect.Tag[Service]): ZIO[Service, Nothing, A] = ZIO.serviceWithZIO[Service](_.poly9[A]) + def poly10[A: EnvironmentTag](a: Wrapped[A])(using izumi.reflect.Tag[Service]): ZIO[Service, Nothing, A] = ZIO.serviceWithZIO[Service](_.poly10[A](a)) + // format: on +} + +object TestBasic { + val get: ZIO[Service with Any, Nothing, Int] = Service.get("foo") + val set: ZIO[Service with Any, Nothing, Unit] = Service.set("foo", 42) + val reset: ZIO[Service with Any, Nothing, Unit] = Service.reset + val io: ZIO[Service with Any, Throwable, Long] = Service.task + val uio: ZIO[Service with Any, Nothing, Long] = Service.uio + val urio: ZIO[Service with String, Nothing, Long] = Service.urio + val poly1: ZIO[Service with Any, Nothing, Unit] = Service.poly1("foo") + val poly2: ZIO[Service with Any, String, Unit] = Service.poly2[String] + val poly3: ZIO[Service with Any, Nothing, String] = Service.poly3[String] + val poly4: ZIO[Service with Any, Long, Unit] = Service.poly4[Int, Long](42) + val poly5: ZIO[Service with Any, Unit, Long] = Service.poly5[Int, Long](42) + val poly6: ZIO[Service with Any, String, Int] = Service.poly6[String, Int] + val poly7: ZIO[Service with Any, Long, Int] = Service.poly7[String, Long, Int]("foo") + val poly8: ZIO[Service with Any, Nothing, (Int, String)] = Service.poly8[Int] + val poly9: ZIO[Service with Any, Nothing, Bar] = Service.poly9[Bar] + val poly10: ZIO[Service with Any, Nothing, String] = Service.poly10(Wrapped("string")) +} diff --git a/output/src/main/scala/rewrite/accessible/CompanionExists.scala b/output/src/main/scala/rewrite/accessible/CompanionExists.scala new file mode 100644 index 0000000..c2efe50 --- /dev/null +++ b/output/src/main/scala/rewrite/accessible/CompanionExists.scala @@ -0,0 +1,49 @@ +package rewrite.accessible + +import zio._ + + +trait ServiceCompanionExists { + def get(key: String): UIO[Int] +} + +object ServiceCompanionExists { + def default: ServiceCompanionExists = ??? + + // format: off + // Generated by ZIO Accessible CodeGen Scalafix Rule + def get(key: String)(using izumi.reflect.Tag[ServiceCompanionExists]): ZIO[ServiceCompanionExists, Nothing, Int] = ZIO.serviceWithZIO[ServiceCompanionExists](_.get(key)) + // format: on +} + + +trait ServiceEmptyCompanion { + def get(key: String): UIO[Int] +} + +object ServiceEmptyCompanion { + + // format: off + // Generated by ZIO Accessible CodeGen Scalafix Rule + def get(key: String)(using izumi.reflect.Tag[ServiceEmptyCompanion]): ZIO[ServiceEmptyCompanion, Nothing, Int] = ZIO.serviceWithZIO[ServiceEmptyCompanion](_.get(key)) + // format: on +} + + +trait ServiceEmptyCompanion2 { + def get(key: String): UIO[Int] +} + +object ServiceEmptyCompanion2 { + + // format: off + // Generated by ZIO Accessible CodeGen Scalafix Rule + def get(key: String)(using izumi.reflect.Tag[ServiceEmptyCompanion2]): ZIO[ServiceEmptyCompanion2, Nothing, Int] = ZIO.serviceWithZIO[ServiceEmptyCompanion2](_.get(key)) + // format: on +} + +object TestCompanionExists { + val get1: ZIO[ServiceCompanionExists with Any, Nothing, Int] = ServiceCompanionExists.get("foo") + val get2: ZIO[ServiceEmptyCompanion with Any, Nothing, Int] = ServiceEmptyCompanion.get("foo") + val get3: ZIO[ServiceEmptyCompanion2 with Any, Nothing, Int] = ServiceEmptyCompanion2.get("foo") +} diff --git a/output/src/main/scala/rewrite/accessible/ImplicitsParams.scala b/output/src/main/scala/rewrite/accessible/ImplicitsParams.scala new file mode 100644 index 0000000..f431315 --- /dev/null +++ b/output/src/main/scala/rewrite/accessible/ImplicitsParams.scala @@ -0,0 +1,28 @@ +package rewrite.accessible + +import zio._ +import zio.stream.ZSink + +case class GroupId(v: Int) +trait TransactionEvent + + +trait ImplicitsParamsService { + def reduceTransactions[R: Tag, E: Tag, A: Tag](groupId: GroupId, sink: ZSink[R, E, TransactionEvent, Nothing, A]): ZIO[R, E, A] + def eventsForGroup(id: GroupId)(implicit tag: Tag[GroupId]): List[TransactionEvent] +} + +object ImplicitsParamsService { + + // format: off + // Generated by ZIO Accessible CodeGen Scalafix Rule + def reduceTransactions[R: Tag, E: Tag, A: Tag](groupId: GroupId, sink: ZSink[R, E, TransactionEvent, Nothing, A])(using izumi.reflect.Tag[ImplicitsParamsService]): ZIO[ImplicitsParamsService & R, E, A] = ZIO.serviceWithZIO[ImplicitsParamsService](_.reduceTransactions[R, E, A](groupId, sink)) + def eventsForGroup(id: GroupId)(using Tag[GroupId])(using izumi.reflect.Tag[ImplicitsParamsService]): ZIO[ImplicitsParamsService, Nothing, List[TransactionEvent]] = ZIO.serviceWith[ImplicitsParamsService](_.eventsForGroup(id)) + // format: on +} + +object ImplicitsParamsServiceTest { + def sink: ZSink[Any, Nothing, TransactionEvent, Nothing, Unit] = ??? + val reduceTransactions: ZIO[ImplicitsParamsService, Nothing, Unit] = ImplicitsParamsService.reduceTransactions(GroupId(42), sink) + val eventsForGroup: ZIO[ImplicitsParamsService, Nothing, List[TransactionEvent]] = ImplicitsParamsService.eventsForGroup(GroupId(42)) +} diff --git a/output/src/main/scala/rewrite/mockable/Basic.scala b/output/src/main/scala/rewrite/mockable/Basic.scala new file mode 100644 index 0000000..8153a42 --- /dev/null +++ b/output/src/main/scala/rewrite/mockable/Basic.scala @@ -0,0 +1,99 @@ +package rewrite.mockable + +import zio._ +import zio.mock.Mock + +trait Foo { val value: String } +case class Bar(value: String) extends Foo +case class Wrapped[T](value: T) + +trait Service { + def get(key: String): UIO[Int] + def set(key: String, value: Int): UIO[Unit] + def reset: UIO[Unit] + def io: IO[String, Long] + def task: Task[Long] + def uio: UIO[Long] + def urio: URIO[String, Long] + def poly1[A: EnvironmentTag](a: A): UIO[Unit] + def poly2[A: EnvironmentTag]: IO[A, Unit] + def poly3[A: EnvironmentTag]: UIO[A] + def poly4[A: EnvironmentTag, B: EnvironmentTag](a: A): IO[B, Unit] + def poly5[A: EnvironmentTag, B: EnvironmentTag](a: A): IO[Unit, B] + def poly6[A: EnvironmentTag, B: EnvironmentTag]: IO[A, B] + def poly7[A: EnvironmentTag, B: EnvironmentTag, C: EnvironmentTag](a: A): IO[B, C] + def poly8[A: EnvironmentTag]: UIO[(A, String)] + def poly9[A <: Foo: EnvironmentTag]: UIO[A] + def poly10[A: EnvironmentTag](a: Wrapped[A]): UIO[A] +} + +trait ServiceCompanion + +object ServiceMock extends Mock[Service] with ServiceCompanion { + val foo = 42 + // format: off + // Generated by ZIO Mockable CodeGen Scalafix Rule + case object Get extends Effect[String, Nothing, Int] + case object Set extends Effect[(String, Int), Nothing, Unit] + case object Reset extends Effect[Unit, Nothing, Unit] + case object Io extends Effect[Unit, String, Long] + case object Task extends Effect[Unit, Throwable, Long] + case object Uio extends Effect[Unit, Nothing, Long] + case object Urio extends Effect[Unit, Nothing, Long] + case object Poly1 extends Poly.Effect.Input[Nothing, Unit] + case object Poly2 extends Poly.Effect.Error[Unit, Unit] + case object Poly3 extends Poly.Effect.Output[Unit, Nothing] + case object Poly4 extends Poly.Effect.InputError[Unit] + case object Poly5 extends Poly.Effect.InputOutput[Unit] + case object Poly6 extends Poly.Effect.ErrorOutput[Unit] + case object Poly7 extends Poly.Effect.InputErrorOutput + case object Poly8 extends Poly.Effect.Output[Unit, Nothing] + case object Poly9 extends Poly.Effect.Output[Unit, Nothing] + case object Poly10 extends Poly.Effect.InputOutput[Nothing] + override val compose: URLayer[mock.Proxy, Service] = ZLayer.fromZIO[mock.Proxy, Nothing, Service]( + ZIO.service[mock.Proxy].flatMap { proxy => + withRuntime[mock.Proxy, Service] { rt => + class MockImpl extends Service { + final override def get(key: String): ZIO[Any, Nothing, Int] = proxy(Get, key) + final override def set(key: String, value: Int): ZIO[Any, Nothing, Unit] = proxy(Set, key, value) + final override def reset: ZIO[Any, Nothing, Unit] = proxy(Reset) + final override def io: ZIO[Any, String, Long] = proxy(Io) + final override def task: ZIO[Any, Throwable, Long] = proxy(Task) + final override def uio: ZIO[Any, Nothing, Long] = proxy(Uio) + final override def urio: ZIO[String, Nothing, Long] = proxy(Urio) + final override def poly1[A](a: A)(using EnvironmentTag[A]): ZIO[Any, Nothing, Unit] = proxy(Poly1.of[A], a) + final override def poly2[A](using EnvironmentTag[A]): ZIO[Any, A, Unit] = proxy(Poly2.of[A]) + final override def poly3[A](using EnvironmentTag[A]): ZIO[Any, Nothing, A] = proxy(Poly3.of[A]) + final override def poly4[A, B](a: A)(using EnvironmentTag[A], EnvironmentTag[B]): ZIO[Any, B, Unit] = proxy(Poly4.of[A, B], a) + final override def poly5[A, B](a: A)(using EnvironmentTag[A], EnvironmentTag[B]): ZIO[Any, Unit, B] = proxy(Poly5.of[A, B], a) + final override def poly6[A, B](using EnvironmentTag[A], EnvironmentTag[B]): ZIO[Any, A, B] = proxy(Poly6.of[A, B]) + final override def poly7[A, B, C](a: A)(using EnvironmentTag[A], EnvironmentTag[B], EnvironmentTag[C]): ZIO[Any, B, C] = proxy(Poly7.of[A, B, C], a) + final override def poly8[A](using EnvironmentTag[A]): ZIO[Any, Nothing, Tuple2[A, String]] = proxy(Poly8.of[Tuple2[A, String]]) + final override def poly9[A <: Foo](using EnvironmentTag[A]): ZIO[Any, Nothing, A] = proxy(Poly9.of[A]) + final override def poly10[A](a: Wrapped[A])(using EnvironmentTag[A]): ZIO[Any, Nothing, A] = proxy(Poly10.of[Wrapped[A], A], a) + } + ZIO.succeed(new MockImpl()) + }}) + // format: on +} + +object TestBasic { + implicitly[ServiceMock.type <:< Mock[Service]] + implicitly[ServiceMock.Get.type <:< ServiceMock.Effect[String, Nothing, Int]] + implicitly[ServiceMock.Set.type <:< ServiceMock.Effect[(String, Int), Nothing, Unit]] + implicitly[ServiceMock.Reset.type <:< ServiceMock.Effect[Unit, Nothing, Unit]] + implicitly[ServiceMock.Io.type <:< ServiceMock.Effect[Unit, String, Long]] + implicitly[ServiceMock.Task.type <:< ServiceMock.Effect[Unit, Throwable, Long]] + implicitly[ServiceMock.Uio.type <:< ServiceMock.Effect[Unit, Nothing, Long]] + implicitly[ServiceMock.Urio.type <:< ServiceMock.Effect[Unit, Nothing, Long]] + implicitly[ServiceMock.Poly1.type <:< ServiceMock.Poly.Effect.Input[Nothing, Unit]] + implicitly[ServiceMock.Poly2.type <:< ServiceMock.Poly.Effect.Error[Unit, Unit]] + implicitly[ServiceMock.Poly3.type <:< ServiceMock.Poly.Effect.Output[Unit, Nothing]] + implicitly[ServiceMock.Poly4.type <:< ServiceMock.Poly.Effect.InputError[Unit]] + implicitly[ServiceMock.Poly5.type <:< ServiceMock.Poly.Effect.InputOutput[Unit]] + implicitly[ServiceMock.Poly6.type <:< ServiceMock.Poly.Effect.ErrorOutput[Unit]] + implicitly[ServiceMock.Poly7.type <:< ServiceMock.Poly.Effect.InputErrorOutput] + implicitly[ServiceMock.Poly8.type <:< ServiceMock.Poly.Effect.Output[Unit, Nothing]] + implicitly[ServiceMock.Poly9.type <:< ServiceMock.Poly.Effect.Output[Unit, Nothing]] + implicitly[ServiceMock.Poly10.type <:< ServiceMock.Poly.Effect.InputOutput[Nothing]] +} diff --git a/output/src/main/scala/rewrite/mockable/Overloaded.scala b/output/src/main/scala/rewrite/mockable/Overloaded.scala new file mode 100644 index 0000000..ce643fa --- /dev/null +++ b/output/src/main/scala/rewrite/mockable/Overloaded.scala @@ -0,0 +1,74 @@ +package rewrite.mockable + +import zio._ +import zio.mock.Mock + +object Overloaded { + type OverloadedPureDefsModule = OverloadedPureDefsModule.Service + object OverloadedPureDefsModule { + trait Service { + def overloaded(n: Int): IO[String, String] + def overloaded(n: Long): IO[String, String] + } + } + + type OverloadedImpureDefsModule = OverloadedImpureDefsModule.Service + object OverloadedImpureDefsModule { + trait Service { + def overloaded(n: Int): String + def overloaded(n: Long): String + } + } +} +import Overloaded._ + + +object OverloadedPureDefsMocks extends Mock[OverloadedPureDefsModule] { + // format: off + // Generated by ZIO Mockable CodeGen Scalafix Rule + object Overloaded { + case object _0 extends Effect[Int, String, String] + case object _1 extends Effect[Long, String, String] + } + override val compose: URLayer[mock.Proxy, OverloadedPureDefsModule] = ZLayer.fromZIO[mock.Proxy, Nothing, OverloadedPureDefsModule]( + ZIO.service[mock.Proxy].flatMap { proxy => + withRuntime[mock.Proxy, OverloadedPureDefsModule] { rt => + class MockImpl extends OverloadedPureDefsModule { + final override def overloaded(n: Int): ZIO[Any, String, String] = proxy(Overloaded._0, n) + final override def overloaded(n: Long): ZIO[Any, String, String] = proxy(Overloaded._1, n) + } + ZIO.succeed(new MockImpl()) + }}) + // format: on +} + + +object OverloadedImpureDefsMocks extends Mock[OverloadedImpureDefsModule] { + // format: off + // Generated by ZIO Mockable CodeGen Scalafix Rule + object Overloaded { + case object _0 extends Method[Int, Throwable, String] + case object _1 extends Method[Long, Throwable, String] + } + override val compose: URLayer[mock.Proxy, OverloadedImpureDefsModule] = ZLayer.fromZIO[mock.Proxy, Nothing, OverloadedImpureDefsModule]( + ZIO.service[mock.Proxy].flatMap { proxy => + withRuntime[mock.Proxy, OverloadedImpureDefsModule] { rt => + class MockImpl extends OverloadedImpureDefsModule { + final override def overloaded(n: Int): String = Unsafe.unsafe { case given Unsafe => rt.unsafe.run(proxy(Overloaded._0, n)).getOrThrow() } + final override def overloaded(n: Long): String = Unsafe.unsafe { case given Unsafe => rt.unsafe.run(proxy(Overloaded._1, n)).getOrThrow() } + } + ZIO.succeed(new MockImpl()) + }}) + // format: on +} + +object OverloadedTest { + import Overloaded._ + implicitly[OverloadedPureDefsMocks.type <:< Mock[OverloadedPureDefsModule]] + implicitly[OverloadedPureDefsMocks.Overloaded._0.type <:< OverloadedPureDefsMocks.Effect[Int, String, String]] + implicitly[OverloadedPureDefsMocks.Overloaded._1.type <:< OverloadedPureDefsMocks.Effect[Long, String, String]] + + implicitly[OverloadedImpureDefsMocks.type <:< Mock[OverloadedImpureDefsModule]] + implicitly[OverloadedImpureDefsMocks.Overloaded._0.type <:< OverloadedImpureDefsMocks.Method[Int, Throwable, String]] + implicitly[OverloadedImpureDefsMocks.Overloaded._1.type <:< OverloadedImpureDefsMocks.Method[Long, Throwable, String]] +} diff --git a/output/src/main/scala/rewrite/mockable/ZStreams.scala b/output/src/main/scala/rewrite/mockable/ZStreams.scala new file mode 100644 index 0000000..44a9881 --- /dev/null +++ b/output/src/main/scala/rewrite/mockable/ZStreams.scala @@ -0,0 +1,56 @@ +package rewrite.mockable + +import zio._ +import zio.stream._ +import zio.mock.Mock + +object ZStreams { + type StreamDefsModule = StreamDefsModule.Service + object StreamDefsModule { + trait Service { + val static: ZStream[Any, String, String] + def zeroParams: ZStream[Any, String, String] + def zeroParamsWithParens(): ZStream[Any, String, String] + def singleParam(a: Int): ZStream[Any, String, String] + def manyParams(a: Int, b: String, c: Long): ZStream[Any, String, String] + def manyParamLists(a: Int)(b: String)(c: Long): ZStream[Any, String, String] + } + } +} + + +object ZStreamsMock extends Mock[ZStreams.StreamDefsModule.Service] { + // format: off + // Generated by ZIO Mockable CodeGen Scalafix Rule + case object Static extends Stream[Unit, String, String] + case object ZeroParams extends Stream[Unit, String, String] + case object ZeroParamsWithParens extends Stream[Unit, String, String] + case object SingleParam extends Stream[Int, String, String] + case object ManyParams extends Stream[(Int, String, Long), String, String] + case object ManyParamLists extends Stream[(Int, String, Long), String, String] + override val compose: URLayer[mock.Proxy, ZStreams.StreamDefsModule.Service] = ZLayer.fromZIO[mock.Proxy, Nothing, ZStreams.StreamDefsModule.Service]( + ZIO.service[mock.Proxy].flatMap { proxy => + withRuntime[mock.Proxy, ZStreams.StreamDefsModule.Service] { rt => + class MockImpl extends ZStreams.StreamDefsModule.Service { + final override val static: ZStream[Any, String, String] = Unsafe.unsafe { case given Unsafe => rt.unsafe.run(proxy(Static)).getOrThrowFiberFailure() } + final override def zeroParams: ZStream[Any, String, String] = Unsafe.unsafe { case given Unsafe => rt.unsafe.run(proxy(ZeroParams)).getOrThrowFiberFailure() } + final override def zeroParamsWithParens(): ZStream[Any, String, String] = Unsafe.unsafe { case given Unsafe => rt.unsafe.run(proxy(ZeroParamsWithParens)).getOrThrowFiberFailure() } + final override def singleParam(a: Int): ZStream[Any, String, String] = Unsafe.unsafe { case given Unsafe => rt.unsafe.run(proxy(SingleParam, a)).getOrThrowFiberFailure() } + final override def manyParams(a: Int, b: String, c: Long): ZStream[Any, String, String] = Unsafe.unsafe { case given Unsafe => rt.unsafe.run(proxy(ManyParams, a, b, c)).getOrThrowFiberFailure() } + final override def manyParamLists(a: Int)(b: String)(c: Long): ZStream[Any, String, String] = Unsafe.unsafe { case given Unsafe => rt.unsafe.run(proxy(ManyParamLists, a, b, c)).getOrThrowFiberFailure() } + } + ZIO.succeed(new MockImpl()) + }}) + // format: on +} + +object TestZStreams { + import ZStreams.StreamDefsModule.Service + implicitly[ZStreamsMock.type <:< Mock[Service]] + implicitly[ZStreamsMock.Static.type <:< ZStreamsMock.Stream[Unit, String, String]] + implicitly[ZStreamsMock.ZeroParams.type <:< ZStreamsMock.Stream[Unit, String, String]] + implicitly[ZStreamsMock.ZeroParamsWithParens.type <:< ZStreamsMock.Stream[Unit, String, String]] + implicitly[ZStreamsMock.SingleParam.type <:< ZStreamsMock.Stream[Int, String, String]] + implicitly[ZStreamsMock.ManyParams.type <:< ZStreamsMock.Stream[(Int, String, Long), String, String]] + implicitly[ZStreamsMock.ManyParamLists.type <:< ZStreamsMock.Stream[(Int, String, Long), String, String]] +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..04267b1 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.9.9 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..790987a --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,5 @@ +resolvers += Resolver.sonatypeRepo("releases") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.0") + +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.10.0") +addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1") \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..63cf7b7 --- /dev/null +++ b/readme.md @@ -0,0 +1,34 @@ +[![scalafix-migrate-zio-macros Scala version support](https://index.scala-lang.org/virtuslabrnd/scalafix-migrate-zio-macros/scalafix-migrate-zio-macros/latest-by-scala-version.svg?platform=jvm)](https://index.scala-lang.org/virtuslabrnd/scalafix-migrate-zio-macros/scalafix-migrate-zio-macros) + +# Scalafix rules for ZIO macro annotations migration + +This project contains set of rules allowing to migrate Scala 2 macro-annotations based utilities of ZIO ecosystem. + +## Available code generation rules: + +### `ZIOAccessibleCodeGen` +Rule detects usages of `zio.macros.accessible` annotations and generates accessors for annotated traits based on original macro-annonations algorithm. +Support: + - [x] - `zio.ZIO` (and it's main aliases) + - [x] - `zio.ZManaged` (and it's main aliases) + - [x] - `zio.ZStream`, `zio.ZSink` (and it's main aliases) + - [x] - accessors for normal methods + - [ ] - `@accessible` - supported + - [ ] - `@accessibleM[_]` - not supported yet + - [ ] - `@accessibleMM[_, _]` - not supported yet + - [ ] - `@throwing` - not supported yet + +### `ZIOMockableCodeGen` +Rule detects usages of `zio.macros.mockable` annotations and generates mocks for annotated traits based on original macro-annonations algorithm. +Support: + - [x] - `zio.ZIO` (and it's main aliases) + - [x] - `zio.ZManaged` (and it's main aliases) + - [x] - `zio.ZStream`, `zio.ZSink` (and it's main aliases) + - [x] - overloaded methods + - [x] - generic, type parametrized methods + + +## Usage +All rules require compilation with SemanticDB enabled and are targeting Scalafix 0.12.x +For information on how to use this projects refer to [Scalafix user guide](https://scalacenter.github.io/scalafix/docs/users/installation.html) + diff --git a/rules/src/main/resources/META-INF/services/scalafix.v1.Rule b/rules/src/main/resources/META-INF/services/scalafix.v1.Rule new file mode 100644 index 0000000..e7f40bd --- /dev/null +++ b/rules/src/main/resources/META-INF/services/scalafix.v1.Rule @@ -0,0 +1,2 @@ +org.virtuslab.rewrites.ZIOAccessibleCodeGen +org.virtuslab.rewrites.ZIOMockableCodeGen \ No newline at end of file diff --git a/rules/src/main/scala/org/virtuslab/rewrites/ZIOCodeGen.scala b/rules/src/main/scala/org/virtuslab/rewrites/ZIOCodeGen.scala new file mode 100644 index 0000000..23e26f4 --- /dev/null +++ b/rules/src/main/scala/org/virtuslab/rewrites/ZIOCodeGen.scala @@ -0,0 +1,202 @@ +package org.virtuslab.rewrites + +import scala.collection.mutable +import scalafix.v1._ +import scala.meta._ +import scalafix.patch.Patch + +trait ZIOCodeGen { self: SemanticRule => + + object DefDef { + def unapply(t: Tree) = t match { + case t: Defn.Def => Some(t) + case t: Decl.Def => Some(t) + case _ => None + } + } + object ValDef { + def unapply(t: Tree) = t match { + case t: Defn.Val => Some(t) + case t: Decl.Val => Some(t) + case _ => None + } + } + + type PatchesBuilder = mutable.Builder[Patch, Seq[Patch]] + object symbols { + val MockableAnnotation = Symbol("zio/mock/mockable#") + val AccessibleAnnotation = Symbol("zio/macros/accessible#") + + val ZIO = Symbol("zio/ZIO#") + val ZIOIO = Symbol("zio/package.IO#") + val ZIOTask = Symbol("zio/package.Task#") + val ZIORIO = Symbol("zio/package.RIO#") + val ZIOUIO = Symbol("zio/package.UIO#") + val ZIOURIO = Symbol("zio/package.URIO#") + + val ZIOZSink = Symbol("zio/stream/ZSink#") + val ZIOSink = Symbol("zio/stream/package.Sink#") + val ZIOZStream = Symbol("zio/stream/ZStream#") + val ZIOStream = Symbol("zio/stream/package.Stream#") + val ZIOUStream = Symbol("zio/stream/package.UStream#") + + val ZIOZManaged = Symbol("zio/managed/ZManaged#") + val ZIOManaged = Symbol("zio/managed/package.Managed#") + val ZIOTaskManaged = Symbol("zio/managed/package.TaskManaged#") + val ZIORManaged = Symbol("zio/managed/package.RManaged#") + val ZIOUManaged = Symbol("zio/managed/package.UManaged#") + val ZIOURManaged = Symbol("zio/managed/package.URManaged#") + + val ZIOMock = Symbol("zio/mock/Mock#") + val IzumiReflectTag = Symbol("izumi/reflect/Tag#") + + val Unit = Symbol("scala/Unit#") + val Nothing = Symbol("scala/Nothing#") + val Any = Symbol("scala/Any#") + val Throwable = Symbol("java/lang/Throwable#") + + implicit class SymbolOps(val self: Symbol) { + def is(v: Symbol): Boolean = self.value == v.value + } + } + import symbols.SymbolOps + object types { + def termName(sym: Symbol): Term.Ref = { + if (sym.isNone) sys.error(sym.toString()) + else if (sym.owner.isRootPackage || sym.owner.isEmptyPackage) Term.Name(sym.displayName) + else Term.Select(termName(sym.owner), Term.Name(sym.displayName)) + } + + def fromSymbol(sym: Symbol)(implicit doc: Symtab): Type = + if (sym.info.exists(t => t.isTypeParameter)) Type.Name(sym.displayName) + else if (sym.owner.isEmptyPackage || sym.owner.isRootPackage) Type.Name(sym.displayName) + else Type.Select(termName(sym.owner), Type.Name(sym.displayName)) + // Type.Name(sym.displayName) + + def Unit(implicit doc: Symtab) = fromSymbol(symbols.Unit) + def Nothing(implicit doc: Symtab) = fromSymbol(symbols.Nothing) + def Any(implicit doc: Symtab) = fromSymbol(symbols.Any) + def Throwable(implicit doc: Symtab) = fromSymbol(symbols.Throwable) + + def ZIO(implicit doc: Symtab) = fromSymbol(symbols.ZIO) + def ZIOZStream(implicit doc: Symtab) = fromSymbol(symbols.ZIOZStream) + def ZIOZSink(implicit doc: Symtab) = fromSymbol(symbols.ZIOZSink) + def ZIOZManaged(implicit doc: Symtab) = fromSymbol(symbols.ZIOZManaged) + + def IzumiReflectTag(implicit doc: Symtab) = + Type.Select(Term.Select(Term.Name("izumi"), Term.Name("reflect")), Type.Name("Tag")) + + def semanticTypeOf(tsymbol: Symbol)(implicit doc: Symtab): SemanticType = tsymbol.info + .map { + _.signature match { + case t: ClassSignature => TypeRef(NoType, tsymbol, t.typeParameters.map(_.symbol).map(semanticTypeOf)) + case t @ TypeSignature(Nil, lowerBound, upperBound) if lowerBound == upperBound => lowerBound + case t @ TypeSignature(Nil, lo @ TypeRef(_, symbols.Nothing, Nil), TypeRef(_, symbols.Any, Nil)) => lo + case t @ TypeSignature(Nil, TypeRef(_, symbols.Nothing, Nil), hiBound) => hiBound + } + } + .getOrElse(sys.error(s"No info for $tsymbol")) + } + + def symbolOf(t: Tree with Stat.WithMods)(implicit doc: SemanticDocument) = + Option(t.symbol) + .filterNot(_.isNone) + .orElse { + // Bug workaround: + // If type has annotation then type symbol is stored in annotation symbol + t.mods.filter(_.is[Mod.Annot]).map(_.symbol).headOption + } + + def normalize(tpe: Type) = tpe + .transform { + case Type.Apply(Type.Name(name), types) if name.startsWith("Tuple") => Type.Tuple(types) + case Type.With(l, r) => Type.And(l, r) + case tpe => tpe + } + .asInstanceOf[Type] + + def fromSemanticType(tpe: SemanticType)(implicit doc: Symtab): Type = normalize { + tpe match { + case TypeRef(prefix, symbol, typeArguments) => + val base = types.fromSymbol(symbol) + if (typeArguments.isEmpty) base + else Type.Apply(base, typeArguments.map(fromSemanticType)) + case WithType(types) => types.map(fromSemanticType).reduce(Type.And(_, _)) + case SingleType(prefix, symbol) => Type.Singleton(types.termName(symbol)) + // Lossy conversions + case ByNameType(tpe) => fromSemanticType(tpe) + case StructuralType(tpe, declarations) => Type.Refine(Some(fromSemanticType(tpe)), stats = Nil) + case AnnotatedType(annotations, tpe) => fromSemanticType(tpe) + } + } + + def dealiasSymbol(symbol: Symbol)(implicit doc: Symtab): Symbol = + symbol.info.getOrElse(sys.error(s"No info for ${symbol}")).signature match { + case TypeSignature(tparams, lowerBound @ TypeRef(_, dealiased, _), upperBound) if lowerBound == upperBound => + dealiased + case _ => + symbol + } + def tryDealias(tpe: SemanticType)(implicit doc: Symtab): SemanticType = tpe match { + case TypeRef(prefix, symbol, typeArguments) => + val dealiasedSymbol = dealiasSymbol(symbol) + + def Any = types.semanticTypeOf(symbols.Any) + def Throwable = types.semanticTypeOf(symbols.Throwable) + def Nothing = types.semanticTypeOf(symbols.Nothing) + + (symbol, dealiasedSymbol, typeArguments) match { + case (symbols.ZIOIO, symbols.ZIO, List(e, a)) => TypeRef(prefix, symbols.ZIO, List(Any, e, a)) + case (symbols.ZIOTask, symbols.ZIO, List(a)) => TypeRef(prefix, symbols.ZIO, List(Any, Throwable, a)) + case (symbols.ZIORIO, symbols.ZIO, List(r, a)) => TypeRef(prefix, symbols.ZIO, List(r, Throwable, a)) + case (symbols.ZIOUIO, symbols.ZIO, List(a)) => TypeRef(prefix, symbols.ZIO, List(Any, Nothing, a)) + case (symbols.ZIOURIO, symbols.ZIO, List(r, a)) => TypeRef(prefix, symbols.ZIO, List(r, Nothing, a)) + + case (symbols.ZIOManaged, symbols.ZIOZManaged, List(e, a)) => TypeRef(prefix, symbols.ZIOZManaged, List(Any, e, a)) + case (symbols.ZIOTaskManaged, symbols.ZIOZManaged, List(a)) => TypeRef(prefix, symbols.ZIOZManaged, List(Any, Throwable, a)) + case (symbols.ZIORManaged, symbols.ZIOZManaged, List(r, a)) => TypeRef(prefix, symbols.ZIOZManaged, List(r, Throwable, a)) + case (symbols.ZIOUManaged, symbols.ZIOZManaged, List(a)) => TypeRef(prefix, symbols.ZIOZManaged, List(Any, Nothing, a)) + case (symbols.ZIOURManaged, symbols.ZIOZManaged, List(r, a)) => TypeRef(prefix, symbols.ZIOZManaged, List(r, Nothing, a)) + + case (symbols.ZIOSink, symbols.ZIOZSink, List(outErr, in, l, z)) => TypeRef(prefix, symbols.ZIOZSink, List(Any, outErr, in, l, z)) + case (symbols.ZIOStream, symbols.ZIOZStream, List(e, a)) => TypeRef(prefix, symbols.ZIOZStream, List(Any, e, a)) + case (symbols.ZIOUStream, symbols.ZIOZStream, List(a)) => TypeRef(prefix, symbols.ZIOZStream, List(Any, Nothing, a)) + case _ => tpe + } + case _ => tpe + } + + sealed trait ImportedSymbol { def symbol: Symbol } + object ImportedSymbol { + case class Wildcard(symbol: Symbol) extends ImportedSymbol + case class Name(symbol: Symbol) extends ImportedSymbol + } + + def cleanupRules(pkgSymbols: Seq[Symbol], topLevelImportedSymbol: Seq[ImportedSymbol])(implicit + doc: SemanticDocument + ): List[String => String] = + List[String => String]( + _.replaceAll(",(\\w)", ", $1"), + _.replace(".`package`", ""), + _.replace("scala.Predef.", ""), + _.replaceAll(raw"scala\.(\w+)$$", "$1"), + _.replaceAll(raw"scala\.(\w+)([\[\]\)\,])", "$1$2"), + _.replace("java.lang.", ""), + _.replace("zio.VersionSpecific.", "zio.") + ) ++ (pkgSymbols.map(ImportedSymbol.Wildcard(_)) ++ topLevelImportedSymbol) + .sortBy(_.symbol.value) + .reverse + .map { importedSymbol => + val symbol = importedSymbol.symbol + val fqcn = types.termName(symbol).syntax + importedSymbol match { + case _: ImportedSymbol.Wildcard => (s: String) => s.replace(s"$fqcn.", "") + case _: ImportedSymbol.Name => (s: String) => s.replace(s"$fqcn.", symbol.displayName) + } + } + + def cleanupSyntax(pkgSymbols: Seq[Symbol], topLevelImportedSymbol: Seq[ImportedSymbol])(body: String)(implicit doc: SemanticDocument) = + cleanupRules(pkgSymbols, topLevelImportedSymbol) + .foldLeft(body) { case (acc, rule) => rule(acc) } + +} diff --git a/rules/src/main/scala/org/virtuslab/rewrites/ZioAccessibleCodeGen.scala b/rules/src/main/scala/org/virtuslab/rewrites/ZioAccessibleCodeGen.scala new file mode 100644 index 0000000..326ab16 --- /dev/null +++ b/rules/src/main/scala/org/virtuslab/rewrites/ZioAccessibleCodeGen.scala @@ -0,0 +1,318 @@ +package org.virtuslab.rewrites + +import scalafix.v1._ +import scala.meta._ +import scala.collection.mutable +import scala.meta.Tree.WithTParamClause +import scala.meta.Tree.WithParamClauseGroups +import scala.meta.Member.ParamClauseGroup +import scala.meta.Stat.WithMods +import scala.meta.Stat.WithTemplate + +class ZIOAccessibleCodeGen extends SemanticRule("ZIOAccessibleCodeGen") with ZIOCodeGen { + + override def fix(implicit doc: SemanticDocument): Patch = { + object names { + object AccessibleAnnotation extends NameExtractor(symbols.AccessibleAnnotation, shouldBeRemoved = true) + + val Constructor = Term.Name("") + + sealed abstract class NameExtractor(symbol: Symbol, val shouldBeRemoved: Boolean = false) { + val name: String = symbol.displayName + val aliases: mutable.Set[String] = mutable.Set.empty + def aliasOrName: String = aliases.headOption.getOrElse(name) + def unapply(t: Name): Boolean = { + t.value == name || aliases.contains(t.value) + } + } + object NameExtractor { + private lazy val extractors = Seq[NameExtractor]( + // format: off + AccessibleAnnotation + // format: on + ) + def unapply(t: Name): Option[NameExtractor] = + extractors.iterator.collectFirst { + case extractor if extractor.unapply(t) => extractor + } + } + } + + val patches = Seq.newBuilder[Patch] + + val (companionObjects, pkgSymbols, topLevelImportedSymbol) = { + val companionObjects = Map.newBuilder[String, Defn.Object] + val pkgSymbols = List.newBuilder[Symbol] + val importedSymbols = Set.newBuilder[ImportedSymbol] + doc.tree.traverse { + case t @ Importee.Name(names.NameExtractor(name)) if name.shouldBeRemoved => + patches += Patch.removeImportee(t) + case t @ Importee.Rename(names.NameExtractor(name), alias) => + if (name.shouldBeRemoved) patches += Patch.removeImportee(t) + name.aliases += alias.value + case t: Defn.Object => companionObjects += t.name.value -> t + case Pkg(ref, stats) => + pkgSymbols += ref.symbol + importedSymbols ++= stats + .collect { case t: Import => + t.importers.flatMap { importer => + importer.importees.collect { + case Importee.Wildcard() => ImportedSymbol.Wildcard(importer.ref.symbol) + case Importee.Name(name) => ImportedSymbol.Name(name.symbol) + } + } + } + .flatten + .filter(_.symbol.info.isDefined) + } + (companionObjects.result(), pkgSymbols.result(), importedSymbols.result().toList) + } + + case class ModuleInfo( + module: Option[Defn.Object], + service: Tree with Member.Type with WithMods with WithTemplate, + serviceTypeParams: List[Type.Param] + ) + + def accessibleAnnotation(t: Stat.WithMods): Option[Mod.Annot] = t.mods.collectFirst { + case annot @ Mod.Annot(Init(names.AccessibleAnnotation(), _, _)) => annot + } + val generatedServices = mutable.Set.empty[Symbol] + doc.tree.traverse { + case defnTree: Stat.WithMods if accessibleAnnotation(defnTree).isDefined => + accessibleAnnotation(defnTree).map(_.tokens).foreach(patches += Patch.removeTokens(_)) + + val moduleInfo = defnTree match { + case tree: Defn.Trait => + Some(ModuleInfo(companionObjects.get(tree.name.value), tree, tree.tparamClause.values)) + case tree: Defn.Class => + Some(ModuleInfo(companionObjects.get(tree.name.value), tree, tree.tparamClause.values)) + case module: Defn.Object => + module.templ.stats.collectFirst { + case service: Defn.Class => ModuleInfo(Some(module), service, service.tparamClause.values) + case service: Defn.Trait => ModuleInfo(Some(module), service, service.tparamClause.values) + } + } + + for { + moduleInfo <- moduleInfo + serviceSymbol <- symbolOf(moduleInfo.service) + if generatedServices.add(serviceSymbol) + val serviceName = moduleInfo.service.name + } { + val accessors = + moduleInfo.service.templ.stats + .collect { + case DefDef(tree) if tree.name != names.Constructor => + makeAccessor( + tree.mods.filter { + case Mod.Annot(Init(_, name, _)) => !name.value.endsWith("deprecated") + case _ => true + }, + tree.name, + typeInfo(tree), // withThrowing(mods, tree), + moduleInfo.serviceTypeParams, + tree.paramClauseGroup, + isVal = false, + serviceName, + serviceSymbol + ) + + case ValDef(tree) => + makeAccessor( + Nil, + tree match { + case Defn.Val(_, List(Pat.Var(name)), _, _) => name + case Decl.Val(_, List(Pat.Var(name)), _) => name + }, + typeInfo(tree), + moduleInfo.serviceTypeParams, + None, + isVal = true, + serviceName, + serviceSymbol + ) + } + + if (accessors.nonEmpty) { + println(s"Creating accessor of ${symbolOf(moduleInfo.service).get} in module: ${moduleInfo.module.flatMap(symbolOf)}") + val genAccessors = cleanupSyntax(pkgSymbols = pkgSymbols, topLevelImportedSymbol = topLevelImportedSymbol) { + s""" + | // format: off + | // Generated by ZIO Accessible CodeGen Scalafix Rule + |${accessors.map(_.syntax).mkString(" ", "\n ", "")} + | // format: on""".stripMargin + } + val genCompanionObject = + s"""object ${serviceName.value} { + |$genAccessors + |}""".stripMargin + patches += { + moduleInfo.module match { + case Some(module @ Defn.Object(_, _, t @ Template(_, _, self, stats))) => + if (stats.nonEmpty) + Patch.addRight( + stats.lastOption.getOrElse(self), + s"\n$genAccessors" + ) + else Patch.replaceTree(module, genCompanionObject) + case _ => Patch.addRight(defnTree, "\n\n" + genCompanionObject) + } + } + } + } + } + + patches.result().asPatch + } + + def makeAccessor( + mods: List[Mod], + name: Term.Name, + capability: Capability, + serviceTypeParams: List[Type.Param], + paramClauseGroup: Option[Member.ParamClauseGroup], + isVal: Boolean, + serviceName: Type.Name, + serviceSymbol: Symbol + )(implicit doc: Symtab): Tree = { + val serviceType = types.fromSymbol(serviceSymbol) + def withR(tpe: Type, r: Type): Type = + if (r.structure != types.Any.structure) Type.And(tpe, r) + else tpe + + val returnType = capability match { + case Capabiltity.Effect(r, e, a) => Type.Apply(types.ZIO, List(withR(serviceType, r), e, a)) + case Capabiltity.Managed(r, e, a) => Type.Apply(types.ZIOZManaged, List(withR(serviceType, r), e, a)) + case Capabiltity.Stream(r, e, a) => Type.Apply(types.ZIOZStream, List(withR(serviceType, r), e, a)) + case Capabiltity.Sink(r, e, a, l, b) => Type.Apply(types.ZIOZSink, List(withR(serviceType, r), e, a, l, b)) + case Capabiltity.Method(a) => Type.Apply(types.ZIO, List(serviceType, types.Nothing, a)) + case Capabiltity.ThrowingMethod(a) => Type.Apply(types.ZIO, List(serviceType, types.Throwable, a)) + } + + val typeArgs = paramClauseGroup.toList.flatMap(_.tparamClause) + val paramLists = paramClauseGroup.toList.flatMap(_.paramClauses) + def selector(lhs: Term = Term.Placeholder()) = { + val methodSelect = Term.Select(lhs, name) + val base = + if (typeArgs.isEmpty) methodSelect + else Term.ApplyType(methodSelect, typeArgs.map(v => Type.Name(v.name.value))) + if (paramLists.isEmpty) base + else + paramLists + .filterNot(_.mod.exists(_.isAny[Mod.Implicit, Mod.Using])) + .map { paramms => + paramms.map { param => + val ident = Term.Name(param.name.value) + val isRepeated = param.decltpe.exists(_.is[Type.Repeated]) + if (isRepeated) Term.Repeated(ident) + else ident + } + } + .foldLeft(base)(Term.Apply(_, _)) + } + def select(fullyQualifiedName: String): Term = fullyQualifiedName.split('.').map(Term.Name(_)).toList match { + case head :: Nil => head + case head :: tail => tail.foldLeft[Term](head)(Term.Select(_, _)) + } + val returnValue: Term = capability match { + case _: Capabiltity.Effect => + Term.Apply( + Term.ApplyType(select("zio.ZIO.serviceWithZIO"), List(serviceName)), + Term.AnonymousFunction(selector()) :: Nil + ) + + case _: Capabiltity.Managed => + Term.Apply( + Term.ApplyType(select("zio.managed.ZManaged.serviceWithManaged"), List(serviceName)), + Term.AnonymousFunction(selector()) :: Nil + ) + + case _: Capabiltity.Stream => + Term.Apply( + Term.ApplyType(select("zio.stream.ZStream.serviceWithStream"), List(serviceName)), + Term.AnonymousFunction(selector()) :: Nil + ) + case Capabiltity.Sink(r, e, a, l, b) => + Term.Apply( + Term.ApplyType( + Term.ApplyType(select("zio.stream.ZSink.environmentWithSink"), List(serviceName)), + List(Type.And(serviceType, r), e, a, l, b) + ), + Term.AnonymousFunction( + selector(Term.ApplyType(Term.Select(Term.Placeholder(), Term.Name("get")), List(serviceType))) + ) :: Nil + ) + + case _: Capabiltity.ThrowingMethod => + val paramName = Term.Name("s") + Term.Apply( + Term.ApplyType(select("zio.ZIO.serviceWithZIO"), List(serviceName)), + Term.Function( + Term.Param(Nil, paramName, None, None) :: Nil, + Term.Apply(select("zio.ZIO.attempt"), selector(paramName) :: Nil) + ) :: Nil + ) + case _ => + Term.Apply( + Term.ApplyType(select("zio.ZIO.serviceWith"), List(serviceName)), + Term.AnonymousFunction(selector()) :: Nil + ) + } + if (isVal) + Defn.Val(mods, List(Pat.Var(name)), Some(returnType), returnValue) + else { + val usingServiceTag = Term.ParamClause( + mod = Some(Mod.Using()), + values = Term.Param( + Nil, + Name.Anonymous(), + decltpe = Some(Type.Apply(types.IzumiReflectTag, serviceType :: Nil)), + None + ) :: Nil + ) + val paramClouses = paramClauseGroup match { + case None => ParamClauseGroup(Type.ParamClause(Nil), usingServiceTag :: Nil) + case Some(group @ ParamClauseGroup(_, paramss)) => + val adaptedParamss = paramss.map(paramss => + if (!paramss.mod.exists(_.is[Mod.Implicit])) paramss + else paramss.copy(values = paramss.values.map(_.copy(name = Name.Anonymous())), mod = Some(Mod.Using())) + ) + group.copy(paramClauses = adaptedParamss :+ usingServiceTag) + } + Defn.Def(mods, name, Some(paramClouses), Some(returnType), returnValue) + } + } + + def typeInfo(t: Tree)(implicit doc: SemanticDocument): Capability = { + val tpe = t.symbol.info + .map(_.signature) + .collect { + case ValueSignature(tpe) => tryDealias(tpe) + case MethodSignature(typeParameters, parameterLists, returnType) => tryDealias(returnType) + } + .get + tpe match { + case TypeRef(_, symbol, tParams) => + (dealiasSymbol(symbol), tParams.map(fromSemanticType)) match { + case (symbols.ZIO, List(r, e, a)) => Capabiltity.Effect(r, e, a) + case (symbols.ZIOZManaged, List(r, e, a)) => Capabiltity.Managed(r, e, a) + case (symbols.ZIOZSink, List(r, e, a, l, b)) => Capabiltity.Sink(r, e, a, l, b) + case (symbols.ZIOZStream, List(r, e, a)) => Capabiltity.Stream(r, e, a) + case _ => Capabiltity.Method(fromSemanticType(tpe)) + } + case _ => Capabiltity.Method(fromSemanticType(tpe)) + } + } + + sealed trait Capability + object Capabiltity { + case class Effect(r: Type, e: Type, a: Type) extends Capability + case class Managed(r: Type, e: Type, a: Type) extends Capability + case class Method(a: Type) extends Capability + case class Sink(r: Type, e: Type, a: Type, l: Type, b: Type) extends Capability + case class Stream(r: Type, e: Type, a: Type) extends Capability + case class ThrowingMethod(a: Type) extends Capability + } + +} diff --git a/rules/src/main/scala/org/virtuslab/rewrites/ZioMockableCodeGen.scala b/rules/src/main/scala/org/virtuslab/rewrites/ZioMockableCodeGen.scala new file mode 100644 index 0000000..ca2bb4b --- /dev/null +++ b/rules/src/main/scala/org/virtuslab/rewrites/ZioMockableCodeGen.scala @@ -0,0 +1,467 @@ +package org.virtuslab.rewrites + +import scalafix.v1._ +import scala.meta._ +import scala.collection.mutable +import scala.meta.internal.semanticdb.Scala.Names.TermName +import scala.meta.Defn.Def +import scala.meta.internal.trees.Fresh + +class ZIOMockableCodeGen extends SemanticRule("ZIOMockableCodeGen") with ZIOCodeGen { + override def fix(implicit doc: SemanticDocument): Patch = { + object names { + object MockableAnnotation extends NameExtractor(symbols.MockableAnnotation, shouldBeRemoved = true) + + sealed abstract class NameExtractor(symbol: Symbol, val shouldBeRemoved: Boolean = false) { + val name: String = symbol.displayName + val aliases: mutable.Set[String] = mutable.Set.empty + def aliasOrName: String = aliases.headOption.getOrElse(name) + def unapply(t: Name): Boolean = { + t.value == name || aliases.contains(t.value) + } + } + object NameExtractor { + private lazy val extractors = Seq[NameExtractor]( + // format: off + MockableAnnotation + // format: on + ) + def unapply(t: Name): Option[NameExtractor] = + extractors.iterator.collectFirst { + case extractor if extractor.unapply(t) => extractor + } + } + } + + implicit val patches: PatchesBuilder = Seq.newBuilder[Patch] + + val (companionObjects, typeSymbols, pkgSymbols, importedSymbols) = { + val companionObjects = Map.newBuilder[String, Defn.Object] + val typeSymbols = mutable.Map.empty[String, Symbol] + val pkgSymbols = List.newBuilder[Symbol] + val importedSymbols = Set.newBuilder[ImportedSymbol] + doc.tree.traverse { + case t @ Importee.Name(names.NameExtractor(name)) if name.shouldBeRemoved => + patches += Patch.removeImportee(t) + case t @ Importee.Rename(names.NameExtractor(name), alias) => + if (name.shouldBeRemoved) patches += Patch.removeImportee(t) + name.aliases += alias.value + + case t: Defn.Object => companionObjects += t.name.value -> t + // SemanticDB does not always set symbols for Type.Names, e.g. ther'e missing in annotation type parameters + case t: Type => + // Yes, the scalameta Types equality is not based on the the product values, the most reliable seems to be depending on structure or type rendering + t.symbol.asNonEmpty.filter(_.info.exists(t => t.isClass || t.isTrait || t.isType)).foreach { symbol => + t match { + case t: Type.Name => if (t.value != "Any") typeSymbols.getOrElseUpdate(t.value, symbol) + case t: Type.Select => + typeSymbols.getOrElseUpdate(t.name.value, symbol) + typeSymbols.getOrElseUpdate(t.toString(), symbol) + case _ => () + } + } + case t @ (_: Importee.Name | _: Importee.Rename) if t.symbol.asNonEmpty.isDefined => + val clsSymbol = { + val symValue = t.symbol.value + if (symValue.endsWith("#")) Some(t.symbol) + else if (symValue.endsWith(".") && t.symbol.info.exists(_.isObject)) { + val clsSymbol = Symbol(symValue.stripSuffix(".") + "#") + Option(clsSymbol).filter(_.info.isDefined) + } else None + } + clsSymbol.foreach { symbol => + val name = t match { + case Importee.Name(name) => + typeSymbols.getOrElseUpdate(name.value, symbol) + case Importee.Rename(name, alias) => + typeSymbols.getOrElseUpdate(name.value, symbol) + typeSymbols.getOrElseUpdate(alias.value, symbol) + } + } + case Pkg(ref, stats) => + pkgSymbols += ref.symbol + importedSymbols ++= stats + .collect { case t: Import => + t.importers.flatMap { importer => + importer.importees.collect { + case Importee.Wildcard() => ImportedSymbol.Wildcard(importer.ref.symbol) + case Importee.Name(name) => ImportedSymbol.Name(name.symbol) + } + } + } + .flatten + .filter(_.symbol.info.isDefined) + } + (companionObjects.result(), typeSymbols.toMap, pkgSymbols.result(), importedSymbols.result().toList) + } + + // All this workarounds are needed becouse types might have missing/malformed symbols (mostly in annotaitons) + def symbolFromType(tpe: Type): Option[Symbol] = tpe match { + case t: Type.Name => + def validate(sym: Symbol) = sym.displayName == t.value + typeSymbols + .get(t.value) + .filter(validate) + .orElse( + pkgSymbols + .map(pkg => Symbol(s"${pkg}${t.value}#")) + .find(_.info.exists(t => t.isType || t.isClass || t.isTrait)) + ) + .orElse(tpe.symbol.asNonEmpty.filterNot(_.isLocal).filter(validate)) + case t: Type.Select => + typeSymbols + .get(t.toString()) + .orElse(typeSymbols.get(t.name.value)) + .orElse(pkgSymbols.map(pkg => Symbol(s"${pkg}${t.name.value}#")).find(_.info.isDefined)) + .orElse(tpe.symbol.asNonEmpty.filterNot(_.isLocal)) + case _ => None + } + + def isTagged(param: Param)(implicit doc: SemanticDocument): Boolean = { + val fromSymbol = + param.tpe.symbol.asNonEmpty + .orElse(symbolFromType(param.tpe)) + .flatMap(_.info) + .map(_.signature) + .exists { + case ClassSignature(_, parents, _, _) => + parents.exists { + case TypeRef(_, symbols.IzumiReflectTag, _) => true + case _ => false + } + case _ => false + } + def fromType = param.tpe.collect { case Type.Name("EnvironmentTag") => true }.nonEmpty + + fromSymbol || fromType + } + + doc.tree.traverse { case annotatedModule: Defn.Object => + val accessors = annotatedModule.mods + .collectFirst { + case annot @ Mod.Annot( + Init(Type.Apply(names.MockableAnnotation(), mockedService :: Nil), _, Nil) + ) => + patches += Patch.removeTokens(annot.tokens) + + val methodInfos = for { + serviceSymbol <- symbolFromType(mockedService).toList + dealiasedServiceSymbol = dealiasSymbol(serviceSymbol) + _ = println(s"Creating Mock[${mockedService}] - symbol: ${dealiasedServiceSymbol.value}") + info <- dealiasedServiceSymbol.info.toList + case ClassSignature(clsTParams, _, _, decls) <- info.signature match { + case cls: ClassSignature => List(cls) + case _ => Nil + } + decl <- decls + case methodSig @ MethodSignature(methodTParams, methodParams, methodRType) <- decl.signature match { + case sig: MethodSignature => List(sig) + case _ => Nil + } + } yield { + val params = methodParams.flatten + .map { param => + val ValueSignature(paramTpe) = param.signature: @unchecked + Param(param.displayName, fromSemanticType(paramTpe)) + } + .filterNot(isTagged) + val typeParams = methodTParams + .map { tParam => + Type.Name(tParam.symbol.displayName) + } + + val capability = tryDealias(methodRType) match { + case t @ TypeRef(_, symbols.ZIO, typeParams) => + val List(r, e, a) = typeParams.map(fromSemanticType): @unchecked + Capability.Effect(r, e, a) + + case TypeRef(_, symbols.ZIOZStream, typeParams) => + val List(r, e, a) = typeParams.map(fromSemanticType): @unchecked + Capability.Stream(r, e, a) + + case TypeRef(_, symbols.ZIOZSink, typeParams) => + val List(r, e, a0, a, b) = typeParams.map(fromSemanticType): @unchecked + Capability.Sink(r, e, a0, a, b) + + case rType => Capability.Method(fromSemanticType(rType)) + } + + val interface = params.map(_.tpe) match { + case Nil => types.Unit + case tpe :: Nil => tpe + case types => Type.Tuple(types) + } + + MethodInfo(decl.symbol, capability, params, typeParams, interface) + } + val declarationOrder = methodInfos.map(_.symbol.displayName).zipWithIndex.toMap + val methods = + methodInfos + .groupBy(_.symbol.displayName) + .toList + .sortBy { case (name, _) => declarationOrder(name) } + + if (methods.isEmpty) { + case class NoMethodsFound() extends Diagnostic { + override def position = annotatedModule.pos + override def message: String = + s"Cannot create diagnostics, have not found any methods in ${mockedService}" + + s""" + |tpe=${mockedService.structureLabeled} + |typeSymol=${typeSymbols.get(mockedService.toString())} + |symbol=${mockedService.symbol} / ${symbolFromType(mockedService)} + |dealiased=${symbolFromType(mockedService).map(dealiasSymbol)} + |info=${symbolFromType(mockedService).map(dealiasSymbol).flatMap(_.info)} + |sig=${symbolFromType(mockedService) + .map(dealiasSymbol) + .flatMap(_.info) + .map(v => v -> v.signature.structureLabeled)} + |typeSymbols: ${typeSymbols.toList.sortBy(_._1).mkString("\n - ")} + |""".stripMargin + } + patches += Patch.lint(NoMethodsFound()) + } + + def sortOverloads(seq: List[MethodInfo]): List[MethodInfo] = { + import scala.math.Ordering.Implicits._ + implicit val paramOrder: Ordering[Param] = Ordering.by(v => (v.name, v.tpe.toString())) + seq.sortBy(_.params) + } + val tags = methods.map { + case (name, info :: Nil) => makeTag(name, info) + case (name, overloads) => + val tagName = name.capitalize + val overloadedTags = sortOverloads(overloads).zipWithIndex.map { case (info, idx) => + makeTag(s"_$idx", info) + } + val ident = " " + s"""object $tagName { + |${ident} ${overloadedTags.mkString(s"\n$ident ")} + |${ident}}""".stripMargin + } + val mocks = methods.flatMap { + case (name, info :: Nil) => + makeMock(name, info, overloadIndex = None) :: Nil + case (name, overloads) => + sortOverloads(overloads).zipWithIndex.map { case (info, idx) => + makeMock(name, info, Some(s"_$idx")) + } + } + + val parents = annotatedModule.templ.inits match { + case Nil => "" + case inits => inits.map(v => s"with $v").mkString(" ") + " " + } + val body = annotatedModule.templ.stats match { + case Nil => "" + case stats => + val ident = " " + ident + stats.mkString(s"\n$ident") + "\n" + } + val generatedObject = cleanupSyntax(pkgSymbols, importedSymbols) { + s""" + |object ${annotatedModule.name.value} extends Mock[$mockedService] $parents{ + |$body // format: off + | // Generated by ZIO Mockable CodeGen Scalafix Rule + | ${tags.mkString("\n ")} + | override val compose: zio.URLayer[zio.mock.Proxy, $mockedService] = zio.ZLayer.fromZIO[zio.mock.Proxy, Nothing, $mockedService]( + | zio.ZIO.service[zio.mock.Proxy].flatMap { proxy => + | withRuntime[zio.mock.Proxy, $mockedService] { rt => + | class MockImpl extends $mockedService { + | ${mocks.mkString("\n ")} + | } + | zio.ZIO.succeed(new MockImpl()) + | }}) + | // format: on + |}""".stripMargin + } + patches += Patch.replaceTree(annotatedModule, generatedObject).atomic + patches += Patch.addGlobalImport(symbols.ZIOMock) + () + } + } + + patches.result().asPatch + } + + def makeTag(name: String, info: MethodInfo)(implicit patches: PatchesBuilder, doc: SemanticDocument) = { + val tagName = name.capitalize + val (i, e, a) = (info.i, info.e, info.a) + + val parent = info.capability match { + case _: Capability.Effect => + (info.polyI, info.polyE, info.polyA) match { + case (false, false, false) => s"Effect[$i, $e, $a]" + case (true, false, false) => s"Poly.Effect.Input[$e, $a]" + case (false, true, false) => s"Poly.Effect.Error[$i, $a]" + case (false, false, true) => s"Poly.Effect.Output[$i, $e]" + case (true, true, false) => s"Poly.Effect.InputError[$a]" + case (true, false, true) => s"Poly.Effect.InputOutput[$e]" + case (false, true, true) => s"Poly.Effect.ErrorOutput[$i]" + case (true, true, true) => s"Poly.Effect.InputErrorOutput" + } + case _: Capability.Stream => + (info.polyI, info.polyE, info.polyA) match { + case (false, false, false) => s"Stream[$i, $e, $a]" + } + case _: Capability.Method => + (info.polyI, info.polyE, info.polyA) match { + case (false, false, false) => s"Method[$i, $e, $a]" + case (true, false, false) => s"Poly.Method.Input[$e, $a]" + case (false, true, false) => s"Poly.Method.Error[$i, $a]" + case (false, false, true) => s"Poly.Method.Output[$i, $e]" + case (true, true, false) => s"Poly.Method.InputError[$a]" + case (true, false, true) => s"Poly.Method.InputOutput[$e]" + case (false, true, true) => s"Poly.Method.ErrorOutput[$i]" + case (true, true, true) => "Poly.Method.InputErrorOutput" + } + + } + s"case object ${tagName} extends $parent" + } + + def makeMock(name: String, info: MethodInfo, overloadIndex: Option[String])(implicit + doc: SemanticDocument + ) = { + val tagName = name.capitalize + val (r, i, e, a) = (info.r, info.i, info.e, info.a) + + // val typeParamArgs = info.symbol.typeParams.map(s => toTypeDef(s)) + val tag = (info.polyI, info.polyE, info.polyA, overloadIndex) match { + case (false, false, false, None) => s"$tagName" + case (true, false, false, None) => s"$tagName.of[$i]" + case (false, true, false, None) => s"$tagName.of[$e]" + case (false, false, true, None) => s"$tagName.of[$a]" + case (true, true, false, None) => s"$tagName.of[$i, $e]" + case (true, false, true, None) => s"$tagName.of[$i, $a]" + case (false, true, true, None) => s"$tagName.of[$e, $a]" + case (true, true, true, None) => s"$tagName.of[$i, $e, $a]" + case (false, false, false, Some(index)) => s"$tagName.$index" + case (true, false, false, Some(index)) => s"$tagName.$index.of[$i]" + case (false, true, false, Some(index)) => s"$tagName.$index.of[$e]" + case (false, false, true, Some(index)) => s"$tagName.$index.of[$a]" + case (true, true, false, Some(index)) => s"$tagName.$index.of[$i, $e]" + case (true, false, true, Some(index)) => s"$tagName.$index.of[$i, $a]" + case (false, true, true, Some(index)) => s"$tagName.$index.of[$e, $a]" + case (true, true, true, Some(index)) => s"$tagName.$index.of[$i, $e, $a]" + } + + val symbolInfo = info.symbol.info + val mods = "final override" + + // TODO: used only for val + val returnType = info.capability match { + case Capability.Method(t) => t + case Capability.Stream(r, e, a) => Type.Apply(types.ZIOZStream, List(r, e, a)) + case _ => Type.Apply(types.ZIO, List(r, e, a)) + } + + def wrapInUnsafe(tree: String) = s"""zio.Unsafe.unsafe { case given zio.Unsafe => $tree }""" + + val returnValue = { + val params = info.params.map(_.name) + def proxyArgs = params match { + case Nil => tag + case params => s"$tag, ${params.mkString(", ")}" + } + info.capability match { + case _: Capability.Effect => s"proxy($proxyArgs)" + case _: Capability.Method => wrapInUnsafe(s"rt.unsafe.run(proxy($proxyArgs)).getOrThrow()") + case _: Capability.Stream => wrapInUnsafe(s"rt.unsafe.run(proxy($proxyArgs)).getOrThrowFiberFailure()") + case _: Capability.Sink => + wrapInUnsafe( + s"rt.unsafe.run(proxy($proxyArgs).catchAll(error => zio.UIO(zio.stream.ZSink.fail(error)))).getOrThrowFiberFailure()" + ) + } + } + if (symbolInfo.exists(_.isVal)) s"$mods val $name: $returnType = $returnValue" + else { + symbolInfo + .map(_.signature) + .collectFirst { case MethodSignature(typeParametersInfo, parameterLists, _) => + val tParams = typeParametersInfo.zip(info.typeParams).map { case (tParamInfo, tParam) => + s"${tParam}${tParamInfo.signature}" + } + val paramClouses = parameterLists.map { params => + Term.ParamClause( + values = params.map { ts => + val name = ts.displayName + val tpe = ts.signature match { + case ValueSignature(tpe) => fromSemanticType(tpe) + } + val isImplicit = ts.isImplicit + Term.Param( + mods = if (isImplicit) List(Mod.Using()) else Nil, + name = if (isImplicit) Name.Anonymous() else Term.Name(name), + decltpe = Some(tpe), + default = None + ) + }, + mod = Option(Mod.Using()).filter(_ => params.exists(_.isImplicit)) + ) + } + val typeParams = if (tParams.isEmpty) "" else tParams.mkString("[", ",", "]") + s"$mods def $name$typeParams${paramClouses.mkString}: $returnType = $returnValue" + } + .get + } + } + + sealed trait Capability + object Capability { + case class Method(a: Type) extends Capability + case class Effect(r: Type, e: Type, a: Type) extends Capability + case class Stream(r: Type, e: Type, a: Type) extends Capability + case class Sink(r: Type, e: Type, a0: Type, a: Type, b: Type) extends Capability + } + + case class Param(name: String, tpe: Type) {} + case class MethodInfo( + symbol: Symbol, + capability: Capability, + params: List[Param], + typeParams: List[Type], + i: Type + )(implicit doc: SemanticDocument) { + + val r: Type = normalize { + capability match { + case Capability.Effect(r, _, _) => r + case Capability.Sink(r, _, _, _, _) => r + case Capability.Stream(r, _, _) => r + case Capability.Method(_) => types.Any + } + } + + val e: Type = normalize { + capability match { + case Capability.Effect(_, e, _) => e + case Capability.Sink(_, e, _, _, _) => e + case Capability.Stream(_, e, _) => e + case Capability.Method(_) => types.Throwable + } + } + + val a: Type = normalize { + capability match { + case Capability.Effect(_, _, a) => a + case Capability.Sink(_, e, a0, a, b) => Type.Apply(types.ZIOZSink, List(types.Any, e, a0, a, b)) + case Capability.Stream(_, _, a) => a + case Capability.Method(a) => a + } + } + + def containsType(tpe: Type, expected: Type): Boolean = { + (tpe, expected) match { + case (Type.Name(n1), Type.Name(n2)) => n1 == n2 + case (Type.Select(_, tpe), _) => containsType(tpe, expected) + case (Type.Apply(_, tps), _) => tps.exists(containsType(_, expected)) + case (Type.Tuple(tps), _) => tps.exists(containsType(_, expected)) + } + } + val polyI: Boolean = typeParams.exists(containsType(i, _)) + val polyE: Boolean = typeParams.exists(containsType(e, _)) + val polyA: Boolean = typeParams.exists(containsType(a, _)) + } + +} diff --git a/tests/src/test/scala/fix/RuleSuite.scala b/tests/src/test/scala/fix/RuleSuite.scala new file mode 100644 index 0000000..ab80d00 --- /dev/null +++ b/tests/src/test/scala/fix/RuleSuite.scala @@ -0,0 +1,8 @@ +package fix + +import scalafix.testkit._ +import org.scalatest.funsuite.AnyFunSuiteLike + +class RuleSuite extends AbstractSemanticRuleSuite with AnyFunSuiteLike { + runAllTests() +}