From d8f04ff7a1fef7648beacf19d081bc331f802ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Wed, 12 Jun 2024 15:08:59 +0200 Subject: [PATCH 01/10] add ThrownExceptionNotInMonixScope rule to the Analyzer Plugin --- .../commons/analyzer/AnalyzerPlugin.scala | 1 + .../ThrownExceptionNotInMonixScope.scala | 59 ++++++++++++++++++ .../ThrownExceptionNotInMonixScopeTest.scala | 61 +++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScope.scala create mode 100644 analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScopeTest.scala diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala index e91ef8d0c..5d0bd0b22 100644 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala @@ -56,6 +56,7 @@ final class AnalyzerPlugin(val global: Global) extends Plugin { plugin => new Any2StringAdd(global), new ThrowableObjects(global), new DiscardedMonixTask(global), + new ThrownExceptionNotInMonixScope(global), new BadSingletonComponent(global), new ConstantDeclarations(global), new BasePackage(global), diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScope.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScope.scala new file mode 100644 index 000000000..762d7e594 --- /dev/null +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScope.scala @@ -0,0 +1,59 @@ +package com.avsystem.commons +package analyzer + +import scala.tools.nsc.Global + +final class ThrownExceptionNotInMonixScope(g: Global) extends AnalyzerRule(g, "thrownExceptionNotInMonixScope") { + + import global.* + + lazy val monixTaskTpe: Type = classType("monix.eval.Task") match { + case NoType => NoType + case tpe => TypeRef(NoPrefix, tpe.typeSymbol, List(definitions.AnyTpe)) + } + private def checkDiscardedNothing(tree: Tree, discarded: Boolean): Unit = tree match { + case tree if !discarded && tree.tpe != null && tree.tpe =:= definitions.NothingTpe => + checkDiscardedNothing(tree, discarded = true) + + case Block(stats: List[Tree], expr: Tree) => + stats.foreach(checkDiscardedNothing(_, discarded = true)) + checkDiscardedNothing(expr, discarded) + + case Template(parents: List[Tree], self: ValDef, body: List[Tree]) => + parents.foreach(checkDiscardedNothing(_, discarded = false)) + checkDiscardedNothing(self, discarded = false) + body.foreach(checkDiscardedNothing(_, discarded = true)) + + case If(_: Tree, thenp: Tree, elsep: Tree) => + checkDiscardedNothing(thenp, discarded) + checkDiscardedNothing(elsep, discarded) + + case LabelDef(_: TermName, _: List[Ident], rhs: Tree) => + checkDiscardedNothing(rhs, discarded = true) + + case Try(block: Tree, catches: List[CaseDef], finalizer: Tree) => + checkDiscardedNothing(block, discarded) + catches.foreach(checkDiscardedNothing(_, discarded)) + checkDiscardedNothing(finalizer, discarded = true) + + case CaseDef(_: Tree, _: Tree, body: Tree) => + checkDiscardedNothing(body, discarded) + + case Match(_: Tree, cases: List[CaseDef]) => + cases.foreach(checkDiscardedNothing(_, discarded)) + + case Annotated(_: Tree, arg: Tree) => + checkDiscardedNothing(arg, discarded) + + case Typed(expr: Tree, _: Tree) => + checkDiscardedNothing(expr, discarded) + + case Apply(TypeApply(Select(prefix: Tree, TermName("map")), List(_)), List(Throw(_))) if prefix.tpe <:< monixTaskTpe => + report(tree.pos, "exception thrown not in Monix Task scope ") + + case tree => + tree.children.foreach(checkDiscardedNothing(_, discarded = false)) + } + + def analyze(unit: CompilationUnit): Unit = checkDiscardedNothing(unit.body, discarded = false) +} diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScopeTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScopeTest.scala new file mode 100644 index 000000000..09bb31bc4 --- /dev/null +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScopeTest.scala @@ -0,0 +1,61 @@ +package com.avsystem.commons +package analyzer + +import org.scalatest.funsuite.AnyFunSuite + +final class ThrownExceptionNotInMonixScopeTest extends AnyFunSuite with AnalyzerTest { + settings.pluginOptions.value ++= List("AVSystemAnalyzer:-discardedMonixTask") + + test("simple") { + assertErrors(10, + //language=Scala + """ + |import monix.eval.Task + | + |object whatever { + | def task: Task[String] = ??? + | def ex: Exception = ??? + | + | // errors from these + | task.map(throw ex) + | + | { + | println(""); task.map(throw ex) + | } + | + | if (true) task.map(throw ex) else task.map(throw ex) + | + | try task.map(throw ex) catch { + | case _: Exception => task.map(throw ex) + | } finally task.map(throw ex) + | + | Seq(1, 2, 3).foreach(_ => task.map(throw ex)) + | + | while (true) task.map(throw ex) + | + | do task.map(throw ex) while (true) + | + | // no errors from these + | task.map(_ => throw ex) + | + | { + | println(""); task.map(_ => throw ex) + | } + | + | if (true) task.map(_ => throw ex) else task.map(_ => throw ex) + | + | try task.map(_ => throw ex) catch { + | case _: Exception => task.map(_ => throw ex) + | } finally task.map(_ => throw ex) + | + | Seq(1, 2, 3).foreach(_ => task.map(_ => throw ex)) + | + | while (true) task.map(_ => throw ex) + | + | do task.map(_ => throw ex) while (true) + |} + | + |""".stripMargin + ) + } +} From 8e4fa2e6bca5f2c276fcab14d561c7ccdc181503 Mon Sep 17 00:00:00 2001 From: halotukozak Date: Fri, 14 Jun 2024 16:25:48 +0200 Subject: [PATCH 02/10] change to all function parameters --- .../commons/analyzer/AnalyzerPlugin.scala | 2 +- .../ThrownExceptionNotInFunction.scala | 14 ++++ .../ThrownExceptionNotInMonixScope.scala | 59 -------------- .../ThrownExceptionNotInFunctionTest.scala | 80 +++++++++++++++++++ .../ThrownExceptionNotInMonixScopeTest.scala | 61 -------------- 5 files changed, 95 insertions(+), 121 deletions(-) create mode 100644 analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala delete mode 100644 analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScope.scala create mode 100644 analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala delete mode 100644 analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScopeTest.scala diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala index 5d0bd0b22..88ad1c13b 100644 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala @@ -56,7 +56,7 @@ final class AnalyzerPlugin(val global: Global) extends Plugin { plugin => new Any2StringAdd(global), new ThrowableObjects(global), new DiscardedMonixTask(global), - new ThrownExceptionNotInMonixScope(global), + new ThrownExceptionNotInFunction(global), new BadSingletonComponent(global), new ConstantDeclarations(global), new BasePackage(global), diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala new file mode 100644 index 000000000..3d6510b7a --- /dev/null +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala @@ -0,0 +1,14 @@ +package com.avsystem.commons +package analyzer + +import scala.tools.nsc.Global + +final class ThrownExceptionNotInFunction(g: Global) extends AnalyzerRule(g, "thrownExceptionNotInFunction") { + + import global.* + + def analyze(unit: CompilationUnit): Unit = unit.body.foreach(analyzeTree { + case t@Apply(f: TypeApply, List(Throw(_))) if definitions.isFunctionType(f.tpe.params.head.tpe) => + report(t.pos, "exception thrown in place of function definition") + }) +} diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScope.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScope.scala deleted file mode 100644 index 762d7e594..000000000 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScope.scala +++ /dev/null @@ -1,59 +0,0 @@ -package com.avsystem.commons -package analyzer - -import scala.tools.nsc.Global - -final class ThrownExceptionNotInMonixScope(g: Global) extends AnalyzerRule(g, "thrownExceptionNotInMonixScope") { - - import global.* - - lazy val monixTaskTpe: Type = classType("monix.eval.Task") match { - case NoType => NoType - case tpe => TypeRef(NoPrefix, tpe.typeSymbol, List(definitions.AnyTpe)) - } - private def checkDiscardedNothing(tree: Tree, discarded: Boolean): Unit = tree match { - case tree if !discarded && tree.tpe != null && tree.tpe =:= definitions.NothingTpe => - checkDiscardedNothing(tree, discarded = true) - - case Block(stats: List[Tree], expr: Tree) => - stats.foreach(checkDiscardedNothing(_, discarded = true)) - checkDiscardedNothing(expr, discarded) - - case Template(parents: List[Tree], self: ValDef, body: List[Tree]) => - parents.foreach(checkDiscardedNothing(_, discarded = false)) - checkDiscardedNothing(self, discarded = false) - body.foreach(checkDiscardedNothing(_, discarded = true)) - - case If(_: Tree, thenp: Tree, elsep: Tree) => - checkDiscardedNothing(thenp, discarded) - checkDiscardedNothing(elsep, discarded) - - case LabelDef(_: TermName, _: List[Ident], rhs: Tree) => - checkDiscardedNothing(rhs, discarded = true) - - case Try(block: Tree, catches: List[CaseDef], finalizer: Tree) => - checkDiscardedNothing(block, discarded) - catches.foreach(checkDiscardedNothing(_, discarded)) - checkDiscardedNothing(finalizer, discarded = true) - - case CaseDef(_: Tree, _: Tree, body: Tree) => - checkDiscardedNothing(body, discarded) - - case Match(_: Tree, cases: List[CaseDef]) => - cases.foreach(checkDiscardedNothing(_, discarded)) - - case Annotated(_: Tree, arg: Tree) => - checkDiscardedNothing(arg, discarded) - - case Typed(expr: Tree, _: Tree) => - checkDiscardedNothing(expr, discarded) - - case Apply(TypeApply(Select(prefix: Tree, TermName("map")), List(_)), List(Throw(_))) if prefix.tpe <:< monixTaskTpe => - report(tree.pos, "exception thrown not in Monix Task scope ") - - case tree => - tree.children.foreach(checkDiscardedNothing(_, discarded = false)) - } - - def analyze(unit: CompilationUnit): Unit = checkDiscardedNothing(unit.body, discarded = false) -} diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala new file mode 100644 index 000000000..0bed6ec83 --- /dev/null +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala @@ -0,0 +1,80 @@ +package com.avsystem.commons +package analyzer + +import org.scalatest.funsuite.AnyFunSuite + +final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTest { + settings.pluginOptions.value ++= List("AVSystemAnalyzer:-discardedMonixTask") + + Seq( + ("Option[_]", "map"), + ("List[_]", "map"), + ("Seq[_]", "map"), + ("Set[_]", "map"), + ("Map[_, _]", "map"), + ("scala.concurrent.Future[_]", "map"), + ("scala.util.Try[_]", "map"), + ("Either[_, _]", "map"), + ("monix.eval.Task[_]", "map"), + ("com.avsystem.commons.misc.Opt[_]", "map"), + ("String => Int", "andThen"), + ("String => Nothing", "andThen"), + ("Nothing => Nothing", "andThen"), + ("String => Int", "compose"), + ("Seq[_]", "foreach"), + ).foreach { case (tpe, function) => + test(s"Testing $function of $tpe") { + assertErrors(10, + //language=Scala + s""" + |object whatever { + | implicit val ec: scala.concurrent.ExecutionContext = ??? // for Future + | + | def sth: $tpe = ??? + | def ex: Exception = ??? + | + | // errors from these + | sth.$function(throw ex) + | + | { + | println(""); sth.$function(throw ex) + | } + | + | if (true) sth.$function(throw ex) else sth.$function(throw ex) + | + | try sth.$function(throw ex) catch { + | case _: Exception => sth.$function(throw ex) + | } finally sth.$function(throw ex) + | + | Seq(1, 2, 3).foreach(_ => sth.$function(throw ex)) + | + | while (true) sth.$function(throw ex) + | + | do sth.$function(throw ex) while (true) + | + | // no errors from these + | sth.$function(_ => throw ex) + | + | { + | println(""); sth.$function(_ => throw ex) + | } + | + | if (true) sth.$function(_ => throw ex) else sth.$function(_ => throw ex) + | + | try sth.$function(_ => throw ex) catch { + | case _: Exception => sth.$function(_ => throw ex) + | } finally sth.$function(_ => throw ex) + | + | Seq(1, 2, 3).foreach(_ => sth.$function(_ => throw ex)) + | + | while (true) sth.$function(_ => throw ex) + | + | do sth.$function(_ => throw ex) while (true) + |} + | + |""".stripMargin + ) + } + } +} + diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScopeTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScopeTest.scala deleted file mode 100644 index 09bb31bc4..000000000 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInMonixScopeTest.scala +++ /dev/null @@ -1,61 +0,0 @@ -package com.avsystem.commons -package analyzer - -import org.scalatest.funsuite.AnyFunSuite - -final class ThrownExceptionNotInMonixScopeTest extends AnyFunSuite with AnalyzerTest { - settings.pluginOptions.value ++= List("AVSystemAnalyzer:-discardedMonixTask") - - test("simple") { - assertErrors(10, - //language=Scala - """ - |import monix.eval.Task - | - |object whatever { - | def task: Task[String] = ??? - | def ex: Exception = ??? - | - | // errors from these - | task.map(throw ex) - | - | { - | println(""); task.map(throw ex) - | } - | - | if (true) task.map(throw ex) else task.map(throw ex) - | - | try task.map(throw ex) catch { - | case _: Exception => task.map(throw ex) - | } finally task.map(throw ex) - | - | Seq(1, 2, 3).foreach(_ => task.map(throw ex)) - | - | while (true) task.map(throw ex) - | - | do task.map(throw ex) while (true) - | - | // no errors from these - | task.map(_ => throw ex) - | - | { - | println(""); task.map(_ => throw ex) - | } - | - | if (true) task.map(_ => throw ex) else task.map(_ => throw ex) - | - | try task.map(_ => throw ex) catch { - | case _: Exception => task.map(_ => throw ex) - | } finally task.map(_ => throw ex) - | - | Seq(1, 2, 3).foreach(_ => task.map(_ => throw ex)) - | - | while (true) task.map(_ => throw ex) - | - | do task.map(_ => throw ex) while (true) - |} - | - |""".stripMargin - ) - } -} From 8aeed886af28c840f8b46ac9ac4ad30f7406df64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Tue, 18 Jun 2024 09:37:55 +0200 Subject: [PATCH 03/10] add some `flatMap` cases --- .../commons/analyzer/ThrownExceptionNotInFunctionTest.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala index 0bed6ec83..dd84d0c98 100644 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala @@ -8,14 +8,17 @@ final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTe Seq( ("Option[_]", "map"), + ("Option[_]", "flatMap"), ("List[_]", "map"), ("Seq[_]", "map"), ("Set[_]", "map"), ("Map[_, _]", "map"), ("scala.concurrent.Future[_]", "map"), + ("scala.concurrent.Future[_]", "flatMap"), ("scala.util.Try[_]", "map"), ("Either[_, _]", "map"), ("monix.eval.Task[_]", "map"), + ("monix.eval.Task[_]", "flatMap"), ("com.avsystem.commons.misc.Opt[_]", "map"), ("String => Int", "andThen"), ("String => Nothing", "andThen"), From 9622a3f735902170f64530e0ed9cba5f8399cb99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Wed, 19 Jun 2024 09:53:53 +0200 Subject: [PATCH 04/10] Nothing is a special bottom type; it is a subtype of every other type. The Scala compiler loves to infer Nothing as a generic type but that is almost always incorrect. Explicit type arguments should be used instead. --- .../commons/analyzer/AnalyzerPlugin.scala | 2 +- .../analyzer/RequireExplicitNothing.scala | 21 +++++ .../ThrownExceptionNotInFunction.scala | 14 ---- .../analyzer/RequireExplicitNothingTest.scala | 32 +++++++ .../ThrownExceptionNotInFunctionTest.scala | 83 ------------------- 5 files changed, 54 insertions(+), 98 deletions(-) create mode 100644 analyzer/src/main/scala/com/avsystem/commons/analyzer/RequireExplicitNothing.scala delete mode 100644 analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala create mode 100644 analyzer/src/test/scala/com/avsystem/commons/analyzer/RequireExplicitNothingTest.scala delete mode 100644 analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala index 88ad1c13b..48e1ba787 100644 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala @@ -56,7 +56,7 @@ final class AnalyzerPlugin(val global: Global) extends Plugin { plugin => new Any2StringAdd(global), new ThrowableObjects(global), new DiscardedMonixTask(global), - new ThrownExceptionNotInFunction(global), + new RequireExplicitNothing(global), new BadSingletonComponent(global), new ConstantDeclarations(global), new BasePackage(global), diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/RequireExplicitNothing.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/RequireExplicitNothing.scala new file mode 100644 index 000000000..ef704f130 --- /dev/null +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/RequireExplicitNothing.scala @@ -0,0 +1,21 @@ +package com.avsystem.commons +package analyzer + +import scala.tools.nsc.Global + +final class RequireExplicitNothing(g: Global) extends AnalyzerRule(g, "requireExplicitNothing") { + + import global.* + + private def wasInferred(t: TypeTree) = t.original == null + + def analyze(unit: CompilationUnit): Unit = unit.body.foreach(analyzeTree { + case tree@TypeTree() if tree.tpe <:< definitions.NothingTpe && wasInferred(tree) => + tree.tpe match { + // Ignore existential types, they supposedly contain "any" + case ExistentialType(_, _) => + + case _ => report(tree.pos, "Inferred type: Nothing") + } + }) +} diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala deleted file mode 100644 index 3d6510b7a..000000000 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.avsystem.commons -package analyzer - -import scala.tools.nsc.Global - -final class ThrownExceptionNotInFunction(g: Global) extends AnalyzerRule(g, "thrownExceptionNotInFunction") { - - import global.* - - def analyze(unit: CompilationUnit): Unit = unit.body.foreach(analyzeTree { - case t@Apply(f: TypeApply, List(Throw(_))) if definitions.isFunctionType(f.tpe.params.head.tpe) => - report(t.pos, "exception thrown in place of function definition") - }) -} diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/RequireExplicitNothingTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/RequireExplicitNothingTest.scala new file mode 100644 index 000000000..9d2cff059 --- /dev/null +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/RequireExplicitNothingTest.scala @@ -0,0 +1,32 @@ +package com.avsystem.commons +package analyzer + +import org.scalatest.funsuite.AnyFunSuite + +final class RequireExplicitNothingTest extends AnyFunSuite with AnalyzerTest { + settings.pluginOptions.value ++= List("AVSystemAnalyzer:-discardedMonixTask") + + test("simple") { + assertErrors(4, + //language=Scala + """| + |object Test { + | //not ok + | val nook1 = throw new Exception + | val nook2 = throw new Exception + | def nook3 = throw new Exception + | val nook4: List[Int] = List.empty //covariant + | + | //ok + | val ok1: Nothing = throw new Exception + | val ok2: Nothing = throw new Exception + | def ok3: Nothing = throw new Exception + | val ok4: List[Nothing] = List.empty[Nothing] + | val ok5: List[Nothing] = Nil + | val ok6: List[Int] = Nil + | val ok7: Set[Int] = Set.empty //invariant + |} + |""".stripMargin + ) + } +} diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala deleted file mode 100644 index dd84d0c98..000000000 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala +++ /dev/null @@ -1,83 +0,0 @@ -package com.avsystem.commons -package analyzer - -import org.scalatest.funsuite.AnyFunSuite - -final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTest { - settings.pluginOptions.value ++= List("AVSystemAnalyzer:-discardedMonixTask") - - Seq( - ("Option[_]", "map"), - ("Option[_]", "flatMap"), - ("List[_]", "map"), - ("Seq[_]", "map"), - ("Set[_]", "map"), - ("Map[_, _]", "map"), - ("scala.concurrent.Future[_]", "map"), - ("scala.concurrent.Future[_]", "flatMap"), - ("scala.util.Try[_]", "map"), - ("Either[_, _]", "map"), - ("monix.eval.Task[_]", "map"), - ("monix.eval.Task[_]", "flatMap"), - ("com.avsystem.commons.misc.Opt[_]", "map"), - ("String => Int", "andThen"), - ("String => Nothing", "andThen"), - ("Nothing => Nothing", "andThen"), - ("String => Int", "compose"), - ("Seq[_]", "foreach"), - ).foreach { case (tpe, function) => - test(s"Testing $function of $tpe") { - assertErrors(10, - //language=Scala - s""" - |object whatever { - | implicit val ec: scala.concurrent.ExecutionContext = ??? // for Future - | - | def sth: $tpe = ??? - | def ex: Exception = ??? - | - | // errors from these - | sth.$function(throw ex) - | - | { - | println(""); sth.$function(throw ex) - | } - | - | if (true) sth.$function(throw ex) else sth.$function(throw ex) - | - | try sth.$function(throw ex) catch { - | case _: Exception => sth.$function(throw ex) - | } finally sth.$function(throw ex) - | - | Seq(1, 2, 3).foreach(_ => sth.$function(throw ex)) - | - | while (true) sth.$function(throw ex) - | - | do sth.$function(throw ex) while (true) - | - | // no errors from these - | sth.$function(_ => throw ex) - | - | { - | println(""); sth.$function(_ => throw ex) - | } - | - | if (true) sth.$function(_ => throw ex) else sth.$function(_ => throw ex) - | - | try sth.$function(_ => throw ex) catch { - | case _: Exception => sth.$function(_ => throw ex) - | } finally sth.$function(_ => throw ex) - | - | Seq(1, 2, 3).foreach(_ => sth.$function(_ => throw ex)) - | - | while (true) sth.$function(_ => throw ex) - | - | do sth.$function(_ => throw ex) while (true) - |} - | - |""".stripMargin - ) - } - } -} - From 4b554f2b8eca3ccbc7fd1073e449d50be37ad0f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Wed, 19 Jun 2024 10:26:10 +0200 Subject: [PATCH 05/10] Revert "Nothing is a special bottom type; it is a subtype of every other type. The Scala compiler loves to infer Nothing as a generic type but that is almost always incorrect. Explicit type arguments should be used instead." This reverts commit 9622a3f735902170f64530e0ed9cba5f8399cb99. --- .../commons/analyzer/AnalyzerPlugin.scala | 2 +- .../analyzer/RequireExplicitNothing.scala | 21 ----- .../ThrownExceptionNotInFunction.scala | 14 ++++ .../analyzer/RequireExplicitNothingTest.scala | 32 ------- .../ThrownExceptionNotInFunctionTest.scala | 83 +++++++++++++++++++ 5 files changed, 98 insertions(+), 54 deletions(-) delete mode 100644 analyzer/src/main/scala/com/avsystem/commons/analyzer/RequireExplicitNothing.scala create mode 100644 analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala delete mode 100644 analyzer/src/test/scala/com/avsystem/commons/analyzer/RequireExplicitNothingTest.scala create mode 100644 analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala index 48e1ba787..88ad1c13b 100644 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala @@ -56,7 +56,7 @@ final class AnalyzerPlugin(val global: Global) extends Plugin { plugin => new Any2StringAdd(global), new ThrowableObjects(global), new DiscardedMonixTask(global), - new RequireExplicitNothing(global), + new ThrownExceptionNotInFunction(global), new BadSingletonComponent(global), new ConstantDeclarations(global), new BasePackage(global), diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/RequireExplicitNothing.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/RequireExplicitNothing.scala deleted file mode 100644 index ef704f130..000000000 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/RequireExplicitNothing.scala +++ /dev/null @@ -1,21 +0,0 @@ -package com.avsystem.commons -package analyzer - -import scala.tools.nsc.Global - -final class RequireExplicitNothing(g: Global) extends AnalyzerRule(g, "requireExplicitNothing") { - - import global.* - - private def wasInferred(t: TypeTree) = t.original == null - - def analyze(unit: CompilationUnit): Unit = unit.body.foreach(analyzeTree { - case tree@TypeTree() if tree.tpe <:< definitions.NothingTpe && wasInferred(tree) => - tree.tpe match { - // Ignore existential types, they supposedly contain "any" - case ExistentialType(_, _) => - - case _ => report(tree.pos, "Inferred type: Nothing") - } - }) -} diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala new file mode 100644 index 000000000..3d6510b7a --- /dev/null +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala @@ -0,0 +1,14 @@ +package com.avsystem.commons +package analyzer + +import scala.tools.nsc.Global + +final class ThrownExceptionNotInFunction(g: Global) extends AnalyzerRule(g, "thrownExceptionNotInFunction") { + + import global.* + + def analyze(unit: CompilationUnit): Unit = unit.body.foreach(analyzeTree { + case t@Apply(f: TypeApply, List(Throw(_))) if definitions.isFunctionType(f.tpe.params.head.tpe) => + report(t.pos, "exception thrown in place of function definition") + }) +} diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/RequireExplicitNothingTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/RequireExplicitNothingTest.scala deleted file mode 100644 index 9d2cff059..000000000 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/RequireExplicitNothingTest.scala +++ /dev/null @@ -1,32 +0,0 @@ -package com.avsystem.commons -package analyzer - -import org.scalatest.funsuite.AnyFunSuite - -final class RequireExplicitNothingTest extends AnyFunSuite with AnalyzerTest { - settings.pluginOptions.value ++= List("AVSystemAnalyzer:-discardedMonixTask") - - test("simple") { - assertErrors(4, - //language=Scala - """| - |object Test { - | //not ok - | val nook1 = throw new Exception - | val nook2 = throw new Exception - | def nook3 = throw new Exception - | val nook4: List[Int] = List.empty //covariant - | - | //ok - | val ok1: Nothing = throw new Exception - | val ok2: Nothing = throw new Exception - | def ok3: Nothing = throw new Exception - | val ok4: List[Nothing] = List.empty[Nothing] - | val ok5: List[Nothing] = Nil - | val ok6: List[Int] = Nil - | val ok7: Set[Int] = Set.empty //invariant - |} - |""".stripMargin - ) - } -} diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala new file mode 100644 index 000000000..dd84d0c98 --- /dev/null +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala @@ -0,0 +1,83 @@ +package com.avsystem.commons +package analyzer + +import org.scalatest.funsuite.AnyFunSuite + +final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTest { + settings.pluginOptions.value ++= List("AVSystemAnalyzer:-discardedMonixTask") + + Seq( + ("Option[_]", "map"), + ("Option[_]", "flatMap"), + ("List[_]", "map"), + ("Seq[_]", "map"), + ("Set[_]", "map"), + ("Map[_, _]", "map"), + ("scala.concurrent.Future[_]", "map"), + ("scala.concurrent.Future[_]", "flatMap"), + ("scala.util.Try[_]", "map"), + ("Either[_, _]", "map"), + ("monix.eval.Task[_]", "map"), + ("monix.eval.Task[_]", "flatMap"), + ("com.avsystem.commons.misc.Opt[_]", "map"), + ("String => Int", "andThen"), + ("String => Nothing", "andThen"), + ("Nothing => Nothing", "andThen"), + ("String => Int", "compose"), + ("Seq[_]", "foreach"), + ).foreach { case (tpe, function) => + test(s"Testing $function of $tpe") { + assertErrors(10, + //language=Scala + s""" + |object whatever { + | implicit val ec: scala.concurrent.ExecutionContext = ??? // for Future + | + | def sth: $tpe = ??? + | def ex: Exception = ??? + | + | // errors from these + | sth.$function(throw ex) + | + | { + | println(""); sth.$function(throw ex) + | } + | + | if (true) sth.$function(throw ex) else sth.$function(throw ex) + | + | try sth.$function(throw ex) catch { + | case _: Exception => sth.$function(throw ex) + | } finally sth.$function(throw ex) + | + | Seq(1, 2, 3).foreach(_ => sth.$function(throw ex)) + | + | while (true) sth.$function(throw ex) + | + | do sth.$function(throw ex) while (true) + | + | // no errors from these + | sth.$function(_ => throw ex) + | + | { + | println(""); sth.$function(_ => throw ex) + | } + | + | if (true) sth.$function(_ => throw ex) else sth.$function(_ => throw ex) + | + | try sth.$function(_ => throw ex) catch { + | case _: Exception => sth.$function(_ => throw ex) + | } finally sth.$function(_ => throw ex) + | + | Seq(1, 2, 3).foreach(_ => sth.$function(_ => throw ex)) + | + | while (true) sth.$function(_ => throw ex) + | + | do sth.$function(_ => throw ex) while (true) + |} + | + |""".stripMargin + ) + } + } +} + From 4f836b16390b1143b9654e5271351ced530550e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Wed, 19 Jun 2024 10:37:25 +0200 Subject: [PATCH 06/10] handle multiple arguments, not only the first one --- .../commons/analyzer/ThrownExceptionNotInFunction.scala | 8 ++++++-- .../analyzer/ThrownExceptionNotInFunctionTest.scala | 1 - 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala index 3d6510b7a..95178ec6c 100644 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala @@ -8,7 +8,11 @@ final class ThrownExceptionNotInFunction(g: Global) extends AnalyzerRule(g, "thr import global.* def analyze(unit: CompilationUnit): Unit = unit.body.foreach(analyzeTree { - case t@Apply(f: TypeApply, List(Throw(_))) if definitions.isFunctionType(f.tpe.params.head.tpe) => - report(t.pos, "exception thrown in place of function definition") + case Apply(f: TypeApply, args: List[Tree]) => + args.zip(f.tpe.params).foreach { + case (arg: Throw, param) if definitions.isFunctionType(param.tpe) => + report(arg.pos, "exception thrown in place of function definition") + case (_, _) => + } }) } diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala index dd84d0c98..166fd8a39 100644 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala @@ -80,4 +80,3 @@ final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTe } } } - From e259d63426667f8204c139c6e4a359113391c956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Wed, 19 Jun 2024 10:45:47 +0200 Subject: [PATCH 07/10] add more tests --- .../ThrownExceptionNotInFunctionTest.scala | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala index 166fd8a39..24780f6a9 100644 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala @@ -6,6 +6,24 @@ import org.scalatest.funsuite.AnyFunSuite final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTest { settings.pluginOptions.value ++= List("AVSystemAnalyzer:-discardedMonixTask") + + test("Testing simple methods") { + assertErrors(1, + //language=Scala + """ + |object whatever { + | def ex: Exception = ??? + | + | def f0(x1: Int => Int) = ??? + | + | //not ok + | f0(throw ex) + | + | //ok + | f0(identity) + |}""".stripMargin) + } + Seq( ("Option[_]", "map"), ("Option[_]", "flatMap"), @@ -26,7 +44,7 @@ final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTe ("String => Int", "compose"), ("Seq[_]", "foreach"), ).foreach { case (tpe, function) => - test(s"Testing $function of $tpe") { + test(s"Testing method $function of $tpe") { assertErrors(10, //language=Scala s""" @@ -79,4 +97,57 @@ final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTe ) } } + + test("Testing multiple arguments") { + assertErrors(10, + //language=Scala + """ + |object whatever { + | def ex: Exception = ??? + | + | def f1(x1: Int => Int, x2: String => String) = ??? + | def f2(x1: Int => Int)(x2: String => String) = ??? + | def f3(x1: Int => Int)(x2: Int)(x3: String => String) = ??? + | def f4(x1: Int, x2: Int, x3: String => String) = ??? + | + | //not ok + | f1(throw ex, throw ex) + | f1(identity, throw ex) + | f1(throw ex, identity) + | + | f2(throw ex)(throw ex) + | f2(identity)(throw ex) + | f2(throw ex)(identity) + | + | f3(throw ex)(42)(throw ex) + | f3(throw ex)(42)(identity) + | f3(identity)(42)(throw ex) + | + | f4(42, 42, throw ex) + | + | //ok + | f1(identity, identity) + | f2(identity)(identity) + | f3(identity)(42)(identity) + | f4(42, 42, identity) + |}""".stripMargin + ) + } + + test("Testing constructor invocation") { + assertErrors(1, + //language=Scala + s""" + |object whatever { + | def ex: Exception = ??? + | + | class A(f: String => Int) + | + | new A(throw ex) + |} + |""".stripMargin + ) + } + + } From ae2b07abecf5f87c2d469733ce91473af910aee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Wed, 19 Jun 2024 10:51:04 +0200 Subject: [PATCH 08/10] add more tests --- .../ThrownExceptionNotInFunctionTest.scala | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala index 24780f6a9..e48c5d6cf 100644 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala @@ -130,24 +130,64 @@ final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTe | f2(identity)(identity) | f3(identity)(42)(identity) | f4(42, 42, identity) + | + | class A { + | def f1(x1: Int => Int, x2: String => String) = ??? + | def f2(x1: Int => Int)(x2: String => String) = ??? + | def f3(x1: Int => Int)(x2: Int)(x3: String => String) = ??? + | def f4(x1: Int, x2: Int, x3: String => String) = ??? + | } + | final val a = new A + | + | //not ok + | a.f1(throw ex, throw ex) + | a.f1(identity, throw ex) + | a.f1(throw ex, identity) + | + | a.f2(throw ex)(throw ex) + | a.f2(identity)(throw ex) + | a.f2(throw ex)(identity) + | + | a.f3(throw ex)(42)(throw ex) + | a.f3(throw ex)(42)(identity) + | a.f3(identity)(42)(throw ex) + | + | a.f4(42, 42, throw ex) + | + | //ok + | a.f1(identity, identity) + | a.f2(identity)(identity) + | a.f3(identity)(42)(identity) + | a.f4(42, 42, identity) |}""".stripMargin ) } test("Testing constructor invocation") { - assertErrors(1, + assertErrors(7, //language=Scala s""" |object whatever { | def ex: Exception = ??? | - | class A(f: String => Int) + | class A(f: Int => Int) | | new A(throw ex) + | + | class B(f: Int => Int)(g: Int => Int) + | + | new B(throw ex)(identity) + | new B(identity)(throw ex) + | new B(throw ex)(throw ex) + | + | class C(f: Int => Int, g: Int => Int) + | + | new C(throw ex, identity) + | new C(identity, throw ex) + | new C(throw ex, throw ex) |} |""".stripMargin ) } - - } + From da13377ba536eec4cd3451ea9fdd829974372f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 24 Jun 2024 17:38:08 +0200 Subject: [PATCH 09/10] add and fix test --- .../ThrownExceptionNotInFunctionTest.scala | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala index e48c5d6cf..1bc0a3797 100644 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala @@ -20,7 +20,7 @@ final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTe | f0(throw ex) | | //ok - | f0(identity) +// | f0(identity) |}""".stripMargin) } @@ -99,7 +99,7 @@ final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTe } test("Testing multiple arguments") { - assertErrors(10, + assertErrors(26, //language=Scala """ |object whatever { @@ -164,7 +164,7 @@ final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTe } test("Testing constructor invocation") { - assertErrors(7, + assertErrors(9, //language=Scala s""" |object whatever { @@ -189,5 +189,21 @@ final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTe |""".stripMargin ) } -} + test("Testing indirect exception throwing") { + assertErrors(1, + //language=Scala + """ + |object whatever { + | def throwEx: Nothing = ??? + | + | def f0(x1: Int => Int) = ??? + | + | //not ok + | f0(throwEx) + | + | //ok + | f0(identity) + |}""".stripMargin) + } +} From 0c3b54c4a1a290420be59288b023f0827d1c03b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 24 Jun 2024 17:38:18 +0200 Subject: [PATCH 10/10] lil enhancement --- .../commons/analyzer/ThrownExceptionNotInFunction.scala | 4 ++-- .../commons/analyzer/ThrownExceptionNotInFunctionTest.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala index 95178ec6c..98551c087 100644 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunction.scala @@ -8,9 +8,9 @@ final class ThrownExceptionNotInFunction(g: Global) extends AnalyzerRule(g, "thr import global.* def analyze(unit: CompilationUnit): Unit = unit.body.foreach(analyzeTree { - case Apply(f: TypeApply, args: List[Tree]) => + case Apply(f: Tree, args: List[Tree]) => args.zip(f.tpe.params).foreach { - case (arg: Throw, param) if definitions.isFunctionType(param.tpe) => + case (arg, param) if definitions.isFunctionType(param.tpe) && arg.tpe <:< definitions.NothingTpe => report(arg.pos, "exception thrown in place of function definition") case (_, _) => } diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala index 1bc0a3797..3c275b5f5 100644 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ThrownExceptionNotInFunctionTest.scala @@ -20,7 +20,7 @@ final class ThrownExceptionNotInFunctionTest extends AnyFunSuite with AnalyzerTe | f0(throw ex) | | //ok -// | f0(identity) + | f0(identity) |}""".stripMargin) }