Skip to content

Fix ControlFlow crashes on multi-method anon classes and Kotlin booleans#96

Merged
knutwannheden merged 1 commit into
mainfrom
fix-controlflow-anon-class-methods
May 6, 2026
Merged

Fix ControlFlow crashes on multi-method anon classes and Kotlin booleans#96
knutwannheden merged 1 commit into
mainfrom
fix-controlflow-anon-class-methods

Conversation

@knutwannheden

@knutwannheden knutwannheden commented May 6, 2026

Copy link
Copy Markdown
Contributor

Motivation

Three independent issues surfaced as ControlFlowIllegalStateExceptions thrown from the ControlFlow analyzer that powers Dataflow / FindLocalFlowPaths and, transitively, recipes like org.openrewrite.staticanalysis.ReplaceStackWithDeque. Reproduced on flagship runs of the "OpenRewrite recipe best practices" recipe (2026-05-05 and 2026-05-06) against:

  • moderneinc/moderne-intellij-plugin (Kotlin) — Condition Node is not a guard!
  • moderneinc/rewrite-sql (Java) — No current node!
  • openrewrite/rewrite-templating (Java) — No current node!

Each surfaces inside ReplaceStackWithDeque.visitVariableFindLocalFlowPaths.noneMatchControlFlow.startingAt(...).findControlFlow().

Summary

  1. No current node! when an anonymous class body contained more than one method and the first one's body ended in a return. The default JavaIsoVisitor walked the inner method declaration as part of the enclosing flow, so visitReturn cleared current, and the next method's leading @Override annotation tripped currentAsBasicBlock(). Override visitMethodDeclaration in ControlFlowAnalysis to analyze each nested method body as its own sub-flow — the same way visitLambda already handles a lambda body.
    1. Reachability stopped at lambda / anonymous-class End nodes. Once Fix ThisAccess to actually match this expressions #95 started recursing into anonymous class bodies, code that appeared after an anonymous class (or a block-body lambda) stopped being marked reachable, because recurseComputeReachableBasicBlock returned early on every End. LAMBDA-typed End nodes do have a successor in the surrounding flow; METHOD-typed End nodes return an empty successor set anyway, so just always traverse the successors.
  1. Condition Node is not a guard! for Kotlin sources because kotlin.Boolean is modelled as a JavaType.FullyQualified and TypeUtils.isAssignableTo(Primitive.Boolean, kotlin.Boolean) returns false. Recognize kotlin.Boolean as boolean-like in Guard.from, and consult the method-invocation return type for cases like someStack.contains(x).

Test plan

  • Added regression test anonymousClassWithMultipleAnnotatedMethods in FindLocalFlowPathsAnonymousClassTest (reproduces case 1 verbatim, also exercises case 2 by checking that the captured o reference inside the first method body is tracked).
  • Verified that the fix unblocks org.openrewrite.staticanalysis.ReplaceStackWithDeque against minimized versions of all three failing repos (separate PR in rewrite-static-analysis).
  • Full rewrite-analysis test suite still passes (./gradlew test).

…and Kotlin booleans

Three independent issues surfaced as `ControlFlowIllegalStateException`s thrown
from the `ControlFlow` analyzer that powers `Dataflow`/`FindLocalFlowPaths`:

1. **"No current node!"** when an anonymous class body contained more than one
   method and the first one's body ended in a `return`. The default
   `JavaIsoVisitor` walked the inner method declaration as part of the enclosing
   flow, so `visitReturn` cleared `current`, and the *next* method's leading
   `@Override` annotation tripped `currentAsBasicBlock()`. Override
   `visitMethodDeclaration` to analyze each nested method body as its own
   sub-flow, the same way `visitLambda` already handles a lambda body.

2. **Reachability stopped at lambda/anonymous-class End nodes.** Once
   `visitNewClass` started recursing into anonymous class bodies, code that
   appeared *after* an anonymous class (or a block-body lambda) stopped being
   marked reachable, because `recurseComputeReachableBasicBlock` returned early
   on every `End`. Lambda-typed `End` nodes have a real successor in the
   surrounding flow; method-typed `End` nodes return an empty successor set,
   so just always traverse the successors.

3. **"Condition Node is not a guard!"** for Kotlin sources because
   `kotlin.Boolean` is modelled as a `JavaType.FullyQualified`, and
   `TypeUtils.isAssignableTo(Primitive.Boolean, kotlin.Boolean)` returns false.
   Recognise `kotlin.Boolean` as boolean-like in `Guard.from`, and consult the
   method-invocation return type for cases like `someStack.contains(x)`.

Reproduced on flagship runs of the "OpenRewrite recipe best practices" recipe
against `moderneinc/moderne-intellij-plugin`, `moderneinc/rewrite-sql`, and
`openrewrite/rewrite-templating`, all surfaced through the
`org.openrewrite.staticanalysis.ReplaceStackWithDeque` recipe.
@knutwannheden knutwannheden force-pushed the fix-controlflow-anon-class-methods branch from 122ad89 to 0b66e4f Compare May 6, 2026 07:35
@knutwannheden knutwannheden merged commit 983ec6f into main May 6, 2026
1 check passed
@knutwannheden knutwannheden deleted the fix-controlflow-anon-class-methods branch May 6, 2026 07:44
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

1 participant