Skip to content

[#3195] feat(migration): fix Kotlin OpenRewrite migration gaps#4605

Merged
MateuszNaKodach merged 5 commits into
axon-5.1.xfrom
openrewrite-kotlin-migration-fixes
Jun 1, 2026
Merged

[#3195] feat(migration): fix Kotlin OpenRewrite migration gaps#4605
MateuszNaKodach merged 5 commits into
axon-5.1.xfrom
openrewrite-kotlin-migration-fixes

Conversation

@MateuszNaKodach
Copy link
Copy Markdown
Contributor

@MateuszNaKodach MateuszNaKodach commented May 22, 2026

Validation

Check one more time on some Kotlin project before merging.

Summary

Address issues discovered while running the AF4 → AF5 OpenRewrite
recipes against a real mixed Kotlin/Java codebase.

Entity / lifecycle:

  • ConfigureEventSourcedAnnotation now emits UUID::class (not
    UUID::class.java) for Kotlin annotation arguments, matching Kotlin's
    KClass-to-Class conversion rule for annotation parameters.
  • ReplaceAggregateLifecycleApply detects Kotlin aliased static imports
    (import …apply as foo) and treats matching local identifiers as
    apply calls, fixing call sites the MethodMatcher could not resolve
    through the alias. The alias scan is keyed off the enclosing
    SourceFile so it works for both J.CompilationUnit and
    K.CompilationUnit, and does not leak state across files.
  • ReplaceAggregateLifecycleApply flattens apply(X).andThen { body }
    chains into sequential eventAppender.append(X) + body statements,
    since EventAppender#append returns void in AF5.

Commands:

  • AddCommandAnnotation now lifts @TargetAggregateIdentifier /
    @TargetEntityId field names onto @Command(routingKey = "…") in
    addition to @RoutingKey, preserving the field annotation.

Sequencing / interceptors:

  • New AnnotateProgrammaticSequencingPolicyRegistration inserts a TODO
    comment above EventProcessingConfigurer#registerSequencingPolicy
    call sites. The TODO points at both AF5 replacements: @SequencingPolicy
    on the handler class, or declarative configuration via
    EventProcessorDefinition.customized(c -> c.sequencingPolicy(...)).
    Matches both the AF4 FQN and the post-Axon4ToAxon5Common-rename AF5
    FQN so the recipe still fires when run through the umbrella.
  • New MigrateMessageInterceptorLambda rewrites
    MessageHandlerInterceptor<…> = (uow, chain) -> … lambdas to the
    three-parameter AF5 shape and updates chain.proceed() to
    chain.proceed(message, processingContext).

Import targets:

  • EventProcessor / StreamingEventProcessor / SubscribingEventProcessor
    redirected to processing.* / processing.streaming.* /
    processing.subscribing.* subpackages.
  • @Timestamp redirected to messaging.eventhandling.annotation.
  • TrackingToken redirected to
    messaging.eventhandling.processing.streaming.token.
  • Added regression tests for MetaData → core.Metadata rename across
    Java and Kotlin sources (the existing ChangeType rule handles it;
    the tests lock the behaviour in).

Test plan

  • Unit tests for each recipe pass (./mvnw -pl migration test).
  • End-to-end Kotlin umbrella test pins the aliased
    AggregateLifecycle.apply rewrite.
  • Re-run umbrella on the reviewer's real Kotlin project.

@MateuszNaKodach MateuszNaKodach requested a review from a team as a code owner May 22, 2026 13:02
@MateuszNaKodach MateuszNaKodach requested review from hatzlj, jangalinski, laura-devriendt-lemon and smcvb and removed request for a team May 22, 2026 13:02
@MateuszNaKodach MateuszNaKodach self-assigned this May 22, 2026
@MateuszNaKodach MateuszNaKodach added the Priority 3: Could Low priority. Issues that are nice to have but have a straightforward workaround. label May 22, 2026
@MateuszNaKodach MateuszNaKodach added this to the Release 5.1.1 milestone May 22, 2026
@MateuszNaKodach MateuszNaKodach marked this pull request as draft May 22, 2026 13:05
@MateuszNaKodach MateuszNaKodach added Type: Enhancement Use to signal an issue enhances an already existing feature of the project. Priority 2: Should High priority. Ideally, these issues are part of the release they’re assigned to. and removed Priority 3: Could Low priority. Issues that are nice to have but have a straightforward workaround. labels May 22, 2026
@MateuszNaKodach MateuszNaKodach marked this pull request as ready for review May 25, 2026 08:50
@MateuszNaKodach MateuszNaKodach marked this pull request as draft May 25, 2026 08:56
Copy link
Copy Markdown
Contributor

@hatzlj hatzlj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave the migration another run on my mixed Kotlin/Java AF4 project with the following outcome

  • Aggregate id type was migrated correctly
  • AggregateLifecycle.apply(..).andThen(...) was flattened correctly to subsequent EventAppender.append(..) calls
  • Command routing keys were inferred correctly from TargetAggregateIdentifier
  • the MessageInterceptor lambda was correctly migrated (including a helpful TODO)
  • all imports (Timestamp, TrackingToken, EventProcessor, ...) worked

What didnt work (but it's nothing blocking because i think it's edge cases)

  • in the one case where i had the AggregateLifecycle.apply(...) defined with an alias, it still skipped just the first occurence when migrating to EventAppender
  • it didnt put the TODO above registerSequencingPolicy in the configuration as you suggested in the PR description

Overall nice additions to the recipe

@hatzlj
Copy link
Copy Markdown
Contributor

hatzlj commented May 29, 2026

I gave the migration another run on my mixed Kotlin/Java AF4 project with the following outcome

  • Aggregate id type was migrated correctly
  • AggregateLifecycle.apply(..).andThen(...) was flattened correctly to subsequent EventAppender.append(..) calls
  • Command routing keys were inferred correctly from TargetAggregateIdentifier
  • the MessageInterceptor lambda was correctly migrated (including a helpful TODO)
  • all imports (Timestamp, TrackingToken, EventProcessor, ...) worked

What didnt work (but it's nothing blocking because i think it's edge cases)

  • in the one case where i had the AggregateLifecycle.apply(...) defined with an alias, it still skipped just the first occurence when migrating to EventAppender
  • it didnt put the TODO above registerSequencingPolicy in the configuration as you suggested in the PR description

Overall nice additions to the recipe

one further thing I recognized @MateuszNaKodach:
org.axonframework.messaging.MetaData changed to org.axonframework.messaging.core.Metadata which was not covered by openrewrite (was using it for creating an empty instance)

Address nine issues discovered while running the AF4 → AF5 OpenRewrite
recipes against a real Kotlin codebase.

Entity / lifecycle:
- ConfigureEventSourcedAnnotation now emits `UUID::class` (not
  `UUID::class.java`) for Kotlin annotation arguments, matching Kotlin's
  KClass-to-Class conversion rule for annotation parameters.
- ReplaceAggregateLifecycleApply scans the compilation unit for aliased
  Kotlin static imports (`import …apply as foo`) and treats matching
  local identifiers as `apply` calls, fixing call sites the
  MethodMatcher could not resolve through the alias.
- ReplaceAggregateLifecycleApply flattens `apply(X).andThen { body }`
  chains into sequential `eventAppender.append(X)` + body statements,
  since `EventAppender#append` returns `void` in AF5.

Commands:
- AddCommandAnnotation now lifts `@TargetAggregateIdentifier` /
  `@TargetEntityId` field names onto `@Command(routingKey = "…")` in
  addition to `@RoutingKey`, preserving the field annotation.

Sequencing / interceptors:
- New AnnotateProgrammaticSequencingPolicyRegistration inserts a
  TODO comment above `EventProcessingConfigurer#registerSequencingPolicy`
  call sites, pointing to the AF5 `@SequencingPolicy` annotation.
- New MigrateMessageInterceptorLambda rewrites
  `MessageHandlerInterceptor<…> = (uow, chain) -> …` lambdas to the
  three-parameter AF5 shape and updates `chain.proceed()` to
  `chain.proceed(message, processingContext)`.

Import targets:
- EventProcessor / StreamingEventProcessor / SubscribingEventProcessor
  redirected to `processing.*` / `processing.streaming.*` /
  `processing.subscribing.*` subpackages.
- \`@Timestamp\` redirected to \`messaging.eventhandling.annotation\`.
- \`TrackingToken\` redirected to
  \`messaging.eventhandling.processing.streaming.token\`.
Add focused tests for the recipes touched in the previous commit so each
new behavior has explicit coverage and a regression boundary.

- AddCommandAnnotationKotlinTest / AddCommandAnnotationJavaRecordTest:
  routing-key lift from @TargetAggregateIdentifier (pre-rename) and
  @TargetEntityId (post-modelling-rename), plus the "no routing key
  marker" baseline.
- AnnotateProgrammaticSequencingPolicyRegistrationTest: TODO comment
  insertion above EventProcessingConfigurer#registerSequencingPolicy
  calls, idempotency on re-run, and unrelated-call passthrough.
- MigrateMessageInterceptorLambdaTest: two-parameter → three-parameter
  lambda rewrite, chain.proceed() argument injection, preservation of
  user-chosen chain parameter names, AF5-shape idempotency, and
  passthrough for unrelated functional interfaces.
- Axon4ToAxon5MessagingTest: StreamingEventProcessor, SubscribingEventProcessor,
  @timestamp, and TrackingToken relocations into their AF5 subpackages.

The TODO comment in MigrateMessageInterceptorLambda is now emitted as a
block comment (/* ... */) instead of a line comment so it does not
swallow the lambda when the initializer sits on the same line as the
variable declaration.
The TODO above migrated `EventProcessingConfigurer#registerSequencingPolicy`
calls implied only `@SequencingPolicy` on the handler class was available.
AF5 also keeps a declarative path through `EventProcessorDefinition`
(`org.axonframework.extension.spring.config`), customised with
`.customized(c -> c.sequencingPolicy(...))`. Picking between annotation
and declarative configuration needs human judgement (which handler class
vs. which processor name), so the recipe still emits a TODO rather than
a mechanical rewrite — it now names both options.
ReplaceAggregateLifecycleApply: the Kotlin import-alias scan is now
rerun for every compilation unit. The earlier `aliasesScanned` guard
fired once for the whole recipe run because OpenRewrite reuses visitor
instances across files, so alias names from the first file would leak
into every subsequent file and an unrelated function happening to share
the alias name would be misidentified as `apply`. Added a regression
test that pairs an aliased CU with a second CU that defines an unrelated
function of the same name.

AnnotateProgrammaticSequencingPolicyRegistration: dropped the dead
fall-through branch and the misleading comment referring to a
non-existent `visitStatement`. `J.MethodInvocation` implements
`Statement`, so `firstEnclosing(Statement.class)` from inside
`visitMethodInvocation` always returns the invocation itself — the
branch the comment promised never fired. Collapsed to the simple
single-branch form.
Three issues surfaced when the umbrella was run on a real mixed
Kotlin/Java AF4 codebase:

ReplaceAggregateLifecycleApply: the aliased-`apply` rewrite never
fired for Kotlin sources because `JavaIsoVisitor#visitCompilationUnit`
only triggers for `J.CompilationUnit`, not the `K.CompilationUnit` the
Kotlin parser produces. The import-alias scan is now keyed off the
enclosing `SourceFile` (looked up via the cursor) and supports both
`J.` and `K.` compilation-unit imports, with the qualid matched by
walking the field-access chain so it tolerates the Kotlin parser's
`J.Empty`-targeted heads. Added an end-to-end test that drives the
umbrella recipe and pins the aliased single-call rewrite.

AnnotateProgrammaticSequencingPolicyRegistration: the recipe matched
the AF4 `EventProcessingConfigurer` FQN only, but `Axon4ToAxon5Common`
renames the type into the AF5 configuration namespace before this
recipe runs. The MethodMatcher binds to the LST's resolved type at the
call site, so the AF5 FQN must also be matched — otherwise the TODO is
silently skipped on every codebase that runs the umbrella in order.
Added a test that pins the post-rename FQN case.

Tests: also added a `MetaData → core.Metadata` pair (Java + Kotlin)
to lock in the rename of `MetaData.emptyInstance()` call sites that a
review run flagged as uncovered — the existing `ChangeType` rule does
handle it, the new tests make that explicit.
@MateuszNaKodach MateuszNaKodach force-pushed the openrewrite-kotlin-migration-fixes branch from 03f530e to 2e13563 Compare June 1, 2026 12:32
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 1, 2026

@MateuszNaKodach MateuszNaKodach merged commit 3682b0c into axon-5.1.x Jun 1, 2026
8 checks passed
@MateuszNaKodach MateuszNaKodach deleted the openrewrite-kotlin-migration-fixes branch June 1, 2026 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority 2: Should High priority. Ideally, these issues are part of the release they’re assigned to. Type: Enhancement Use to signal an issue enhances an already existing feature of the project.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants