Skip to content

Commit 3ae3e2e

Browse files
committed
Hoist private static final constants onto the recipe class
Anonymous classes can not declare constants, so nested visitors that declare them were skipped. Move them up to the enclosing recipe class instead, unless that would collide with an existing field.
1 parent a4f043f commit 3ae3e2e

3 files changed

Lines changed: 266 additions & 16 deletions

File tree

‎src/main/java/org/openrewrite/java/recipes/InlineNestedVisitorClass.java‎

Lines changed: 105 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@
4444
import org.openrewrite.style.Style;
4545

4646
import java.util.ArrayList;
47+
import java.util.HashSet;
4748
import java.util.List;
49+
import java.util.Set;
4850
import java.util.concurrent.atomic.AtomicInteger;
4951

5052
@Value
@@ -57,8 +59,9 @@ public class InlineNestedVisitorClass extends Recipe {
5759

5860
String description = "Recipes that return a named, private, static nested visitor class straight from " +
5961
"`getVisitor()` (or `getScanner()`) can declare that visitor anonymously instead, which keeps the " +
60-
"visitor next to the recipe metadata that configures it. Only applied when the nested class is used " +
61-
"exactly once, and when nothing would be lost by inlining it.";
62+
"visitor next to the recipe metadata that configures it. Any `private static final` constants the " +
63+
"nested class declares are hoisted onto the recipe class, as anonymous classes can not declare them. " +
64+
"Only applied when the nested class is used exactly once, and when nothing would be lost by inlining it.";
6265

6366
@Override
6467
public TreeVisitor<?, ExecutionContext> getVisitor() {
@@ -71,7 +74,9 @@ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, Ex
7174
return cd;
7275
}
7376

77+
boolean canHoist = getCursor().getParentTreeCursor().getValue() instanceof J.CompilationUnit;
7478
List<J.ClassDeclaration> inlined = new ArrayList<>();
79+
List<Statement> hoisted = new ArrayList<>();
7580
List<Statement> mapped = ListUtils.map(statements, statement -> {
7681
if (!(statement instanceof J.MethodDeclaration)) {
7782
return statement;
@@ -89,8 +94,28 @@ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, Ex
8994
if (supertype == null) {
9095
return statement;
9196
}
92-
inlined.add(nested);
97+
// Anonymous classes can not declare constants, so they move up onto the recipe class instead
98+
List<Statement> constants = staticFields(nested);
99+
if (!constants.isEmpty() && (!canHoist || collidesWithExistingField(statements, constants))) {
100+
return statement;
101+
}
102+
93103
Cursor cursor = getCursor();
104+
J.Block body = nested.getBody();
105+
if (!constants.isEmpty()) {
106+
boolean leading = constants.contains(body.getStatements().get(0));
107+
List<Statement> kept = ListUtils.map(body.getStatements(), s -> constants.contains(s) ? null : s);
108+
if (leading) {
109+
kept = ListUtils.mapFirst(kept, first -> first.withPrefix(singleLine(first.getPrefix())));
110+
}
111+
body = body.withStatements(kept);
112+
for (Statement constant : constants) {
113+
hoisted.add(shiftIndent(constant, cursor, -1));
114+
}
115+
}
116+
117+
inlined.add(nested);
118+
J.Block anonymousBody = shiftIndent(body, cursor, 1);
94119
return (Statement) new JavaIsoVisitor<Integer>() {
95120
@Override
96121
public J.NewClass visitNewClass(J.NewClass nc, Integer p) {
@@ -99,7 +124,7 @@ public J.NewClass visitNewClass(J.NewClass nc, Integer p) {
99124
}
100125
return nc
101126
.withClazz(supertype.withPrefix(Space.SINGLE_SPACE))
102-
.withBody(indentOneLevel(nested.getBody(), cursor));
127+
.withBody(anonymousBody);
103128
}
104129
}.visitNonNull(method, 0);
105130
});
@@ -112,6 +137,12 @@ public J.NewClass visitNewClass(J.NewClass nc, Integer p) {
112137
Space prefix = statements.get(0).getPrefix();
113138
remaining = ListUtils.mapFirst(remaining, first -> first.withPrefix(prefix));
114139
}
140+
if (!hoisted.isEmpty() && !remaining.isEmpty()) {
141+
Space prefix = remaining.get(0).getPrefix();
142+
remaining = ListUtils.mapFirst(remaining, first -> first.withPrefix(blankLineBefore(prefix)));
143+
remaining = ListUtils.concatAll(
144+
ListUtils.mapFirst(hoisted, first -> first.withPrefix(prefix)), remaining);
145+
}
115146
return cd.withBody(cd.getBody().withStatements(remaining));
116147
}
117148

@@ -186,13 +217,53 @@ private boolean canInline(J.ClassDeclaration nested) {
186217
return false;
187218
}
188219
if (statement instanceof J.VariableDeclarations &&
189-
((J.VariableDeclarations) statement).hasModifier(J.Modifier.Type.Static)) {
220+
((J.VariableDeclarations) statement).hasModifier(J.Modifier.Type.Static) &&
221+
!isPrivateStaticFinalField(statement)) {
190222
return false;
191223
}
192224
}
193225
return countReferences(nested) == 1;
194226
}
195227

