Skip to content

Commit 457b8fe

Browse files
committed
Add ExtractSuperConstructorArgument recipe for JEP 513
Extracts complex `super(..)` arguments (method invocations and object creations) into local variables declared right before the `super(..)` call, now that JEP 513 permits statements before the explicit constructor invocation. The transformation is strictly behavior preserving: argument expressions already evaluate before the super constructor body, and a super argument can never reference the instance under construction. Arguments are extracted in their original left-to-right order; trivial side-effect-free arguments (literals and local/parameter references) are left in place.
1 parent a58ef3a commit 457b8fe

4 files changed

Lines changed: 564 additions & 2 deletions

File tree

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.migrate.lang;
17+
18+
import lombok.Getter;
19+
import org.jspecify.annotations.Nullable;
20+
import org.openrewrite.ExecutionContext;
21+
import org.openrewrite.Preconditions;
22+
import org.openrewrite.Recipe;
23+
import org.openrewrite.TreeVisitor;
24+
import org.openrewrite.internal.StringUtils;
25+
import org.openrewrite.java.JavaIsoVisitor;
26+
import org.openrewrite.java.JavaTemplate;
27+
import org.openrewrite.java.VariableNameUtils;
28+
import org.openrewrite.java.search.UsesJavaVersion;
29+
import org.openrewrite.java.tree.Expression;
30+
import org.openrewrite.java.tree.J;
31+
import org.openrewrite.java.tree.JavaType;
32+
import org.openrewrite.java.tree.Statement;
33+
import org.openrewrite.java.tree.TypeUtils;
34+
35+
import java.util.ArrayList;
36+
import java.util.LinkedHashSet;
37+
import java.util.List;
38+
import java.util.Set;
39+
40+
import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER;
41+
42+
public class ExtractSuperConstructorArgument extends Recipe {
43+
44+
@Getter
45+
final String displayName = "Extract complex `super(..)` arguments into local variables";
46+
47+
@Getter
48+
final String description = "[JEP 513](https://openjdk.org/jeps/513) allows statements before an explicit `super(..)` constructor " +
49+
"invocation. When a `super(..)` call computes one of its arguments through a method invocation or object " +
50+
"creation, this recipe extracts the non-trivial arguments into local variables declared right before the " +
51+
"`super(..)` call, surfacing the work done before construction.\n\n" +
52+
"This is a strictly behavior-preserving transformation: argument expressions are already evaluated " +
53+
"before the super constructor body runs, and a super argument can never reference the instance under " +
54+
"construction, so hoisting them into preceding statements changes neither the order of side effects nor " +
55+
"the set of legal references. Arguments are extracted in their original left-to-right order, and trivial " +
56+
"arguments (literals and local variable references, which have no side effects) are left in place. " +
57+
"Statements that follow `super(..)` are deliberately *not* moved, as reordering them relative to the " +
58+
"super constructor's side effects could change behavior.";
59+
60+
@Override
61+
public TreeVisitor<?, ExecutionContext> getVisitor() {
62+
return Preconditions.check(new UsesJavaVersion<>(25), new JavaIsoVisitor<ExecutionContext>() {
63+
@Override
64+
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
65+
J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx);
66+
if (!md.isConstructor() || md.getBody() == null) {
67+
return md;
68+
}
69+
70+
J.MethodInvocation superCall = findExplicitSuperCall(md.getBody().getStatements());
71+
if (superCall == null || superCall.getMethodType() == null) {
72+
return md;
73+
}
74+
List<Expression> args = superCall.getArguments();
75+
List<JavaType> paramTypes = superCall.getMethodType().getParameterTypes();
76+
List<String> paramNames = superCall.getMethodType().getParameterNames();
77+
if (paramTypes.size() != args.size()) {
78+
return md;
79+
}
80+
81+
// Only act when at least one argument actually does work worth surfacing
82+
boolean anyComplex = false;
83+
for (Expression arg : args) {
84+
if (arg instanceof J.MethodInvocation || arg instanceof J.NewClass) {
85+
anyComplex = true;
86+
break;
87+
}
88+
}
89+
if (!anyComplex) {
90+
return md;
91+
}
92+
93+
// Plan the extraction: everything but trivial, side-effect-free arguments is hoisted, in order
94+
Set<String> usedNames = new LinkedHashSet<>();
95+
String[] names = new String[args.size()];
96+
Set<String> imports = new LinkedHashSet<>();
97+
StringBuilder declarations = new StringBuilder();
98+
List<Expression> declarationArgs = new ArrayList<>();
99+
for (int i = 0; i < args.size(); i++) {
100+
Expression arg = args.get(i);
101+
if (isInlineSafe(arg)) {
102+
continue;
103+
}
104+
String typeName = denotableTypeName(paramTypes.get(i));
105+
if (typeName == null) {
106+
// Cannot safely name this parameter's type; bail rather than partially extract and risk reordering
107+
return md;
108+
}
109+
String name = uniqueName(baseName(arg, paramNames.get(i)), usedNames);
110+
names[i] = name;
111+
declarations.append(typeName).append(' ').append(name).append(" = #{any()};\n");
112+
declarationArgs.add(arg);
113+
if (paramTypes.get(i) instanceof JavaType.FullyQualified) {
114+
imports.add(((JavaType.FullyQualified) paramTypes.get(i)).getFullyQualifiedName());
115+
}
116+
}
117+
118+
// 1. Declare the extracted arguments right before the `super(..)` call, preserving their order
119+
JavaTemplate.Builder declarationTemplate = JavaTemplate.builder(declarations.toString()).contextSensitive();
120+
for (String fqn : imports) {
121+
declarationTemplate.imports(fqn);
122+
maybeAddImport(fqn);
123+
}
124+
md = declarationTemplate.build()
125+
.apply(getCursor(), superCall.getCoordinates().before(), declarationArgs.toArray());
126+
127+
// 2. Replace the now-redundant arguments with references to the new local variables
128+
StringBuilder argumentList = new StringBuilder();
129+
List<Expression> inlineArgs = new ArrayList<>();
130+
for (int i = 0; i < args.size(); i++) {
131+
if (i > 0) {
132+
argumentList.append(", ");
133+
}
134+
if (names[i] != null) {
135+
argumentList.append(names[i]);
136+
} else {
137+
argumentList.append("#{any()}");
138+
inlineArgs.add(args.get(i));
139+
}
140+
}
141+
return (J.MethodDeclaration) new JavaIsoVisitor<ExecutionContext>() {
142+
@Override
143+
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation mi, ExecutionContext ctx2) {
144+
mi = super.visitMethodInvocation(mi, ctx2);
145+
if (isExplicitSuperCall(mi)) {
146+
return JavaTemplate.builder(argumentList.toString()).contextSensitive().build()
147+
.apply(getCursor(), mi.getCoordinates().replaceArguments(), inlineArgs.toArray());
148+
}
149+
return mi;
150+
}
151+
}.visitNonNull(md, ctx, getCursor().getParentOrThrow());
152+
}
153+
154+
private J.@Nullable MethodInvocation findExplicitSuperCall(List<Statement> statements) {
155+
for (Statement statement : statements) {
156+
if (statement instanceof J.MethodInvocation && isExplicitSuperCall((J.MethodInvocation) statement)) {
157+
return (J.MethodInvocation) statement;
158+
}
159+
}
160+
return null;
161+
}
162+
163+
private boolean isExplicitSuperCall(J.MethodInvocation mi) {
164+
return mi.getSelect() == null && "super".equals(mi.getSimpleName());
165+
}
166+
167+
/**
168+
* An argument is safe to leave in place only if it cannot be observably reordered relative to the
169+
* extracted arguments, i.e. it has no side effects, cannot throw, and cannot trigger class initialization.
170+
* That holds for literals and references to local variables or parameters, but not for field accesses
171+
* (a static field read may trigger class initialization) or any compound expression.
172+
*/
173+
private boolean isInlineSafe(Expression arg) {
174+
if (arg instanceof J.Literal) {
175+
return true;
176+
}
177+
if (arg instanceof J.Identifier) {
178+
JavaType.Variable fieldType = ((J.Identifier) arg).getFieldType();
179+
return fieldType != null && !(fieldType.getOwner() instanceof JavaType.FullyQualified);
180+
}
181+
return false;
182+
}
183+
184+
private @Nullable String denotableTypeName(JavaType type) {
185+
if (type instanceof JavaType.Primitive) {
186+
return ((JavaType.Primitive) type).getKeyword();
187+
}
188+
// Only non-generic class types can be safely named without risking out-of-scope type variables
189+
if (type instanceof JavaType.FullyQualified && !(type instanceof JavaType.Parameterized)) {
190+
return ((JavaType.FullyQualified) type).getClassName().replace('$', '.');
191+
}
192+
return null;
193+
}
194+
195+
private String baseName(Expression arg, @Nullable String paramName) {
196+
if (isValidBaseName(paramName)) {
197+
return paramName;
198+
}
199+
if (arg instanceof J.MethodInvocation) {
200+
return ((J.MethodInvocation) arg).getSimpleName();
201+
}
202+
JavaType.FullyQualified created = TypeUtils.asFullyQualified(arg.getType());
203+
if (created != null) {
204+
return StringUtils.uncapitalize(created.getClassName());
205+
}
206+
return "value";
207+
}
208+
209+
private boolean isValidBaseName(@Nullable String name) {
210+
return name != null && !name.isEmpty() && Character.isJavaIdentifierStart(name.charAt(0)) &&
211+
!"arg0".equals(name);
212+
}
213+
214+
private String uniqueName(String base, Set<String> usedNames) {
215+
String candidate = VariableNameUtils.generateVariableName(base, getCursor(), INCREMENT_NUMBER);
216+
for (int n = 1; usedNames.contains(candidate); n++) {
217+
candidate = VariableNameUtils.generateVariableName(base + n, getCursor(), INCREMENT_NUMBER);
218+
}
219+
usedNames.add(candidate);
220+
return candidate;
221+
}
222+
});
223+
}
224+
}

src/main/resources/META-INF/rewrite/java-version-25.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ recipeList:
3434
- org.openrewrite.java.migrate.UpgradePluginsForJava25
3535
- org.openrewrite.java.migrate.io.ReplaceSystemOutWithIOPrint
3636
- org.openrewrite.java.migrate.lang.MigrateProcessWaitForDuration
37+
- org.openrewrite.java.migrate.lang.ExtractSuperConstructorArgument
3738
- org.openrewrite.java.migrate.lang.ReplaceUnusedVariablesWithUnderscore
3839
- org.openrewrite.java.migrate.util.MigrateInflaterDeflaterToClose
3940
- org.openrewrite.java.migrate.util.MigrateStringReaderToReaderOf

0 commit comments

Comments
 (0)