Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 139 additions & 4 deletions src/main/java/org/openrewrite/java/migrate/lombok/LombokUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@
import org.jspecify.annotations.Nullable;
import org.openrewrite.Cursor;
import org.openrewrite.internal.StringUtils;
import org.openrewrite.java.AnnotationMatcher;
import org.openrewrite.java.marker.CompactConstructor;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.Flag;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.*;

import java.util.ArrayList;
import java.util.List;

import static lombok.AccessLevel.*;
import static org.openrewrite.java.tree.J.Modifier.Type.*;
Expand Down Expand Up @@ -227,4 +228,138 @@ static AccessLevel getAccessLevel(J.MethodDeclaration methodDeclaration) {
return PACKAGE;
}

/**
* Returns the "required" fields for a class in declaration order: non-static final fields
* without initializers, plus non-static {@code @lombok.NonNull} fields without initializers.
*/
static List<J.VariableDeclarations.NamedVariable> getRequiredFields(J.ClassDeclaration classDecl) {
List<J.VariableDeclarations.NamedVariable> result = new ArrayList<>();
for (Statement stmt : classDecl.getBody().getStatements()) {
if (!(stmt instanceof J.VariableDeclarations)) {
continue;
}
J.VariableDeclarations varDecls = (J.VariableDeclarations) stmt;
if (varDecls.hasModifier(Static)) {
continue;
}
boolean isFinal = varDecls.hasModifier(Final);
boolean hasNonNull = varDecls.getLeadingAnnotations().stream()
.anyMatch(new AnnotationMatcher("@lombok.NonNull")::matches);
if (!isFinal && !hasNonNull) {
continue;
}
for (J.VariableDeclarations.NamedVariable var : varDecls.getVariables()) {
if (var.getInitializer() == null) {
result.add(var);
}
}
}
return result;
}

/**
* Returns all non-static fields for a class in declaration order.
*/
static List<J.VariableDeclarations.NamedVariable> getAllNonStaticFields(J.ClassDeclaration classDecl) {
List<J.VariableDeclarations.NamedVariable> result = new ArrayList<>();
for (Statement stmt : classDecl.getBody().getStatements()) {
if (!(stmt instanceof J.VariableDeclarations)) {
continue;
}
J.VariableDeclarations varDecls = (J.VariableDeclarations) stmt;
if (varDecls.hasModifier(Static)) {
continue;
}
result.addAll(varDecls.getVariables());
}
return result;
}

/**
* Checks that a constructor's body consists entirely of simple field assignments
* matching the given fields in order, with matching parameter types.
*/
static boolean isConstructorAssigningExactFields(J.MethodDeclaration constructor,
List<J.VariableDeclarations.NamedVariable> expectedFields) {
if (constructor.getBody() == null) {
return false;
}
List<Statement> statements = constructor.getBody().getStatements();
if (statements.size() != expectedFields.size()) {
return false;
}

// Get constructor parameters (accounting for J.Empty when no params)
List<Statement> params = constructor.getParameters();
if (expectedFields.isEmpty()) {
return false;
}
if (params.size() != expectedFields.size() || params.get(0) instanceof J.Empty) {
return false;
}

JavaType.FullyQualified declaringType = constructor.getMethodType() != null
? constructor.getMethodType().getDeclaringType() : null;

for (int i = 0; i < expectedFields.size(); i++) {
J.VariableDeclarations.NamedVariable expectedField = expectedFields.get(i);

// Check parameter type matches field type
if (!(params.get(i) instanceof J.VariableDeclarations)) {
return false;
}
J.VariableDeclarations paramDecl = (J.VariableDeclarations) params.get(i);
J.VariableDeclarations.NamedVariable param = paramDecl.getVariables().get(0);
if (!TypeUtils.isOfType(param.getType(), expectedField.getType())) {
return false;
}

// Check statement is a simple field assignment
if (!(statements.get(i) instanceof J.Assignment)) {
return false;
}
J.Assignment assignment = (J.Assignment) statements.get(i);

// Check left side is the expected field
String assignedFieldName = getAssignedFieldName(assignment.getVariable(), declaringType);
if (assignedFieldName == null || !assignedFieldName.equals(expectedField.getSimpleName())) {
return false;
}

// Check right side references the constructor parameter
if (!isReferenceTo(assignment.getAssignment(), param)) {
return false;
}
}
return true;
}

