Skip to content

Commit

Permalink
Add flags to selectively disable serializability checks in messages/e…
Browse files Browse the repository at this point in the history
…vents/state (#383)
  • Loading branch information
PawelLipski committed Dec 19, 2023
1 parent d7820a5 commit 7bc7726
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 147 deletions.
15 changes: 15 additions & 0 deletions docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,21 @@ This option disables detection of messages/events/state based on type of argumen
This option disables detection of messages/events/state based on return type of the function given as argument to method. This detection is enabled by default. If you want to disable it, add the following setting:<br>
`Compile / scalacOptions += "-P:serializability-checker-plugin:--disable-detection-higher-order-function"`<br><br>

- `--exclude-messages`

This option disables serializability checks on all messages. These checks are enabled by default. If you want to disable them, add the following setting:<br>
`Compile / scalacOptions += "-P:serializability-checker-plugin:--exclude-messages"`<br><br>

- `--exclude-persistent-events`

This option disables serializability checks on all persistent events. These checks are enabled by default. If you want to disable them, add the following setting:<br>
`Compile / scalacOptions += "-P:serializability-checker-plugin:--exclude-persistent-events"`<br><br>

- `--exclude-persistent-states`

This option disables serializability checks on all persistent state classes. These checks are enabled by default. If you want to disable them, add the following setting:<br>
`Compile / scalacOptions += "-P:serializability-checker-plugin:--exclude-persistent-states"`<br><br>

- `--types-explicitly-marked-as-serializable=<type1>,<type2>,...`

This option can be used to pass a comma-separated list of fully-qualified names of types that should be considered serializable by the checker, even if they do **not** extend a designated serializability trait.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.virtuslab.ash

sealed trait ClassKind {
val name: String
}

object ClassKind {
case object Message extends ClassKind {
val name = "message"
}
case object PersistentEvent extends ClassKind {
val name = "persistent event"
}
case object PersistentState extends ClassKind {
val name = "persistent state"
}

case object Ignore extends ClassKind {
val name = "ignore"
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ class SerializabilityCheckerCompilerPlugin(override val global: Global) extends
return false

pluginOptions.verbose = options.contains(verbose)

pluginOptions.detectFromGenerics = !options.contains(disableGenerics)
pluginOptions.detectFromGenericMethods = !options.contains(disableGenericMethods)
pluginOptions.detectFromMethods = !options.contains(disableMethods)
pluginOptions.detectFromUntypedMethods = !options.contains(disableMethodsUntyped)
pluginOptions.detectFromHigherOrderFunctions = !options.contains(disableHigherOrderFunctions)

pluginOptions.includeMessages = !options.contains(excludeMessages)
pluginOptions.includePersistentEvents = !options.contains(excludePersistentEvents)
pluginOptions.includePersistentStates = !options.contains(excludePersistentStates)

options.find(_.startsWith(typesExplicitlyMarkedAsSerializable)).foreach { opt =>
pluginOptions.typesExplicitlyMarkedAsSerializable =
opt.stripPrefix(typesExplicitlyMarkedAsSerializable).split(",").toSeq.map(_.strip())
Expand All @@ -41,6 +47,9 @@ class SerializabilityCheckerCompilerPlugin(override val global: Global) extends
|$disableMethods - disables detection of messages/events/state based on type of arguments to a method, e.g. akka.actor.typed.ActorRef.tell
|$disableMethodsUntyped - disables detection of messages/events/state based on type of arguments to a method that takes Any, used for Akka Classic
|$disableHigherOrderFunctions - disables detection of messages/events/state based on return type of the function given as argument to method
|$excludeMessages - exclude all messages from the serializability check
|$excludePersistentEvents - exclude all events from the serializability check
|$excludePersistentStates - exclude all states from the serializability check
|$typesExplicitlyMarkedAsSerializable - comma-separated list of fully-qualified names of types that should be considered serializable by this checker, even if they do NOT extend a designated serializability trait
|""".stripMargin)
}
Expand All @@ -54,6 +63,9 @@ object SerializabilityCheckerCompilerPlugin {
val disableMethods = "--disable-detection-methods"
val disableMethodsUntyped = "--disable-detection-untyped-methods"
val disableHigherOrderFunctions = "--disable-detection-higher-order-function"
val excludeMessages = "--exclude-messages"
val excludePersistentEvents = "--exclude-persistent-events"
val excludePersistentStates = "--exclude-persistent-states"
val typesExplicitlyMarkedAsSerializable = "--types-explicitly-marked-as-serializable="
}
val serializabilityTraitType = "org.virtuslab.ash.annotation.SerializabilityTrait"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class SerializabilityCheckerCompilerPluginComponent(val pluginOptions: Serializa
import global._

// just to avoid using tuples where possible
private case class TypeWithClassType(typ: Type, classType: ClassType)
private case class TypeWithClassKind(typ: Type, classKind: ClassKind)

override val phaseName: String = "serializability-checker"
override val runsAfter: List[String] = List("refchecks")
Expand Down Expand Up @@ -45,74 +45,74 @@ class SerializabilityCheckerCompilerPluginComponent(val pluginOptions: Serializa

private val ignoredTypePrefixes = List("akka.")

private val genericsToTypes: Map[String, Seq[ClassType]] = Map(
"akka.actor.typed.ActorSystem" -> Seq(ClassType.Message),
"akka.actor.typed.ActorRef" -> Seq(ClassType.Message),
"akka.actor.typed.Behavior" -> Seq(ClassType.Message),
"akka.actor.typed.RecipientRef" -> Seq(ClassType.Message),
"akka.pattern.PipeToSupport.PipeableFuture" -> Seq(ClassType.Message),
"akka.pattern.PipeToSupport.PipeableCompletionStage" -> Seq(ClassType.Message),
"akka.persistence.typed.scaladsl.ReplyEffect" -> Seq(ClassType.PersistentEvent, ClassType.PersistentState),
"akka.persistence.typed.scaladsl.Effect" -> Seq(ClassType.PersistentEvent),
"akka.persistence.typed.scaladsl.EffectBuilder" -> Seq(ClassType.PersistentEvent),
"akka.projection.eventsourced.EventEnvelope" -> Seq(ClassType.PersistentEvent, ClassType.PersistentState))

private val genericMethodsToTypes: Map[String, Seq[ClassType]] = Map(
"akka.actor.typed.scaladsl.ActorContext.ask" -> Seq(ClassType.Message, ClassType.Message),
"akka.actor.typed.scaladsl.AskPattern.Askable.$qmark" -> Seq(ClassType.Message),
"akka.pattern.PipeToSupport.pipe" -> Seq(ClassType.Message),
"akka.pattern.PipeToSupport.pipeCompletionStage" -> Seq(ClassType.Message))

private val concreteMethodsToTypes: Map[String, Seq[ClassType]] = Map(
"akka.actor.typed.ActorRef.ActorRefOps.$bang" -> Seq(ClassType.Message),
"akka.actor.typed.ActorRef.tell" -> Seq(ClassType.Message),
"akka.actor.typed.RecipientRef.tell" -> Seq(ClassType.Message))

private val concreteUntypedMethodsToTypes: Map[String, Seq[ClassType]] = Map(
"akka.actor.ActorRef.tell" -> Seq(ClassType.Message, ClassType.Ignore),
"akka.actor.ActorRef.$bang" -> Seq(ClassType.Message),
"akka.actor.ActorRef.forward" -> Seq(ClassType.Message),
"akka.pattern.AskSupport.ask" -> Seq(ClassType.Ignore, ClassType.Message, ClassType.Ignore),
"akka.pattern.AskSupport.askWithStatus" -> Seq(ClassType.Ignore, ClassType.Message, ClassType.Ignore),
"akka.pattern.AskableActorRef.ask" -> Seq(ClassType.Message),
"akka.pattern.AskableActorRef.askWithStatus" -> Seq(ClassType.Message),
"akka.pattern.AskableActorRef.$qmark" -> Seq(ClassType.Message),
"akka.pattern.AskableActorSelection.ask" -> Seq(ClassType.Message),
"akka.pattern.AskableActorSelection.$qmark" -> Seq(ClassType.Message),
"akka.pattern.ExplicitAskSupport.ask" -> Seq(ClassType.Ignore, ClassType.Message, ClassType.Ignore))

private val concreteHigherOrderFunctionsToTypes: Map[String, Seq[ClassType]] = Map(
"akka.pattern.ExplicitlyAskableActorRef.ask" -> Seq(ClassType.Message),
"akka.pattern.ExplicitlyAskableActorRef.$qmark" -> Seq(ClassType.Message),
"akka.pattern.ExplicitlyAskableActorSelection.ask" -> Seq(ClassType.Message),
"akka.pattern.ExplicitlyAskableActorSelection.$qmark" -> Seq(ClassType.Message))

private val combinedMap: Map[String, Seq[ClassType]] =
genericsToTypes ++ genericMethodsToTypes ++ concreteMethodsToTypes ++ concreteUntypedMethodsToTypes ++ concreteHigherOrderFunctionsToTypes
private val genericsToKinds: Map[String, Seq[ClassKind]] = Map(
"akka.actor.typed.ActorSystem" -> Seq(ClassKind.Message),
"akka.actor.typed.ActorRef" -> Seq(ClassKind.Message),
"akka.actor.typed.Behavior" -> Seq(ClassKind.Message),
"akka.actor.typed.RecipientRef" -> Seq(ClassKind.Message),
"akka.pattern.PipeToSupport.PipeableFuture" -> Seq(ClassKind.Message),
"akka.pattern.PipeToSupport.PipeableCompletionStage" -> Seq(ClassKind.Message),
"akka.persistence.typed.scaladsl.ReplyEffect" -> Seq(ClassKind.PersistentEvent, ClassKind.PersistentState),
"akka.persistence.typed.scaladsl.Effect" -> Seq(ClassKind.PersistentEvent),
"akka.persistence.typed.scaladsl.EffectBuilder" -> Seq(ClassKind.PersistentEvent),
"akka.projection.eventsourced.EventEnvelope" -> Seq(ClassKind.PersistentEvent, ClassKind.PersistentState))

private val genericMethodsToKinds: Map[String, Seq[ClassKind]] = Map(
"akka.actor.typed.scaladsl.ActorContext.ask" -> Seq(ClassKind.Message, ClassKind.Message),
"akka.actor.typed.scaladsl.AskPattern.Askable.$qmark" -> Seq(ClassKind.Message),
"akka.pattern.PipeToSupport.pipe" -> Seq(ClassKind.Message),
"akka.pattern.PipeToSupport.pipeCompletionStage" -> Seq(ClassKind.Message))

private val concreteMethodsToKinds: Map[String, Seq[ClassKind]] = Map(
"akka.actor.typed.ActorRef.ActorRefOps.$bang" -> Seq(ClassKind.Message),
"akka.actor.typed.ActorRef.tell" -> Seq(ClassKind.Message),
"akka.actor.typed.RecipientRef.tell" -> Seq(ClassKind.Message))

private val concreteUntypedMethodsToKinds: Map[String, Seq[ClassKind]] = Map(
"akka.actor.ActorRef.tell" -> Seq(ClassKind.Message, ClassKind.Ignore),
"akka.actor.ActorRef.$bang" -> Seq(ClassKind.Message),
"akka.actor.ActorRef.forward" -> Seq(ClassKind.Message),
"akka.pattern.AskSupport.ask" -> Seq(ClassKind.Ignore, ClassKind.Message, ClassKind.Ignore),
"akka.pattern.AskSupport.askWithStatus" -> Seq(ClassKind.Ignore, ClassKind.Message, ClassKind.Ignore),
"akka.pattern.AskableActorRef.ask" -> Seq(ClassKind.Message),
"akka.pattern.AskableActorRef.askWithStatus" -> Seq(ClassKind.Message),
"akka.pattern.AskableActorRef.$qmark" -> Seq(ClassKind.Message),
"akka.pattern.AskableActorSelection.ask" -> Seq(ClassKind.Message),
"akka.pattern.AskableActorSelection.$qmark" -> Seq(ClassKind.Message),
"akka.pattern.ExplicitAskSupport.ask" -> Seq(ClassKind.Ignore, ClassKind.Message, ClassKind.Ignore))

private val concreteHigherOrderFunctionsToKinds: Map[String, Seq[ClassKind]] = Map(
"akka.pattern.ExplicitlyAskableActorRef.ask" -> Seq(ClassKind.Message),
"akka.pattern.ExplicitlyAskableActorRef.$qmark" -> Seq(ClassKind.Message),
"akka.pattern.ExplicitlyAskableActorSelection.ask" -> Seq(ClassKind.Message),
"akka.pattern.ExplicitlyAskableActorSelection.$qmark" -> Seq(ClassKind.Message))

private val symbolsToKinds: Map[String, Seq[ClassKind]] =
genericsToKinds ++ genericMethodsToKinds ++ concreteMethodsToKinds ++ concreteUntypedMethodsToKinds ++ concreteHigherOrderFunctionsToKinds

override def apply(unit: global.CompilationUnit): Unit = {
val body = unit.body
val reporter = CrossVersionReporter(global)

val genericsNames = genericsToTypes.keySet
val genericMethods = genericMethodsToTypes.keySet
val concreteMethods = concreteMethodsToTypes.keySet
val concreteUntypedMethods = concreteUntypedMethodsToTypes.keySet
val concreteHigherOrderFunctions = concreteHigherOrderFunctionsToTypes.keySet
val genericsNames = genericsToKinds.keySet
val genericMethods = genericMethodsToKinds.keySet
val concreteMethods = concreteMethodsToKinds.keySet
val concreteUntypedMethods = concreteUntypedMethodsToKinds.keySet
val concreteHigherOrderFunctions = concreteHigherOrderFunctionsToKinds.keySet

def extractTypes(args: List[Tree], x: Tree): List[(TypeWithClassType, Position)] =
def extractTypes(args: List[Tree], x: Tree): List[(TypeWithClassKind, Position)] =
args
.map(_.tpe)
.zip(combinedMap(x.symbol.fullName))
.map(typeClassTypeTuple => (TypeWithClassType(typeClassTypeTuple._1, typeClassTypeTuple._2), x.pos))
.zip(symbolsToKinds(x.symbol.fullName))
.map(typeClassTypeTuple => (TypeWithClassKind(typeClassTypeTuple._1, typeClassTypeTuple._2), x.pos))

val detectedTypes: Iterable[(TypeWithClassType, Position)] = body
val detectedTypes: Iterable[(TypeWithClassKind, Position)] = body
.collect {
case _: ApplyToImplicitArgs => Nil
case x: TypeTree if genericsNames.contains(x.tpe.typeSymbol.fullName) && pluginOptions.detectFromGenerics =>
x.tpe.typeArgs
.zip(combinedMap(x.tpe.typeSymbol.fullName))
.map(typeClassTypeTuple => (TypeWithClassType(typeClassTypeTuple._1, typeClassTypeTuple._2), x.pos))
.zip(genericsToKinds(x.tpe.typeSymbol.fullName))
.map(typeClassTypeTuple => (TypeWithClassKind(typeClassTypeTuple._1, typeClassTypeTuple._2), x.pos))
case x @ TypeApply(_, args)
if genericMethods.contains(x.symbol.fullName) && pluginOptions.detectFromGenericMethods =>
extractTypes(args, x)
Expand All @@ -127,13 +127,16 @@ class SerializabilityCheckerCompilerPluginComponent(val pluginOptions: Serializa
pluginOptions.detectFromHigherOrderFunctions =>
extractTypes(args, x).flatMap { resultTuple =>
resultTuple._1.typ.typeArguments match {
case List(_, out) => Some(resultTuple.copy(_1 = TypeWithClassType(out, resultTuple._1.classType)))
case List(_, out) => Some(resultTuple.copy(_1 = TypeWithClassKind(out, resultTuple._1.classKind)))
case _ => None
}
}
}
.flatten
.filterNot(_._1.classType == ClassType.Ignore)
.filter(_._1.classKind != ClassKind.Ignore)
.filter(pluginOptions.includeMessages || _._1.classKind != ClassKind.Message)
.filter(pluginOptions.includePersistentEvents || _._1.classKind != ClassKind.PersistentEvent)
.filter(pluginOptions.includePersistentStates || _._1.classKind != ClassKind.PersistentState)
.groupBy(_._1.typ)
.map(_._2.head)

Expand Down Expand Up @@ -167,8 +170,8 @@ class SerializabilityCheckerCompilerPluginComponent(val pluginOptions: Serializa
reporter.error(
detectedPosition,
s"""${typeWithClassType.typ
.toString()} is used as Akka ${typeWithClassType.classType.name} but does not extend a trait annotated with $serializabilityTraitType.
|Passing an object of a class that does NOT extend a trait annotated with $serializabilityTraitType as a ${typeWithClassType.classType.name}
.toString()} is used as Akka ${typeWithClassType.classKind.name} but does not extend a trait annotated with $serializabilityTraitType.
|Passing an object of a class that does NOT extend a trait annotated with $serializabilityTraitType as a ${typeWithClassType.classKind.name}
|may cause Akka to fall back to Java serialization during runtime.
|
|""".stripMargin)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ class SerializabilityCheckerOptions(
var detectFromMethods: Boolean = true,
var detectFromUntypedMethods: Boolean = true,
var detectFromHigherOrderFunctions: Boolean = true,
var includeMessages: Boolean = true,
var includePersistentEvents: Boolean = true,
var includePersistentStates: Boolean = true,
var typesExplicitlyMarkedAsSerializable: Seq[String] = Seq.empty)
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
package org.random.project

import akka.actor.typed.ActorSystem
import akka.persistence.query.Offset
import akka.projection.eventsourced.EventEnvelope
import akka.projection.eventsourced.scaladsl.EventSourcedProvider
import akka.projection.scaladsl.SourceProvider
import akka.persistence.typed.scaladsl.ReplyEffect
import akka.actor.typed.Behavior
import org.virtuslab.ash.annotation.SerializabilityTrait

object ReplyEffectTestEvent {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.random.project

import akka.persistence.typed.scaladsl.ReplyEffect

object ReplyEffectTestEventAndState {
trait Event extends MySerializable
trait State extends MySerializable

def test: ReplyEffect[Event, State] = ???
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
package org.random.project

import akka.actor.typed.ActorSystem
import akka.persistence.query.Offset
import akka.projection.eventsourced.EventEnvelope
import akka.projection.eventsourced.scaladsl.EventSourcedProvider
import akka.projection.scaladsl.SourceProvider
import akka.persistence.typed.scaladsl.ReplyEffect
import akka.actor.typed.Behavior
import org.virtuslab.ash.annotation.SerializabilityTrait

object ReplyEffectTestState {
Expand Down
Loading

0 comments on commit 7bc7726

Please sign in to comment.