Repository: 7mind/izumi
Pull request #1255 (2020-09-29 22:04:26)

Update scalafmt-core to 2.7.3
Updates [org.scalameta:scalafmt-core](https://github.com/scalameta/scalafmt) from 2.7.2 to 2.7.3.
[GitHub Release Notes](https://github.com/scalameta/scalafmt/releases/tag/v2.7.3) - [Version Diff](https://github.com/scalameta/scalafmt/compare/v2.7.2...v2.7.3)

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/3569d798765e3b78dd6e5137daa8656746901411/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 = "org.scalameta", artifactId = "scalafmt-core" } ]
```
</details>

labels: library-update, semver-patch


Patch:
diff --git a/.scalafmt.conf b/.scalafmt.conf
index 5a105293e..03718611b 100644
--- a/.scalafmt.conf
+++ b/.scalafmt.conf
@@ -1,4 +1,4 @@
-version = "2.7.2"
+version = "2.7.3"
 project.git = true
 
 maxColumn = 170
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/DebugProperties.scala b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/DebugProperties.scala
index 02ace7eb0..9ed30dcda 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/DebugProperties.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/DebugProperties.scala
@@ -9,5 +9,5 @@ import izumi.fundamentals.platform.properties
   */
 object DebugProperties extends properties.DebugProperties {
   final val `izumi.debug.macro.distage.constructors` = BoolProperty("izumi.debug.macro.distage.constructors")
-  final val `izumi.debug.macro.distage.providermagnet` = BoolProperty("izumi.debug.macro.distage.providermagnet")
+  final val `izumi.debug.macro.distage.functoid` = BoolProperty("izumi.debug.macro.distage.functoid")
 }
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/constructors.scala b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/constructors.scala
index a1bc20875..28ba6049d 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/constructors.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/constructors.scala
@@ -62,8 +62,8 @@ private[constructors] sealed trait AnyConstructorOptionalMakeDSL[T] extends Any
 object AnyConstructorOptionalMakeDSL {
   private[constructors] final class Impl[T](val provider: Functoid[T]) extends AnyVal with AnyConstructorOptionalMakeDSL[T]
 
-  @inline def apply[T](providerMagnet: Functoid[T]): AnyConstructorOptionalMakeDSL.Impl[T] = {
-    new AnyConstructorOptionalMakeDSL.Impl[T](providerMagnet)
+  @inline def apply[T](functoid: Functoid[T]): AnyConstructorOptionalMakeDSL.Impl[T] = {
+    new AnyConstructorOptionalMakeDSL.Impl[T](functoid)
   }
 
   def errorConstructor[T](tpe: String, nonWhitelistedMethods: List[String]): AnyConstructorOptionalMakeDSL.Impl[T] = {
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/ClassConstructorMacro.scala b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/ClassConstructorMacro.scala
index 940614b63..491032ccd 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/ClassConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/ClassConstructorMacro.scala
@@ -18,14 +18,14 @@ object ClassConstructorMacro {
 
     targetType match {
       case t: SingletonTypeApi =>
-        val providerMagnet = symbolOf[Functoid.type].asClass.module
+        val functoid = symbolOf[Functoid.type].asClass.module
         val term = t match {
           case t: ThisTypeApi => This(t.sym)
           case t: ConstantTypeApi => q"${t.value}"
           case _ => q"${t.termSymbol}"
         }
         c.Expr[ClassConstructor[T]] {
-          q"{ new ${weakTypeOf[ClassConstructor[T]]}($providerMagnet.singleton[$targetType]($term)) }"
+          q"{ new ${weakTypeOf[ClassConstructor[T]]}($functoid.singleton[$targetType]($term)) }"
         }
 
       case _ =>
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/definition/dsl/AbstractBindingDefDSL.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/definition/dsl/AbstractBindingDefDSL.scala
index 6acf36a6f..a91bce994 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/definition/dsl/AbstractBindingDefDSL.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/definition/dsl/AbstractBindingDefDSL.scala
@@ -191,15 +191,15 @@ object AbstractBindingDefDSL {
           // after first `aliased` no more changes are possible
           val newRef = SingletonBinding(key, ImplDef.ReferenceImpl(b.implementation.implType, b.key, weak = false), b.tags, pos)
           refs = newRef :: refs
-        case Modify(providerMagnetModifier) =>
+        case Modify(functoidModifier) =>
           b.implementation match {
             case ImplDef.ProviderImpl(implType, function) =>
-              val newProvider = providerMagnetModifier(Functoid(function)).get
+              val newProvider = functoidModifier(Functoid(function)).get
               if (newProvider.ret <:< implType) {
                 b = b.withImplDef(ImplDef.ProviderImpl(implType, newProvider))
               } else {
                 throw new InvalidFunctoidModifier(
-                  s"Cannot apply invalid Functoid modifier $providerMagnetModifier, new return type `${newProvider.ret}` is not a subtype of the old return type `${function.ret}`"
+                  s"Cannot apply invalid Functoid modifier $functoidModifier, new return type `${newProvider.ret}` is not a subtype of the old return type `${function.ret}`"
                 )
               }
             case _ => ()
@@ -307,7 +307,7 @@ object AbstractBindingDefDSL {
     final case class SetId(id: Identifier) extends SingletonInstruction
     final case class SetIdFromImplName() extends SingletonInstruction
     final case class AliasTo(key: DIKey.BasicKey, pos: SourceFilePosition) extends SingletonInstruction
-    final case class Modify[T](providerMagnetModifier: Functoid[T] => Functoid[T]) extends SingletonInstruction
+    final case class Modify[T](functoidModifier: Functoid[T] => Functoid[T]) extends SingletonInstruction
   }
 
   sealed trait SetInstruction
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/exceptions/IncompatibleEffectTypesException.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/exceptions/IncompatibleEffectTypesException.scala
index 069c42aff..c9ebea8b1 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/exceptions/IncompatibleEffectTypesException.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/exceptions/IncompatibleEffectTypesException.scala
@@ -1,13 +1,21 @@
 package izumi.distage.model.exceptions
 
+import izumi.distage.model.plan.ExecutableOp.MonadicOp
 import izumi.distage.model.reflection.SafeType
 
-class IncompatibleEffectTypesException(val provisionerEffectType: SafeType, val actionEffectType: SafeType)
-  extends DIException(
-    s"""Incompatible effect types: Can't execute effect in `$actionEffectType` which is neither Identity, nor a subtype of the effect that Provisioner was launched in: `$provisionerEffectType`
+class IncompatibleEffectTypesException(val op: MonadicOp, val provisionerEffectType: SafeType, val actionEffectType: SafeType)
+  extends DIException(IncompatibleEffectTypesException.format(op, provisionerEffectType, actionEffectType))
+
+object IncompatibleEffectTypesException {
+  def format(op: MonadicOp, provisionerEffectType: SafeType, actionEffectType: SafeType): String = {
+    s"""Incompatible effect types when trying to execute operation:
+       |
+       |  - $op
+       |
+       |Can't execute an effect in `$actionEffectType` which is neither equivalent to `izumi.fundamentals.platform.Identity`, nor a subtype of the Injector's effect type: `$provisionerEffectType`
        |
-       |Clarification:
-       |  - To execute `.fromEffect` and `.fromResource` bindings for effects other than `Identity` you need to create `Injector` method with the F type corresponding to the type of effects/resources you inject, as in `Injector[F]()`
-       |  - Subtype type constructors are allowed. e.g. when using ZIO you can execute actions with type IO[Nothing, ?] when running in `Injector[IO[Throwable, ?]]()`
-   """.stripMargin
-  )
+       |  - To execute `make[_].fromEffect` and `make[_].fromResource` bindings for effects other than `Identity`, you must parameterize the `Injector` with the corresponding effect type when creating it, as in `Injector[F]()`.
+       |  - Subtype type constructors are allowed. e.g. when using ZIO you can execute effects in `IO[Nothing, ?]` when using an `Injector[IO[Throwable, ?]]()`.
+       """.stripMargin
+  }
+}
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/impl/OrderedPlanOps.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/impl/OrderedPlanOps.scala
index 855b54ac9..ca84fb49a 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/impl/OrderedPlanOps.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/impl/OrderedPlanOps.scala
@@ -2,20 +2,25 @@ package izumi.distage.model.plan.impl
 
 import izumi.distage.model.Locator
 import izumi.distage.model.definition.Identifier
-import izumi.distage.model.exceptions.{InvalidPlanException, MissingInstanceException}
+import izumi.distage.model.effect.DIEffect
+import izumi.distage.model.exceptions.{IncompatibleEffectTypesException, InvalidPlanException, MissingInstanceException}
 import izumi.distage.model.plan.ExecutableOp.ProxyOp.{InitProxy, MakeProxy}
-import izumi.distage.model.plan.ExecutableOp.{ImportDependency, ProxyOp, SemiplanOp}
+import izumi.distage.model.plan.ExecutableOp.{ImportDependency, MonadicOp, ProxyOp, SemiplanOp}
 import izumi.distage.model.plan.{OrderedPlan, Roots, SemiPlan}
-import izumi.distage.model.reflection._
 import izumi.distage.model.recursive.LocatorRef
-import izumi.reflect.Tag
+import izumi.distage.model.reflection.{DIKey, _}
+import izumi.fundamentals.collections.nonempty.NonEmptyList
+import izumi.reflect.{Tag, TagK}
 
 private[plan] trait OrderedPlanOps extends Any { this: OrderedPlan =>
 
   /**
-    * Check for any unresolved dependencies, if this
-    * returns Right then the wiring is generally correct,
-    * modulo runtime exceptions in constructors,
+    * Check for any unresolved dependencies,
+    * or for any `make[_].fromEffect` or `make[_].fromResource` bindings that are incompatible with the passed `F`,
+    * or for any other issue that would cause [[izumi.distage.model.Injector#produce(plan:OrderedPlan)* Injector.produce]] to fail
+    *
+    * If this returns `F.unit` then the wiring is generally correct,
+    * modulo runtime exceptions in user code,
     * and `Injector.produce` should succeed.
     *
     * However, presence of imports does not *always* mean
@@ -23,40 +28,82 @@ private[plan] trait OrderedPlanOps extends Any { this: OrderedPlan =>
     * `Locator`, by BootstrapContext, or they may be materialized by
     * a custom [[izumi.distage.model.provisioning.strategies.ImportStrategy]]
     *
-    * @return this plan or a list of unresolved parameters
+    * An effect is compatible if it's a subtype of `F` or is a type equivalent to [[izumi.fundamentals.platform.functional.Identity]] (e.g. `cats.Id`)
+    *
+    * Will `F.fail` the effect with [[izumi.distage.model.exceptions.InvalidPlanException]] if there are issues.
+    *
+    * @tparam F effect type to check against
     */
-  final def unresolvedImports: Either[Seq[ImportDependency], OrderedPlan] = {
-    val nonMagicImports = getImports.filter {
-      case i if i.target == DIKey.get[LocatorRef] =>
-        false
-      case _ =>
-        true
-    }
-    if (nonMagicImports.isEmpty) Right(this) else Left(nonMagicImports)
+  final def assertValid[F[_]: DIEffect: TagK](): F[Unit] = {
+    DIEffect[F].maybeSuspend(assertValidOrThrow[F]())
+  }
+
+  /**
+    * Same as [[assertValid]], but throws an [[izumi.distage.model.exceptions.InvalidPlanException]] if there are unresolved imports
+    *
+    * @throws izumi.distage.model.exceptions.InvalidPlanException if there are issues
+    */
+  final def assertValidOrThrow[F[_]: TagK](): Unit = {
+    isValid.fold(())(throw _)
   }
 
   /** Same as [[unresolvedImports]], but returns a pretty-printed exception if there are unresolved imports */
-  final def assertImportsResolved: Either[InvalidPlanException, OrderedPlan] = {
+  final def isValid[F[_]: TagK]: Option[InvalidPlanException] = {
     import izumi.fundamentals.platform.strings.IzString._
-    unresolvedImports.left.map {
-      unresolved =>
-        new InvalidPlanException(unresolved.map(op => MissingInstanceException.format(op.target, op.references)).niceList(shift = ""))
-    }
+    val unresolved = unresolvedImports.fromNonEmptyList.map(op => MissingInstanceException.format(op.target, op.references))
+    val effects = incompatibleEffectType[F].fromNonEmptyList.map(op => IncompatibleEffectTypesException.format(op, SafeType.getK[F], op.effectHKTypeCtor))
+    for {
+      allErrors <- NonEmptyList.from(unresolved ++ effects)
+    } yield new InvalidPlanException(allErrors.toList.niceList(shift = ""))
+  }
+
+  /**
+    * Check for any unresolved dependencies.
+    *
+    * If this returns `None` then the wiring is generally correct,
+    * modulo runtime exceptions in user code,
+    * and `Injector.produce` should succeed.
+    *
+    * However, presence of imports does not *always* mean
+    * that a plan is invalid, imports may be fulfilled by a parent
+    * `Locator`, by BootstrapContext, or they may be materialized by
+    * a custom [[izumi.distage.model.provisioning.strategies.ImportStrategy]]
+    *
+    * @return a non-empty list of unresolved imports if present
+    */
+  final def unresolvedImports: Option[NonEmptyList[ImportDependency]] = {
+    val locatorRefKey = DIKey[LocatorRef]
+    val nonMagicImports = steps
+      .iterator.collect {
+        case i: ImportDependency if i.target != locatorRefKey => i
+      }.toList
+    NonEmptyList.from(nonMagicImports)
   }
 
   /**
-    * Same as [[unresolvedImports]], but throws an [[izumi.distage.model.exceptions.InvalidPlanException]] if there are unresolved imports
+    * Check for any `make[_].fromEffect` or `make[_].fromResource` bindings that are incompatible with the passed `F`.
+    *
+    * An effect is compatible if it's a subtype of `F` or is a type equivalent to [[izumi.fundamentals.platform.functional.Identity]] (e.g. `cats.Id`)
     *
-    * @throws InvalidPlanException
+    * @tparam F effect type to check against
+    * @return a non-empty list of operations incompatible with `F` if present
     */
-  final def assertImportsResolvedOrThrow(): Unit = {
-    assertImportsResolved.fold(throw _, _ => ())
+  final def incompatibleEffectType[F[_]: TagK]: Option[NonEmptyList[MonadicOp]] = {
+    val effectType = SafeType.getK[F]
+    val badSteps = steps
+      .iterator.collect {
+        case op: MonadicOp if !(op.effectHKTypeCtor <:< effectType || op.effectHKTypeCtor <:< SafeType.identityEffectType) => op
+      }.toList
+    NonEmptyList.from(badSteps)
   }
 
   /**
     * Be careful, don't use this method blindly, it can disrupt graph connectivity when used improperly.
     *
     * Proper usage assume that `keys` contains complete subgraph reachable from graph roots.
+    *
+    * Note: this processes a complete plan, if you have bindings you can achieve a similar transformation before planning
+    *       by deleting the `keys` from bindings: `module -- keys`
     */
   final def replaceWithImports(keys: Set[DIKey]): OrderedPlan = {
     val newSteps = steps.flatMap {
@@ -73,9 +120,9 @@ private[plan] trait OrderedPlanOps extends Any { this: OrderedPlan =>
     }
 
     OrderedPlan(
-      newSteps,
-      declaredRoots,
-      topology.removeKeys(keys),
+      steps = newSteps,
+      declaredRoots = declaredRoots,
+      topology = topology.removeKeys(keys),
     )
   }
 
@@ -114,4 +161,12 @@ private[plan] trait OrderedPlanOps extends Any { this: OrderedPlan =>
     }
     SemiPlan(safeSteps.toVector, Roots(declaredRoots))
   }
+
+  @deprecated("Renamed to `assertValidPlanOrThrow`", "0.11")
+  final def assertImportsResolvedOrThrow[F[_]: TagK](): Unit = assertValidOrThrow[F]()
+
+  /** Same as [[unresolvedImports]], but returns a pretty-printed exception if there are unresolved imports */
+  @deprecated("Renamed to `isValidPlan`", "0.11")
+  final def assertImportsResolved[F[_]: TagK]: Option[InvalidPlanException] = isValid[F]
+
 }
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/providers/package.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/providers/package.scala
index 1557a44de..7f00fd38c 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/providers/package.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/providers/package.scala
@@ -1,9 +1,9 @@
 package izumi.distage.model
 
 package object providers {
-  @deprecated("Use `distage.Functoid` instead of ProviderMagnet", "0.11.0")
+  @deprecated("Use `distage.Functoid` instead of `distage.ProviderMagnet`", "0.11.0")
   type ProviderMagnet[+A] = Functoid[A]
 
-  @deprecated("Use `distage.Functoid` instead of ProviderMagnet", "0.11.0")
+  @deprecated("Use `distage.Functoid` instead of `distage.ProviderMagnet`", "0.11.0")
   final val ProviderMagnet: Functoid.type = Functoid
 }
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/reflection/DIKey.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/reflection/DIKey.scala
index 51097fe4b..d4393ea21 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/reflection/DIKey.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/reflection/DIKey.scala
@@ -17,8 +17,8 @@ sealed trait DIKey {
 }
 
 object DIKey {
-  def apply[T: Tag]: DIKey = TypeKey(SafeType.get[T])
-  def apply[T: Tag](id: Identifier): DIKey = DIKey.get[T].named(id)
+  def apply[T: Tag]: DIKey = DIKey.TypeKey(SafeType.get[T])
+  def apply[T: Tag](id: Identifier): DIKey = DIKey.IdKey(SafeType.get[T], id.id)(id.idContract)
 
   def get[T: Tag]: DIKey.TypeKey = DIKey.TypeKey(SafeType.get[T])
 
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/reflection/SafeType.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/reflection/SafeType.scala
index 29a1ef0d7..71c8f4ff0 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/reflection/SafeType.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/reflection/SafeType.scala
@@ -8,8 +8,9 @@ final case class SafeType private (
   tag: LightTypeTag,
   /*private[distage] val */ cls: Class[_],
 ) {
-  override def hashCode: Int = tag.hashCode()
   override def toString: String = tag.repr
+
+  override def hashCode: Int = tag.hashCode()
   override def equals(obj: Any): Boolean = {
     obj match {
       case that: SafeType =>
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/reflection/macros/FunctoidMacro.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/reflection/macros/FunctoidMacro.scala
index 11daa28ff..3819d2d65 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/reflection/macros/FunctoidMacro.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/reflection/macros/FunctoidMacro.scala
@@ -12,10 +12,10 @@ import izumi.fundamentals.reflection.TrivialMacroLogger
 import scala.reflect.macros.blackbox
 
 /**
-  * To see macro debug output during compilation, set `-Dizumi.debug.macro.distage.providermagnet=true` java property!
+  * To see macro debug output during compilation, set `-Dizumi.debug.macro.distage.functoid=true` java property!
   *
   * {{{
-  *   sbt -Dizumi.debug.macro.distage.providermagnet=true compile
+  *   sbt -Dizumi.debug.macro.distage.functoid=true compile
   * }}}
   *
   * @see [[izumi.distage.constructors.DebugProperties]]
@@ -24,7 +24,7 @@ class FunctoidMacro(val c: blackbox.Context) {
 
   final val macroUniverse: Aux[c.universe.type] = StaticDIUniverse(c)
 
-  private final val logger = TrivialMacroLogger.make[this.type](c, DebugProperties.`izumi.debug.macro.distage.providermagnet`.name)
+  private final val logger = TrivialMacroLogger.make[this.type](c, DebugProperties.`izumi.debug.macro.distage.functoid`.name)
   private final val reflectionProvider = ReflectionProviderDefaultImpl(macroUniverse)
 
   import c.universe._
diff --git a/distage/distage-core/src/main/scala/distage/Distage.scala b/distage/distage-core/src/main/scala/distage/Distage.scala
index ab2123ad6..2276d7abd 100644
--- a/distage/distage-core/src/main/scala/distage/Distage.scala
+++ b/distage/distage-core/src/main/scala/distage/Distage.scala
@@ -70,9 +70,9 @@ trait Distage {
   type HasConstructor[T] = constructors.HasConstructor[T]
   val HasConstructor: constructors.HasConstructor.type = constructors.HasConstructor
 
-  @deprecated("Use `distage.Functoid` instead of ProviderMagnet", "0.11.0")
+  @deprecated("Use `distage.Functoid` instead of `distage.ProviderMagnet`", "0.11.0")
   type ProviderMagnet[+A] = Functoid[A]
-  @deprecated("Use `distage.Functoid` instead of ProviderMagnet", "0.11.0")
+  @deprecated("Use `distage.Functoid` instead of `distage.ProviderMagnet`", "0.11.0")
   val ProviderMagnet: model.providers.Functoid.type = model.providers.Functoid
 
   @deprecated("GCMode has been renamed to `Roots`", "old name will be deleted in 0.11.1")
diff --git a/distage/distage-core/src/main/scala/distage/package.scala b/distage/distage-core/src/main/scala/distage/package.scala
index a411da399..b4fae8278 100644
--- a/distage/distage-core/src/main/scala/distage/package.scala
+++ b/distage/distage-core/src/main/scala/distage/package.scala
@@ -68,9 +68,9 @@ package object distage extends Distage {
   override type HasConstructor[T] = constructors.HasConstructor[T]
   override val HasConstructor: constructors.HasConstructor.type = constructors.HasConstructor
 
-  @deprecated("Use `distage.Functoid` instead of ProviderMagnet", "0.11.0")
+  @deprecated("Use `distage.Functoid` instead of `distage.ProviderMagnet`", "0.11.0")
   override type ProviderMagnet[+A] = Functoid[A]
-  @deprecated("Use `distage.Functoid` instead of ProviderMagnet", "0.11.0")
+  @deprecated("Use `distage.Functoid` instead of `distage.ProviderMagnet`", "0.11.0")
   override val ProviderMagnet: model.providers.Functoid.type = model.providers.Functoid
 
   @deprecated("GCMode has been renamed to `Roots`", "old name will be deleted in 0.11.1")
diff --git a/distage/distage-core/src/main/scala/izumi/distage/modules/support/AnyBIO3SupportModule.scala b/distage/distage-core/src/main/scala/izumi/distage/modules/support/AnyBIO3SupportModule.scala
index ac4d44cfc..4587714f8 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/modules/support/AnyBIO3SupportModule.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/modules/support/AnyBIO3SupportModule.scala
@@ -24,7 +24,9 @@ class AnyBIO3SupportModule[F[-_, +_, +_]: TagK3](implicit tagBIO: TagKK[F[Any, +
   include(BIO3InstancesModule[F])
   addConverted3To2[F[Any, +?, +?]]
 
-  // workaround for https://github.com/zio/izumi-reflect/issues/82 & https://github.com/zio/izumi-reflect/issues/83
+  // workaround for
+  // - https://github.com/zio/izumi-reflect/issues/82
+  // - https://github.com/zio/izumi-reflect/issues/83
   def addConverted3To2[G[+e, +a] >: F[Any, e @v, a @v] <: F[Any, e @v, a @v]: TagKK]: Unit = {
     make[BIOAsync[G]].from {
       implicit F: BIOAsync3[F] => BIORoot.BIOConvert3To2[BIOAsync3, F, Any]
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala
index c6ae63173..97fcac022 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala
@@ -86,7 +86,7 @@ class PlannerDefaultImpl(
 
     val index = completedPlan.index
 
-    val maybeBrokenLoops = new Toposort().cycleBreaking(
+    val maybeBrokenLoops = Toposort.cycleBreaking(
       predcessors = IncidenceMatrix(topology.dependencies.graph),
       break = new LoopBreaker(analyzer, mirrorProvider, index, topology, completedPlan),
     )
diff --git a/distage/distage-core/src/main/scala/izumi/distage/provisioning/PlanInterpreterDefaultRuntimeImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/provisioning/PlanInterpreterDefaultRuntimeImpl.scala
index fc0bf70f1..d4d56e2cc 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/provisioning/PlanInterpreterDefaultRuntimeImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/provisioning/PlanInterpreterDefaultRuntimeImpl.scala
@@ -136,7 +136,7 @@ class PlanInterpreterDefaultRuntimeImpl(
     }
 
     @nowarn("msg=Unused import")
-    def makeMeta() = {
+    def makeMeta(): LocatorMeta = {
       import scala.collection.compat._
       LocatorMeta(meta.view.mapValues(Duration.fromNanos).toMap)
     }
@@ -146,8 +146,11 @@ class PlanInterpreterDefaultRuntimeImpl(
     }
 
     for {
+      // do imports first before everything
       _ <- F.traverse_(imports)(processStep)
-      _ <- verifyEffectType[F](otherSteps, addFailure = f => F.maybeSuspend(mutFailures += f))
+      // verify effect type for everything else first before everything
+      _ <- verifyEffectType[F](otherSteps)(failure => F.maybeSuspend(mutFailures += failure))
+
       failedImportsOrEffects <- F.maybeSuspend(mutFailures.nonEmpty)
       immutable = mutProvisioningContext.toImmutable
       res <-
@@ -236,7 +239,7 @@ class PlanInterpreterDefaultRuntimeImpl(
     }
   }
 
-  private[this] def verifyEffectType[F[_]: TagK](ops: Vector[ExecutableOp], addFailure: ProvisioningFailure => F[Unit])(implicit F: DIEffect[F]): F[Unit] = {
+  private[this] def verifyEffectType[F[_]: TagK](ops: Vector[ExecutableOp])(addFailure: ProvisioningFailure => F[Unit])(implicit F: DIEffect[F]): F[Unit] = {
     val provisionerEffectType = SafeType.getK[F]
     val monadicOps = ops.collect { case m: MonadicOp => m }
     F.traverse_(monadicOps) {
@@ -245,7 +248,7 @@ class PlanInterpreterDefaultRuntimeImpl(
         val isEffect = actionEffectType != SafeType.identityEffectType
 
         if (isEffect && !(actionEffectType <:< provisionerEffectType)) {
-          addFailure(ProvisioningFailure(op, new IncompatibleEffectTypesException(provisionerEffectType, actionEffectType)))
+          addFailure(ProvisioningFailure(op, new IncompatibleEffectTypesException(op, provisionerEffectType, actionEffectType)))
         } else {
           F.unit
         }
diff --git a/distage/distage-core/src/main/scala/izumi/distage/provisioning/strategies/EffectStrategyDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/provisioning/strategies/EffectStrategyDefaultImpl.scala
index e9fe0a918..36731531a 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/provisioning/strategies/EffectStrategyDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/provisioning/strategies/EffectStrategyDefaultImpl.scala
@@ -23,7 +23,7 @@ class EffectStrategyDefaultImpl extends EffectStrategy {
 
     val isEffect = actionEffectType != SafeType.identityEffectType
     if (isEffect && !(actionEffectType <:< provisionerEffectType)) {
-      throw new IncompatibleEffectTypesException(provisionerEffectType, actionEffectType)
+      throw new IncompatibleEffectTypesException(op, provisionerEffectType, actionEffectType)
     }
 
     val effectKey = op.effectKey
diff --git a/distage/distage-core/src/main/scala/izumi/distage/provisioning/strategies/ResourceStrategyDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/provisioning/strategies/ResourceStrategyDefaultImpl.scala
index a04226b20..8e826ff77 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/provisioning/strategies/ResourceStrategyDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/provisioning/strategies/ResourceStrategyDefaultImpl.scala
@@ -25,7 +25,7 @@ class ResourceStrategyDefaultImpl extends ResourceStrategy {
 
     val isEffect = actionEffectType != SafeType.identityEffectType
     if (isEffect && !(actionEffectType <:< provisionerEffectType)) {
-      throw new IncompatibleEffectTypesException(provisionerEffectType, actionEffectType)
+      throw new IncompatibleEffectTypesException(op, provisionerEffectType, actionEffectType)
     }
 
     val resourceKey = op.effectKey
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 e9f97746b..c9afc8c14 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
@@ -178,7 +178,7 @@ object StaticPluginCheckerMacro {
 
     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 {
+    val imports = finalPlan.unresolvedImports.map(_.toList).getOrElse(Seq.empty).filter {
       case i if moduleRequirements.fold(false)(_.requiredKeys contains i.target) => false
       case _ => true
     }
diff --git a/doc/microsite/src/main/tut/distage/debugging.md b/doc/microsite/src/main/tut/distage/debugging.md
index a122e82a4..26574d7e1 100644
--- a/doc/microsite/src/main/tut/distage/debugging.md
+++ b/doc/microsite/src/main/tut/distage/debugging.md
@@ -5,8 +5,7 @@ Debugging
 
 ### Testing Plans
 
-Use `OrderedPlan#assertImportsResolvedOrThrow` method to test whether all dependencies in a given plan are present and the
-plan will execute correctly when passed to `Injector#produce`.
+Use `OrderedPlan#assertValid` method to test whether the plan will execute correctly when passed to `Injector#produce`.
 
 ```scala mdoc:reset:to-string
 import distage.{DIKey, Roots, ModuleDef, Injector, Activation}
@@ -16,24 +15,25 @@ class B
 
 val badModule = new ModuleDef {
   make[A]
+  make[Int].fromEffect(zio.Task { ??? })
 }
 
-val badPlan = Injector().plan(badModule, Activation.empty, Roots.Everything)
+val badPlan = Injector[cats.effect.IO]().plan(badModule, Activation.empty, Roots.Everything)
 ```
 
 ```scala mdoc:crash:to-string
-badPlan.assertImportsResolvedOrThrow()
+badPlan.assertValid[cats.effect.IO]().unsafeRunSync()
 ```
 
 ```scala mdoc:to-string
 val goodModule = new ModuleDef {
   make[A]
-  make[B]
+  make[B].fromEffect(cats.effect.IO(new B))
 }
 
-val plan = Injector().plan(goodModule, Activation.empty, Roots.Everything)
+val plan = Injector[cats.effect.IO]().plan(goodModule, Activation.empty, Roots.Everything)
 
-plan.assertImportsResolvedOrThrow()
+plan.assertValid[cats.effect.IO]().unsafeRunSync()
 ```
 
 ### Pretty-printing plans
