From f524b18d96594ae99fb8bf4618385f28a632cc77 Mon Sep 17 00:00:00 2001 From: Ouwesh Seeroo Date: Sat, 15 Aug 2026 01:38:01 +0400 Subject: [PATCH] Add Lombok utility class migration recipe --- .../migrate/lombok/UseLombokUtilityClass.java | 546 ++++++++++++++++ .../resources/META-INF/rewrite/lombok.yml | 1 + .../resources/META-INF/rewrite/recipes.csv | 3 +- .../lombok/UseLombokUtilityClassTest.java | 584 ++++++++++++++++++ 4 files changed, 1133 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/openrewrite/java/migrate/lombok/UseLombokUtilityClass.java create mode 100644 src/test/java/org/openrewrite/java/migrate/lombok/UseLombokUtilityClassTest.java diff --git a/src/main/java/org/openrewrite/java/migrate/lombok/UseLombokUtilityClass.java b/src/main/java/org/openrewrite/java/migrate/lombok/UseLombokUtilityClass.java new file mode 100644 index 0000000000..4eeb315e67 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lombok/UseLombokUtilityClass.java @@ -0,0 +1,546 @@ +/* + * Copyright 2026 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.java.migrate.lombok; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.ScanningRecipe; +import org.openrewrite.SourceFile; +import org.openrewrite.Tree; +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.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.Statement; +import org.openrewrite.java.tree.TypeUtils; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.Collections.singleton; +import static java.util.Comparator.comparing; + +@EqualsAndHashCode(callSuper = false) +@Value +public class UseLombokUtilityClass extends ScanningRecipe { + + private static final AnnotationMatcher UTILITY_CLASS_MATCHER = new AnnotationMatcher("@lombok.experimental.UtilityClass"); + private static final Pattern FLAG_USAGE_PATTERN = Pattern.compile("^\\s*lombok\\.utilityClass\\.flagUsage\\s*=\\s*([^\\s#]+).*$", Pattern.CASE_INSENSITIVE); + private static final Pattern CLEAR_FLAG_USAGE_PATTERN = Pattern.compile("^\\s*clear\\s+lombok\\.utilityClass\\.flagUsage\\s*$", Pattern.CASE_INSENSITIVE); + private static final Pattern IMPORT_PATTERN = Pattern.compile("^\\s*import\\s+(.+?)\\s*$", Pattern.CASE_INSENSITIVE); + private static final Pattern STOP_BUBBLING_PATTERN = Pattern.compile("^\\s*config\\.stopBubbling\\s*=\\s*(true|false).*$", Pattern.CASE_INSENSITIVE); + + String displayName = "Use Lombok `@UtilityClass` where applicable"; + + String description = "Replace static-only utility classes with Lombok's `@UtilityClass` annotation."; + + Set tags = singleton("lombok"); + + @Override + public UtilityClassAccumulator getInitialValue(ExecutionContext ctx) { + return new UtilityClassAccumulator(); + } + + @Override + public TreeVisitor getScanner(UtilityClassAccumulator acc) { + return new TreeVisitor() { + @Override + public Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (!(tree instanceof SourceFile)) { + return tree; + } + + SourceFile sourceFile = (SourceFile) tree; + recordLombokConfig(acc, sourceFile); + if (sourceFile instanceof J.CompilationUnit) { + new ReferenceScanner(acc.unsafeUtilityClassTypes).visit((J.CompilationUnit) sourceFile, ctx); + } + return sourceFile; + } + }; + } + + @Override + public TreeVisitor getVisitor(UtilityClassAccumulator acc) { + return new JavaIsoVisitor() { + private final Map candidates = new HashMap<>(); + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + UtilityClassCandidate candidate = candidate(classDecl, getCursor(), acc); + if (candidate == null) { + return super.visitClassDeclaration(classDecl, ctx); + } + + candidates.put(classDecl.getId(), candidate); + boolean utilityClassNameConflict = hasUtilityClassNameConflict(); + if (!utilityClassNameConflict) { + maybeAddImport("lombok.experimental.UtilityClass"); + } + J.ClassDeclaration cd = JavaTemplate.builder(utilityClassNameConflict ? + "@lombok.experimental.UtilityClass" : + "@UtilityClass") + .imports("lombok.experimental.UtilityClass") + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "lombok")) + .build() + .apply(getCursor(), classDecl.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName))); + return super.visitClassDeclaration(cd, ctx); + } + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx); + UtilityClassCandidate candidate = enclosingCandidate(); + if (candidate == null || !candidate.methodIds.contains(md.getId())) { + return md; + } + return maybeAutoFormat(method, md.withModifiers(withoutStatic(md.getModifiers())), ctx); + } + + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations variableDeclarations, + ExecutionContext ctx) { + J.VariableDeclarations vd = super.visitVariableDeclarations(variableDeclarations, ctx); + UtilityClassCandidate candidate = enclosingCandidate(); + if (candidate == null || !candidate.fieldIds.contains(vd.getId())) { + return vd; + } + return maybeAutoFormat(variableDeclarations, vd.withModifiers(withoutStatic(vd.getModifiers())), ctx); + } + + private @Nullable UtilityClassCandidate enclosingCandidate() { + J.ClassDeclaration enclosing = getCursor().firstEnclosing(J.ClassDeclaration.class); + return enclosing == null ? null : candidates.get(enclosing.getId()); + } + + private boolean hasUtilityClassNameConflict() { + J.CompilationUnit compilationUnit = getCursor().firstEnclosing(J.CompilationUnit.class); + if (compilationUnit != null && compilationUnit.getClasses().stream() + .anyMatch(type -> "UtilityClass".equals(type.getSimpleName()))) { + return true; + } + + Cursor parent = getCursor().getParent(); + while (parent != null) { + if (parent.getValue() instanceof J.ClassDeclaration) { + J.ClassDeclaration enclosing = (J.ClassDeclaration) parent.getValue(); + if ("UtilityClass".equals(enclosing.getSimpleName()) || + enclosing.getBody().getStatements().stream() + .filter(J.ClassDeclaration.class::isInstance) + .map(J.ClassDeclaration.class::cast) + .anyMatch(type -> "UtilityClass".equals(type.getSimpleName()))) { + return true; + } + } + parent = parent.getParent(); + } + return false; + } + }; + } + + private static @Nullable UtilityClassCandidate candidate(J.ClassDeclaration classDecl, + Cursor cursor, + UtilityClassAccumulator acc) { + JavaType.FullyQualified type = classDecl.getType(); + if (type == null || + classDecl.getKind() != J.ClassDeclaration.Kind.Type.Class || + !isTopLevelOrStaticallyLegalMemberClass(cursor) || + classDecl.hasModifier(J.Modifier.Type.Abstract) || + hasTypeParameters(classDecl) || + classDecl.getExtends() != null || + (classDecl.getImplements() != null && !classDecl.getImplements().isEmpty()) || + classDecl.getLeadingAnnotations().stream().anyMatch(UTILITY_CLASS_MATCHER::matches) || + acc.unsafeUtilityClassTypes.contains(normalizeTypeName(type.getFullyQualifiedName())) || + isUtilityClassFlaggedAsError(cursor, acc.lombokConfigs)) { + return null; + } + + Set methodIds = new HashSet<>(); + Set fieldIds = new HashSet<>(); + for (Statement statement : classDecl.getBody().getStatements()) { + if (statement instanceof J.MethodDeclaration) { + J.MethodDeclaration method = (J.MethodDeclaration) statement; + if (method.isConstructor() || + !method.hasModifier(J.Modifier.Type.Static) || + "main".equalsIgnoreCase(method.getSimpleName())) { + return null; + } + methodIds.add(method.getId()); + } else if (statement instanceof J.VariableDeclarations) { + J.VariableDeclarations field = (J.VariableDeclarations) statement; + if (!field.hasModifier(J.Modifier.Type.Static)) { + return null; + } + fieldIds.add(field.getId()); + } else { + return null; + } + } + + if (methodIds.isEmpty() && fieldIds.isEmpty()) { + return null; + } + return new UtilityClassCandidate(methodIds, fieldIds); + } + + private static boolean isTopLevelOrStaticallyLegalMemberClass(Cursor cursor) { + if (!isTopLevelOrMemberClass(cursor)) { + return false; + } + + Cursor parent = cursor.getParent(); + while (parent != null) { + if (parent.getValue() instanceof J.ClassDeclaration && + !isTopLevelClass(parent) && + !isStaticOrImplicit(parent)) { + return false; + } + parent = parent.getParent(); + } + return true; + } + + private static boolean isTopLevelOrMemberClass(Cursor cursor) { + Cursor parent = cursor.getParent(); + while (parent != null) { + Object value = parent.getValue(); + if (value instanceof J.CompilationUnit) { + return true; + } + if (value instanceof J.Block) { + Cursor blockParent = parent.getParent(); + return blockParent != null && blockParent.getValue() instanceof J.ClassDeclaration; + } + if (value instanceof J.MethodDeclaration || value instanceof J.NewClass) { + return false; + } + parent = parent.getParent(); + } + return false; + } + + private static boolean isTopLevelClass(Cursor classCursor) { + Cursor parent = classCursor.getParent(); + while (parent != null) { + Object value = parent.getValue(); + if (value instanceof J.CompilationUnit) { + return true; + } + if (value instanceof J.ClassDeclaration || value instanceof J.MethodDeclaration || value instanceof J.NewClass) { + return false; + } + parent = parent.getParent(); + } + return false; + } + + private static boolean isStaticOrImplicit(Cursor classCursor) { + J.ClassDeclaration classDecl = (J.ClassDeclaration) classCursor.getValue(); + J.ClassDeclaration.Kind.Type kind = classDecl.getKind(); + if (classDecl.hasModifier(J.Modifier.Type.Static) || + kind == J.ClassDeclaration.Kind.Type.Interface || + kind == J.ClassDeclaration.Kind.Type.Annotation || + kind == J.ClassDeclaration.Kind.Type.Enum || + kind == J.ClassDeclaration.Kind.Type.Record) { + return true; + } + + Cursor parent = classCursor.getParent(); + while (parent != null) { + if (parent.getValue() instanceof J.ClassDeclaration) { + J.ClassDeclaration enclosing = (J.ClassDeclaration) parent.getValue(); + J.ClassDeclaration.Kind.Type enclosingKind = enclosing.getKind(); + return enclosingKind == J.ClassDeclaration.Kind.Type.Interface || + enclosingKind == J.ClassDeclaration.Kind.Type.Annotation || + enclosingKind == J.ClassDeclaration.Kind.Type.Enum || + enclosingKind == J.ClassDeclaration.Kind.Type.Record; + } + parent = parent.getParent(); + } + return false; + } + + private static boolean hasTypeParameters(J.ClassDeclaration classDecl) { + List typeParameters = classDecl.getTypeParameters(); + return typeParameters != null && !typeParameters.isEmpty(); + } + + private static List withoutStatic(List modifiers) { + List result = new ArrayList<>(modifiers); + result.removeIf(modifier -> modifier.getType() == J.Modifier.Type.Static); + return result; + } + + private static void recordType(Set typeNames, @Nullable JavaType type) { + JavaType.FullyQualified fullyQualified = TypeUtils.asFullyQualified(type); + if (fullyQualified != null) { + typeNames.add(normalizeTypeName(fullyQualified.getFullyQualifiedName())); + } + } + + private static void recordDeclaringType(Set typeNames, JavaType.@Nullable Method methodType) { + if (methodType != null && methodType.getDeclaringType() != null) { + typeNames.add(normalizeTypeName(methodType.getDeclaringType().getFullyQualifiedName())); + } + } + + private static String normalizeTypeName(String typeName) { + return typeName.replace('$', '.'); + } + + private static void recordLombokConfig(UtilityClassAccumulator acc, SourceFile sourceFile) { + Path sourcePath = sourceFile.getSourcePath().normalize(); + if (sourcePath.getFileName() == null || + (!"lombok.config".equals(sourcePath.getFileName().toString()) && + !sourcePath.getFileName().toString().endsWith(".config"))) { + return; + } + + @Nullable String flagUsage = null; + boolean hasFlagUsage = false; + boolean stopBubbling = false; + boolean importsAllowed = true; + List imports = new ArrayList<>(); + boolean hasUnresolvedImport = false; + for (String line : sourceFile.printAll().split("\\R")) { + String uncommented = line.replaceFirst("\\s+#.*$", "").trim(); + if (uncommented.isEmpty()) { + continue; + } + Matcher importMatcher = IMPORT_PATTERN.matcher(uncommented); + if (importsAllowed && importMatcher.matches()) { + Path importedConfig = resolveImport(sourcePath, importMatcher.group(1)); + if (importedConfig == null) { + hasUnresolvedImport = true; + } else { + imports.add(pathKey(importedConfig)); + } + continue; + } + + importsAllowed = false; + Matcher clearFlagUsageMatcher = CLEAR_FLAG_USAGE_PATTERN.matcher(uncommented); + if (clearFlagUsageMatcher.matches()) { + hasFlagUsage = true; + flagUsage = null; + continue; + } + Matcher flagUsageMatcher = FLAG_USAGE_PATTERN.matcher(uncommented); + if (flagUsageMatcher.matches()) { + hasFlagUsage = true; + flagUsage = flagUsageMatcher.group(1); + continue; + } + Matcher stopBubblingMatcher = STOP_BUBBLING_PATTERN.matcher(uncommented); + if (stopBubblingMatcher.matches()) { + stopBubbling = Boolean.parseBoolean(stopBubblingMatcher.group(1)); + } + } + acc.lombokConfigs.put(pathKey(sourcePath), new LombokConfig(flagUsage, hasFlagUsage, stopBubbling, imports, hasUnresolvedImport)); + } + + private static boolean isUtilityClassFlaggedAsError(Cursor cursor, Map lombokConfigs) { + SourceFile sourceFile = cursor.firstEnclosing(SourceFile.class); + if (sourceFile == null) { + return false; + } + + Path directory = sourceFile.getSourcePath().getParent(); + Set visitedConfigs = new HashSet<>(); + while (true) { + LombokConfigResolution resolution = resolveConfig(configurationPathKey(directory), lombokConfigs, visitedConfigs, false); + if (resolution.hasUnresolvedImport) { + return true; + } + if (resolution.hasFlagUsage) { + return "error".equalsIgnoreCase(resolution.flagUsage); + } + if (resolution.stopBubbling || directory == null) { + return false; + } + directory = directory.getParent(); + } + } + + private static @Nullable Path resolveImport(Path sourcePath, String importPath) { + if (importPath.contains("!")) { + return null; + } + try { + Path imported = Paths.get(importPath); + if (!imported.isAbsolute() && sourcePath.getParent() != null) { + imported = sourcePath.getParent().resolve(imported); + } + return imported.normalize(); + } catch (RuntimeException ignored) { + return null; + } + } + + private static LombokConfigResolution resolveConfig(String configPath, + Map lombokConfigs, + Set visitedConfigs, + boolean imported) { + if (!visitedConfigs.add(configPath)) { + return LombokConfigResolution.NONE; + } + + LombokConfig config = lombokConfigs.get(configPath); + if (config == null) { + return imported ? LombokConfigResolution.UNRESOLVED_IMPORT : LombokConfigResolution.NONE; + } + if (config.hasFlagUsage) { + return new LombokConfigResolution(config.flagUsage, true, config.stopBubbling, config.hasUnresolvedImport); + } + if (config.hasUnresolvedImport) { + return new LombokConfigResolution(null, false, config.stopBubbling, true); + } + + boolean stopBubbling = config.stopBubbling; + for (int i = config.imports.size() - 1; i >= 0; i--) { + LombokConfigResolution importedConfig = resolveConfig(config.imports.get(i), lombokConfigs, visitedConfigs, true); + stopBubbling |= importedConfig.stopBubbling; + if (importedConfig.hasUnresolvedImport || importedConfig.hasFlagUsage) { + return new LombokConfigResolution(importedConfig.flagUsage, importedConfig.hasFlagUsage, stopBubbling, importedConfig.hasUnresolvedImport); + } + } + return new LombokConfigResolution(null, false, stopBubbling, false); + } + + private static String configurationPathKey(@Nullable Path directory) { + return pathKey(directory == null ? Paths.get("lombok.config") : directory.resolve("lombok.config")); + } + + private static String pathKey(Path path) { + return path.normalize().toString().replace('\\', '/'); + } + + private static class ReferenceScanner extends JavaIsoVisitor { + private final Set unsafeUtilityClassTypes; + + private ReferenceScanner(Set unsafeUtilityClassTypes) { + this.unsafeUtilityClassTypes = unsafeUtilityClassTypes; + } + + @Override + public J.NewClass visitNewClass(J.NewClass newClass, ExecutionContext ctx) { + J.NewClass nc = super.visitNewClass(newClass, ctx); + recordType(unsafeUtilityClassTypes, nc.getType()); + recordDeclaringType(unsafeUtilityClassTypes, nc.getConstructorType()); + return nc; + } + + @Override + public J.MemberReference visitMemberReference(J.MemberReference memberRef, ExecutionContext ctx) { + J.MemberReference mr = super.visitMemberReference(memberRef, ctx); + JavaType.Method methodType = mr.getMethodType(); + if (methodType != null && + ("".equals(methodType.getName()) || "new".equals(mr.getReference().getSimpleName()))) { + recordDeclaringType(unsafeUtilityClassTypes, methodType); + } + return mr; + } + + @Override + public J.Import visitImport(J.Import anImport, ExecutionContext ctx) { + J.Import imp = super.visitImport(anImport, ctx); + if (imp.isStatic() && !"*".equals(imp.getQualid().getSimpleName())) { + unsafeUtilityClassTypes.add(normalizeTypeName(imp.getTypeName())); + } + return imp; + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + if (cd.getExtends() != null) { + recordType(unsafeUtilityClassTypes, cd.getExtends().getType()); + } + return cd; + } + } + + static class UtilityClassAccumulator { + final Set unsafeUtilityClassTypes = new HashSet<>(); + final Map lombokConfigs = new HashMap<>(); + } + + private static class LombokConfig { + private final @Nullable String flagUsage; + private final boolean hasFlagUsage; + private final boolean stopBubbling; + private final List imports; + private final boolean hasUnresolvedImport; + + private LombokConfig(@Nullable String flagUsage, + boolean hasFlagUsage, + boolean stopBubbling, + List imports, + boolean hasUnresolvedImport) { + this.flagUsage = flagUsage; + this.hasFlagUsage = hasFlagUsage; + this.stopBubbling = stopBubbling; + this.imports = imports; + this.hasUnresolvedImport = hasUnresolvedImport; + } + } + + private static class LombokConfigResolution { + private static final LombokConfigResolution NONE = new LombokConfigResolution(null, false, false, false); + private static final LombokConfigResolution UNRESOLVED_IMPORT = new LombokConfigResolution(null, false, false, true); + + private final @Nullable String flagUsage; + private final boolean hasFlagUsage; + private final boolean stopBubbling; + private final boolean hasUnresolvedImport; + + private LombokConfigResolution(@Nullable String flagUsage, + boolean hasFlagUsage, + boolean stopBubbling, + boolean hasUnresolvedImport) { + this.flagUsage = flagUsage; + this.hasFlagUsage = hasFlagUsage; + this.stopBubbling = stopBubbling; + this.hasUnresolvedImport = hasUnresolvedImport; + } + } + + private static final class UtilityClassCandidate { + private final Set methodIds; + private final Set fieldIds; + + private UtilityClassCandidate(Set methodIds, Set fieldIds) { + this.methodIds = methodIds; + this.fieldIds = fieldIds; + } + } +} diff --git a/src/main/resources/META-INF/rewrite/lombok.yml b/src/main/resources/META-INF/rewrite/lombok.yml index 8739fe42cd..e88069e509 100644 --- a/src/main/resources/META-INF/rewrite/lombok.yml +++ b/src/main/resources/META-INF/rewrite/lombok.yml @@ -29,6 +29,7 @@ recipeList: - org.openrewrite.java.migrate.lombok.UseNoArgsConstructor - org.openrewrite.java.migrate.lombok.UseRequiredArgsConstructor - org.openrewrite.java.migrate.lombok.UseAllArgsConstructor + - org.openrewrite.java.migrate.lombok.UseLombokUtilityClass - org.openrewrite.maven.ChangeDependencyScope: groupId: org.projectlombok artifactId: lombok diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index dbb7a45637..bb0feb8866 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -423,7 +423,7 @@ Limitations: - If the correct name for a method is already taken by another method then the name will not be corrected. - Method name swaps or circular renaming within a class cannot be performed because the names block each other. E.g. `int getFoo() { return ba; } int getBa() { return foo; }` stays as it is.",1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.LombokBestPractices,Lombok Best Practices,Applies all recipes that enforce best practices for using Lombok.,27,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.LombokBestPractices,Lombok Best Practices,Applies all recipes that enforce best practices for using Lombok.,28,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.LombokOnXToOnX_,Migrate Lombok's `@__` syntax to `onX_` for Java 8+,"Migrates Lombok's `onX` annotations from the Java 7 style using `@__` to the Java 8+ style using `onX_`. For example, `@Getter(onMethod=@__({@Id}))` becomes `@Getter(onMethod_={@Id})`.",1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.LombokValToFinalVar,Prefer `final var` over `lombok.val`,Prefer the Java standard library's `final var` and `var` over third-party usage of Lombok's `lombok.val` and `lombok.var` in Java 10 or higher.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.LombokValueToRecord,Convert `@lombok.Value` class to Record,Convert Lombok `@Value` annotated classes to standard Java Records.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,"[{""name"":""useExactToString"",""type"":""Boolean"",""displayName"":""Add a `toString()` implementation matching Lombok"",""description"":""When set the `toString` format from Lombok is used in the migrated record.""}]", @@ -431,6 +431,7 @@ maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.l maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UseAllArgsConstructor,Use `@AllArgsConstructor` where applicable,Prefer the Lombok `@AllArgsConstructor` annotation over explicitly written out constructors that assign all non-static fields.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UseLombokGetter,Convert getter methods to annotations,Convert trivial getter methods to `@Getter` annotations on their respective fields.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UseLombokSetter,Convert setter methods to annotations,Convert trivial setter methods to `@Setter` annotations on their respective fields.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UseLombokUtilityClass,Use Lombok `@UtilityClass` where applicable,Replace static-only utility classes with Lombok's `@UtilityClass` annotation.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UseNoArgsConstructor,Use `@NoArgsConstructor` where applicable,Prefer the Lombok `@NoArgsConstructor` annotation over explicitly written out constructors.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UseRequiredArgsConstructor,Use `@RequiredArgsConstructor` where applicable,Prefer the Lombok `@RequiredArgsConstructor` annotation over explicitly written out constructors that only assign final fields.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.log.UseCommonsLog,Use `@CommonsLog` instead of explicit fields,Prefer the lombok annotation `@CommonsLog` over explicitly written out `org.apache.commons.logging.Log` fields.,1,Log,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,"[{""name"":""fieldName"",""type"":""String"",""displayName"":""Name of the log field"",""description"":""Name of the log field to replace. If not specified, the field name is not checked and any field that satisfies the other checks is converted."",""example"":""LOGGER""}]", diff --git a/src/test/java/org/openrewrite/java/migrate/lombok/UseLombokUtilityClassTest.java b/src/test/java/org/openrewrite/java/migrate/lombok/UseLombokUtilityClassTest.java new file mode 100644 index 0000000000..a0d1bf2431 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/lombok/UseLombokUtilityClassTest.java @@ -0,0 +1,584 @@ +/* + * Copyright 2026 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.java.migrate.lombok; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.Issue; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.test.SourceSpecs.text; + +class UseLombokUtilityClassTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new UseLombokUtilityClass()) + .parser(JavaParser.fromJavaVersion().classpath("lombok")); + } + + @DocumentExample + @Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/512") + @Test + void utilityClass() { + rewriteRun( + //language=java + java( + """ + class Numbers { + private static final int OFFSET = 1; + private static int calls; + + static int add(int left, int right) { + calls++; + return left + right + OFFSET; + } + } + """, + """ + import lombok.experimental.UtilityClass; + + @UtilityClass + class Numbers { + private final int OFFSET = 1; + private int calls; + + int add(int left, int right) { + calls++; + return left + right + OFFSET; + } + } + """ + ) + ); + } + + @Test + void usesQualifiedAnnotationWhenUtilityClassTypeExists() { + rewriteRun( + //language=java + java( + """ + class UtilityClass { + } + + class Numbers { + static int add(int left, int right) { + return left + right; + } + } + """, + """ + class UtilityClass { + } + + @lombok.experimental.UtilityClass + class Numbers { + int add(int left, int right) { + return left + right; + } + } + """ + ) + ); + } + + @Test + void usesQualifiedAnnotationWhenNestedUtilityClassTypeExists() { + rewriteRun( + //language=java + java( + """ + class Outer { + class UtilityClass { + } + + class Numbers { + static int add(int left, int right) { + return left + right; + } + } + } + """, + """ + class Outer { + class UtilityClass { + } + + @lombok.experimental.UtilityClass + class Numbers { + int add(int left, int right) { + return left + right; + } + } + } + """ + ) + ); + } + + @Test + void doesNotConvertInstantiatedClass() { + rewriteRun( + //language=java + java( + """ + package example; + + public class Numbers { + public static int add(int left, int right) { + return left + right; + } + } + """ + ), + //language=java + java( + """ + package example; + + class UsesNumbers { + private final Numbers numbers = new Numbers(); + } + """ + ) + ); + } + + @Test + void doesNotConvertConstructorMethodReference() { + rewriteRun( + //language=java + java( + """ + import java.util.function.Supplier; + + class Numbers { + static int add(int left, int right) { + return left + right; + } + } + + class UsesNumbers { + private final Supplier factory = Numbers::new; + } + """ + ) + ); + } + + @Test + void doesNotConvertInheritedClass() { + rewriteRun( + //language=java + java( + """ + class Numbers { + static int add(int left, int right) { + return left + right; + } + } + + class ExtendedNumbers extends Numbers { + } + """ + ) + ); + } + + @Test + void doesNotConvertNonStarStaticImport() { + rewriteRun( + //language=java + java( + """ + package example; + + public class Numbers { + public static int add(int left, int right) { + return left + right; + } + } + """ + ), + //language=java + java( + """ + package other; + + import static example.Numbers.add; + + class UsesNumbers { + int addOne(int value) { + return add(value, 1); + } + } + """ + ) + ); + } + + @Test + void doesNotConvertNestedNonStarStaticImport() { + rewriteRun( + //language=java + java( + """ + package example; + + public class Outer { + public static class Numbers { + public static int add(int left, int right) { + return left + right; + } + } + } + """ + ), + //language=java + java( + """ + package other; + + import static example.Outer.Numbers.add; + + class UsesNumbers { + int addOne(int value) { + return add(value, 1); + } + } + """ + ) + ); + } + + @Test + void doesNotConvertClassesWithInstanceMembers() { + rewriteRun( + //language=java + java( + """ + class Numbers { + private int offset; + + static int add(int left, int right) { + return left + right; + } + } + """ + ), + //language=java + java( + """ + class MoreNumbers { + int add(int left, int right) { + return left + right; + } + } + """ + ) + ); + } + + @Test + void doesNotConvertClassesWithConstructorsOrMainMethods() { + rewriteRun( + //language=java + java( + """ + class Numbers { + private Numbers() { + } + + static int add(int left, int right) { + return left + right; + } + } + """ + ), + //language=java + java( + """ + class Application { + public static void main(String[] args) { + } + } + """ + ) + ); + } + + @Test + void doesNotConvertNestedClassWithinNonStaticMemberClass() { + rewriteRun( + //language=java + java( + """ + class Outer { + class Inner { + class Utilities { + static final int VALUE = 1; + } + } + } + """ + ) + ); + } + + @Test + void doesNotConvertWhenLombokConfigForbidsUtilityClass() { + rewriteRun( + text( + """ + LOMBOK.UTILITYCLASS.FLAGUSAGE = ERROR + """, + spec -> spec.path("lombok.config") + ), + //language=java + java( + """ + package example; + + class Numbers { + static int add(int left, int right) { + return left + right; + } + } + """, + spec -> spec.path("src/main/java/example/Numbers.java") + ) + ); + } + + @Test + void honorsCloserLombokConfig() { + rewriteRun( + text( + """ + lombok.utilityClass.flagUsage = error + """, + spec -> spec.path("lombok.config") + ), + text( + """ + lombok.utilityClass.flagUsage = warning + """, + spec -> spec.path("src/main/lombok.config") + ), + //language=java + java( + """ + package example; + + class Numbers { + static int add(int left, int right) { + return left + right; + } + } + """, + """ + package example; + + import lombok.experimental.UtilityClass; + + @UtilityClass + class Numbers { + int add(int left, int right) { + return left + right; + } + } + """, + spec -> spec.path("src/main/java/example/Numbers.java") + ) + ); + } + + @Test + void honorsClearLombokConfig() { + rewriteRun( + text( + """ + lombok.utilityClass.flagUsage = error + """, + spec -> spec.path("lombok.config") + ), + text( + """ + clear lombok.utilityClass.flagUsage + """, + spec -> spec.path("src/main/lombok.config") + ), + //language=java + java( + """ + package example; + + class Numbers { + static int add(int left, int right) { + return left + right; + } + } + """, + """ + package example; + + import lombok.experimental.UtilityClass; + + @UtilityClass + class Numbers { + int add(int left, int right) { + return left + right; + } + } + """, + spec -> spec.path("src/main/java/example/Numbers.java") + ) + ); + } + + @Test + void honorsImportedLombokConfig() { + rewriteRun( + text( + """ + import utility.config + """, + spec -> spec.path("lombok.config") + ), + text( + """ + lombok.utilityClass.flagUsage = error + """, + spec -> spec.path("utility.config") + ), + //language=java + java( + """ + package example; + + class Numbers { + static int add(int left, int right) { + return left + right; + } + } + """, + spec -> spec.path("src/main/java/example/Numbers.java") + ) + ); + } + + @Test + void skipsUnresolvedImportedLombokConfig() { + rewriteRun( + text( + """ + import missing.config + """, + spec -> spec.path("lombok.config") + ), + //language=java + java( + """ + package example; + + class Numbers { + static int add(int left, int right) { + return left + right; + } + } + """, + spec -> spec.path("src/main/java/example/Numbers.java") + ) + ); + } + + @Test + void honorsStopBubblingLombokConfig() { + rewriteRun( + text( + """ + lombok.utilityClass.flagUsage = error + """, + spec -> spec.path("lombok.config") + ), + text( + """ + config.stopBubbling = true + """, + spec -> spec.path("src/main/lombok.config") + ), + //language=java + java( + """ + package example; + + class Numbers { + static int add(int left, int right) { + return left + right; + } + } + """, + """ + package example; + + import lombok.experimental.UtilityClass; + + @UtilityClass + class Numbers { + int add(int left, int right) { + return left + right; + } + } + """, + spec -> spec.path("src/main/java/example/Numbers.java") + ) + ); + } + + @Test + void convertsClassNestedInInterfaceMember() { + rewriteRun( + //language=java + java( + """ + interface Outer { + class Container { + class Utilities { + static final int VALUE = 1; + } + } + } + """, + """ + import lombok.experimental.UtilityClass; + + interface Outer { + class Container { + @UtilityClass + class Utilities { + final int VALUE = 1; + } + } + } + """ + ) + ); + } +}