228+
private boolean isPrivateStaticFinalField(Statement statement) {
229+
if (!(statement instanceof J.VariableDeclarations)) {
230+
return false;
231+
}
232+
J.VariableDeclarations field = (J.VariableDeclarations) statement;
233+
return field.hasModifier(J.Modifier.Type.Private) &&
234+
field.hasModifier(J.Modifier.Type.Static) &&
235+
field.hasModifier(J.Modifier.Type.Final);
236+
}
237+
238+
private List<Statement> staticFields(J.ClassDeclaration nested) {
239+
List<Statement> constants = new ArrayList<>();
240+
for (Statement statement : nested.getBody().getStatements()) {
241+
if (isPrivateStaticFinalField(statement)) {
242+
constants.add(statement);
243+
}
244+
}
245+
return constants;
246+
}
247+
248+
private boolean collidesWithExistingField(List<Statement> statements, List<Statement> constants) {
249+
Set<String> existing = new HashSet<>();
250+
for (Statement statement : statements) {
251+
if (statement instanceof J.VariableDeclarations) {
252+
for (J.VariableDeclarations.NamedVariable variable : ((J.VariableDeclarations) statement).getVariables()) {
253+
existing.add(variable.getSimpleName());
254+
}
255+
}
256+
}
257+
for (Statement constant : constants) {
258+
for (J.VariableDeclarations.NamedVariable variable : ((J.VariableDeclarations) constant).getVariables()) {
259+
if (!existing.add(variable.getSimpleName())) {
260+
return true;
261+
}
262+
}
263+
}
264+
return false;
265+
}
266+
196267
private int countReferences(J.ClassDeclaration nested) {
197268
JavaType.FullyQualified type = nested.getType();
198269
if (type == null) {
@@ -212,33 +283,36 @@ public J.Identifier visitIdentifier(J.Identifier identifier, AtomicInteger count
212283
return references.get();
213284
}
214285

215-
private J.Block indentOneLevel(J.Block body, Cursor cursor) {
216-
J.Block shifted = ShiftFormat.indent(body, cursor, 1);
286+
private <J2 extends J> J2 shiftIndent(J2 tree, Cursor cursor, int levels) {
287+
J2 shifted = ShiftFormat.indent(tree, cursor, levels);
217288
JavaSourceFile cu = cursor.firstEnclosingOrThrow(JavaSourceFile.class);
218289
TabsAndIndentsStyle style = Style.from(TabsAndIndentsStyle.class, cu, IntelliJ::tabsAndIndents);
219290
String indent = style.getUseTabCharacter() ? "\t" : StringUtils.repeat(" ", style.getIndentSize());
220-
return (J.Block) new JavaIsoVisitor<Integer>() {
291+
//noinspection unchecked
292+
return (J2) new JavaIsoVisitor<Integer>() {
221293
@Override
222294
public Space visitSpace(Space space, Space.Location loc, Integer p) {
223-
return space.withComments(ListUtils.map(space.getComments(), comment -> indentComment(comment, indent)));
295+
return space.withComments(ListUtils.map(space.getComments(), comment -> shiftComment(comment, indent, levels)));
224296
}
225297
}.visitNonNull(shifted, 0);
226298
}
227299

228-
private Comment indentComment(Comment comment, String indent) {
300+
private Comment shiftComment(Comment comment, String indent, int levels) {
229301
if (comment instanceof TextComment) {
230302
TextComment textComment = (TextComment) comment;
231303
if (textComment.getText().contains("\n")) {
232-
return textComment.withText(textComment.getText().replace("\n", "\n" + indent));
304+
return textComment.withText(levels > 0 ?
305+
textComment.getText().replace("\n", "\n" + indent) :
306+
textComment.getText().replace("\n" + indent, "\n"));
233307
}
234308
} else if (comment instanceof Javadoc.DocComment) {
235309
Javadoc.DocComment docComment = (Javadoc.DocComment) comment;
236-
return docComment.withBody(ListUtils.map(docComment.getBody(), doc -> indentJavadoc(doc, indent)));
310+
return docComment.withBody(ListUtils.map(docComment.getBody(), doc -> shiftJavadoc(doc, indent, levels)));
237311
}
238312
return comment;
239313
}
240314

241-
private Javadoc indentJavadoc(Javadoc doc, String indent) {
315+
private Javadoc shiftJavadoc(Javadoc doc, String indent, int levels) {
242316
if (!(doc instanceof Javadoc.LineBreak)) {
243317
return doc;
244318
}
@@ -247,7 +321,24 @@ private Javadoc indentJavadoc(Javadoc doc, String indent) {
247321
while (i < margin.length() && Character.isWhitespace(margin.charAt(i))) {
248322
i++;
249323
}
250-
return ((Javadoc.LineBreak) doc).withMargin(margin.substring(0, i) + indent + margin.substring(i));
324+
String whitespace = margin.substring(0, i);
325+
if (levels > 0) {
326+
whitespace += indent;
327+
} else if (whitespace.endsWith(indent)) {
328+
whitespace = whitespace.substring(0, whitespace.length() - indent.length());
329+
}
330+
return ((Javadoc.LineBreak) doc).withMargin(whitespace + margin.substring(i));
331+
}
332+
333+
private Space singleLine(Space prefix) {
334+
String whitespace = prefix.getWhitespace();
335+
int last = whitespace.lastIndexOf('\n');
336+
return last < 0 ? prefix : prefix.withWhitespace(whitespace.substring(last));
337+
}
338+
339+
private Space blankLineBefore(Space prefix) {
340+
String whitespace = prefix.getWhitespace();
341+
return whitespace.startsWith("\n\n") ? prefix : prefix.withWhitespace("\n" + whitespace);
251342
}
252343

253344
private @Nullable TypeTree supertypeOf(J.ClassDeclaration nested) {

‎src/main/resources/META-INF/rewrite/recipes.csv‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ maven,org.openrewrite.recipe:rewrite-rewrite,org.openrewrite.java.recipes.Exampl
1313
maven,org.openrewrite.recipe:rewrite-rewrite,org.openrewrite.java.recipes.ExecutionContextParameterName,Use a standard name for `ExecutionContext`,Visitors that are parameterized with `ExecutionContext` should use the parameter name `ctx`.,1,Recipes,Java,,Basic building blocks for transforming Java code.,"[{""name"":""parameterName"",""type"":""String"",""displayName"":""Parameter name"",""description"":""The name or prefix to use for the `ExecutionContext` parameter."",""example"":""ctx""}]",
1414
maven,org.openrewrite.recipe:rewrite-rewrite,org.openrewrite.java.recipes.FindRecipes,Find OpenRewrite recipes,"This recipe finds all OpenRewrite recipes, primarily to produce a data table that is being used to experiment with fine-tuning a large language model to produce more recipes.",1,Recipes,Java,,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.table.RewriteRecipeSource"",""displayName"":""Rewrite recipe source code"",""instanceName"":""Rewrite recipe source code"",""description"":""This table contains the source code of recipes along with their metadata for use in an experiment fine-tuning large language models to produce more recipes."",""columns"":[{""name"":""displayName"",""type"":""String"",""displayName"":""Recipe name"",""description"":""The name of the recipe.""},{""name"":""description"",""type"":""String"",""displayName"":""Recipe description"",""description"":""The description of the recipe.""},{""name"":""recipeType"",""type"":""RecipeType"",""displayName"":""Recipe type"",""description"":""Differentiate between Java and YAML recipes, as they may be two independent data sets used in LLM fine-tuning.""},{""name"":""sourceCode"",""type"":""String"",""displayName"":""Recipe source code"",""description"":""The full source code of the recipe.""},{""name"":""options"",""type"":""String"",""displayName"":""Recipe options"",""description"":""JSON format of recipe options.""}]}]"
1515
maven,org.openrewrite.recipe:rewrite-rewrite,org.openrewrite.java.recipes.GenerateDeprecatedMethodRecipes,Generate `InlineMethodCalls` recipes for deprecated delegating methods,"Finds `@Deprecated` method declarations whose body is a single delegation call to another method in the same class, and generates a declarative YAML recipe file containing `InlineMethodCalls` entries for each.",1,Recipes,Java,,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.java.recipes.DeprecatedMethodDelegations"",""displayName"":""Deprecated method delegations"",""instanceName"":""Deprecated method delegations"",""description"":""Deprecated methods that delegate to another method in the same class, suitable for inlining via `InlineMethodCalls`."",""columns"":[{""name"":""methodPattern"",""type"":""String"",""displayName"":""Method pattern"",""description"":""The method pattern of the deprecated method.""},{""name"":""replacement"",""type"":""String"",""displayName"":""Replacement"",""description"":""The replacement expression to inline.""},{""name"":""recipeYaml"",""type"":""String"",""displayName"":""Recipe YAML"",""description"":""A YAML snippet that can be copied into a recipe list.""}]}]"
16-
maven,org.openrewrite.recipe:rewrite-rewrite,org.openrewrite.java.recipes.InlineNestedVisitorClass,Inline nested visitor classes into the returning method,"Recipes that return a named, private, static nested visitor class straight from `getVisitor()` (or `getScanner()`) can declare that visitor anonymously instead, which keeps the visitor next to the recipe metadata that configures it. Only applied when the nested class is used exactly once, and when nothing would be lost by inlining it.",1,Recipes,Java,,Basic building blocks for transforming Java code.,,
16+
maven,org.openrewrite.recipe:rewrite-rewrite,org.openrewrite.java.recipes.InlineNestedVisitorClass,Inline nested visitor classes into the returning method,"Recipes that return a named, private, static nested visitor class straight from `getVisitor()` (or `getScanner()`) can declare that visitor anonymously instead, which keeps the visitor next to the recipe metadata that configures it. Any `private static final` constants the nested class declares are hoisted onto the recipe class, as anonymous classes can not declare them. Only applied when the nested class is used exactly once, and when nothing would be lost by inlining it.",1,Recipes,Java,,Basic building blocks for transforming Java code.,,
1717
maven,org.openrewrite.recipe:rewrite-rewrite,org.openrewrite.java.recipes.IsLiteralNullRecipe,"Use `J.Literal.isLiteralValue(expression, null)`","Replace `expression instanceof J.Literal && ((J.Literal) expression).getValue() == null` with `J.Literal.isLiteralValue(expression, null)`.",1,Recipes,Java,,Basic building blocks for transforming Java code.,,
1818
maven,org.openrewrite.recipe:rewrite-rewrite,org.openrewrite.java.recipes.JavaRecipeBestPractices,Java Recipe best practices,Best practices for Java recipe development.,25,Recipes,Java,,Basic building blocks for transforming Java code.,,
1919
maven,org.openrewrite.recipe:rewrite-rewrite,org.openrewrite.java.recipes.MissingOptionExample,Find missing `@Option` `example` values,"Find `@Option` annotations that are missing `example` values for documentation, and add a TODO comment.",1,Recipes,Java,,Basic building blocks for transforming Java code.,,

0 commit comments

Comments
 (0)