@@ -66,7 +66,7 @@ To debug macros used by `distage` you may use the following Java Properties:
 ```bash
 sbt -Dizumi.debug.macro.rtti=true compile # fundamentals-reflection & LightTypeTag macros
 sbt -Dizumi.debug.macro.distage.constructors=true compile # izumi.distage.constructors.* macros
-sbt -Dizumi.debug.macro.distage.providermagnet=true compile # Functoid macro
+sbt -Dizumi.debug.macro.distage.functoid=true compile # Functoid macro
 ```
 
 ### Graphviz rendering
diff --git a/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/collections/nonempty/NonEmptyList.scala b/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/collections/nonempty/NonEmptyList.scala
index bbee21e62..950d8f1fb 100644
--- a/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/collections/nonempty/NonEmptyList.scala
+++ b/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/collections/nonempty/NonEmptyList.scala
@@ -1469,10 +1469,16 @@ object NonEmptyList {
     */
   def unapplySeq[T](nonEmptyList: NonEmptyList[T]): Option[Seq[T]] = Some(nonEmptyList.toList)
 
-  /*
-    // TODO: Figure out how to get case NonEmptyList() to not compile
-    def unapplySeq[T](nonEmptyList: NonEmptyList[T]): Option[(T, Seq[T])] = Some(nonEmptyList.head, nonEmptyList.tail)
-   */
+  /**
+    * Optionally construct a <code>NonEmptyList</code> containing the elements, if any, of a given <code>List</code>.
+    *
+    * @param list the <code>List</code> with which to construct a <code>NonEmptyList</code>
+    * @return a <code>NonEmptyList</code> containing the elements of the given <code>List</code>, if non-empty, wrapped in
+    *     a <code>Some</code>; else <code>None</code> if the <code>List</code> is empty
+    */
+  def from[T](list: List[T]): Option[NonEmptyList[T]] = {
+    if (list.isEmpty) None else Some(new NonEmptyList(list))
+  }
 
   /**
     * Optionally construct a <code>NonEmptyList</code> containing the elements, if any, of a given <code>Seq</code>.
@@ -1481,9 +1487,15 @@ object NonEmptyList {
     * @return a <code>NonEmptyList</code> containing the elements of the given <code>Seq</code>, if non-empty, wrapped in
     *     a <code>Some</code>; else <code>None</code> if the <code>Seq</code> is empty
     */
-  def from[T](seq: Iterable[T]): Option[NonEmptyList[T]] =
-    seq.headOption match {
-      case None => None
-      case Some(first) => Some(new NonEmptyList(first :: seq.tail.toList))
-    }
+  def from[T](seq: Iterable[T]): Option[NonEmptyList[T]] = {
+    from(seq.toList)
+  }
+
+  def from[T](iterableOnce: IterableOnce[T]): Option[NonEmptyList[T]] = {
+    from(iterableOnce.iterator.toList)
+  }
+
+  implicit final class OptionOps[A](private val option: Option[NonEmptyList[A]]) extends AnyVal {
+    def fromNonEmptyList: List[A] = if (option.isEmpty) Nil else option.get.toList
+  }
 }
