From 821100b6bc68d35fb16fa744b88dd81347ff082f Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Thu, 16 Apr 2026 17:52:05 +0200 Subject: [PATCH] Fix FinalizePrivateFields breaking compilation when field is read in a lambda initializer Skip private fields whose only assignment is in the constructor body when they are read inside a lambda or anonymous class in an instance field initializer or instance initializer block. Since initializers run before the constructor body, finalizing such fields breaks javac's definite assignment analysis. Fixes #861 --- .../staticanalysis/FinalizePrivateFields.java | 63 +++++++++++++++++++ .../FinalizePrivateFieldsTest.java | 51 +++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/src/main/java/org/openrewrite/staticanalysis/FinalizePrivateFields.java b/src/main/java/org/openrewrite/staticanalysis/FinalizePrivateFields.java index 582dfb79d..6f690772e 100644 --- a/src/main/java/org/openrewrite/staticanalysis/FinalizePrivateFields.java +++ b/src/main/java/org/openrewrite/staticanalysis/FinalizePrivateFields.java @@ -87,6 +87,18 @@ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, Ex .map(Map.Entry::getKey) .collect(toSet()); + // Skip fields only assigned in the constructor body that are read inside a lambda or + // anonymous class in an instance initializer: finalizing them breaks javac's definite + // assignment analysis, since initializers run before the constructor body. + Set uninitializedFinalizable = privateFields.stream() + .filter(v -> v.getInitializer() == null && v.getVariableType() != null) + .map(J.VariableDeclarations.NamedVariable::getVariableType) + .filter(privateFieldsToBeFinalized::contains) + .collect(toSet()); + if (!uninitializedFinalizable.isEmpty()) { + privateFieldsToBeFinalized.removeAll(findFieldsReadInDeferredInitializer(classDecl, uninitializedFinalizable)); + } + return super.visitClassDeclaration(classDecl, ctx); } @@ -154,6 +166,57 @@ private static boolean isInnerClass(J.ClassDeclaration classDecl) { return classDecl.getType() != null && classDecl.getType().getOwningClass() != null; } + private static Set findFieldsReadInDeferredInitializer(J.ClassDeclaration classDecl, Set candidates) { + Set found = new HashSet<>(); + JavaIsoVisitor> reader = new JavaIsoVisitor>() { + int lambdaOrAnonDepth; + + @Override + public J.Lambda visitLambda(J.Lambda lambda, Set set) { + lambdaOrAnonDepth++; + J.Lambda l = super.visitLambda(lambda, set); + lambdaOrAnonDepth--; + return l; + } + + @Override + public J.NewClass visitNewClass(J.NewClass newClass, Set set) { + boolean anonymous = newClass.getBody() != null; + if (anonymous) { + lambdaOrAnonDepth++; + } + J.NewClass nc = super.visitNewClass(newClass, set); + if (anonymous) { + lambdaOrAnonDepth--; + } + return nc; + } + + @Override + public J.Identifier visitIdentifier(J.Identifier identifier, Set set) { + if (lambdaOrAnonDepth > 0 && candidates.contains(identifier.getFieldType())) { + set.add(identifier.getFieldType()); + } + return super.visitIdentifier(identifier, set); + } + }; + for (Statement stmt : classDecl.getBody().getStatements()) { + if (stmt instanceof J.VariableDeclarations) { + J.VariableDeclarations vd = (J.VariableDeclarations) stmt; + if (!vd.hasModifier(J.Modifier.Type.Static)) { + for (J.VariableDeclarations.NamedVariable v : vd.getVariables()) { + if (v.getInitializer() != null) { + reader.visit(v.getInitializer(), found); + } + } + } + } else if (stmt instanceof J.Block && !((J.Block) stmt).isStatic()) { + reader.visit(stmt, found); + } + } + return found; + } + private static class CollectPrivateFieldsAssignmentCounts extends JavaIsoVisitor> { /** diff --git a/src/test/java/org/openrewrite/staticanalysis/FinalizePrivateFieldsTest.java b/src/test/java/org/openrewrite/staticanalysis/FinalizePrivateFieldsTest.java index 2971e7077..6e6858153 100644 --- a/src/test/java/org/openrewrite/staticanalysis/FinalizePrivateFieldsTest.java +++ b/src/test/java/org/openrewrite/staticanalysis/FinalizePrivateFieldsTest.java @@ -897,6 +897,57 @@ public class Reproducer { ); } + @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/861") + @Test + void fieldReadByLambdaInInstanceFieldInitializer() { + rewriteRun( + //language=java + java( + """ + import java.util.function.Function; + + public class Foo { + private String name; + protected Function logAndAccept = + throwable -> { + System.out.println(name); + return null; + }; + + public Foo(String name) { + this.name = name; + } + } + """ + ) + ); + } + + @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/861") + @Test + void fieldReadByAnonymousClassInInstanceFieldInitializer() { + rewriteRun( + //language=java + java( + """ + public class Foo { + private String name; + protected Runnable runner = new Runnable() { + @Override + public void run() { + System.out.println(name); + } + }; + + public Foo(String name) { + this.name = name; + } + } + """ + ) + ); + } + @Test void keepIndentation() { rewriteRun(