Migration Recipes: hkj-openrewrite

What You'll Learn

  • Every OpenRewrite recipe Higher-Kinded-J ships, and what each one does
  • How to run the recipes from Gradle or Maven (preview and apply)
  • Which recipes rewrite source automatically and which are detection-only, and why

The hkj-openrewrite module ships OpenRewrite recipes that automate migrations across Higher-Kinded-J releases and surface code that should adopt newer, safer APIs. Use them to upgrade between versions without hand-editing every call site.

This page is the authoritative catalogue. The same recipe set is also referenced by the migration-nudge check in the hkj-checker compile-time plugin, which surfaces the detection-only recipes as compile-time nudges for projects that prefer feedback in the editor.


Quick Start

Gradle

plugins {
    id("org.openrewrite.rewrite") version "7.28.1"
}

dependencies {
    rewrite("io.github.higher-kinded-j:hkj-openrewrite:LATEST_VERSION")
}

rewrite {
    activeRecipe("org.higherkindedj.openrewrite.AddArityBounds")
}
./gradlew rewriteDryRun   # preview changes
./gradlew rewriteRun      # apply changes

Maven

<build>
  <plugins>
    <plugin>
      <groupId>org.openrewrite.maven</groupId>
      <artifactId>rewrite-maven-plugin</artifactId>
      <!-- Use the latest 5.x release; it pairs with the rewrite 8.x core. -->
      <version>LATEST</version>
      <configuration>
        <activeRecipes>
          <recipe>org.higherkindedj.openrewrite.AddArityBounds</recipe>
        </activeRecipes>
      </configuration>
      <dependencies>
        <dependency>
          <groupId>io.github.higher-kinded-j</groupId>
          <artifactId>hkj-openrewrite</artifactId>
          <version>LATEST_VERSION</version>
        </dependency>
      </dependencies>
    </plugin>
  </plugins>
</build>
mvn rewrite:dryRun   # preview changes
mvn rewrite:run      # apply changes

Pick the hkj-openrewrite version that contains the recipe group you need: the arity recipes exist from 0.3.0 onward, and the 0.5.0 deprecation recipes from the 0.5.0-era release onward. Newer releases retain the older recipes.


Recipe Catalogue

Three recipe groups are shipped; each one has a composite that runs every recipe in the group, plus individual recipes you can activate on their own.

GroupCompositeMode
Arity migration (0.2.x to 0.3.0)org.higherkindedj.openrewrite.AddArityBoundsrewrites source
Effect algebra helpersorg.higherkindedj.openrewrite.EffectAlgebraMigrationdetection only
0.5.0 deprecationsorg.higherkindedj.openrewrite.MigrateDeprecationsTo0_5_0rewrites source

Arity migration (0.2.x to 0.3.0)

Version 0.3.0 introduced an embedded kind system. Kind, the type classes, and witness types now carry compile-time arity information via WitnessArity<A extends TypeArity> and TypeArity (Unary for * -> *, Binary for * -> * -> *). These recipes add the required bounds and implements clauses so older code compiles against the newer API.

RecipeDescription
org.higherkindedj.openrewrite.AddArityBoundsComposite; runs both arity recipes below
org.higherkindedj.openrewrite.AddWitnessArityToWitnessAdds implements WitnessArity<TypeArity.Unary> or WitnessArity<TypeArity.Binary> to witness classes
org.higherkindedj.openrewrite.AddArityBoundsToTypeParametersAdds extends WitnessArity<TypeArity.Unary> or WitnessArity<TypeArity.Binary> bounds to type parameters used as a witness with Kind, Kind2, type classes, or transformers; covers method parameters, return types, fields, local variables, the class hierarchy, nested generics, and wildcard bounds; appended as an intersection bound when a bound already exists

The required WitnessArity and TypeArity imports are added automatically by both recipes.

What changes