diff --git a/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/graphs/Linearized.scala b/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/graphs/Linearized.scala
index 44ca9bd5b..0d7fd5322 100644
--- a/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/graphs/Linearized.scala
+++ b/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/graphs/Linearized.scala
@@ -19,7 +19,7 @@ final case class Linearized[N, +M](
 
 object Linearized {
   def fromDag[N, M](dag: DAG[N, M]): Linearized[N, M] = {
-    new Toposort().cycleBreaking(dag.predcessors, ToposortLoopBreaker.dontBreak) match {
+    Toposort.cycleBreaking(dag.predcessors, ToposortLoopBreaker.dontBreak) match {
       case Left(value) =>
         throw new IllegalStateException(s"Non-linerizable DAG, this can't be. Error: $value; $dag")
       case Right(value) =>
@@ -29,7 +29,7 @@ object Linearized {
 
   def from[N, M](dg: DG[N, M], breaker: ToposortLoopBreaker[N]): Either[ToposortError[N], Linearized[N, M]] = {
     for {
-      sorted <- new Toposort().cycleBreaking(dg.predcessors, breaker)
+      sorted <- Toposort.cycleBreaking(dg.predcessors, breaker)
     } yield {
       Linearized(sorted, dg.meta)
     }
diff --git a/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/graphs/tools/Toposort.scala b/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/graphs/tools/Toposort.scala
index da523c942..b5b1e8b0c 100644
--- a/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/graphs/tools/Toposort.scala
+++ b/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/graphs/tools/Toposort.scala
@@ -6,9 +6,9 @@ import izumi.fundamentals.graphs.struct.IncidenceMatrix
 
 import scala.annotation.{nowarn, tailrec}
 
-final class Toposort {
+object Toposort {
 
-  def cycleBreaking[T](predcessors: IncidenceMatrix[T], break: ToposortLoopBreaker[T]): Either[ToposortError[T], Seq[T]] = {
+  final def cycleBreaking[T](predcessors: IncidenceMatrix[T], break: ToposortLoopBreaker[T]): Either[ToposortError[T], Seq[T]] = {
     cycleBreaking(predcessors.links, Seq.empty, break)
   }
 
@@ -16,7 +16,6 @@ final class Toposort {
   @tailrec
   private[this] def cycleBreaking[T](predcessors: Map[T, Set[T]], done: Seq[T], break: ToposortLoopBreaker[T]): Either[ToposortError[T], Seq[T]] = {
     import scala.collection.compat._
-
     val (noPreds, hasPreds) = predcessors.partition(_._2.isEmpty)
 
     if (noPreds.isEmpty) {
@@ -48,11 +47,11 @@ final class Toposort {
     }
   }
 
-  private def isInvolvedIntoCycle[T](toPreds: Map[T, Set[T]])(key: T): Boolean = {
+  private[this] def isInvolvedIntoCycle[T](toPreds: Map[T, Set[T]])(key: T): Boolean = {
     test(toPreds, Set.empty, key, key)
   }
 
-  private def test[T](toPreds: Map[T, Set[T]], stack: Set[T], toTest: T, needle: T): Boolean = {
+  private[this] def test[T](toPreds: Map[T, Set[T]], stack: Set[T], toTest: T, needle: T): Boolean = {
     val deps = toPreds.getOrElse(toTest, Set.empty)
 
     if (deps.contains(needle)) {


Test patch:
diff --git a/distage/distage-core/.jvm/src/test/scala/izumi/distage/compat/CatsExtensionsTestJvm.scala b/distage/distage-core/.jvm/src/test/scala/izumi/distage/compat/CatsExtensionsTestJvm.scala
index 646117bae..4282999b4 100644
--- a/distage/distage-core/.jvm/src/test/scala/izumi/distage/compat/CatsExtensionsTestJvm.scala
+++ b/distage/distage-core/.jvm/src/test/scala/izumi/distage/compat/CatsExtensionsTestJvm.scala
@@ -9,7 +9,7 @@ import izumi.distage.fixtures.BasicCases._
 import izumi.distage.fixtures.CircularCases._
 import izumi.distage.model.PlannerInput
 import izumi.distage.model.definition.ModuleDef
-import izumi.distage.model.plan.{ExecutableOp, OrderedPlan}
+import izumi.distage.model.plan.{ExecutableOp, OrderedPlan, SemiPlan}
 import izumi.distage.model.plan.ExecutableOp.WiringOp.UseInstance
 import izumi.distage.model.plan.ExecutableOp.{ImportDependency, SemiplanOp}
 import izumi.distage.model.recursive.Bootloader
@@ -56,7 +56,7 @@ final class CatsExtensionsTestJvm extends AnyWordSpec with GivenWhenThen {
         case o => o
       }
 //      val plan2 = injector.finish(traversed)
-      val plan2: OrderedPlan = ???
+      val plan2: OrderedPlan = (??? : Id[SemiPlan] => OrderedPlan)(traversed)
 
       assert(filterDynamic(plan2.steps) === filterDynamic(testDependencyPlan.steps))
 
diff --git a/distage/distage-core/src/test/scala/izumi/distage/injector/AxisTest.scala b/distage/distage-core/src/test/scala/izumi/distage/injector/AxisTest.scala
index 92ea7c0f1..9d902caa3 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/injector/AxisTest.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/injector/AxisTest.scala
@@ -6,7 +6,7 @@ import izumi.distage.fixtures.SetCases.SetCase1
 import izumi.distage.model.PlannerInput
 import izumi.distage.model.definition.StandardAxis.{Mode, Repo}
 import izumi.distage.model.definition.{Activation, BootstrapModuleDef, ModuleDef}
-import izumi.distage.model.exceptions.{BadSetAxis, UnconfiguredSetElementAxis}
+import izumi.distage.model.exceptions.BadSetAxis
 import izumi.distage.model.plan.Roots
 import org.scalatest.wordspec.AnyWordSpec
 
diff --git a/distage/distage-core/src/test/scala/izumi/distage/injector/BasicTest.scala b/distage/distage-core/src/test/scala/izumi/distage/injector/BasicTest.scala
index dc5c9b4f8..9a5986981 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/injector/BasicTest.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/injector/BasicTest.scala
@@ -7,7 +7,7 @@ import izumi.distage.model.PlannerInput
 import izumi.distage.model.definition.Binding.SetElementBinding
 import izumi.distage.model.definition.BindingTag
 import izumi.distage.model.definition.StandardAxis.Repo
-import izumi.distage.model.exceptions.{BadMutatorAxis, ConflictResolutionException, ProvisioningException, UnconfiguredMutatorAxis}
+import izumi.distage.model.exceptions.{BadMutatorAxis, ConflictResolutionException, ProvisioningException}
 import izumi.distage.model.plan.ExecutableOp.ImportDependency
 import izumi.fundamentals.graphs.ConflictResolutionError
 import org.scalatest.exceptions.TestFailedException