Repository: 7mind/izumi
Pull request #1195 (2020-08-25 00:02:50)

Update classgraph to 4.8.88
Updates [io.github.classgraph:classgraph](https://github.com/classgraph/classgraph) from 4.8.87 to 4.8.88.


I'll automatically update this PR to resolve conflicts as long as you don't change it yourself.

If you'd like to skip this version, you can just close this PR. If you have any feedback, just mention me in the comments below.

Configure Scala Steward for your repository with a [`.scala-steward.conf`](https://github.com/fthomas/scala-steward/blob/179507babd976dd9e0d9d5d9983ff43436b13e3b/docs/repo-specific-configuration.md) file.

Have a fantastic day writing Scala!

<details>
<summary>Ignore future updates</summary>

Add this to your `.scala-steward.conf` file to ignore future updates of this dependency:
```
updates.ignore = [ { groupId = "io.github.classgraph", artifactId = "classgraph" } ]
```
</details>

labels: library-update, semver-patch


Patch:
diff --git a/distage/distage-extension-plugins/src/main/scala/distage/plugins/DistagePlugins.scala b/distage/distage-extension-plugins/src/main/scala/distage/plugins/DistagePlugins.scala
index 50f06bd0d..3d146a4fa 100644
--- a/distage/distage-extension-plugins/src/main/scala/distage/plugins/DistagePlugins.scala
+++ b/distage/distage-extension-plugins/src/main/scala/distage/plugins/DistagePlugins.scala
@@ -8,7 +8,7 @@ trait DistagePlugins {
   type PluginBase = plugins.PluginBase
   val PluginBase: plugins.PluginBase.type = plugins.PluginBase
 
-  type PluginDef = plugins.PluginDef
+  type PluginDef[T <: String] = plugins.PluginDef[T]
 
   type BootstrapPlugin = plugins.BootstrapPlugin
   val BootstrapPlugin: plugins.BootstrapPlugin.type = plugins.BootstrapPlugin
diff --git a/distage/distage-extension-plugins/src/main/scala/distage/plugins/package.scala b/distage/distage-extension-plugins/src/main/scala/distage/plugins/package.scala
index 8f9637477..2d2648ff0 100644
--- a/distage/distage-extension-plugins/src/main/scala/distage/plugins/package.scala
+++ b/distage/distage-extension-plugins/src/main/scala/distage/plugins/package.scala
@@ -7,7 +7,7 @@ package object plugins extends DistagePlugins {
   override type PluginBase = izumi.distage.plugins.PluginBase
   override val PluginBase: izumi.distage.plugins.PluginBase.type = izumi.distage.plugins.PluginBase
 
-  override type PluginDef = izumi.distage.plugins.PluginDef
+  override type PluginDef[T <: String] = izumi.distage.plugins.PluginDef[T]
 
   override type BootstrapPlugin = izumi.distage.plugins.BootstrapPlugin
   override val BootstrapPlugin: izumi.distage.plugins.BootstrapPlugin.type = izumi.distage.plugins.BootstrapPlugin
diff --git a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/ForcedRecompilationToken.scala b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/ForcedRecompilationToken.scala
new file mode 100644
index 000000000..1b6159d0e
--- /dev/null
+++ b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/ForcedRecompilationToken.scala
@@ -0,0 +1,38 @@
+package izumi.distage.plugins
+
+import scala.language.experimental.macros
+import scala.reflect.api.Universe
+import scala.reflect.macros.whitebox
+
+/**
+  * This macro enables `distage`'s compile-time checks to work well with Scala's incremental compilation,
+  * it forces recompilation of the macro that performs compile-time plan checking every time a PluginDef,
+  * or a constructor bound in PluginDef changes. It does that by generating a new unique type for each
+  * compiler session and assigning it to a class inheriting PluginDef. The "change of super type" of a plugin
+  * forces recompilation of all code that references it, and specifically the [[izumi.distage.staticinjector.plugins.StaticPluginChecker]] macro.
+  * This allows compile-time checking macro to provide rapid feedback during development.
+  *
+  * @see [[https://izumi.7mind.io/distage/distage-framework.html#compile-time-checks Compile-time checks]]
+  */
+final abstract class ForcedRecompilationToken[T]
+
+object ForcedRecompilationToken {
+  implicit def materialize[T <: String]: ForcedRecompilationToken[T] = macro UniqueRecompilationTokenMacro.whiteboxMaterializeImpl
+
+  object UniqueRecompilationTokenMacro {
+    final val compilerLaunchId = java.util.UUID.randomUUID().toString
+    var cachedTypedTree: Universe#Tree = null
+
+    def whiteboxMaterializeImpl(c: whitebox.Context): c.Tree = {
+      import c.universe._
+      UniqueRecompilationTokenMacro.synchronized {
+        if (cachedTypedTree eq null) {
+          val uuidStrConstantType = internal.constantType(Constant(compilerLaunchId))
+          val tree = c.typecheck(q"null : _root_.izumi.distage.plugins.ForcedRecompilationToken[$uuidStrConstantType]")
+          cachedTypedTree = tree
+        }
+        cachedTypedTree.asInstanceOf[c.Tree]
+      }
+    }
+  }
+}
diff --git a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginConfig.scala b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginConfig.scala
index b20709b9f..ea0a6293c 100644
--- a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginConfig.scala
+++ b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginConfig.scala
@@ -1,5 +1,7 @@
 package izumi.distage.plugins
 
+import izumi.distage.plugins.StaticPluginLoader.StaticPluginLoaderMacro
+
 import scala.language.experimental.macros
 
 final case class PluginConfig(
@@ -39,7 +41,7 @@ object PluginConfig {
     * WARN: will _not_ find plugins defined in current module, only those defined in dependency modules (similarly to
     *       how you cannot call Scala macros defined in the current module)
     */
-  def staticallyAvailablePlugins(pluginsPackage: String): PluginConfig = macro StaticPluginScannerMacro.staticallyAvailablePluginConfig
+  def staticallyAvailablePlugins(pluginsPackage: String): PluginConfig = macro StaticPluginLoaderMacro.staticallyAvailablePluginConfig
 
   /** Create a [[PluginConfig]] that simply returns the specified plugins */
   def const(plugins: Seq[PluginBase]): PluginConfig = PluginConfig(Nil, Nil, cachePackages = false, debug = false, plugins, Nil)
diff --git a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginDef.scala b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginDef.scala
index 244dfbacc..a20e565ac 100644
--- a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginDef.scala
+++ b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginDef.scala
@@ -2,4 +2,4 @@ package izumi.distage.plugins
 
 import izumi.distage.model.definition.ModuleDef
 
-trait PluginDef extends PluginBase with ModuleDef
+abstract class PluginDef[T <: String](implicit ev: ForcedRecompilationToken[T]) extends PluginBase with ModuleDef
diff --git a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/StaticPluginLoader.scala b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/StaticPluginLoader.scala
new file mode 100644
index 000000000..0a01e6f13
--- /dev/null
+++ b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/StaticPluginLoader.scala
@@ -0,0 +1,60 @@
+package izumi.distage.plugins
+
+import scala.language.experimental.macros
+
+import izumi.distage.plugins.load.PluginLoaderDefaultImpl
+import izumi.fundamentals.reflection.ReflectionUtil
+
+import scala.reflect.macros.blackbox
+import scala.reflect.runtime.{universe => ru}
+
+object StaticPluginLoader {
+  def staticallyAvailablePlugins(pluginsPackage: String): List[PluginBase] = macro StaticPluginLoaderMacro.staticallyAvailablePlugins
+
+  object StaticPluginLoaderMacro {
+
+    def staticallyAvailablePluginConfig(c: blackbox.Context)(pluginsPackage: c.Expr[String]): c.Expr[PluginConfig] = {
+      val plugins = staticallyAvailablePlugins(c)(pluginsPackage)
+      c.universe.reify {
+        PluginConfig.const(plugins.splice)
+      }
+    }
+
+    def staticallyAvailablePlugins(c: blackbox.Context)(pluginsPackage: c.Expr[String]): c.Expr[List[PluginBase]] = {
+      import c.universe._
+
+      val pluginPath = ReflectionUtil.getStringLiteral(c)(pluginsPackage.tree)
+
+      val loadedPlugins = if (pluginPath == "") {
+        Seq.empty
+      } else {
+        new PluginLoaderDefaultImpl().load(PluginConfig.packages(Seq(pluginPath)))
+      }
+
+      val quoted: List[Tree] = instantiatePluginsInCode(c)(loadedPlugins)
+
+      c.Expr[List[PluginBase]](q"$quoted")
+    }
+
+    def instantiatePluginsInCode(c: blackbox.Context)(loadedPlugins: Seq[PluginBase]): List[c.Tree] = {
+      import c.universe._
+      loadedPlugins.map {
+        plugin =>
+          val clazz = plugin.getClass
+          val runtimeMirror = ru.runtimeMirror(clazz.getClassLoader)
+          val runtimeClassSymbol = runtimeMirror.classSymbol(clazz)
+
+          val macroMirror: Mirror = c.mirror
+
+          if (runtimeClassSymbol.isModuleClass) {
+            val tgt = macroMirror.staticModule(runtimeClassSymbol.module.fullName)
+            q"$tgt"
+          } else {
+            val tgt = macroMirror.staticClass(runtimeClassSymbol.fullName)
+            q"new $tgt"
+          }
+      }.toList
+    }
+  }
+
+}
diff --git a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/StaticPluginScanner.scala b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/StaticPluginScanner.scala
deleted file mode 100644
index 434a31f18..000000000
--- a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/StaticPluginScanner.scala
+++ /dev/null
@@ -1,7 +0,0 @@
-package izumi.distage.plugins
-
-import scala.language.experimental.macros
-
-object StaticPluginScanner {
-  def staticallyAvailablePlugins(pluginsPackage: String): List[PluginBase] = macro StaticPluginScannerMacro.staticallyAvailablePlugins
-}
diff --git a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/StaticPluginScannerMacro.scala b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/StaticPluginScannerMacro.scala
deleted file mode 100644
index defd8a659..000000000
--- a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/StaticPluginScannerMacro.scala
+++ /dev/null
@@ -1,49 +0,0 @@
-package izumi.distage.plugins
-
-import izumi.distage.plugins.load.PluginLoaderDefaultImpl
-import izumi.fundamentals.reflection.ReflectionUtil
-
-import scala.reflect.macros.blackbox
-import scala.reflect.runtime.{universe => ru}
-
-object StaticPluginScannerMacro {
-
-  def staticallyAvailablePluginConfig(c: blackbox.Context)(pluginsPackage: c.Expr[String]): c.Expr[PluginConfig] = {
-    val plugins = staticallyAvailablePlugins(c)(pluginsPackage)
-    c.universe.reify {
-      PluginConfig.const(plugins.splice)
-    }
-  }
-
-  def staticallyAvailablePlugins(c: blackbox.Context)(pluginsPackage: c.Expr[String]): c.Expr[List[PluginBase]] = {
-    import c.universe._
-
-    val pluginPath = ReflectionUtil.getStringLiteral(c)(pluginsPackage.tree)
-
-    val loadedPlugins = if (pluginPath == "") {
-      Seq.empty
-    } else {
-      new PluginLoaderDefaultImpl().load(PluginConfig.packages(Seq(pluginPath)))
-    }
-
-    val quoted: List[Tree] = loadedPlugins.map {
-      p =>
-        val clazz = p.getClass
-        val runtimeMirror = ru.runtimeMirror(clazz.getClassLoader)
-        val runtimeClassSymbol = runtimeMirror.classSymbol(clazz)
-
-        val macroMirror: Mirror = c.mirror
-
-        if (runtimeClassSymbol.isModuleClass) {
-          val tgt = macroMirror.staticModule(runtimeClassSymbol.module.fullName)
-          q"$tgt"
-        } else {
-          val tgt = macroMirror.staticClass(runtimeClassSymbol.fullName)
-          q"new $tgt"
-        }
-    }.toList
-
-    c.Expr[List[PluginBase]](q"$quoted")
-  }
-
-}
diff --git a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/load/PluginLoaderDefaultImpl.scala b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/load/PluginLoaderDefaultImpl.scala
index b641b97a1..759a9a45f 100644
--- a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/load/PluginLoaderDefaultImpl.scala
+++ b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/load/PluginLoaderDefaultImpl.scala
@@ -24,7 +24,7 @@ class PluginLoaderDefaultImpl extends PluginLoader {
     val disabledPackages = config.packagesDisabled
 
     val pluginBase = classOf[PluginBase]
-    val pluginDef = classOf[PluginDef]
+    val pluginDef = classOf[PluginDef[_]]
     val whitelistedClasses = Seq(pluginDef.getName)
 
     def loadPkgs(pkgs: Seq[String]): Seq[PluginBase] = {
@@ -62,7 +62,7 @@ object PluginLoaderDefaultImpl {
       .map(_.acceptPackages(enabledPackages: _*))
       .map(_.acceptClasses(whitelistClasses :+ base: _*))
       .map(_.rejectPackages(disabledPackages: _*))
-      .map(_.enableClassInfo())
+      .map(_.enableExternalClasses())
       .map(if (debug) _.verbose() else identity)
       .map(_.scan())
       .get
diff --git a/distage/distage-framework/src/main/scala/izumi/distage/staticinjector/plugins/macros/StaticPluginCheckerMacro.scala b/distage/distage-framework/src/main/scala/izumi/distage/staticinjector/plugins/macros/StaticPluginCheckerMacro.scala
index bd42d871a..c1653538a 100644
--- a/distage/distage-framework/src/main/scala/izumi/distage/staticinjector/plugins/macros/StaticPluginCheckerMacro.scala
+++ b/distage/distage-framework/src/main/scala/izumi/distage/staticinjector/plugins/macros/StaticPluginCheckerMacro.scala
@@ -8,6 +8,7 @@ import izumi.distage.config.AppConfigModule
 import izumi.distage.config.model.AppConfig
 import izumi.distage.framework.services.ActivationInfoExtractor
 import izumi.distage.model.PlannerInput
+import izumi.distage.plugins.StaticPluginLoader.StaticPluginLoaderMacro
 import izumi.distage.plugins.load.PluginLoaderDefaultImpl
 import izumi.distage.plugins.merge.SimplePluginMergeStrategy
 import izumi.distage.plugins.{PluginBase, PluginConfig}
@@ -75,7 +76,9 @@ object StaticPluginCheckerMacro {
     configFileRegex: c.Expr[String],
   ): c.Expr[Unit] = {
 
-    val abort = c.abort(c.enclosingPosition, _: String): Unit
+    val abort: String => Unit = {
+      c.abort(c.enclosingPosition, _: String): Unit
+    }
 
     val pluginPath = ReflectionUtil.getStringLiteral(c)(pluginsPackage.tree)
 
@@ -87,37 +90,42 @@ object StaticPluginCheckerMacro {
 
     val configRegex = ReflectionUtil.getStringLiteral(c)(configFileRegex.tree)
 
-    val configModule = if (configRegex == "") {
-      None
-    } else {
-      val scanResult = new ClassGraph().scan()
-      val configUrls =
-        try {
-          val resourceList = scanResult.getResourcesMatchingPattern(configRegex.r.pattern)
+    val configModule: Option[AppConfigModule] = {
+      if (configRegex == "") {
+        None
+      } else {
+        val scanResult = new ClassGraph().scan()
+        val configUrls =
           try {
-            resourceList.getURLs.asScala.toList
-          } finally resourceList.close()
-        } finally scanResult.close()
-
-      val referenceConfig = configUrls.foldLeft(ConfigFactory.empty())(_ withFallback ConfigFactory.parseURL(_)).resolve()
-
-      Some(new AppConfigModule(AppConfig(referenceConfig)))
+            val resourceList = scanResult.getResourcesMatchingPattern(configRegex.r.pattern)
+            try {
+              resourceList.getURLs.asScala.toList
+            } finally resourceList.close()
+          } finally scanResult.close()
+        val referenceConfig = {
+          configUrls.foldLeft(ConfigFactory.empty())(_ withFallback ConfigFactory.parseURL(_)).resolve()
+        }
+
+        Some(new AppConfigModule(AppConfig(referenceConfig)))
+      }
     }
 
-    val gcRootPath = ReflectionUtil.getStringLiteral(c)(gcRoot.tree)
-
-    val gcRootModule: Option[ModuleBase] = if (gcRootPath == "") {
-      None
-    } else {
-      Some(constructClass[ModuleBase](gcRootPath, abort))
+    val gcRootModule: Option[ModuleBase] = {
+      val gcRootPath = ReflectionUtil.getStringLiteral(c)(gcRoot.tree)
+      if (gcRootPath == "") {
+        None
+      } else {
+        Some(constructClass[ModuleBase](gcRootPath, abort))
+      }
     }
 
-    val requirementsPath = ReflectionUtil.getStringLiteral(c)(requirements.tree)
-
-    val requirementsModule = if (requirementsPath == "") {
-      None
-    } else {
-      Some(constructClass[ModuleRequirements](requirementsPath, abort))
+    val requirementsModule = {
+      val requirementsPath = ReflectionUtil.getStringLiteral(c)(requirements.tree)
+      if (requirementsPath == "") {
+        None
+      } else {
+        Some(constructClass[ModuleRequirements](requirementsPath, abort))
+      }
     }
 
     val activationsVals = ReflectionUtil.getStringLiteral(c)(activations.tree).split(',').toSeq.filter(_.nonEmpty)
@@ -126,20 +134,24 @@ object StaticPluginCheckerMacro {
       loadedPlugins = loadedPlugins,
       configModule = configModule,
       additional = Module.empty,
-      root = gcRootModule,
+      rootModule = gcRootModule,
       moduleRequirements = requirementsModule,
       activations = activationsVals,
       abort = abort,
     )
 
-    c.universe.reify(())
+    import c.universe._
+
+    val pluginsList: List[c.Tree] = StaticPluginLoaderMacro.instantiatePluginsInCode(c)(loadedPlugins)
+
+    c.Expr[Unit](q"{ lazy val _ = $pluginsList ; () }")
   }
 
   def check(
     loadedPlugins: Seq[PluginBase],
     configModule: Option[AppConfigModule],
     additional: ModuleBase,
-    root: Option[ModuleBase],
+    rootModule: Option[ModuleBase],
     moduleRequirements: Option[ModuleRequirements],
     activations: Seq[String],
     abort: String => Unit,
@@ -150,7 +162,7 @@ object StaticPluginCheckerMacro {
       make[AppConfig].from(AppConfig(ConfigFactory.empty()))
     })
 
-    val module = config overridenBy SimplePluginMergeStrategy.merge(loadedPlugins :+ additional.morph[PluginBase] :+ root.toList.merge.morph[PluginBase])
+    val module = config overridenBy SimplePluginMergeStrategy.merge(loadedPlugins :+ additional.morph[PluginBase] :+ rootModule.toList.merge.morph[PluginBase])
 
     val logger = IzLogger.NullLogger
 
@@ -162,12 +174,12 @@ object StaticPluginCheckerMacro {
       new RoleAppActivationParser.Impl(logger).parseActivation(activations.map(_.split2(':')), activationInfo)
     }
 
-    val finalPlan = injector.plan(PlannerInput(module, activation, root.fold(Set.empty[DIKey])(_.keys))).locateImports(bootstrap)
-    val imports = finalPlan.unresolvedImports.left.getOrElse(Seq.empty).filter {
+    val rootKeys = rootModule.fold(Set.empty[DIKey])(_.keys)
+    val finalPlan = injector.plan(PlannerInput(module, activation, rootKeys)).locateImports(bootstrap)
+    val imports = finalPlan.unresolvedImports.left.toOption.getOrElse(Seq.empty).filter {
       case i if moduleRequirements.fold(false)(_.requiredKeys contains i.target) => false
       case _ => true
     }
-    import izumi.fundamentals.platform.strings.IzString._
 
     if (imports.nonEmpty)
       abort(
diff --git a/fundamentals/fundamentals-language/src/main/scala/izumi/fundamentals/platform/language/SourceFilePositionMaterializer.scala b/fundamentals/fundamentals-language/src/main/scala/izumi/fundamentals/platform/language/SourceFilePositionMaterializer.scala
index c063563d9..58a8da16a 100644
--- a/fundamentals/fundamentals-language/src/main/scala/izumi/fundamentals/platform/language/SourceFilePositionMaterializer.scala
+++ b/fundamentals/fundamentals-language/src/main/scala/izumi/fundamentals/platform/language/SourceFilePositionMaterializer.scala
@@ -13,6 +13,8 @@ object SourceFilePositionMaterializer {
   object SourcePositionMaterializerMacro {
 
     // scalactic.source.Position does all that manually and uses `setType`s to avoid retypechecking, oh well might as well...
+    // PS: To ensure that retypechecking does not happen, put a known bad type into `setType` and watch compiler crash.
+    //     If it _doesn't_ crash, that means one of the tree nodes was missing a `setType`
 
     def literal(c: blackbox.Context)(tpe: c.Type)(a: Any): c.universe.Literal = {
       c.internal.setType(c.universe.Literal(c.universe.Constant(a)), tpe)
diff --git a/project/Versions.scala b/project/Versions.scala
index 05a798371..56d3346e7 100644
--- a/project/Versions.scala
+++ b/project/Versions.scala
@@ -9,7 +9,7 @@ object V {
   val silencer = "1.7.1"
   val neme_plugin = "0.0.5"
 
-  val scalatest = "3.2.1"
+  val scalatest = "3.2.2"
 
   val cats = "2.1.1"
   val cats_effect = "2.1.4"
@@ -31,7 +31,7 @@ object V {
   val scalamock = "5.0.0"
 
   // java-only dependencies below
-  val classgraph = "4.8.87"
+  val classgraph = "4.8.88"
   val slf4j = "1.7.30"
   val typesafe_config = "1.4.0"
 


Test patch:
diff --git a/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/PluginLoaderTest.scala b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/PluginLoaderTest.scala
index ce44f886c..99e0a9755 100644
--- a/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/PluginLoaderTest.scala
+++ b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/PluginLoaderTest.scala
@@ -1,6 +1,6 @@
 package com.github.pshirshov.test
 
-import com.github.pshirshov.test.plugins.{DependingPlugin, EmptyTestPlugin, StaticTestPlugin}
+import com.github.pshirshov.test.plugins.{DependingPlugin, EmptyTestPlugin, ObjectTestPlugin, StaticTestPlugin}
 import distage.plugins.PluginLoader
 import izumi.distage.plugins.PluginConfig
 import org.scalatest.wordspec.AnyWordSpec
@@ -8,7 +8,7 @@ import org.scalatest.wordspec.AnyWordSpec
 class PluginLoaderTest extends AnyWordSpec {
   "Load plugins list at runtime time" in {
     val plugins = PluginLoader().load(PluginConfig.packages(Seq("com.github.pshirshov.test.plugins")))
-    assert(plugins.size == 5)
+    assert(plugins.size == 6)
     assert(
       plugins.map(_.getClass).toSet == Set(
         EmptyTestPlugin.getClass,
@@ -16,6 +16,7 @@ class PluginLoaderTest extends AnyWordSpec {
         classOf[DependingPlugin],
         classOf[DependingPlugin.NestedDoublePlugin],
         DependingPlugin.NestedDoublePlugin.getClass,
+        ObjectTestPlugin.getClass,
       )
     )
   }
diff --git a/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/DependingPlugin.scala b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/DependingPlugin.scala
new file mode 100644
index 000000000..9100d544c
--- /dev/null
+++ b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/DependingPlugin.scala
@@ -0,0 +1,17 @@
+package com.github.pshirshov.test.plugins
+
+import com.github.pshirshov.test.sneaky.SneakyPlugin
+import izumi.fundamentals.platform.build.ExposedTestScope
+
+@ExposedTestScope
+class DependingPlugin extends SneakyPlugin {
+  make[String].from((_: TestDep, _: TestDep3, _: TestService) => "abc")
+}
+
+@ExposedTestScope
+object DependingPlugin extends App {
+  class NestedDoublePlugin extends SneakyPlugin
+  object NestedDoublePlugin extends SneakyPlugin {
+    make[TestDepending]
+  }
+}
diff --git a/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/EmptyTestPlugin.scala b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/EmptyTestPlugin.scala
new file mode 100644
index 000000000..8899c0e29
--- /dev/null
+++ b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/EmptyTestPlugin.scala
@@ -0,0 +1,5 @@
+package com.github.pshirshov.test.plugins
+
+import com.github.pshirshov.test.sneaky.SneakyPlugin
+
+object EmptyTestPlugin extends SneakyPlugin
diff --git a/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/Fixture.scala b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/Fixture.scala
new file mode 100644
index 000000000..3ceef0e3d
--- /dev/null
+++ b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/Fixture.scala
@@ -0,0 +1,28 @@
+package com.github.pshirshov.test.plugins
+
+import izumi.fundamentals.platform.build.ExposedTestScope
+import logstage.IzLogger
+
+@ExposedTestScope
+class TestDepending(val testDep: TestDep, val testService: TestService)
+
+@ExposedTestScope
+trait TestDep
+
+@ExposedTestScope
+class TestDep1 extends TestDep
+
+@ExposedTestScope
+class TestDep2 extends TestDep
+
+@ExposedTestScope
+class TestDep3 extends TestDep
+
+@ExposedTestScope
+class TestService(
+  val testConf: TestConf,
+  val log: IzLogger,
+)
+
+@ExposedTestScope
+final case class TestConf(int: Int)
diff --git a/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/ObjectTestPlugin.scala b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/ObjectTestPlugin.scala
new file mode 100644
index 000000000..a6e0af716
--- /dev/null
+++ b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/ObjectTestPlugin.scala
@@ -0,0 +1,7 @@
+package com.github.pshirshov.test.plugins
+
+import com.github.pshirshov.test.sneaky.SneakyPlugin
+
+object ObjectTestPlugin extends SneakyPlugin {
+  make[TestDep3]
+}
diff --git a/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/StaticTestPlugin.scala b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/StaticTestPlugin.scala
index 22755b115..bc040100f 100644
--- a/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/StaticTestPlugin.scala
+++ b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/plugins/StaticTestPlugin.scala
@@ -1,18 +1,15 @@
 package com.github.pshirshov.test.plugins
 
 import com.github.pshirshov.test.sneaky.SneakyPlugin
-import distage.{Axis, ModuleDef}
+import distage.Axis
 import izumi.distage.config.ConfigModuleDef
 import izumi.fundamentals.platform.build.ExposedTestScope
-import izumi.logstage.api.IzLogger
 
 object Test extends Axis {
   case object X extends AxisValueDef
   case object Y extends AxisValueDef
 }
 
-object EmptyTestPlugin extends SneakyPlugin
-
 @ExposedTestScope
 class StaticTestPlugin extends SneakyPlugin with ConfigModuleDef {
   make[TestDep].tagged(Test.X).from[TestDep1]
@@ -20,40 +17,3 @@ class StaticTestPlugin extends SneakyPlugin with ConfigModuleDef {
   make[TestService]
   make[TestConf].fromConfig("test.testconf")
 }
-
-@ExposedTestScope
-class DependingPlugin extends SneakyPlugin {
-  include(DependingPlugin.module)
-  make[TestDepending]
-}
-
-@ExposedTestScope
-object DependingPlugin {
-  val module: ModuleDef = new ModuleDef {
-    // FIXME: make[] redundant generator ???
-    make[Unit].from((_: TestDep, _: TestService) => ()) // FIXME: Provider equals generates conflicting exception on merge
-  }
-  class NestedDoublePlugin extends SneakyPlugin
-  object NestedDoublePlugin extends SneakyPlugin
-}
-
-@ExposedTestScope
-class TestDepending(val testDep: TestDep, val testService: TestService)
-
-@ExposedTestScope
-trait TestDep
-
-@ExposedTestScope
-class TestDep1 extends TestDep
-
-@ExposedTestScope
-class TestDep2 extends TestDep
-
-@ExposedTestScope
-class TestService(
-  val testConf: TestConf,
-  val log: IzLogger,
-)
-
-@ExposedTestScope
-final case class TestConf(int: Int)
diff --git a/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/sneaky/SneakyPlugin.scala b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/sneaky/SneakyPlugin.scala
index 2a4af83d8..3a8b5e8fe 100644
--- a/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/sneaky/SneakyPlugin.scala
+++ b/distage/distage-extension-plugins/src/test/scala/com/github/pshirshov/test/sneaky/SneakyPlugin.scala
@@ -1,10 +1,11 @@
 package com.github.pshirshov.test.sneaky
 
-import izumi.distage.plugins.PluginDef
+import izumi.distage.model.definition.ModuleDef
+import izumi.distage.plugins.{ForcedRecompilationToken, PluginBase}
 import izumi.fundamentals.platform.build.ExposedTestScope
 
 /**
   * This is just to verify that plugin enumerator picks up transitively inherited plugins
   */
 @ExposedTestScope
-trait SneakyPlugin extends PluginDef
+abstract class SneakyPlugin[T <: String](implicit val ev: ForcedRecompilationToken[T]) extends PluginBase with ModuleDef
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
index 94c301a13..269be5b0e 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
@@ -83,7 +83,7 @@ class RoleAppTest extends AnyWordSpec with WithProperties {
           override protected def pluginConfig: PluginConfig = {
             PluginConfig.const(
               Seq(
-                new ResourcesPluginBase().morph[PluginBase],
+                new ResourcesPluginBase {}.morph[PluginBase],
                 new ConflictPlugin,
                 new TestPlugin,
                 new AdoptedAutocloseablesCasePlugin,
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/StaticPluginCheckerTest.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/StaticPluginCheckerTest.scala
index 6fe246c0b..820dd583a 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/StaticPluginCheckerTest.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/StaticPluginCheckerTest.scala
@@ -1,9 +1,9 @@
 package izumi.distage.roles.test
 
-import izumi.distage.staticinjector.plugins.{LogstageModuleRequirements, StaticPluginChecker}
 import com.github.pshirshov.test.plugins.{DependingPlugin, StaticTestPlugin}
-import org.scalatest.wordspec.AnyWordSpec
+import izumi.distage.staticinjector.plugins.{LogstageModuleRequirements, StaticPluginChecker}
 import org.scalatest.exceptions.TestFailedException
+import org.scalatest.wordspec.AnyWordSpec
 
 class StaticPluginCheckerTest extends AnyWordSpec {
 
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/StaticPluginScannerTest.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/StaticPluginLoaderTest.scala
similarity index 72%
rename from distage/distage-framework/src/test/scala/izumi/distage/roles/test/StaticPluginScannerTest.scala
rename to distage/distage-framework/src/test/scala/izumi/distage/roles/test/StaticPluginLoaderTest.scala
index 2440b85f5..83f9cbef8 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/StaticPluginScannerTest.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/StaticPluginLoaderTest.scala
@@ -1,17 +1,17 @@
 package izumi.distage.roles.test
 
-import com.github.pshirshov.test.plugins.{DependingPlugin, EmptyTestPlugin, StaticTestPlugin}
-import izumi.distage.plugins.{PluginConfig, StaticPluginScanner}
+import com.github.pshirshov.test.plugins.{DependingPlugin, EmptyTestPlugin, ObjectTestPlugin, StaticTestPlugin}
+import izumi.distage.plugins.{PluginConfig, StaticPluginLoader}
 import izumi.distage.plugins.load.PluginLoader
 import org.scalatest.wordspec.AnyWordSpec
 
-class StaticPluginScannerTest extends AnyWordSpec {
+class StaticPluginLoaderTest extends AnyWordSpec {
 
   "Static plugin scanner" should {
 
     "Prepopulate plugins list in compile time" in {
-      val plugins = StaticPluginScanner.staticallyAvailablePlugins("com.github.pshirshov.test.plugins")
-      assert(plugins.size == 5)
+      val plugins = StaticPluginLoader.staticallyAvailablePlugins("com.github.pshirshov.test.plugins")
+      assert(plugins.size == 6)
       assert(
         plugins.map(_.getClass).toSet == Set(
           EmptyTestPlugin.getClass,
@@ -19,13 +19,14 @@ class StaticPluginScannerTest extends AnyWordSpec {
           classOf[DependingPlugin],
           classOf[DependingPlugin.NestedDoublePlugin],
           DependingPlugin.NestedDoublePlugin.getClass,
+          ObjectTestPlugin.getClass,
         )
       )
     }
 
     "Prepopulate plugins list in compile time (PluginConfig)" in {
       val plugins = PluginLoader().load(PluginConfig.staticallyAvailablePlugins("com.github.pshirshov.test.plugins"))
-      assert(plugins.size == 5)
+      assert(plugins.size == 6)
       assert(
         plugins.map(_.getClass).toSet == Set(
           EmptyTestPlugin.getClass,
@@ -33,6 +34,7 @@ class StaticPluginScannerTest extends AnyWordSpec {
           classOf[DependingPlugin],
           classOf[DependingPlugin.NestedDoublePlugin],
           DependingPlugin.NestedDoublePlugin.getClass,
+          ObjectTestPlugin.getClass,
         )
       )
     }
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/ResourcesPlugin.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/ResourcesPlugin.scala
index 10de9a263..fbe817d88 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/ResourcesPlugin.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/ResourcesPlugin.scala
@@ -18,7 +18,7 @@ class ConflictPlugin extends PluginDef {
   make[UnsolvableConflict].from[UnsolvableConflict2]
 }
 
-class ResourcesPluginBase extends ModuleDef {
+trait ResourcesPluginBase extends ModuleDef {
   make[ExecutorService].from(Executors.newCachedThreadPool())
   make[IntegrationResource1[IO]]
   make[JustResource1[IO]]
@@ -34,7 +34,7 @@ class ResourcesPluginBase extends ModuleDef {
     .ref[JustResource3[IO]]
 }
 
-class ResourcesPlugin extends ResourcesPluginBase with PluginDef {
+class ResourcesPlugin extends PluginDef with ResourcesPluginBase {
   make[XXX_ResourceEffectsRecorder[IO]]
 
   make[IntegrationResource0[IO]]
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
index 39348bca5..1450e75a0 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
@@ -15,7 +15,7 @@ import izumi.distage.roles.test.fixtures.roles.TestRole00.{IntegrationOnlyCfg, I
 import izumi.fundamentals.platform.resources.ArtifactVersion
 import izumi.reflect.TagK
 
-class TestPluginBase[F[_]: TagK] extends CatsDIEffectModule with PluginDef with ConfigModuleDef with RoleModuleDef {
+class TestPluginBase[F[_]: TagK] extends PluginDef with CatsDIEffectModule with ConfigModuleDef with RoleModuleDef {
   tag(Env.Prod)
 
   private def version = Option(System.getProperty(TestPlugin.versionProperty)) match {
diff --git a/doc/microsite/src/main/tut/distage/distage-testkit.md b/doc/microsite/src/main/tut/distage/distage-testkit.md
index ca96de72d..b0e54a0e5 100644
--- a/doc/microsite/src/main/tut/distage/distage-testkit.md
+++ b/doc/microsite/src/main/tut/distage/distage-testkit.md
@@ -335,6 +335,10 @@ Our demonstration application will use the
 @scaladoc[StandardAxis.Repo](izumi.distage.model.definition.StandardAxis$$Repo$) `Dummy` and `Prod`
 tags:
 
+```scala mdoc:invisible
+implicit def _hack_whyDoesItNotWorkInMdocHuh_forcedRecompilationToken: izumi.distage.plugins.ForcedRecompilationToken["abc"] = null
+```
+
 ```scala mdoc:fakepackage:to-string
 "fakepackage app": Unit