ComponentBefore (0.2.x)After (0.3.0)
Kind<F, A>no boundsKind<F extends WitnessArity<?>, A>
Kind2<F, A, B>no boundsKind2<F extends WitnessArity<TypeArity.Binary>, A, B>
Type classesFunctor<F>Functor<F extends WitnessArity<TypeArity.Unary>>
Witness classesfinal class Witness {}final class Witness implements WitnessArity<TypeArity.Unary> {}

Common compilation errors after upgrading

Compiler errorCauseFix
type argument F is not within boundsF used with Kind<F, A> without boundsAdd <F extends WitnessArity<TypeArity.Unary>>
Witness is not a valid substitute for FWitness class missing WitnessArityAdd the implements WitnessArity<...> clause
cannot find symbol: WitnessArityMissing importImport org.higherkindedj.hkt.WitnessArity and TypeArity

The witness-arity check in hkj-checker catches the same class of mistake at compile time for projects that have already migrated.

Effect algebra helpers

These recipes assist migration toward @EffectAlgebra and @ComposeEffects generated infrastructure. Each one tags matching code with an OpenRewrite search-result marker so it appears in the dry-run report and in OpenRewrite data tables; they do not rewrite your source.

RecipeDetects
org.higherkindedj.openrewrite.EffectAlgebraMigrationComposite; runs all three below
org.higherkindedj.openrewrite.AddHandleErrorCaseRecipeFree switch statements and switch expressions that match Pure, Suspend, or FlatMapped but are missing a HandleError or Ap case
org.higherkindedj.openrewrite.ConvertRawFreeToFreePathRecipeDirect Free.liftF() and Free.suspend() calls that could use generated *Ops or the FreePath API
org.higherkindedj.openrewrite.DetectInjectBoilerplateRecipeManual InjectInstances.injectLeft, injectRight, and injectRightThen calls that @ComposeEffects would generate

Why detection-only?

The replacement is user-specific generated code. The *Ops constructors and the @ComposeEffects Support class are named after each user's effect algebra and composition, so a correct replacement cannot be synthesised generically. Converting Free.suspend to FreePath.from also changes the static type and would not compile without migrating the surrounding composition. Flagging the call sites for a human keeps the recipes safe; the migration-nudge checker is the equivalent at compile time.

Review the matches with ./gradlew rewriteDryRun (Gradle) or mvn rewrite:dryRun (Maven).

0.5.0 deprecation migration

Rewrites call sites of APIs deprecated for removal in 0.5.0 to their signature-compatible replacements. These recipes do rewrite source; both are pure method renames implemented via org.openrewrite.java.ChangeMethodName.

RecipeRename
org.higherkindedj.openrewrite.MigrateDeprecationsTo0_5_0Composite; runs both renames below
org.higherkindedj.openrewrite.RenameStateTKindNarrowKStateTKind.narrowK(..) becomes StateTKind.narrow(..); the wildcard Kind overload bypassed witness type safety
org.higherkindedj.openrewrite.RenameKindValidatorNarrowWithPatternKindValidator.narrowWithPattern(..) becomes KindValidator.narrowHolder(..)
rewrite {
    activeRecipe("org.higherkindedj.openrewrite.MigrateDeprecationsTo0_5_0")
}

Key Takeaways

  • Three groups, three modes. Arity migration and 0.5.0 deprecations rewrite source; the effect algebra helpers only flag call sites because their targets are user-specific generated code.
  • Always dry-run first. rewriteDryRun (Gradle) or mvn rewrite:dryRun (Maven) shows the diff; rewriteRun applies it.
  • Use the composite recipe. Activating one of the three composites runs the whole group and avoids ordering mistakes; the individual recipes are there when you need finer control.
  • Recipe coverage tracks the library. The same migrations also surface as compile-time checks in hkj-checker; pick whichever feedback loop fits the team.

See Also

  • Compile-Time Checks - The migration-nudge, free-switch-exhaustive, and witness-arity checks are the compile-time equivalents of the recipes above
  • Build Plugins - The Gradle and Maven plugins that wire hkj-openrewrite into a project alongside the other tooling
  • Release History - Per-version notes; recipe groups are documented in the release that introduces them

Previous: Compile-Time Checks Next: Diagnostics