diff --git a/src/main/java/org/openrewrite/java/migrate/AddLombokMapstructBindingMavenDependencyOnly.java b/src/main/java/org/openrewrite/java/migrate/AddLombokMapstructBindingMavenDependencyOnly.java new file mode 100644 index 0000000000..3c03858987 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/AddLombokMapstructBindingMavenDependencyOnly.java @@ -0,0 +1,113 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.maven.AddDependencyVisitor; +import org.openrewrite.maven.MavenIsoVisitor; +import org.openrewrite.maven.tree.Plugin; +import org.openrewrite.maven.tree.ResolvedPom; +import org.openrewrite.xml.tree.Xml; + +import java.util.List; + +@Value +@EqualsAndHashCode(callSuper = false) +public class AddLombokMapstructBindingMavenDependencyOnly extends Recipe { + String displayName = "Add `lombok-mapstruct-binding` dependency for Maven when both MapStruct and Lombok are used"; + + String description = "Add the `lombok-mapstruct-binding` Maven dependency when this module uses both Lombok and MapStruct " + + "and the binding is not already a dependency or configured on the compiler annotation processor path."; + + @Override + public TreeVisitor getVisitor() { + return new MavenIsoVisitor() { + @Override + public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) { + if (getResolutionResult().findDependencies("org.projectlombok", "lombok", null).isEmpty() || + getResolutionResult().findDependencies("org.mapstruct", "mapstruct", null).isEmpty() || + !getResolutionResult().findDependencies("org.projectlombok", "lombok-mapstruct-binding", null).isEmpty()) { + return document; + } + ResolvedPom pom = getResolutionResult().getPom(); + // The resolved model can omit even locally declared plugins with inherited=false. + List plugins = ListUtils.concatAll(pom.getPlugins(), nonInheritedPlugins(pom.getRequested().getPlugins())); + List managedPlugins = ListUtils.concatAll(pom.getPluginManagement(), nonInheritedPlugins(pom.getRequested().getPluginManagement())); + if (hasBindingProcessor(pom, plugins) || + !overridesManagedProcessorPaths(plugins) && hasBindingProcessor(pom, managedPlugins)) { + return document; + } + return (Xml.Document) new AddDependencyVisitor("org.projectlombok", "lombok-mapstruct-binding", "0.2.0", + null, null, null, null, null, null, null).visitNonNull(document, ctx); + } + + private List nonInheritedPlugins(List plugins) { + return ListUtils.map(plugins, plugin -> "false".equals(plugin.getInherited()) ? plugin : null); + } + + private boolean overridesManagedProcessorPaths(List plugins) { + for (Plugin plugin : plugins) { + if (!"org.apache.maven.plugins".equals(plugin.getGroupId()) || + !"maven-compiler-plugin".equals(plugin.getArtifactId())) { + continue; + } + JsonNode configuration = plugin.getConfiguration(); + if (configuration != null && + ("override".equals(configuration.path("combine.self").asText()) || + "override".equals(configuration.path("annotationProcessorPaths").path("combine.self").asText()))) { + return true; + } + } + return false; + } + + private boolean hasBindingProcessor(ResolvedPom pom, List plugins) { + for (Plugin plugin : plugins) { + if (!"org.apache.maven.plugins".equals(plugin.getGroupId()) || + !"maven-compiler-plugin".equals(plugin.getArtifactId())) { + continue; + } + JsonNode configuration = plugin.getConfiguration(); + if (configuration == null) { + continue; + } + JsonNode paths = configuration.path("annotationProcessorPaths").path("path"); + if (paths.isArray()) { + for (JsonNode path : paths) { + if (isBinding(pom, path)) { + return true; + } + } + } else if (isBinding(pom, paths)) { + return true; + } + } + return false; + } + + private boolean isBinding(ResolvedPom pom, JsonNode path) { + return "org.projectlombok".equals(pom.getValue(path.path("groupId").asText(""))) && + "lombok-mapstruct-binding".equals(pom.getValue(path.path("artifactId").asText(""))); + } + }; + } +} diff --git a/src/main/resources/META-INF/rewrite/java-version-17.yml b/src/main/resources/META-INF/rewrite/java-version-17.yml index 628b2c13d4..61c295a60f 100644 --- a/src/main/resources/META-INF/rewrite/java-version-17.yml +++ b/src/main/resources/META-INF/rewrite/java-version-17.yml @@ -379,20 +379,3 @@ recipeList: artifactId: lombok-mapstruct-binding version: 0.2.0 - org.openrewrite.java.migrate.AddMapstructAnnotationProcessorPath ---- -type: specs.openrewrite.org/v1beta/recipe -name: org.openrewrite.java.migrate.AddLombokMapstructBindingMavenDependencyOnly -displayName: Add `lombok-mapstruct-binding` dependency for Maven when both MapStruct and Lombok are used -description: >- - Add the `lombok-mapstruct-binding` Maven dependency when it does not already exist. Expects the caller to have checked - that both MapStruct and Lombok are used. -preconditions: - - org.openrewrite.java.dependencies.search.DoesNotIncludeDependency: - groupId: org.projectlombok - artifactId: lombok-mapstruct-binding - - org.openrewrite.Singleton -recipeList: - - org.openrewrite.maven.AddDependency: - groupId: org.projectlombok - artifactId: lombok-mapstruct-binding - version: 0.2.0 diff --git a/src/test/java/org/openrewrite/java/migrate/AddLombokMapstructBindingMavenDependencyOnlyTest.java b/src/test/java/org/openrewrite/java/migrate/AddLombokMapstructBindingMavenDependencyOnlyTest.java new file mode 100644 index 0000000000..9b3b0e45f5 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/AddLombokMapstructBindingMavenDependencyOnlyTest.java @@ -0,0 +1,318 @@ +/* + * 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; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.mavenProject; +import static org.openrewrite.maven.Assertions.pomXml; + +class AddLombokMapstructBindingMavenDependencyOnlyTest implements RewriteTest { + private static final String DEPENDENCIES = """ + + + org.projectlombok + lombok + 1.18.36 + provided + + + org.mapstruct + mapstruct + 1.6.3 + + + """; + private static final String BINDING = """ + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + """; + private static final String PROCESSOR = """ + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + """; + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new AddLombokMapstructBindingMavenDependencyOnly()); + } + + @Test + void legacyConfigurationAndIdempotency() { + rewriteRun( + spec -> spec.cycles(3).expectedCyclesThatMakeChanges(1), + pomXml(pom(DEPENDENCIES, ""), withBinding(pom(DEPENDENCIES, ""))) + ); + } + + @Test + void bindingMissingFromCompilerPaths() { + String before = pom(DEPENDENCIES, plugin("maven-compiler-plugin", PROCESSOR.replace("lombok-mapstruct-binding", "lombok"))); + rewriteRun(pomXml(before, withBinding(before))); + } + + @Test + void singleProcessorWithDefaultPluginGroup() { + rewriteRun(pomXml(pom(DEPENDENCIES, plugin("maven-compiler-plugin", PROCESSOR)))); + } + + @Test + void unrelatedPluginDoesNotSuppressDependency() { + String before = pom(DEPENDENCIES, plugin("maven-surefire-plugin", PROCESSOR)); + rewriteRun(pomXml(before, withBinding(before))); + } + + @Test + void unrelatedProcessorGroupDoesNotSuppressDependency() { + String before = pom(DEPENDENCIES, plugin("maven-compiler-plugin", PROCESSOR.replace("org.projectlombok", "com.example"))); + rewriteRun(pomXml(before, withBinding(before))); + } + + @Test + void existingDependencyKeepsScopeAndVersion() { + String dependencies = DEPENDENCIES.replace("", """ + + org.projectlombok + lombok-mapstruct-binding + ${binding.version} + provided + + """); + rewriteRun(pomXml(pom(dependencies, "").replace("", """ + + 0.2.0 + + """))); + } + + @Test + void unrelatedModuleDoesNotReceiveDependency() { + String unrelated = """ + + 4.0.0 + com.example + unrelated + 1 + + + org.projectlombok + lombok + 1.18.36 + + + + """; + rewriteRun( + mavenProject("configured", pomXml(pom(DEPENDENCIES, plugin("maven-compiler-plugin", PROCESSOR)))), + mavenProject("unrelated", pomXml(unrelated)), + mavenProject("missing", pomXml(pom(DEPENDENCIES, ""), withBinding(pom(DEPENDENCIES, "")))) + ); + } + + @Test + void inheritedCompilerConfiguration() { + String parent = pom(DEPENDENCIES, plugin("maven-compiler-plugin", PROCESSOR)) + .replace("app", "parent") + .replace("1", "1\n pom"); + rewriteRun( + mavenProject("parent", + pomXml(parent), + mavenProject("child", pomXml(""" + + 4.0.0 + + com.example + parent + 1 + + child + + """)) + ) + ); + } + + @Test + void managedCompilerConfiguration() { + String compiler = plugin("maven-compiler-plugin", PROCESSOR) + .replace("", "\n ") + .replace("", "\n "); + rewriteRun(pomXml(pom(DEPENDENCIES, compiler))); + } + + @ParameterizedTest + @ValueSource(strings = {"annotationProcessorPaths", "configuration"}) + void explicitOverrideContainingBinding(String overriddenElement) { + rewriteRun(pomXml(pom(DEPENDENCIES, plugin("maven-compiler-plugin", PROCESSOR) + .replace("<" + overriddenElement + ">", "<" + overriddenElement + " combine.self=\"override\">")))); + } + + @Test + void managedProcessorInheritedByChildWithoutProcessorPaths() { + String managedCompiler = plugin("maven-compiler-plugin", PROCESSOR) + .replace("", "\n ") + .replace("", "\n "); + String parent = pom("", managedCompiler) + .replace("app", "parent") + .replace("1", "1\n pom"); + String child = pom(DEPENDENCIES, """ + + + + maven-compiler-plugin + + 17 + + + + + """) + .replace("com.example", """ + + com.example + parent + 1 + """); + rewriteRun( + mavenProject("parent", + pomXml(parent), + mavenProject("child", pomXml(child)) + ) + ); + } + + @ParameterizedTest + @ValueSource(strings = {"annotationProcessorPaths", "configuration"}) + void managedProcessorOverriddenInChild(String overriddenElement) { + String managedCompiler = plugin("maven-compiler-plugin", PROCESSOR) + .replace("", "\n ") + .replace("", "\n "); + String parent = pom("", managedCompiler) + .replace("app", "parent") + .replace("1", "1\n pom"); + String processors = PROCESSOR.replace("lombok-mapstruct-binding", "lombok").replace("0.2.0", "1.18.36") + + PROCESSOR.replace("org.projectlombok", "org.mapstruct").replace("lombok-mapstruct-binding", "mapstruct-processor").replace("0.2.0", "1.6.3"); + String child = pom(DEPENDENCIES, plugin("maven-compiler-plugin", processors) + .replace("<" + overriddenElement + ">", "<" + overriddenElement + " combine.self=\"override\">")) + .replace("com.example", """ + + com.example + parent + 1 + """); + rewriteRun( + mavenProject("parent", + pomXml(parent), + mavenProject("child", pomXml(child, withBinding(child))) + ) + ); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void nonInheritedCompilerProcessorIsActiveInDeclaringPom(boolean managed) { + String compiler = plugin("maven-compiler-plugin", PROCESSOR) + .replace("3.13.0", "3.13.0\n false"); + if (managed) { + compiler = compiler.replace("", "\n ") + .replace("", "\n "); + } + rewriteRun(pomXml(pom(DEPENDENCIES, compiler))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void compilerProcessorNotInheritedByChild(boolean managed) { + String compiler = plugin("maven-compiler-plugin", PROCESSOR) + .replace("3.13.0", "3.13.0\n false"); + if (managed) { + compiler = compiler.replace("", "\n ") + .replace("", "\n "); + } + String parent = pom("", compiler) + .replace("app", "parent") + .replace("1", "1\n pom"); + String child = pom(DEPENDENCIES, "") + .replace("com.example", """ + + com.example + parent + 1 + """); + rewriteRun( + mavenProject("parent", + pomXml(parent), + mavenProject("child", pomXml(child, withBinding(child))) + ) + ); + } + + @Test + void processorCoordinatesUseProperties() { + String before = pom(DEPENDENCIES, plugin("maven-compiler-plugin", PROCESSOR + .replace("org.projectlombok", "${binding.group}") + .replace("lombok-mapstruct-binding", "${binding.artifact}"))) + .replace("", """ + + org.projectlombok + lombok-mapstruct-binding + + """); + rewriteRun(pomXml(before)); + } + + private static String pom(String dependencies, String build) { + return """ + + 4.0.0 + com.example + app + 1 + %s%s + """.formatted(dependencies.indent(2), build); + } + + private static String plugin(String artifact, String paths) { + return """ + + + + %s + 3.13.0 + + + %s + + + + + """.formatted(artifact, paths.indent(24)); + } + + private static String withBinding(String before) { + return before.replace(" ", BINDING.indent(2) + " "); + } +} diff --git a/src/test/java/org/openrewrite/java/migrate/AddLombokMapstructBindingTest.java b/src/test/java/org/openrewrite/java/migrate/AddLombokMapstructBindingTest.java index f4ed93ba5f..c163d41086 100644 --- a/src/test/java/org/openrewrite/java/migrate/AddLombokMapstructBindingTest.java +++ b/src/test/java/org/openrewrite/java/migrate/AddLombokMapstructBindingTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; +import org.openrewrite.Issue; import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; @@ -84,6 +85,200 @@ void doesNotDuplicateGradle() { ); } + @Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/1319") + @Test + void bindingAlreadyOnAnnotationProcessorPath() { + rewriteRun( + pomXml( + """ + + 4.0.0 + com.example + example-service + 1.0.0 + + + + org.projectlombok + lombok + 1.18.36 + provided + + + org.mapstruct + mapstruct + 1.6.3 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + + org.projectlombok + lombok + 1.18.36 + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + org.mapstruct + mapstruct-processor + 1.6.3 + + + + + + + + """ + ) + ); + } + + @Test + void configuredModuleDoesNotSuppressOtherModules() { + rewriteRun( + spec -> spec.cycles(3).expectedCyclesThatMakeChanges(1), + mavenProject("configured", pomXml( + """ + + 4.0.0 + com.example + example-service + 1.0.0 + + + + org.projectlombok + lombok + 1.18.42 + provided + + + org.mapstruct + mapstruct + 1.6.3 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + + org.projectlombok + lombok + 1.18.42 + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + org.mapstruct + mapstruct-processor + 1.6.3 + + + + + + + + """ + )), + mavenProject("missing", pomXml( + """ + + com.mycompany.app + my-app + 1 + + + org.projectlombok + lombok + 1.18.42 + + + org.mapstruct + mapstruct + 1.6.3 + + + + """, + """ + + com.mycompany.app + my-app + 1 + + + org.projectlombok + lombok + 1.18.42 + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + org.mapstruct + mapstruct + 1.6.3 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + 1.18.42 + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + org.mapstruct + mapstruct-processor + 1.6.3 + + + + + + + + """ + )) + ); + } + @Test void addForMaven() { rewriteRun(