private static @Nullable String getAssignedFieldName(Expression variable, JavaType.@Nullable FullyQualified declaringType) {
if (variable instanceof J.Identifier) {
J.Identifier id = (J.Identifier) variable;
if (id.getFieldType() != null && (declaringType == null || declaringType == id.getFieldType().getOwner())) {
return id.getSimpleName();
}
} else if (variable instanceof J.FieldAccess) {
J.FieldAccess fa = (J.FieldAccess) variable;
Expression target = fa.getTarget();
if (target instanceof J.Identifier && "this".equals(((J.Identifier) target).getSimpleName())) {
if (fa.getName().getFieldType() != null &&
(declaringType == null || declaringType == fa.getName().getFieldType().getOwner())) {
return fa.getSimpleName();
}
}
}
return null;
}

private static boolean isReferenceTo(Expression expr, J.VariableDeclarations.NamedVariable param) {
if (expr instanceof J.Identifier) {
J.Identifier id = (J.Identifier) expr;
return id.getSimpleName().equals(param.getSimpleName()) &&
TypeUtils.isOfType(id.getType(), param.getType());
}
return false;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* 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
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* 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.java.migrate.lombok;

import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.jspecify.annotations.Nullable;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.AnnotationMatcher;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.marker.CompactConstructor;
import org.openrewrite.java.service.AnnotationService;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.TypeUtils;

import java.util.List;

import static java.util.Comparator.comparing;

@EqualsAndHashCode(callSuper = false)
@Value
public class UseAllArgsConstructor extends Recipe {

private static final AnnotationMatcher ALL_ARGS_MATCHER = new AnnotationMatcher("@lombok.AllArgsConstructor");
private static final AnnotationMatcher REQUIRED_ARGS_MATCHER = new AnnotationMatcher("@lombok.RequiredArgsConstructor");
private static final AnnotationMatcher OVERRIDE_MATCHER = new AnnotationMatcher("java.lang.Override");

String displayName = "Use `@AllArgsConstructor` where applicable";

String description = "Prefer the Lombok `@AllArgsConstructor` annotation over explicitly written out constructors " +
"that assign all non-static fields.";

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.@Nullable MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
if (!method.isConstructor() ||
method.getMarkers().findFirst(CompactConstructor.class).isPresent()) {
return super.visitMethodDeclaration(method, ctx);
}

J.ClassDeclaration enclosing = getCursor().firstEnclosing(J.ClassDeclaration.class);
if (enclosing == null) {
return super.visitMethodDeclaration(method, ctx);
}

// Skip if class already has @AllArgsConstructor or @RequiredArgsConstructor
if (enclosing.getLeadingAnnotations().stream().anyMatch(ann ->
ALL_ARGS_MATCHER.matches(ann) || REQUIRED_ARGS_MATCHER.matches(ann))) {
return super.visitMethodDeclaration(method, ctx);
}

List<J.VariableDeclarations.NamedVariable> allFields = LombokUtils.getAllNonStaticFields(enclosing);
if (allFields.isEmpty()) {
return super.visitMethodDeclaration(method, ctx);
}

if (!LombokUtils.isConstructorAssigningExactFields(method, allFields)) {
return super.visitMethodDeclaration(method, ctx);
}

AccessLevel accessLevel = LombokUtils.getAccessLevel(method);
List<J.Annotation> constructorAnnotations = service(AnnotationService.class).getAllAnnotations(getCursor());
constructorAnnotations.removeIf(OVERRIDE_MATCHER::matches);

doAfterVisit(new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
if (TypeUtils.isOfType(classDecl.getType(), enclosing.getType())) {
String template = UseRequiredArgsConstructor.buildAnnotationTemplate(
"AllArgsConstructor", accessLevel, constructorAnnotations);
maybeAddImport("lombok.AllArgsConstructor");
if (accessLevel != AccessLevel.PUBLIC) {
maybeAddImport("lombok.AccessLevel");
}
return JavaTemplate.builder(template)
.imports("lombok.*")
.javaParser(JavaParser.fromJavaVersion().classpath("lombok"))
.build()
.apply(getCursor(), classDecl.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName)));
}
return super.visitClassDeclaration(classDecl, ctx);
}
});
return null;
}
};
}
}
Loading
Loading