diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b62037c..185f767 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout current branch (full) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -74,11 +74,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: mkdir -p modules/fs2/rules/target target/rules-aggregate/target modules/cats-effect/rules/target modules/http4s/rules/target modules/cats/rules/target project/target + run: mkdir -p modules/fs2/rules/target target/rules-aggregate/target modules/cats-effect/rules/target modules/http4s/rules/target modules/mtl/rules/target modules/cats/rules/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: tar cf targets.tar modules/fs2/rules/target target/rules-aggregate/target modules/cats-effect/rules/target modules/http4s/rules/target modules/cats/rules/target project/target + run: tar cf targets.tar modules/fs2/rules/target target/rules-aggregate/target modules/cats-effect/rules/target modules/http4s/rules/target modules/mtl/rules/target modules/cats/rules/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') @@ -98,7 +98,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout current branch (full) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -172,7 +172,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout current branch (full) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -195,7 +195,7 @@ jobs: - name: Submit Dependencies uses: scalacenter/sbt-dependency-submission@v2 with: - modules-ignore: cats-output_2.13 cats-output_2.12 http4s_2.13 http4s_2.12 fs2_2.13 fs2_2.12 cats-effect_2.13 cats-effect_2.12 cats-tests_2.13 cats-tests_2.12 fs2-tests_2.13 fs2-tests_2.12 cats-effect-tests_2.13 cats-effect-tests_2.12 typelevel-scalafix_2.13 typelevel-scalafix_2.12 fs2-output_2.13 fs2-output_2.12 fs2-input_2.13 fs2-input_2.12 cats-effect-input_2.13 cats-effect-input_2.12 http4s-tests_2.13 http4s-tests_2.12 http4s-input_2.13 http4s-input_2.12 cats-effect-output_2.13 cats-effect-output_2.12 http4s-output_2.13 http4s-output_2.12 cats_2.13 cats_2.12 cats-input_2.13 cats-input_2.12 + modules-ignore: mtl-output_2.13 mtl-output_2.12 cats-output_2.13 cats-output_2.12 http4s_2.13 http4s_2.12 fs2_2.13 fs2_2.12 mtl-tests_2.13 mtl-tests_2.12 cats-effect_2.13 cats-effect_2.12 cats-tests_2.13 cats-tests_2.12 fs2-tests_2.13 fs2-tests_2.12 cats-effect-tests_2.13 cats-effect-tests_2.12 typelevel-scalafix_2.13 typelevel-scalafix_2.12 fs2-output_2.13 fs2-output_2.12 fs2-input_2.13 fs2-input_2.12 cats-effect-input_2.13 cats-effect-input_2.12 http4s-tests_2.13 http4s-tests_2.12 mtl-input_2.13 mtl-input_3 http4s-input_2.13 http4s-input_2.12 mtl_2.13 mtl_2.12 cats-effect-output_2.13 cats-effect-output_2.12 http4s-output_2.13 http4s-output_2.12 cats_2.13 cats_2.12 cats-input_2.13 cats-input_2.12 configs-ignore: test scala-tool scala-doc-tool test-internal validate-steward: @@ -203,19 +203,19 @@ jobs: strategy: matrix: os: [ubuntu-22.04] - java: [temurin@11] + java: [temurin@17] runs-on: ${{ matrix.os }} steps: - name: Checkout current branch (fast) - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - name: Setup Java (temurin@11) - id: setup-java-temurin-11 - if: matrix.java == 'temurin@11' + - name: Setup Java (temurin@17) + id: setup-java-temurin-17 + if: matrix.java == 'temurin@17' uses: actions/setup-java@v5 with: distribution: temurin - java-version: 11 + java-version: 17 - uses: coursier/setup-action@v1 with: diff --git a/.scalafmt.conf b/.scalafmt.conf index 9c7b741..0b77021 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -11,3 +11,9 @@ indent { defnSite = 2 extendSite = 2 } + +fileOverride { + "glob:**/scala-3/**" { + runner.dialect = scala3 + } +} \ No newline at end of file diff --git a/README.md b/README.md index d978515..dfd3bac 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,18 @@ Then you can add the *typelevel-scalafix* rules to your sbt project using the `s ```scala // To add all Scalafix rules -ThisBuild / scalafixDependencies += "org.typelevel" %% "typelevel-scalafix" % "0.2.0" +ThisBuild / scalafixDependencies += "org.typelevel" %% "typelevel-scalafix" % "0.6.0" // To add only cats Scalafix rules -ThisBuild / scalafixDependencies += "org.typelevel" %% "typelevel-scalafix-cats" % "0.2.0" +ThisBuild / scalafixDependencies += "org.typelevel" %% "typelevel-scalafix-cats" % "0.6.0" // To add only cats-effect Scalafix rules -ThisBuild / scalafixDependencies += "org.typelevel" %% "typelevel-scalafix-cats-effect" % "0.2.0" +ThisBuild / scalafixDependencies += "org.typelevel" %% "typelevel-scalafix-cats-effect" % "0.6.0" +// To add only cats-mtl Scalafix rules +ThisBuild / scalafixDependencies += "org.typelevel" %% "typelevel-scalafix-mtl" % "0.6.0" // To add only fs2 Scalafix rules -ThisBuild / scalafixDependencies += "org.typelevel" %% "typelevel-scalafix-fs2" % "0.2.0" +ThisBuild / scalafixDependencies += "org.typelevel" %% "typelevel-scalafix-fs2" % "0.6.0" // To add only http4s Scalafix rules -ThisBuild / scalafixDependencies += "org.typelevel" %% "typelevel-scalafix-http4s" % "0.2.0" +ThisBuild / scalafixDependencies += "org.typelevel" %% "typelevel-scalafix-http4s" % "0.6.0" ``` ## Usage @@ -41,6 +43,7 @@ rules = [ TypelevelFs2SyncCompiler TypelevelHttp4sLiteralsSyntax TypelevelIORandomUUID + TypelevelMTLSubmarine ] ``` @@ -62,6 +65,7 @@ Not all rules function with Scala 3 yet. | TypelevelUnusedShowInterpolator | :white_check_mark: | :x: | | TypelevelFs2SyncCompiler | :white_check_mark: | :x: | | TypelevelHttp4sLiteralsSyntax | :white_check_mark: | :white_check_mark: | +| TypelevelMTLSubmarine | :white_check_mark: | :white_check_mark: | ## Rules for cats @@ -170,6 +174,47 @@ val test = IO.randomUUID This rule works on variable declarations, usaged within methods as well as for comprehensions. +## Rules for cats-mtl + +### TypelevelMTLSubmarine + +This rule reports Cats and `IO` error handlers used on expressions that require +`cats.mtl.Raise[F, E]`. When `Raise` comes from `Handle.allow`, raised values travel as +traceless `Handle.Submarine` exceptions. A general error handler can catch one before +`Handle` sees it. + +The rule covers Cats typeclass methods and syntax, plus concrete `IO` methods. It does +not follow user-defined wrappers or constructors such as `Stream.eval`, and it does not +match concrete `SyncIO` or `fs2.Stream` handlers. `Raise` and `Handle` aliases are +supported; user-defined subtypes are not. + +Partial and narrow handlers are reported when they can match `Handle.Submarine`, a +`RuntimeException`. This includes wildcards and the standard exception supertypes, but +not disjoint types such as `IOException`. Unknown patterns are reported. + +Scala 2.13 and Scala 3 are supported. Scala 3 also supports context-function values such +as `Raise[F, E] ?=> F[A]`. + +Example: + +```scala +import cats.effect.IO +import cats.mtl.{Raise, Handle} + +def raiseError: Raise[IO, String] ?=> IO[Unit] = r ?=> + r.raise("boom") + +def standardError: IO[Unit] = + IO.raiseError(new RuntimeException("boom")) + +Handle.allow[String] { + for { + _ <- raiseError.onError(e => IO.println("Error: " + e)) // reported + _ <- standardError.onError(e => IO.println("Error: " + e)) // not reported + } yield () +} +``` + ## Rules for fs2 ### TypelevelFs2SyncCompiler diff --git a/build.sbt b/build.sbt index 1b2c06a..79cdca8 100644 --- a/build.sbt +++ b/build.sbt @@ -7,6 +7,7 @@ lazy val CatsVersion = "2.12.0" lazy val CatsEffectVersion = "3.6.3" lazy val Fs2Version = "3.12.2" lazy val Http4sVersion = "0.23.32" +lazy val MtlVersion = "1.7.0" ThisBuild / startYear := Some(2022) ThisBuild / developers ++= List( @@ -19,12 +20,12 @@ ThisBuild / scalafixScalaBinaryVersion := CrossVersion.binaryScalaVersion(scalaV lazy val `typelevel-scalafix` = project .in(file(".")) - .aggregate(`typelevel-scalafix-rules`, cats.all, catsEffect.all, fs2.all, http4s.all) + .aggregate(`typelevel-scalafix-rules`, cats.all, catsEffect.all, fs2.all, http4s.all, mtl.all) .enablePlugins(NoPublishPlugin) lazy val `typelevel-scalafix-rules` = project .in(file("target/rules-aggregate")) - .dependsOn(cats.rules, catsEffect.rules, fs2.rules, http4s.rules) + .dependsOn(cats.rules, catsEffect.rules, fs2.rules, http4s.rules, mtl.rules) .settings( moduleName := "typelevel-scalafix", tlVersionIntroduced ++= List("2.12", "2.13").map(_ -> "0.1.2").toMap, @@ -83,3 +84,17 @@ lazy val http4s = scalafixProject("http4s") "org.http4s" %% "http4s-core" % Http4sVersion ) ) + +// typelevel/mtl Scalafix rules +lazy val mtl = scalafixProject("mtl") + .rulesSettings( + tlVersionIntroduced ++= List("2.12", "2.13").map(_ -> "0.6.0").toMap + ) + .inputSettings( + scalaVersion := "3.7.2", + crossScalaVersions := Seq(V.scala213, "3.7.2"), + libraryDependencies ++= Seq( + "org.typelevel" %% "cats-effect" % CatsEffectVersion, + "org.typelevel" %% "cats-mtl" % MtlVersion + ) + ) diff --git a/modules/cats-effect/rules/src/main/scala/org/typelevel/fix/UnusedIO.scala b/modules/cats-effect/rules/src/main/scala/org/typelevel/fix/UnusedIO.scala index e1624ce..57b90e4 100644 --- a/modules/cats-effect/rules/src/main/scala/org/typelevel/fix/UnusedIO.scala +++ b/modules/cats-effect/rules/src/main/scala/org/typelevel/fix/UnusedIO.scala @@ -51,17 +51,15 @@ class UnusedIO extends SemanticRule("TypelevelUnusedIO") { checkSignature(outer, ref) case Term.ApplyInfix.Initial(_, op, _, _) => checkSignature(outer, op) - case Term.Apply.Initial(fn @ Term.Name(_), _) if IOCompanionSym.matches(fn) => + case Term.Apply.After_4_6_0(fn @ Term.Name(_), _) if IOCompanionSym.matches(fn) => Patch.lint(UnusedIODiagnostic(outer)) - case Term.Apply.Initial(fn @ Term.Name(_), _) => + case Term.Apply.After_4_6_0(fn @ Term.Name(_), _) => checkSignature(outer, fn) case Term.ApplyUnary(fn @ Term.Name(_), _) => checkSignature(outer, fn) - case Term.ApplyUsing.Initial(fn @ Term.Name(_), _) => - checkSignature(outer, fn) case Term.Select(_, prop @ Term.Name(_)) => checkSignature(outer, prop) - case Term.Apply.Initial(Term.Select(_, method), _) => + case Term.Apply.After_4_6_0(Term.Select(_, method), _) => checkSignature(outer, method) case Term.Annotate(expr, _) => checkInner(expr) diff --git a/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineDirectProtectedArgumentTest.scala b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineDirectProtectedArgumentTest.scala new file mode 100644 index 0000000..9782b7a --- /dev/null +++ b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineDirectProtectedArgumentTest.scala @@ -0,0 +1,20 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl._ +import cats.effect._ + +// scalafmt: { maxColumn = 160 } +object DirectProtectedArgument { + + def raiseDependentRecovery[F[_]](error: Throwable)(implicit r: Raise[F, String]): F[Unit] = + r.raise(error.getMessage) + + def normalSource[F[_]: Async](implicit r: Raise[F, String]): F[Unit] = { + Async[F].handleErrorWith(Async[F].unit)(raiseDependentRecovery[F]) + Async[F].redeemWith(Async[F].unit)(raiseDependentRecovery[F], _ => Async[F].unit) + } + +} diff --git a/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineEffectPropagationTest.scala b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineEffectPropagationTest.scala new file mode 100644 index 0000000..e2794d7 --- /dev/null +++ b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineEffectPropagationTest.scala @@ -0,0 +1,120 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl._ +import cats.effect._ +import cats.effect.implicits._ +import cats.syntax.all._ + +// scalafmt: { maxColumn = 160 } +object EffectPropagation { + + def methodRaise[F[_]](implicit r: Raise[F, String]): F[Unit] = + r.raise("something went wrong") + + def syntax[F[_]: Async](implicit r: Raise[F, String]): F[Unit] = { + methodRaise[F].map(identity).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].void.recover(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].as(()).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (Async[F].unit *> methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (methodRaise[F] <* Async[F].unit).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (Async[F].unit >> methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].product(Async[F].unit).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.product(methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].productL(Async[F].unit).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.productR(methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.map2(methodRaise[F])((_, _) => ()).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].fproduct(identity).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].fproductLeft(identity).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].tupleLeft(()).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].tupleRight(()).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F] + .as((_: Unit) => ()) + .<&>(Async[F].unit) + .attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].flatMap(_ => Async[F].unit).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.flatMap(_ => methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.flatTap(_ => methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (Async[F].unit >>= (_ => methodRaise[F])).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.mproduct(_ => methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.map(identity).attempt + (for { + _ <- Async[F].unit + _ <- methodRaise[F] + } yield ()).attempt.void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def direct[F[_]: Async](implicit r: Raise[F, String]): F[Unit] = { + Async[F].attempt(Async[F].map(methodRaise[F])(identity)).void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].attempt(Async[F].flatMap(Async[F].unit)(_ => methodRaise[F])).void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].attempt(Async[F].map(Async[F].unit)(identity)).void + Async[F].attempt(Async[F].flatMap(Async[F].unit)(_ => Async[F].unit)).void + } + + def safeSyntax[F[_]: Async]: F[Unit] = { + Async[F].unit.map(identity).attempt + Async[F].unit.void.recover(_ => ()) + Async[F].unit.as(()).attempt + (Async[F].unit *> Async[F].unit).attempt + (Async[F].unit <* Async[F].unit).attempt + (Async[F].unit >> Async[F].unit).attempt + Async[F].unit.product(Async[F].unit).attempt + Async[F].unit.productL(Async[F].unit).attempt + Async[F].unit.productR(Async[F].unit).attempt + Async[F].unit.map2(Async[F].unit)((_, _) => ()).attempt + Async[F].unit.fproduct(identity).attempt + Async[F].unit.fproductLeft(identity).attempt + Async[F].unit.tupleLeft(()).attempt + Async[F].unit.tupleRight(()).attempt + Async[F].pure((_: Unit) => ()).<&>(Async[F].unit).attempt + Async[F].unit.flatMap(_ => Async[F].unit).attempt + Async[F].unit.flatTap(_ => Async[F].unit).attempt + (Async[F].unit >>= (_ => Async[F].unit)).attempt + Async[F].unit.mproduct(_ => Async[F].unit).attempt + (for { + _ <- Async[F].unit + _ <- Async[F].unit + } yield ()).attempt.void + } + + def expressionWrappers[F[_]: Async](implicit r: Raise[F, String]): F[Unit] = { + (methodRaise[F]: F[Unit]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (((methodRaise[F]))).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (Async[F].unit: F[Unit]).attempt.void + } + + def conditional[F[_]: Async](condition: Boolean)(implicit r: Raise[F, String]): F[Unit] = { + (if (condition) Async[F].unit else methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (if (condition) methodRaise[F] else Async[F].unit).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (if (condition) Async[F].unit else Async[F].unit).attempt.void + } + + def matched[F[_]: Async](condition: Boolean)(implicit r: Raise[F, String]): F[Unit] = { + (condition match { + case true => Async[F].unit + case false => methodRaise[F] + }).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (condition match { + case true => methodRaise[F] + case false => Async[F].unit + }).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (condition match { + case true => Async[F].unit + case false => Async[F].unit + }).attempt.void + } + + def blocks[F[_]: Async](implicit r: Raise[F, String]): F[Unit] = { + ({ + Async[F].unit + methodRaise[F] + }).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + ({ + methodRaise[F] + Async[F].unit + }).attempt.void + } + +} diff --git a/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineHandleAllowFTest.scala b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineHandleAllowFTest.scala new file mode 100644 index 0000000..15e829a --- /dev/null +++ b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineHandleAllowFTest.scala @@ -0,0 +1,49 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl._ +import cats.effect._ +import cats.syntax.all._ + +// scalafmt: { maxColumn = 160 } +object HandleAllowF { + + def methodRaise[F[_]](implicit r: Raise[F, String]): F[Unit] = + r.raise("something went wrong") + + def method[F[_]: Async]: F[Unit] = + Async[F].unit + + def safeGeneric[F[_]: Async] = Handle.allowF[F, String] { _ => + for { + _ <- method[F].attempt + _ <- Async[F].recover(method[F]) { case _ => () } + _ <- method[F].ensure(new Exception(""))(_ => false) + } yield () + } + + def attemptedGeneric[F[_]: Async]: F[Either[String, Unit]] = + Handle + .allowF[F, String] { implicit h => + methodRaise[F] + } + .attempt + + def dangerousGeneric[F[_]: Async] = Handle.allowF[F, String] { implicit h => + for { + _ <- methodRaise[F].attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + _ <- Async[F].recover(methodRaise[F]) { case _ => () } // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + _ <- methodRaise[F].ensure(new Exception(""))(_ => false) + _ <- methodRaise[F].void + } yield () + } + + def io = Handle.allowF[IO, String] { implicit h => + method[IO].onError(_ => IO.unit) + methodRaise[IO].onError(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[IO].debug() + } + +} diff --git a/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineHandleRequirementTest.scala b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineHandleRequirementTest.scala new file mode 100644 index 0000000..091a636 --- /dev/null +++ b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineHandleRequirementTest.scala @@ -0,0 +1,25 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl._ +import cats.effect._ +import cats.syntax.all._ + +// scalafmt: { maxColumn = 160 } +object HandleRequirement { + + def method[F[_]](implicit h: Handle[F, String]): F[Unit] = + h.raise("something went wrong") + + def applicativeErrorSyntax[F[_]: Async](implicit h: Handle[F, String]): F[Unit] = + method[F].attempt.void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + + def applicativeErrorDirect[F[_]: Async](implicit h: Handle[F, String]): F[Unit] = + Async[F].attempt(method[F]).void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + + def io(implicit h: Handle[IO, String]): IO[Unit] = + method[IO].attempt.void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + +} diff --git a/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineLocalMethodCaptureTest.scala b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineLocalMethodCaptureTest.scala new file mode 100644 index 0000000..a11547e --- /dev/null +++ b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineLocalMethodCaptureTest.scala @@ -0,0 +1,33 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl._ +import cats.effect._ + +// scalafmt: { maxColumn = 160 } +object LocalMethodCapture { + + def captured = Handle.allowF[IO, String] { implicit h => + def boom: IO[Unit] = h.raise("boom") + + boom.attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def chainedCapture = Handle.allowF[IO, String] { implicit h => + def boom: IO[Unit] = h.raise("boom") + def indirect: IO[Unit] = boom + + indirect.recover(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def normal = Handle.allowF[IO, String] { _ => + def safe: IO[Unit] = IO.unit + def recursive(remaining: Int): IO[Unit] = + if (remaining <= 0) IO.unit else recursive(remaining - 1) + + safe.attempt *> recursive(1).attempt + } + +} diff --git a/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineLocalValueCaptureTest.scala b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineLocalValueCaptureTest.scala new file mode 100644 index 0000000..5f8d7ad --- /dev/null +++ b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineLocalValueCaptureTest.scala @@ -0,0 +1,31 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl._ +import cats.effect._ + +// scalafmt: { maxColumn = 160 } +object LocalValueCapture { + + def captured = Handle.allowF[IO, String] { implicit h => + val direct: IO[Unit] = h.raise("boom") + + direct.attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def capturedMethod = Handle.allowF[IO, String] { implicit h => + def boom: IO[Unit] = h.raise("boom") + lazy val action: IO[Unit] = boom + + action.recover(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def normal = Handle.allowF[IO, String] { _ => + val safe: IO[Unit] = IO.unit + + safe.attempt + } + +} diff --git a/modules/mtl/input/src/main/scala-2/fix/MTLSubmarinePartialAndNarrowHandlersTest.scala b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarinePartialAndNarrowHandlersTest.scala new file mode 100644 index 0000000..7644008 --- /dev/null +++ b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarinePartialAndNarrowHandlersTest.scala @@ -0,0 +1,33 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl._ +import cats.effect._ +import cats.syntax.all._ + +import java.io.IOException + +// scalafmt: { maxColumn = 160 } +object PartialAndNarrowHandlers { + + def methodRaise[F[_]](implicit r: Raise[F, String]): F[Unit] = + r.raise("boom") + + def precise[F[_]: Async](implicit r: Raise[F, String]): F[Unit] = { + methodRaise[F].attemptNarrow[RuntimeException] // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].attemptNarrow[IOException] + Async[F].attemptNarrow[IOException, Unit](methodRaise[F]) + methodRaise[F].recover { case _: RuntimeException => () } // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].recover { case _: IOException => () } + Async[F].recover(methodRaise[F]) { case _: IOException => () } + methodRaise[F].recover { case _ => () } // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def unknown[F[_]: Async](handler: PartialFunction[Throwable, Unit])(implicit + r: Raise[F, String] + ): F[Unit] = + methodRaise[F].recover(handler) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + +} diff --git a/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineRaiseAliasTest.scala b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineRaiseAliasTest.scala new file mode 100644 index 0000000..ecafc60 --- /dev/null +++ b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineRaiseAliasTest.scala @@ -0,0 +1,42 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl._ +import cats.effect._ +import cats.syntax.all._ + +// scalafmt: { maxColumn = 160 } +object RaiseAlias { + + type AliasedRaise[F[_], E] = Raise[F, E] + type ChainedRaiseAlias[F[_], E] = AliasedRaise[F, E] + type AliasedHandle[F[_], E] = Handle[F, E] + type ChainedHandleAlias[F[_], E] = AliasedHandle[F, E] + + def aliasedRaise[F[_]](implicit r: AliasedRaise[F, String]): F[Unit] = + r.raise("something went wrong") + + def chainedRaise[F[_]](implicit r: ChainedRaiseAlias[F, String]): F[Unit] = + r.raise("something went wrong") + + def aliasedHandle[F[_]](implicit h: AliasedHandle[F, String]): F[Unit] = + h.raise("something went wrong") + + def chainedHandle[F[_]](implicit h: ChainedHandleAlias[F, String]): F[Unit] = + h.raise("something went wrong") + + def aliasedRaiseHandling[F[_]: Async](implicit r: AliasedRaise[F, String]): F[Unit] = + aliasedRaise[F].attempt.void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + + def chainedRaiseHandling[F[_]: Async](implicit r: ChainedRaiseAlias[F, String]): F[Unit] = + Async[F].attempt(chainedRaise[F]).void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + + def aliasedHandleHandling[F[_]: Async](implicit h: AliasedHandle[F, String]): F[Unit] = + aliasedHandle[F].recover(_ => ()).void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + + def chainedHandleHandling[F[_]: Async](implicit h: ChainedHandleAlias[F, String]): F[Unit] = + Async[F].handleError(chainedHandle[F])(_ => ()).void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + +} diff --git a/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineRaiseInstanceTest.scala b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineRaiseInstanceTest.scala new file mode 100644 index 0000000..e061eb5 --- /dev/null +++ b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineRaiseInstanceTest.scala @@ -0,0 +1,99 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl._ +import cats.effect._ +import cats.syntax.all._ + +// scalafmt: { maxColumn = 160 } +object RaiseInstance { + + def applicativeErrorSyntax[F[_]: Async](implicit r: Raise[F, String]): F[Unit] = { + // ApplicativeError syntax + r.raise[String, Unit]("").handleError(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").handleErrorWith(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attemptNarrow[RuntimeException] // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attemptT.value // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").recover(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").recoverWith(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").redeem(_ => (), _ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").onError(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").orElse(Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").adaptErr(_ => new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").orRaise(new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").voidError // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + // ensure other methods aren't affected + r.raise[String, Unit]("").tupleRight("") + r.raise[String, Unit]("").void + } + + def monadErrorSyntax[F[_]: Async](implicit r: Raise[F, String]): F[Unit] = { + r.raise[String, Unit]("").ensure(new Exception(""))(_ => false) + r.raise[String, Unit]("").ensureOr(_ => new Exception(""))(_ => false) + r.raise[String, Unit]("").redeemWith(_ => Async[F].unit, _ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attemptTap(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").adaptError(_ => new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, String]("").reject(_ => new Exception("")) + // ensure other methods aren't affected + r.raise[String, Unit]("").tupleRight("") + r.raise[String, Unit]("").void + } + + def applicativeErrorDirect[F[_]: Async](implicit r: Raise[F, String]): F[Unit] = { + Async[F].handleError(r.raise[String, Unit](""))(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].handleErrorWith(r.raise[String, Unit](""))(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].attempt(r.raise[String, Unit]("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].attemptNarrow[RuntimeException, Unit](r.raise[String, Unit]("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].attemptT(r.raise[String, Unit]("")).value // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].recover(r.raise[String, Unit](""))(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].recoverWith(r.raise[String, Unit](""))(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].redeem(r.raise[String, Unit](""))(_ => (), _ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].onError(r.raise[String, Unit](""))(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].adaptError(r.raise[String, Unit](""))(_ => new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].voidError(r.raise[String, Unit]("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + // ensure other methods aren't affected + Async[F].tupleRight(r.raise[String, Unit](""), "") + Async[F].void(r.raise[String, Unit]("")) + } + + def monadErrorDirect[F[_]: Async](implicit r: Raise[F, String]): F[Unit] = { + Async[F].ensure(r.raise[String, Unit](""))(new Exception(""))(_ => false) + Async[F].ensureOr(r.raise[String, Unit](""))(_ => new Exception(""))(_ => false) + Async[F].redeemWith(r.raise[String, Unit](""))(_ => Async[F].unit, _ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].attemptTap(r.raise[String, Unit](""))(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + // ensure other methods aren't affected + Async[F].tupleRight(r.raise[String, Unit](""), "") + Async[F].void(r.raise[String, Unit]("")) + } + + def io(implicit r: Raise[IO, String]): IO[Unit] = { + // applicative error + r.raise[String, Unit]("").handleError(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").handleErrorWith(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attemptNarrow[RuntimeException] // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attemptT.value // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").recover(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").recoverWith(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").redeem(_ => (), _ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").onError(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").orElse(IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").adaptErr(_ => new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").orRaise(new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").voidError // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + // monad error + r.raise[String, Unit]("").ensure(new Exception(""))(_ => false) + r.raise[String, Unit]("").ensureOr(_ => new Exception(""))(_ => false) + r.raise[String, Unit]("").redeemWith(_ => IO.unit, _ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attemptTap(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").adaptError(_ => new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, String]("").reject(_ => new Exception("")) + // ensure other methods aren't affected + r.raise[String, Unit]("").void + r.raise[String, Unit]("").debug() + } + +} diff --git a/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineRaiseObjectTest.scala b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineRaiseObjectTest.scala new file mode 100644 index 0000000..dfef498 --- /dev/null +++ b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineRaiseObjectTest.scala @@ -0,0 +1,25 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl._ +import cats.effect._ +import cats.syntax.all._ + +// scalafmt: { maxColumn = 160 } +object RaiseObject { + + def generic[F[_]: Async](implicit r: Raise[F, String]): F[Unit] = { + Raise.raise[F, String, Unit]("").attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].recover(Raise.raise[F, String, Unit]("")) { case _ => () } // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Raise.raise[F, String, Unit]("").void + } + + def io(implicit r: Raise[IO, String]): IO[Unit] = { + Raise.raise[IO, String, Unit]("").onError(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Raise.raise[IO, String, String]("").reject(_ => new Exception("")) + Raise.raise[IO, String, Unit]("").debug() + } + +} diff --git a/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineRaiseSyntaxTest.scala b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineRaiseSyntaxTest.scala new file mode 100644 index 0000000..24fe543 --- /dev/null +++ b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineRaiseSyntaxTest.scala @@ -0,0 +1,26 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl._ +import cats.mtl.syntax.raise._ +import cats.effect._ +import cats.syntax.all._ + +// scalafmt: { maxColumn = 160 } +object RaiseSyntax { + + def generic[F[_]: Async](implicit r: Raise[F, String]): F[Unit] = { + "".raise[F, Unit].attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].handleError("".raise[F, Unit])(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + "".raise[F, Unit].void + } + + def io(implicit r: Raise[IO, String]): IO[Unit] = { + "".raise[IO, Unit].onError(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + "".raise[IO, String].reject(_ => new Exception("")) + "".raise[IO, Unit].debug() + } + +} diff --git a/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineWrappedRaiseTest.scala b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineWrappedRaiseTest.scala new file mode 100644 index 0000000..bc6ee8c --- /dev/null +++ b/modules/mtl/input/src/main/scala-2/fix/MTLSubmarineWrappedRaiseTest.scala @@ -0,0 +1,21 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl._ +import cats.effect._ +import cats.syntax.all._ + +// scalafmt: { maxColumn = 160 } +object WrappedRaise { + + trait Wrapper[A] + + def normal[F[_]: Async](implicit wrapper: Wrapper[Raise[F, String]]): F[Unit] = + Async[F].unit + + def handlingNormal[F[_]: Async](implicit wrapper: Wrapper[Raise[F, String]]): F[Unit] = + normal[F].attempt.void + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineContextFunctionValueTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineContextFunctionValueTest.scala new file mode 100644 index 0000000..9cbae96 --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineContextFunctionValueTest.scala @@ -0,0 +1,27 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.effect.* +import cats.syntax.all.* + +// scalafmt: { maxColumn = 160 } +object ContextFunctionValue { + + def valueHandling[F[_]: Async](using Raise[F, String]): F[Unit] = { + val action: Raise[F, String] ?=> F[Unit] = r ?=> r.raise("something went wrong") + + action.attempt.void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def parameterHandling[F[_]: Async]( + action: Raise[F, String] ?=> F[Unit] + )(using Raise[F, String]): F[Unit] = + action.recover(_ => ()).void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + + def normalParameter[F[_]: Async](action: Async[F] ?=> F[Unit]): F[Unit] = + action.attempt.void + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineDirectProtectedArgumentTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineDirectProtectedArgumentTest.scala new file mode 100644 index 0000000..4cb09a2 --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineDirectProtectedArgumentTest.scala @@ -0,0 +1,20 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.effect.* + +// scalafmt: { maxColumn = 160 } +object DirectProtectedArgument { + + def raiseDependentRecovery[F[_]](error: Throwable)(using r: Raise[F, String]): F[Unit] = + r.raise(error.getMessage) + + def normalSource[F[_]: Async](using r: Raise[F, String]): F[Unit] = { + Async[F].handleErrorWith(Async[F].unit)(raiseDependentRecovery[F]) + Async[F].redeemWith(Async[F].unit)(raiseDependentRecovery[F], _ => Async[F].unit) + } + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineEffectPropagationTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineEffectPropagationTest.scala new file mode 100644 index 0000000..001a39f --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineEffectPropagationTest.scala @@ -0,0 +1,120 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.effect.* +import cats.effect.implicits.* +import cats.syntax.all.* + +// scalafmt: { maxColumn = 160 } +object EffectPropagation { + + def methodRaise[F[_]](using r: Raise[F, String]): F[Unit] = + r.raise("something went wrong") + + def syntax[F[_]: Async](using r: Raise[F, String]): F[Unit] = { + methodRaise[F].map(identity).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].void.recover(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].as(()).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (Async[F].unit *> methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (methodRaise[F] <* Async[F].unit).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (Async[F].unit >> methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].product(Async[F].unit).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.product(methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].productL(Async[F].unit).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.productR(methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.map2(methodRaise[F])((_, _) => ()).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].fproduct(identity).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].fproductLeft(identity).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].tupleLeft(()).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].tupleRight(()).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F] + .as((_: Unit) => ()) + .<&>(Async[F].unit) + .attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].flatMap(_ => Async[F].unit).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.flatMap(_ => methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.flatTap(_ => methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (Async[F].unit >>= (_ => methodRaise[F])).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.mproduct(_ => methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].unit.map(identity).attempt + (for { + _ <- Async[F].unit + _ <- methodRaise[F] + } yield ()).attempt.void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def direct[F[_]: Async](using r: Raise[F, String]): F[Unit] = { + Async[F].attempt(Async[F].map(methodRaise[F])(identity)).void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].attempt(Async[F].flatMap(Async[F].unit)(_ => methodRaise[F])).void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].attempt(Async[F].map(Async[F].unit)(identity)).void + Async[F].attempt(Async[F].flatMap(Async[F].unit)(_ => Async[F].unit)).void + } + + def safeSyntax[F[_]: Async]: F[Unit] = { + Async[F].unit.map(identity).attempt + Async[F].unit.void.recover(_ => ()) + Async[F].unit.as(()).attempt + (Async[F].unit *> Async[F].unit).attempt + (Async[F].unit <* Async[F].unit).attempt + (Async[F].unit >> Async[F].unit).attempt + Async[F].unit.product(Async[F].unit).attempt + Async[F].unit.productL(Async[F].unit).attempt + Async[F].unit.productR(Async[F].unit).attempt + Async[F].unit.map2(Async[F].unit)((_, _) => ()).attempt + Async[F].unit.fproduct(identity).attempt + Async[F].unit.fproductLeft(identity).attempt + Async[F].unit.tupleLeft(()).attempt + Async[F].unit.tupleRight(()).attempt + Async[F].pure((_: Unit) => ()).<&>(Async[F].unit).attempt + Async[F].unit.flatMap(_ => Async[F].unit).attempt + Async[F].unit.flatTap(_ => Async[F].unit).attempt + (Async[F].unit >>= (_ => Async[F].unit)).attempt + Async[F].unit.mproduct(_ => Async[F].unit).attempt + (for { + _ <- Async[F].unit + _ <- Async[F].unit + } yield ()).attempt.void + } + + def expressionWrappers[F[_]: Async](using r: Raise[F, String]): F[Unit] = { + (methodRaise[F]: F[Unit]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (((methodRaise[F]))).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (Async[F].unit: F[Unit]).attempt.void + } + + def conditional[F[_]: Async](condition: Boolean)(using r: Raise[F, String]): F[Unit] = { + (if (condition) Async[F].unit else methodRaise[F]).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (if (condition) methodRaise[F] else Async[F].unit).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (if (condition) Async[F].unit else Async[F].unit).attempt.void + } + + def matched[F[_]: Async](condition: Boolean)(using r: Raise[F, String]): F[Unit] = { + (condition match { + case true => Async[F].unit + case false => methodRaise[F] + }).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (condition match { + case true => methodRaise[F] + case false => Async[F].unit + }).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + (condition match { + case true => Async[F].unit + case false => Async[F].unit + }).attempt.void + } + + def blocks[F[_]: Async](using r: Raise[F, String]): F[Unit] = { + ({ + Async[F].unit + methodRaise[F] + }).attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + ({ + methodRaise[F] + Async[F].unit + }).attempt.void + } + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineHandleAllowContextFunctionTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineHandleAllowContextFunctionTest.scala new file mode 100644 index 0000000..1aa7552 --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineHandleAllowContextFunctionTest.scala @@ -0,0 +1,42 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.effect.* +import cats.syntax.all.* + +// scalafmt: { maxColumn = 160 } +object HandleAllowContextFunction { + + def methodRaise[F[_]]: Raise[F, String] ?=> F[Unit] = + summon[Raise[F, String]].raise("something went wrong") + + def method[F[_]: Async]: F[Unit] = + Async[F].unit + + def safeGeneric[F[_]: Async] = Handle.allow[String] { + method[F].attempt + } + + def attemptedGeneric[F[_]: Async]: F[Either[String, Unit]] = + Handle + .allow[String] { + methodRaise[F] + } + .attempt + + def dangerousGeneric[F[_]: Async] = Handle.allow[String] { + methodRaise[F].attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].handleError(methodRaise[F])(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].ensure(new Exception(""))(_ => false) + methodRaise[F].void + } + + def io = Handle.allow[String] { + methodRaise[IO].onError(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[IO].debug() + } + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineHandleAllowGivenTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineHandleAllowGivenTest.scala new file mode 100644 index 0000000..5202929 --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineHandleAllowGivenTest.scala @@ -0,0 +1,42 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.effect.* +import cats.syntax.all.* + +// scalafmt: { maxColumn = 160 } +object HandleAllowGiven { + + def methodRaise[F[_]](using r: Raise[F, String]): F[Unit] = + r.raise("something went wrong") + + def method[F[_]: Async]: F[Unit] = + Async[F].unit + + def safeGeneric[F[_]: Async] = Handle.allow[String] { + for { + _ <- method[F].attempt + _ <- Async[F].recover(method[F]) { case _ => () } + _ <- method[F].ensure(new Exception(""))(_ => false) + } yield () + } + + def dangerousGeneric[F[_]: Async] = Handle.allow[String] { + for { + _ <- methodRaise[F].attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + _ <- Async[F].recover(methodRaise[F]) { case _ => () } // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + _ <- methodRaise[F].ensure(new Exception(""))(_ => false) + _ <- methodRaise[F].void + } yield () + } + + def io = Handle.allow[String] { + method[IO].onError(_ => IO.unit) + methodRaise[IO].onError(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[IO].debug() + } + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineHandleRequirementTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineHandleRequirementTest.scala new file mode 100644 index 0000000..93332c0 --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineHandleRequirementTest.scala @@ -0,0 +1,38 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.effect.* +import cats.syntax.all.* + +// scalafmt: { maxColumn = 160 } +object HandleRequirement { + + def method[F[_]](using h: Handle[F, String]): F[Unit] = + h.raise("something went wrong") + + def contextFunctionMethod[F[_]]: Handle[F, String] ?=> F[Unit] = + summon[Handle[F, String]].raise("something went wrong") + + def normalContextFunctionMethod[F[_]]: Async[F] ?=> F[Unit] = + summon[Async[F]].unit + + def applicativeErrorSyntax[F[_]: Async](using h: Handle[F, String]): F[Unit] = + method[F].attempt.void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + + def applicativeErrorDirect[F[_]: Async](using h: Handle[F, String]): F[Unit] = + Async[F].attempt(method[F]).void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + + def io(using h: Handle[IO, String]): IO[Unit] = + method[IO].attempt.void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + + def contextFunction = Handle.allow[String] { + contextFunctionMethod[IO].attempt.void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def normalContextFunction(using Async[IO]): IO[Unit] = + normalContextFunctionMethod[IO].attempt.void + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineLocalMethodCaptureTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineLocalMethodCaptureTest.scala new file mode 100644 index 0000000..7923c8d --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineLocalMethodCaptureTest.scala @@ -0,0 +1,37 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.effect.* + +// scalafmt: { maxColumn = 160 } +object LocalMethodCapture { + + def captured = Handle.allow[String] { + val h = summon[Handle[IO, String]] + + def boom: IO[Unit] = h.raise("boom") + + boom.attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def chainedCapture = Handle.allow[String] { + val h = summon[Handle[IO, String]] + + def boom: IO[Unit] = h.raise("boom") + def indirect: IO[Unit] = boom + + indirect.recover(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def normal = Handle.allow[String] { + def safe: IO[Unit] = IO.unit + def recursive(remaining: Int): IO[Unit] = + if remaining <= 0 then IO.unit else recursive(remaining - 1) + + safe.attempt *> recursive(1).attempt + } + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineLocalValueCaptureTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineLocalValueCaptureTest.scala new file mode 100644 index 0000000..9a185b3 --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineLocalValueCaptureTest.scala @@ -0,0 +1,34 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.effect.* + +// scalafmt: { maxColumn = 160 } +object LocalValueCapture { + + def captured = Handle.allow[String] { + val h = summon[Handle[IO, String]] + val direct: IO[Unit] = h.raise("boom") + + direct.attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def capturedMethod = Handle.allow[String] { + val h = summon[Handle[IO, String]] + + def boom: IO[Unit] = h.raise("boom") + lazy val action: IO[Unit] = boom + + action.recover(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def normal = Handle.allow[String] { + val safe: IO[Unit] = IO.unit + + safe.attempt + } + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarinePartialAndNarrowHandlersTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarinePartialAndNarrowHandlersTest.scala new file mode 100644 index 0000000..0a014db --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarinePartialAndNarrowHandlersTest.scala @@ -0,0 +1,33 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.effect.* +import cats.syntax.all.* + +import java.io.IOException + +// scalafmt: { maxColumn = 160 } +object PartialAndNarrowHandlers { + + def methodRaise[F[_]](using r: Raise[F, String]): F[Unit] = + r.raise("boom") + + def precise[F[_]: Async](using Raise[F, String]): F[Unit] = { + methodRaise[F].attemptNarrow[RuntimeException] // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].attemptNarrow[IOException] + Async[F].attemptNarrow[IOException, Unit](methodRaise[F]) + methodRaise[F].recover { case _: RuntimeException => () } // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + methodRaise[F].recover { case _: IOException => () } + Async[F].recover(methodRaise[F]) { case _: IOException => () } + methodRaise[F].recover { case _ => () } // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + } + + def unknown[F[_]: Async](handler: PartialFunction[Throwable, Unit])(using + Raise[F, String] + ): F[Unit] = + methodRaise[F].recover(handler) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineRaiseAliasTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineRaiseAliasTest.scala new file mode 100644 index 0000000..a83f7e3 --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineRaiseAliasTest.scala @@ -0,0 +1,42 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.effect.* +import cats.syntax.all.* + +// scalafmt: { maxColumn = 160 } +object RaiseAlias { + + type AliasedRaise[F[_], E] = Raise[F, E] + type ChainedRaiseAlias[F[_], E] = AliasedRaise[F, E] + type AliasedHandle[F[_], E] = Handle[F, E] + type ChainedHandleAlias[F[_], E] = AliasedHandle[F, E] + + def aliasedRaise[F[_]](using r: AliasedRaise[F, String]): F[Unit] = + r.raise("something went wrong") + + def chainedRaise[F[_]](using r: ChainedRaiseAlias[F, String]): F[Unit] = + r.raise("something went wrong") + + def aliasedHandle[F[_]](using h: AliasedHandle[F, String]): F[Unit] = + h.raise("something went wrong") + + def chainedHandle[F[_]](using h: ChainedHandleAlias[F, String]): F[Unit] = + h.raise("something went wrong") + + def aliasedRaiseHandling[F[_]: Async](using r: AliasedRaise[F, String]): F[Unit] = + aliasedRaise[F].attempt.void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + + def chainedRaiseHandling[F[_]: Async](using r: ChainedRaiseAlias[F, String]): F[Unit] = + Async[F].attempt(chainedRaise[F]).void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + + def aliasedHandleHandling[F[_]: Async](using h: AliasedHandle[F, String]): F[Unit] = + aliasedHandle[F].recover(_ => ()).void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + + def chainedHandleHandling[F[_]: Async](using h: ChainedHandleAlias[F, String]): F[Unit] = + Async[F].handleError(chainedHandle[F])(_ => ()).void // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineRaiseInstanceTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineRaiseInstanceTest.scala new file mode 100644 index 0000000..88c91ee --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineRaiseInstanceTest.scala @@ -0,0 +1,99 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.effect.* +import cats.syntax.all.* + +// scalafmt: { maxColumn = 160 } +object RaiseInstance { + + def applicativeErrorSyntax[F[_]: Async](using r: Raise[F, String]): F[Unit] = { + r.raise[String, Unit]("").handleError(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").handleErrorWith(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attemptNarrow[RuntimeException] // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attemptT.value // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").recover(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").recoverWith(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").redeem(_ => (), _ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").onError(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").orElse(Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").adaptErr(_ => new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").orRaise(new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").voidError // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + // ensure other methods aren't affected + r.raise[String, Unit]("").tupleRight("") + r.raise[String, Unit]("").void + } + + def monadErrorSyntax[F[_]: Async](using r: Raise[F, String]): F[Unit] = { + r.raise[String, Unit]("").ensure(new Exception(""))(_ => false) + r.raise[String, Unit]("").ensureOr(_ => new Exception(""))(_ => false) + r.raise[String, Unit]("").redeemWith(_ => Async[F].unit, _ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attemptTap(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").adaptError(_ => new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, String]("").reject(_ => new Exception("")) + // ensure other methods aren't affected + r.raise[String, Unit]("").tupleRight("") + r.raise[String, Unit]("").void + } + + def applicativeErrorDirect[F[_]: Async](using r: Raise[F, String]): F[Unit] = { + Async[F].handleError(r.raise[String, Unit](""))(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].handleErrorWith(r.raise[String, Unit](""))(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].attempt(r.raise[String, Unit]("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].attemptNarrow[RuntimeException, Unit](r.raise[String, Unit]("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].attemptT(r.raise[String, Unit]("")).value // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].recover(r.raise[String, Unit](""))(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].recoverWith(r.raise[String, Unit](""))(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].redeem(r.raise[String, Unit](""))(_ => (), _ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].onError(r.raise[String, Unit](""))(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].adaptError(r.raise[String, Unit](""))(_ => new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].voidError(r.raise[String, Unit]("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + // ensure other methods aren't affected + Async[F].tupleRight(r.raise[String, Unit](""), "") + Async[F].void(r.raise[String, Unit]("")) + } + + def monadErrorDirect[F[_]: Async](using r: Raise[F, String]): F[Unit] = { + Async[F].ensure(r.raise[String, Unit](""))(new Exception(""))(_ => false) + Async[F].ensureOr(r.raise[String, Unit](""))(_ => new Exception(""))(_ => false) + Async[F].redeemWith(r.raise[String, Unit](""))(_ => Async[F].unit, _ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].attemptTap(r.raise[String, Unit](""))(_ => Async[F].unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + // ensure other methods aren't affected + Async[F].tupleRight(r.raise[String, Unit](""), "") + Async[F].void(r.raise[String, Unit]("")) + } + + def io(using r: Raise[IO, String]): IO[Unit] = { + // applicative error + r.raise[String, Unit]("").handleError(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").handleErrorWith(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attemptNarrow[RuntimeException] // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attemptT.value // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").recover(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").recoverWith(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").redeem(_ => (), _ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").onError(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").orElse(IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").adaptErr(_ => new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").orRaise(new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").voidError // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + // monad error + r.raise[String, Unit]("").ensure(new Exception(""))(_ => false) + r.raise[String, Unit]("").ensureOr(_ => new Exception(""))(_ => false) + r.raise[String, Unit]("").redeemWith(_ => IO.unit, _ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").attemptTap(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, Unit]("").adaptError(_ => new Exception("")) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + r.raise[String, String]("").reject(_ => new Exception("")) + // ensure other methods aren't affected + r.raise[String, Unit]("").void + r.raise[String, Unit]("").product(r.raise[String, Unit]("")) + r.raise[String, Unit]("").debug() + } + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineRaiseObjectTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineRaiseObjectTest.scala new file mode 100644 index 0000000..694ea7e --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineRaiseObjectTest.scala @@ -0,0 +1,25 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.effect.* +import cats.syntax.all.* + +// scalafmt: { maxColumn = 160 } +object RaiseObject { + + def generic[F[_]: Async](using Raise[F, String]): F[Unit] = { + Raise.raise[F, String, Unit]("").attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].recover(Raise.raise[F, String, Unit]("")) { case _ => () } // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Raise.raise[F, String, Unit]("").void + } + + def io(using Raise[IO, String]): IO[Unit] = { + Raise.raise[IO, String, Unit]("").onError(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Raise.raise[IO, String, String]("").reject(_ => new Exception("")) + Raise.raise[IO, String, Unit]("").debug() + } + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineRaiseSyntaxTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineRaiseSyntaxTest.scala new file mode 100644 index 0000000..e29e1ce --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineRaiseSyntaxTest.scala @@ -0,0 +1,26 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.mtl.syntax.raise.* +import cats.effect.* +import cats.syntax.all.* + +// scalafmt: { maxColumn = 160 } +object RaiseSyntax { + + def generic[F[_]: Async](using Raise[F, String]): F[Unit] = { + "".raise[F, Unit].attempt // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + Async[F].handleError("".raise[F, Unit])(_ => ()) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + "".raise[F, Unit].void + } + + def io(using Raise[IO, String]): IO[Unit] = { + "".raise[IO, Unit].onError(_ => IO.unit) // assert: TypelevelMTLSubmarine.mtlSubmarineErrorHandling + "".raise[IO, String].reject(_ => new Exception("")) + "".raise[IO, Unit].debug() + } + +} diff --git a/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineWrappedRaiseTest.scala b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineWrappedRaiseTest.scala new file mode 100644 index 0000000..f2971be --- /dev/null +++ b/modules/mtl/input/src/main/scala-3/fix/MTLSubmarineWrappedRaiseTest.scala @@ -0,0 +1,21 @@ +/* +rule = TypelevelMTLSubmarine + */ +package fix + +import cats.mtl.* +import cats.effect.* +import cats.syntax.all.* + +// scalafmt: { maxColumn = 160 } +object WrappedRaise { + + trait Wrapper[A] + + def normal[F[_]: Async](using wrapper: Wrapper[Raise[F, String]]): F[Unit] = + Async[F].unit + + def handlingNormal[F[_]: Async](using wrapper: Wrapper[Raise[F, String]]): F[Unit] = + normal[F].attempt.void + +} diff --git a/modules/mtl/rules/src/main/resources/META-INF/services/scalafix.v1.Rule b/modules/mtl/rules/src/main/resources/META-INF/services/scalafix.v1.Rule new file mode 100644 index 0000000..51bd4be --- /dev/null +++ b/modules/mtl/rules/src/main/resources/META-INF/services/scalafix.v1.Rule @@ -0,0 +1 @@ +org.typelevel.fix.MTLSubmarine diff --git a/modules/mtl/rules/src/main/scala/org/typelevel/fix/MTLSubmarine.scala b/modules/mtl/rules/src/main/scala/org/typelevel/fix/MTLSubmarine.scala new file mode 100644 index 0000000..d4f73a7 --- /dev/null +++ b/modules/mtl/rules/src/main/scala/org/typelevel/fix/MTLSubmarine.scala @@ -0,0 +1,480 @@ +/* + * Copyright 2022 Typelevel + * + * 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. + */ + +package org.typelevel.fix + +import scalafix.v1._ + +import scala.meta._ + +class MTLSubmarine extends SemanticRule("TypelevelMTLSubmarine") { + + private final class LocalDefinitions(val bySymbol: Map[Symbol, Term]) + + private final class Call( + val receiver: Option[Term], + val method: Term.Name, + val argumentLists: List[List[Term]], + val typeArguments: List[Type], + val callee: Term + ) { + def name: String = method.value + def symbol(implicit doc: SemanticDocument): Symbol = method.symbol + } + + private final class HandlerCall( + val call: Call, + val protectedEffect: Term, + val handler: Option[Term] + ) + + private val Syntax_M = + SymbolMatcher.exact("cats/syntax/ApplicativeErrorOps#") + + SymbolMatcher.exact("cats/syntax/ApplicativeErrorFUnitOps#") + + SymbolMatcher.exact("cats/syntax/MonadErrorOps#") + + private val Direct_M = + SymbolMatcher.exact("cats/ApplicativeError#") + + SymbolMatcher.exact("cats/MonadError#") + + private val PropagationDirect_M = + SymbolMatcher.exact("cats/Functor#") + + SymbolMatcher.exact("cats/FlatMap#") + + SymbolMatcher.exact("cats/Apply#") + + SymbolMatcher.exact("cats/Applicative#") + + SymbolMatcher.exact("cats/Monad#") + + SymbolMatcher.exact("cats/Semigroupal#") + + private val UnaryPropagationMethods = + Set("map", "void", "as", "fproduct", "fproductLeft", "tupleLeft", "tupleRight") + private val BinaryPropagationMethods = + Set( + "product", + "productL", + "productR", + "map2", + "<&>", + ">>", + "*>", + "<*" + ) + private val FlatMapPropagationMethods = Set("flatMap", ">>=", "flatTap", "mproduct") + + private val ErrorHandlingMethods = Set( + "adaptErr", + "adaptError", + "attempt", + "attemptNarrow", + "attemptT", + "attemptTap", + "handleError", + "handleErrorWith", + "onError", + "orElse", + "orRaise", + "recover", + "recoverWith", + "redeem", + "redeemWith", + "voidError" + ) + + private val ParameterlessErrorHandlingMethods = + Set("attempt", "attemptNarrow", "attemptT", "voidError") + + private val PartialErrorHandlingMethods = + Set("adaptErr", "adaptError", "onError", "recover", "recoverWith") + + private val SubmarineClassSupertypes_M = + SymbolMatcher.exact("java/lang/RuntimeException#") + + SymbolMatcher.exact("java/lang/Exception#") + + SymbolMatcher.exact("java/lang/Throwable#") + + SymbolMatcher.exact("java/lang/Object#") + + SymbolMatcher.exact("scala/Any#") + + SymbolMatcher.exact("scala/AnyRef#") + + private val Raise_M = + SymbolMatcher.exact("cats/mtl/Raise#") + + private val Handle_M = + SymbolMatcher.exact("cats/mtl/Handle#") + + private val RaiseAll_M = + SymbolMatcher.exact("cats/mtl/Raise#") + + SymbolMatcher.exact("cats/mtl/Raise.") + + SymbolMatcher.exact("cats/mtl/syntax/RaiseOps#") + + private val ContextFunction_M = + (1 to 22).map(i => SymbolMatcher.exact(s"scala/ContextFunction$i#")).reduce(_ + _) + + private val IO_M = + SymbolMatcher.normalized("cats/effect/IO#handleError().") + + SymbolMatcher.normalized("cats/effect/IO#handleErrorWith().") + + SymbolMatcher.normalized("cats/effect/IO#recover().") + + SymbolMatcher.normalized("cats/effect/IO#recoverWith().") + + SymbolMatcher.normalized("cats/effect/IO#attempt().") + + SymbolMatcher.normalized("cats/effect/IO#redeem().") + + SymbolMatcher.normalized("cats/effect/IO#redeemWith().") + + SymbolMatcher.normalized("cats/effect/IO#adaptError().") + + SymbolMatcher.normalized("cats/effect/IO#onError().") + + SymbolMatcher.normalized("cats/effect/IO#orElse().") + + SymbolMatcher.normalized("cats/effect/IO#voidError().") + + SymbolMatcher.normalized("cats/effect/IO#attemptTap().") + + override def fix(implicit doc: SemanticDocument): Patch = { + implicit val localDefinitions: LocalDefinitions = new LocalDefinitions( + doc.tree + .collect { + case d: Defn.Def if d.parent.exists(_.isInstanceOf[Term.Block]) => + List(d.symbol -> d.body) + + case d: Defn.Val if d.parent.exists(_.isInstanceOf[Term.Block]) => + d.pats match { + case List(p: Pat.Var) => List(p.symbol -> d.rhs) + case _ => Nil + } + } + .flatten + .toMap + ) + + def diagnostic(term: Term): Option[Patch] = + handlerCall(term) + .filter(handlerMayObserveSubmarine) + .filter(call => producedByRaise(call.protectedEffect)) + .map { handlerCall => + val call = handlerCall.call + Patch.lint( + new MTLSubmarine.SubmarineErrorHandlingDiagnostic(call.method, Some(call.name)) + ) + } + + doc.tree + .collect { + case t: Term.Apply => diagnostic(t) + case t: Term.ApplyType + if normalizedCall(t).exists(call => ParameterlessErrorHandlingMethods(call.name)) => + diagnostic(t) + case t: Term.Select + if !t.parent.exists(_.isInstanceOf[Term.ApplyType]) && + normalizedCall(t).exists(call => ParameterlessErrorHandlingMethods(call.name)) => + diagnostic(t) + } + .flatten + .asPatch + } + + private def handlerMayObserveSubmarine( + handlerCall: HandlerCall + )(implicit doc: SemanticDocument): Boolean = + if (handlerCall.call.name == "attemptNarrow") + narrowErrorType(handlerCall.call).forall(typePatternMayMatchSubmarine) + else if (PartialErrorHandlingMethods(handlerCall.call.name)) + handlerCall.handler.exists(partialFunctionMayMatchSubmarine) + else true + + private def narrowErrorType(call: Call): Option[Type] = + call.typeArguments.headOption + + private def partialFunctionMayMatchSubmarine( + term: Term + )(implicit doc: SemanticDocument): Boolean = + term match { + case Term.PartialFunction(cases) => cases.exists(c => patternMayMatchSubmarine(c.pat)) + case _ => true + } + + private def patternMayMatchSubmarine(pat: Pat)(implicit doc: SemanticDocument): Boolean = + pat match { + case Pat.Wildcard() => true + case Pat.Typed(_, tpe) => typePatternMayMatchSubmarine(tpe) + case Pat.Alternative(a, b) => patternMayMatchSubmarine(a) || patternMayMatchSubmarine(b) + case Pat.Bind(_, nested) => patternMayMatchSubmarine(nested) + case _: Lit => false + case _ => true + } + + private def typePatternMayMatchSubmarine( + tpe: Type + )(implicit doc: SemanticDocument): Boolean = { + val symbol = tpe.symbol + + SubmarineClassSupertypes_M.matches(symbol) || + doc.info(symbol).forall(info => !info.isClass) + } + + private def producedByRaise( + term: Term, + visitedLocalDefinitions: Set[Symbol] = Set.empty + )(implicit doc: SemanticDocument, localDefinitions: LocalDefinitions): Boolean = + term match { + case Term.Ascribe(expr, _) => producedByRaise(expr, visitedLocalDefinitions) + case Term.Block(stats) => + stats.lastOption + .collect { case result: Term => producedByRaise(result, visitedLocalDefinitions) } + .getOrElse(false) + case Term.If.After_4_4_0(_, thenBranch, elseBranch, _) => + producedByRaise(thenBranch, visitedLocalDefinitions) || + producedByRaise(elseBranch, visitedLocalDefinitions) + case Term.Match.After_4_9_9(_, cases, _) => + cases.exists(c => producedByRaise(c.body, visitedLocalDefinitions)) + case _ => + val call = normalizedCall(term) + methodRequiresImplicitRaise(call) || + originatesFromRaiseOperation(call) || + originatesFromLocalDefinition(call, visitedLocalDefinitions) || + originatesFromPropagation(call, visitedLocalDefinitions) || + originatesFromForComprehension(term, visitedLocalDefinitions) + } + + private def originatesFromLocalDefinition( + call: Option[Call], + visitedLocalDefinitions: Set[Symbol] + )(implicit doc: SemanticDocument, localDefinitions: LocalDefinitions): Boolean = + call.exists { call => + val symbol = call.symbol + !visitedLocalDefinitions(symbol) && + localDefinitions.bySymbol + .get(symbol) + .exists(producedByRaise(_, visitedLocalDefinitions + symbol)) + } + + private def originatesFromRaiseOperation(call: Option[Call])(implicit doc: SemanticDocument) = + call.exists(call => isOwner(call.symbol, RaiseAll_M)) + + private def originatesFromPropagation( + call: Option[Call], + visitedLocalDefinitions: Set[Symbol] + )(implicit doc: SemanticDocument, localDefinitions: LocalDefinitions): Boolean = + call.exists { call => + call.receiver.exists { receiver => + val isCatsOperation = call.symbol.value.startsWith("cats/") + val isDirect = isOwner(call.symbol, PropagationDirect_M) + val firstArguments = call.argumentLists.headOption.getOrElse(Nil) + + if (!isCatsOperation) false + else if (UnaryPropagationMethods(call.name)) { + val source = if (isDirect) firstArguments.headOption else Some(receiver) + source.exists(producedByRaise(_, visitedLocalDefinitions)) + } else if (BinaryPropagationMethods(call.name)) { + val sources = if (isDirect) firstArguments.take(2) else receiver :: firstArguments.take(1) + sources.exists(producedByRaise(_, visitedLocalDefinitions)) + } else if (FlatMapPropagationMethods(call.name)) { + val source = if (isDirect) firstArguments.headOption else Some(receiver) + val callback = + if (isDirect) call.argumentLists.drop(1).headOption.flatMap(_.headOption) + else firstArguments.headOption + + source.exists(producedByRaise(_, visitedLocalDefinitions)) || + callback.exists(callbackProducesRaise(_, visitedLocalDefinitions)) + } else false + } + } + + private def callbackProducesRaise( + term: Term, + visitedLocalDefinitions: Set[Symbol] + )(implicit doc: SemanticDocument, localDefinitions: LocalDefinitions): Boolean = + term match { + case Term.Function.After_4_6_0(_, body: Term) => + producedByRaise(body, visitedLocalDefinitions) + case other => producedByRaise(other, visitedLocalDefinitions) + } + + private def originatesFromForComprehension( + term: Term, + visitedLocalDefinitions: Set[Symbol] + )(implicit doc: SemanticDocument, localDefinitions: LocalDefinitions): Boolean = { + def generatorsProduceRaise(enumerators: List[Enumerator]): Boolean = + enumerators.exists { + case Enumerator.Generator(_, rhs) => producedByRaise(rhs, visitedLocalDefinitions) + case _ => false + } + + term match { + case Term.For.After_4_9_9(enumerators, _) => generatorsProduceRaise(enumerators) + case Term.ForYield.After_4_9_9(enumerators, _) => generatorsProduceRaise(enumerators) + case _ => false + } + } + + private def handlerCall(term: Term)(implicit doc: SemanticDocument): Option[HandlerCall] = + normalizedCall(term) + .filter(call => ErrorHandlingMethods(call.name)) + .flatMap { call => + val isDirect = isOwner(call.symbol, Direct_M) + val isSyntax = Syntax_M.matches(call.symbol.owner) + val isIO = IO_M.matches(call.callee) + + if (isDirect) + call.argumentLists.headOption.flatMap(_.headOption).map { protectedEffect => + val handler = call.argumentLists.drop(1).headOption.flatMap(_.headOption) + new HandlerCall(call, protectedEffect, handler) + } + else if (isSyntax || isIO) + call.receiver.map { protectedEffect => + val handler = call.argumentLists.headOption.flatMap(_.headOption) + new HandlerCall(call, protectedEffect, handler) + } + else None + } + + private def normalizedCall(term: Term): Option[Call] = { + @annotation.tailrec + def loop( + t: Term, + argumentLists: List[List[Term]], + typeArguments: List[Type] + ): Option[Call] = + t match { + case Term.Apply.After_4_6_0(fun, args) => + loop(fun, args :: argumentLists, typeArguments) + + case Term.ApplyType.After_4_6_0(fun, tpes) => + loop(fun, argumentLists, tpes.values.toList ::: typeArguments) + + case Term.ApplyInfix.Initial(receiver, method: Term.Name, _, args) => + Some(new Call(Some(receiver), method, List(args), typeArguments, method)) + + case select @ Term.Select(receiver, method: Term.Name) => + Some(new Call(Some(receiver), method, argumentLists, typeArguments, select)) + + case method: Term.Name => + Some(new Call(None, method, argumentLists, typeArguments, method)) + + case _ => + None + } + + loop(term, Nil, Nil) + } + + private def isOwner(sym: Symbol, matcher: SymbolMatcher): Boolean = { + @annotation.tailrec + def loop(s: Symbol): Boolean = + if (s == Symbol.None) false + else { + val o = s.owner + if (matcher.matches(o)) true + else loop(o) + } + loop(sym) + } + + private def methodRequiresImplicitRaise( + call: Option[Call] + )(implicit doc: SemanticDocument): Boolean = + call.flatMap(call => doc.info(call.symbol)).exists { info => + info.signature match { + case m: MethodSignature => + requiresRaiseViaParams(m) || requiresRaiseViaContextFunction(m) + + case ValueSignature(tpe) => + requiresRaiseViaContextFunction(tpe) + + case _ => + false + } + } + + // any implicit parameter whose type constructor is Raise + private def requiresRaiseViaParams(m: MethodSignature)(implicit doc: SemanticDocument): Boolean = + m.parameterLists.exists(_.exists { p => + doc.info(p.symbol).exists { pi => + val isUsingOrImplicit = pi.isImplicit + isUsingOrImplicit && paramIsRaise(pi.signature) + } + }) + + // Scala 3: handle def f: Raise[F, E] ?=> R + private def requiresRaiseViaContextFunction( + m: MethodSignature + )(implicit doc: SemanticDocument): Boolean = + requiresRaiseViaContextFunction(m.returnType) + + private def requiresRaiseViaContextFunction( + tpe: SemanticType + )(implicit doc: SemanticDocument): Boolean = + tpe match { + case TypeRef(_, sym, args) if ContextFunction_M.matches(sym) => + args.exists(typeIsRaise) + + case _ => + false + } + + private def paramIsRaise(sig: Signature)(implicit doc: SemanticDocument): Boolean = + sig match { + case ValueSignature(tpe) => typeIsRaise(tpe) + case _ => false + } + + private def typeIsRaise(tpe: SemanticType)(implicit doc: SemanticDocument): Boolean = + typeIsRaise(tpe, Set.empty) + + private def typeIsRaise( + tpe: SemanticType, + visited: Set[Symbol] + )(implicit doc: SemanticDocument): Boolean = + tpe match { + // matches Raise[F, E] or Handle[F, E] for any F and E + case TypeRef(_, sym, _) if Raise_M.matches(sym) || Handle_M.matches(sym) => + true + + case TypeRef(_, sym, _) => + !visited(sym) && doc.info(sym).exists { + _.signature match { + case t: TypeSignature => + typeIsRaise(t.upperBound, visited + sym) + + case _ => + false + } + } + + case AnnotatedType(_, underlying) => + typeIsRaise(underlying, visited) + + case _ => + false + } + +} + +object MTLSubmarine { + + final class SubmarineErrorHandlingDiagnostic( + tree: Tree, + method: Option[String] + ) extends Diagnostic { + + override def message: String = { + val operation = method.fold("error-handling methods")(m => s"the error-handling method `$m`") + s"Avoid calling $operation " + + s"on expressions that require `cats.mtl.Raise[F, *]`. " + + s"Errors raised through `Raise` may be represented by the traceless " + + s"`cats.mtl.Handle.Submarine` exception. Handling that exception with " + + s"`ApplicativeError`, `MonadError`, or `IO` can bypass marker-aware typed error handling. " + + s"Use `cats.mtl.Handle[F, *].handle` or `cats.mtl.Handle[F, *].handleWith` " + + s"instead." + } + + def position: Position = tree.pos + + override def categoryID: String = "mtlSubmarineErrorHandling" + } + +} diff --git a/modules/mtl/tests/src/test/scala/org/typelevel/fix/RuleSuite.scala b/modules/mtl/tests/src/test/scala/org/typelevel/fix/RuleSuite.scala new file mode 100644 index 0000000..5231846 --- /dev/null +++ b/modules/mtl/tests/src/test/scala/org/typelevel/fix/RuleSuite.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Typelevel + * + * 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. + */ + +package org.typelevel.fix + +import scalafix.testkit._ +import org.scalatest.funsuite.AnyFunSuiteLike + +class RuleSuite extends AbstractSemanticRuleSuite with AnyFunSuiteLike { + runAllTests() +} diff --git a/project/plugins.sbt b/project/plugins.sbt index 81edfb8..d45806b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,2 +1,2 @@ -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.0") -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.8.3") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.8") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.8.7")