diff --git a/src/main/java/org/openrewrite/staticanalysis/MoveFieldsToTopOfClass.java b/src/main/java/org/openrewrite/staticanalysis/MoveFieldsToTopOfClass.java new file mode 100644 index 0000000000..7818031b05 --- /dev/null +++ b/src/main/java/org/openrewrite/staticanalysis/MoveFieldsToTopOfClass.java @@ -0,0 +1,131 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.staticanalysis; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.Statement; + +import java.time.Duration; +import java.util.Comparator; +import java.util.List; + +import static java.util.stream.Collectors.toList; + +@EqualsAndHashCode(callSuper = false) +@Value +public class MoveFieldsToTopOfClass extends Recipe { + + @Override + public String getDisplayName() { + return "Move fields to the top of class declaration"; + } + + @Override + public String getDescription() { + return "Reorders class members so that all field declarations appear before any method declarations, " + + "constructors, or other class members. This improves code organization and readability by " + + "grouping field declarations together at the top of the class. Comments associated with fields " + + "are preserved during the reordering."; + } + + @Override + public Duration getEstimatedEffortPerOccurrence() { + return Duration.ofMinutes(2); + } + + @Override + public TreeVisitor getVisitor() { + return new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + + List statements = + // getStatements() returns a new List each time, so no need to defensively copy + cd.getBody().getStatements() + .stream() + .sorted(statementComparator) + .collect(toList()); + + return cd.withBody(cd.getBody().withStatements(statements)); + } + + private final Comparator statementComparator = + (s1, s2) -> { + boolean s1IsField = s1 instanceof J.VariableDeclarations; + boolean s2IsField = s2 instanceof J.VariableDeclarations; + + // Fields come before non-fields + if (s1IsField && !s2IsField) { + return -1; + } + if (!s1IsField && s2IsField) { + return 1; + } + if (!s1IsField) { + return 0; // Both are non-fields, preserve order + + // Both are fields - sort by visibility and modifiers + } + J.VariableDeclarations field2 = (J.VariableDeclarations) s2; + + int priority1 = getFieldSortOrder(field1); + int priority2 = getFieldSortOrder(field2); + + return Integer.compare(priority1, priority2); + }; + + + // bitmasks for field sorting + // order: public static final < static final < protected static final < private static final < + // public static < static < protected static < private static < + // public final < final < protected final < private final < + // public < package-private < protected < private + private static final int PACKAGE_PRIVATE = 1; + private static final int PROTECTED = PACKAGE_PRIVATE << 1; + private static final int PRIVATE = PROTECTED << 1; + private static final int NON_FINAL = PRIVATE << 1; + private static final int NON_STATIC = NON_FINAL << 1; + + private int getFieldSortOrder(J.VariableDeclarations field) { + int order = 0; + if (field.hasModifier(J.Modifier.Type.Protected)) { + order |= PROTECTED; + } else if (field.hasModifier(J.Modifier.Type.Private)) { + order |= PRIVATE; + } else { + order |= PACKAGE_PRIVATE; + } + + if (!field.hasModifier(J.Modifier.Type.Static)) { + order |= NON_STATIC; + } + + if (!field.hasModifier(J.Modifier.Type.Final)) { + order |= NON_FINAL; + } + return order; + } + + }; + } +} diff --git a/src/main/resources/META-INF/rewrite/static-analysis.yml b/src/main/resources/META-INF/rewrite/static-analysis.yml index c38c1d0036..f4fa0161b1 100644 --- a/src/main/resources/META-INF/rewrite/static-analysis.yml +++ b/src/main/resources/META-INF/rewrite/static-analysis.yml @@ -25,6 +25,7 @@ recipeList: - org.openrewrite.java.format.EmptyNewlineAtEndOfFile - org.openrewrite.staticanalysis.ForLoopControlVariablePostfixOperators - org.openrewrite.staticanalysis.FinalizePrivateFields +# - org.openrewrite.staticanalysis.MoveFieldsToTopOfClass - org.openrewrite.java.format.MethodParamPad - org.openrewrite.java.format.NoWhitespaceAfter - org.openrewrite.java.format.NoWhitespaceBefore diff --git a/src/test/java/org/openrewrite/staticanalysis/MoveFieldsToTopOfClassTest.java b/src/test/java/org/openrewrite/staticanalysis/MoveFieldsToTopOfClassTest.java new file mode 100644 index 0000000000..3ba93f8313 --- /dev/null +++ b/src/test/java/org/openrewrite/staticanalysis/MoveFieldsToTopOfClassTest.java @@ -0,0 +1,527 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.staticanalysis; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class MoveFieldsToTopOfClassTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new MoveFieldsToTopOfClass()); + } + + @DocumentExample + @Test + void moveFieldsToTop() { + rewriteRun( + //language=java + java( + """ + class A { + void foo(String bar) { + int i = Integer.parseInt(bar); + } + // Important field + String myField = "important"; + } + """, + """ + class A { + // Important field + String myField = "important"; + void foo(String bar) { + int i = Integer.parseInt(bar); + } + } + """ + ) + ); + } + + @Test + void moveMultipleFields() { + rewriteRun( + //language=java + java( + """ + class Example { + public void method1() { + System.out.println("method1"); + } + + private String field1 = "value1"; + + public Example() { + // constructor + } + + protected int field2 = 42; + + public void method2() { + System.out.println("method2"); + } + + public static final String CONSTANT = "constant"; + } + """, + """ + class Example { + + public static final String CONSTANT = "constant"; + + protected int field2 = 42; + + private String field1 = "value1"; + public void method1() { + System.out.println("method1"); + } + + public Example() { + // constructor + } + + public void method2() { + System.out.println("method2"); + } + } + """ + ) + ); + } + + @Test + void fieldsAlreadyAtTop() { + rewriteRun( + //language=java + java( + """ + class AlreadyOrdered { + public static final String CONSTANT = "constant"; + protected int field2 = 42; + private String field1 = "value1"; + + public void method() { + System.out.println("method"); + } + + public AlreadyOrdered() { + // constructor + } + } + """ + ) + ); + } + + @Test + void onlyFieldsInClass() { + rewriteRun( + //language=java + java( + """ + class OnlyFields { + public static final String CONSTANT = "constant"; + protected int field2 = 42; + private String field1 = "value1"; + } + """ + ) + ); + } + + @Test + void onlyMethodsInClass() { + rewriteRun( + //language=java + java( + """ + class OnlyMethods { + public void method1() { + System.out.println("method1"); + } + + public OnlyMethods() { + // constructor + } + + public void method2() { + System.out.println("method2"); + } + } + """ + ) + ); + } + + @Test + void emptyClass() { + rewriteRun( + //language=java + java( + """ + class Empty { + } + """ + ) + ); + } + + @Test + void fieldsBetweenMethods() { + rewriteRun( + //language=java + java( + """ + class Mixed { + public void firstMethod() { + System.out.println("first"); + } + + private String field1 = "value1"; + + public void secondMethod() { + System.out.println("second"); + } + + private int field2 = 10; + + public void thirdMethod() { + System.out.println("third"); + } + } + """, + """ + class Mixed { + + private String field1 = "value1"; + + private int field2 = 10; + public void firstMethod() { + System.out.println("first"); + } + + public void secondMethod() { + System.out.println("second"); + } + + public void thirdMethod() { + System.out.println("third"); + } + } + """ + ) + ); + } + + @Test + void staticAndInstanceFields() { + rewriteRun( + //language=java + java( + """ + class StaticAndInstance { + public void method() { + System.out.println("method"); + } + + private static final String STATIC_FIELD = "static"; + private String instanceField = "instance"; + public final int publicField = 100; + + public StaticAndInstance() { + // constructor + } + } + """, + """ + class StaticAndInstance { + + private static final String STATIC_FIELD = "static"; + public final int publicField = 100; + private String instanceField = "instance"; + public void method() { + System.out.println("method"); + } + + public StaticAndInstance() { + // constructor + } + } + """ + ) + ); + } + + @Test + void innerClassesAndFields() { + rewriteRun( + //language=java + java( + """ + class OuterClass { + public void outerMethod() { + System.out.println("outer method"); + } + + private String outerField = "outer"; + + class InnerClass { + public void innerMethod() { + System.out.println("inner method"); + } + + private String innerField = "inner"; + } + + protected int anotherOuterField = 42; + } + """, + """ + class OuterClass { + + protected int anotherOuterField = 42; + + private String outerField = "outer"; + public void outerMethod() { + System.out.println("outer method"); + } + + class InnerClass { + + private String innerField = "inner"; + public void innerMethod() { + System.out.println("inner method"); + } + } + } + """ + ) + ); + } + + @Test + void preserveCommentsWithFields() { + rewriteRun( + //language=java + java( + """ + class A { + void foo(String bar) { + int i = Integer.parseInt(bar); + } + + // this is my field, not yours + String myField = "important"; + } + """, + """ + class A { + + // this is my field, not yours + String myField = "important"; + void foo(String bar) { + int i = Integer.parseInt(bar); + } + } + """ + ) + ); + } + + @Test + void preserveMultilineCommentsWithFields() { + rewriteRun( + //language=java + java( + """ + class A { + public void method() { + System.out.println("method"); + } + + /* + * This is a multiline comment + * for my field + */ + private String field1 = "value1"; + + /** + * Javadoc comment for field2 + * @deprecated this field is old + */ + @Deprecated + private int field2 = 42; + } + """, + """ + class A { + + /* + * This is a multiline comment + * for my field + */ + private String field1 = "value1"; + + /** + * Javadoc comment for field2 + * @deprecated this field is old + */ + @Deprecated + private int field2 = 42; + public void method() { + System.out.println("method"); + } + } + """ + ) + ); + } + + @Test + void preserveCommentsWithMixedMembers() { + rewriteRun( + //language=java + java( + """ + class Mixed { + // Method comment + public void firstMethod() { + System.out.println("first"); + } + + // Field comment 1 + private String field1 = "value1"; + + /* Another method comment */ + public void secondMethod() { + System.out.println("second"); + } + + // Field comment 2 + private int field2 = 10; + } + """, + """ + class Mixed { + + // Field comment 1 + private String field1 = "value1"; + + // Field comment 2 + private int field2 = 10; + // Method comment + public void firstMethod() { + System.out.println("first"); + } + + /* Another method comment */ + public void secondMethod() { + System.out.println("second"); + } + } + """ + ) + ); + } + + @Test + void variableDeclarationsWithMultipleVariables() { + rewriteRun( + //language=java + java( + """ + class MultipleVars { + public void method() { + System.out.println("method"); + } + + private int x = 1, y = 2, z = 3; + + public MultipleVars() { + // constructor + } + } + """, + """ + class MultipleVars { + + private int x = 1, y = 2, z = 3; + public void method() { + System.out.println("method"); + } + + public MultipleVars() { + // constructor + } + } + """ + ) + ); + } + + @Test + void variableDeclarationsAnnotations() { + rewriteRun( + //language=java + java( + """ + class A { + + public void method() { + System.out.println("method"); + } + + @Deprecated + private final int annotationNewLine; + + @Deprecated + private final int annotationNewLineWithNewLines; + + @Deprecated private final int annotationSameLine; + @Deprecated private final int annotationSameLineNoNewline; + + @Deprecated private final int annotationSameLineWithNewLines; + } + """, + """ + class A { + + @Deprecated + private final int annotationNewLine; + + @Deprecated + private final int annotationNewLineWithNewLines; + + @Deprecated private final int annotationSameLine; + @Deprecated private final int annotationSameLineNoNewline; + + @Deprecated private final int annotationSameLineWithNewLines; + + public void method() { + System.out.println("method"); + } + } + """ + ) + ); + } +}