diff --git a/rewrite-core/src/main/java/org/openrewrite/semver/Semver.java b/rewrite-core/src/main/java/org/openrewrite/semver/Semver.java index 155e3107f6f..d4d6388702a 100644 --- a/rewrite-core/src/main/java/org/openrewrite/semver/Semver.java +++ b/rewrite-core/src/main/java/org/openrewrite/semver/Semver.java @@ -34,6 +34,14 @@ public static boolean isVersion(@Nullable String version) { return LatestRelease.RELEASE_PATTERN.matcher(version).matches(); } + /** + * Validates the given version against an optional pattern + * + * @param toVersion the version to validate. Node-style [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors) may be used. + * @param metadataPattern optional metadata appended to the version. Allows version selection to be extended beyond the original Node Semver semantics. So for example, + * Setting 'version' to "25-29" can be paired with a metadata pattern of "-jre" to select Guava 29.0-jre + * @return the validation result + */ public static Validated validate(String toVersion, @Nullable String metadataPattern) { return Validated.testNone( "metadataPattern", diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/search/ParentPomInsight.java b/rewrite-maven/src/main/java/org/openrewrite/maven/search/ParentPomInsight.java index 176a7a14983..916542e6de5 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/search/ParentPomInsight.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/search/ParentPomInsight.java @@ -20,12 +20,18 @@ import org.jspecify.annotations.Nullable; import org.openrewrite.*; import org.openrewrite.marker.SearchResult; +import org.openrewrite.maven.MavenDownloadingException; import org.openrewrite.maven.MavenIsoVisitor; +import org.openrewrite.maven.internal.MavenPomDownloader; import org.openrewrite.maven.table.ParentPomsInUse; +import org.openrewrite.maven.tree.MavenResolutionResult; +import org.openrewrite.maven.tree.Parent; import org.openrewrite.maven.tree.ResolvedPom; import org.openrewrite.semver.Semver; +import org.openrewrite.semver.VersionComparator; import org.openrewrite.xml.tree.Xml; +import static java.util.Collections.emptyList; import static org.openrewrite.internal.StringUtils.matchesGlob; @EqualsAndHashCode(callSuper = false) @@ -52,6 +58,12 @@ public class ParentPomInsight extends Recipe { @Nullable String version; + @Option(displayName = "Recursive", + description = "Whether to search recursively through the parents. True by default.", + required = false) + @Nullable + Boolean recursive; + @Override public String getDisplayName() { return "Maven parent insight"; @@ -79,26 +91,43 @@ public Validated validate() { @Override public TreeVisitor getVisitor() { return new MavenIsoVisitor() { + @Nullable + final VersionComparator versionComparator = version == null ? null : Semver.validate(version, null).getValue(); + @Override public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) { Xml.Tag t = super.visitTag(tag, ctx); - if (isParentTag()) { - ResolvedPom resolvedPom = getResolutionResult().getPom(); - String groupId = resolvedPom.getValue(tag.getChildValue("groupId").orElse(null)); - String artifactId = resolvedPom.getValue(tag.getChildValue("artifactId").orElse(null)); + if (!isParentTag()) { + return t; + } + + MavenResolutionResult mrr = getResolutionResult(); + MavenPomDownloader mpd = new MavenPomDownloader(mrr.getProjectPoms(), ctx, mrr.getMavenSettings(), mrr.getActiveProfiles()); + + Parent ancestor = mrr.getPom().getRequested().getParent(); + String relativePath = tag.getChildValue("relativePath").orElse(null); + while (ancestor != null) { + String groupId = ancestor.getGroupId(); + String artifactId = ancestor.getArtifactId(); if (matchesGlob(groupId, groupIdPattern) && matchesGlob(artifactId, artifactIdPattern)) { - String parentVersion = resolvedPom.getValue(tag.getChildValue("version").orElse(null)); - if (version != null) { - if (!Semver.validate(version, null).getValue() - .isValid(null, parentVersion)) { - return t; - } + String parentVersion = ancestor.getVersion(); + if (versionComparator == null || versionComparator.isValid(null, parentVersion)) { + // Found a parent pom that matches the criteria + inUse.insertRow(ctx, new ParentPomsInUse.Row( + mrr.getPom().getArtifactId(), groupId, artifactId, parentVersion, relativePath)); + return SearchResult.found(t); } - // Found a parent pom that matches the criteria - String relativePath = tag.getChildValue("relativePath").orElse(null); - inUse.insertRow(ctx, new ParentPomsInUse.Row( - resolvedPom.getArtifactId(), groupId, artifactId, parentVersion, relativePath)); - return SearchResult.found(t); + } + if (Boolean.FALSE.equals(recursive)) { + return t; + } + try { + ResolvedPom ancestorPom = mpd.download(ancestor.getGav(), null, null, mrr.getPom().getRepositories()) + .resolve(emptyList(), mpd, ctx); + ancestor = ancestorPom.getRequested().getParent(); + relativePath = null; + } catch (MavenDownloadingException e) { + return e.warn(t); } } return t; diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/search/ParentPomInsightTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/search/ParentPomInsightTest.java index 7101ed97a6b..d98d3ba8816 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/search/ParentPomInsightTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/search/ParentPomInsightTest.java @@ -20,6 +20,7 @@ import org.openrewrite.maven.table.ParentPomsInUse; import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.SourceSpec; import static org.assertj.core.api.Assertions.assertThat; import static org.openrewrite.java.Assertions.mavenProject; @@ -28,7 +29,25 @@ class ParentPomInsightTest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { - spec.recipe(new ParentPomInsight("org.springframework.boot", "spring-boot-starter-parent", null)); + spec.recipe(new ParentPomInsight("org.springframework.boot", "spring-boot-starter-parent", null, null)); + } + + @Test + void noParent() { + rewriteRun( + pomXml( + //language=xml + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + """ + ) + ); } @DocumentExample @@ -89,7 +108,7 @@ void findParent() { void multiModuleOnlyRoot() { rewriteRun( spec -> spec - .recipe(new ParentPomInsight("*", "*", null)) + .recipe(new ParentPomInsight("*", "*", null, null)) .dataTableAsCsv(ParentPomsInUse.class.getName(), """ projectArtifactId,groupId,artifactId,version,relativePath sample,org.springframework.boot,"spring-boot-starter-parent",2.5.0, @@ -105,13 +124,13 @@ void multiModuleOnlyRoot() { org.sample sample 1.0.0 - + org.springframework.boot spring-boot-starter-parent 2.5.0 - + module1 module2 @@ -125,13 +144,132 @@ void multiModuleOnlyRoot() { org.sample sample 1.0.0 - + org.springframework.boot spring-boot-starter-parent 2.5.0 + + + module1 + module2 + + + """ + ), + mavenProject("module1", + pomXml( + """ + + + 4.0.0 + + org.sample + sample + 1.0.0 + ../ + + module1 + + """, + """ + + + 4.0.0 + + org.sample + sample + 1.0.0 + ../ + + module1 + + """, + spec -> spec.path("module1/pom.xml") + )), + mavenProject("module2", + pomXml( + """ + + + 4.0.0 + + org.sample + sample + 1.0.0 + ../ + + module2 + + """, + """ + + + 4.0.0 + + org.sample + sample + 1.0.0 + ../ + + module2 + + """, + spec -> spec.path("module2/pom.xml") + ) + ) + ) + ); + } + @Test + void ancestorMatchesVersion() { + rewriteRun( + spec -> spec + .recipe(new ParentPomInsight("*", "*", "2.5.0", null)) + .dataTableAsCsv(ParentPomsInUse.class.getName(), """ + projectArtifactId,groupId,artifactId,version,relativePath + sample,org.springframework.boot,"spring-boot-starter-parent",2.5.0, + module1,org.springframework.boot,"spring-boot-starter-parent",2.5.0, + module2,org.springframework.boot,"spring-boot-starter-parent",2.5.0, + """), + mavenProject("sample", + pomXml( + """ + + + 4.0.0 + org.sample + sample + 1.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.5.0 + + + + module1 + module2 + + + """, + """ + + + 4.0.0 + org.sample + sample + 1.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.5.0 + + module1 module2 @@ -208,7 +346,7 @@ void multiModuleOnlyRoot() { void matchNonSnapshot() { rewriteRun( spec -> spec - .recipe(new ParentPomInsight("*", "*", "~2")) + .recipe(new ParentPomInsight("*", "*", "~2", false)) .dataTableAsCsv(ParentPomsInUse.class.getName(), """ projectArtifactId,groupId,artifactId,version,relativePath sample,org.springframework.boot,"spring-boot-starter-parent",2.5.0, @@ -222,13 +360,13 @@ void matchNonSnapshot() { org.sample sample 1.0.0-SNAPSHOT - + org.springframework.boot spring-boot-starter-parent 2.5.0 - + module1 module2 @@ -242,13 +380,13 @@ void matchNonSnapshot() { org.sample sample 1.0.0-SNAPSHOT - + org.springframework.boot spring-boot-starter-parent 2.5.0 - + module1 module2 @@ -294,4 +432,569 @@ void matchNonSnapshot() { ) ); } + + @Test + void directParentMatchesFullGAV() { + rewriteRun( + spec -> { + spec.recipe(new ParentPomInsight("org.springframework.boot", "spring-boot-starter-parent", "3.3.3", null)); + spec.dataTable(ParentPomsInUse.Row.class, rows -> assertThat(rows) + .singleElement() + .satisfies(row -> { + assertThat(row.getProjectArtifactId()).isEqualTo("my-app"); + assertThat(row.getGroupId()).isEqualTo("org.springframework.boot"); + assertThat(row.getArtifactId()).isEqualTo("spring-boot-starter-parent"); + assertThat(row.getVersion()).isEqualTo("3.3.3"); + assertThat(row.getRelativePath()).isNull(); + } + )); + }, + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """, + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void directParentMatchesGAVMinorVersion() { + rewriteRun( + spec -> { + spec.recipe(new ParentPomInsight("org.springframework.boot", "spring-boot-starter-parent", "3.3.x", null)); + spec.dataTable(ParentPomsInUse.Row.class, rows -> assertThat(rows) + .singleElement() + .satisfies(row -> { + assertThat(row.getProjectArtifactId()).isEqualTo("my-app"); + assertThat(row.getGroupId()).isEqualTo("org.springframework.boot"); + assertThat(row.getArtifactId()).isEqualTo("spring-boot-starter-parent"); + assertThat(row.getVersion()).isEqualTo("3.3.3"); + assertThat(row.getRelativePath()).isNull(); + } + )); + }, + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """, + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void directParentMatchesGroupIdGlob() { + rewriteRun( + spec -> { + spec.recipe(new ParentPomInsight("org.springframework.*", "spring-boot-starter-parent", null, null)); + spec.dataTable(ParentPomsInUse.Row.class, rows -> assertThat(rows) + .singleElement() + .satisfies(row -> { + assertThat(row.getProjectArtifactId()).isEqualTo("my-app"); + assertThat(row.getGroupId()).isEqualTo("org.springframework.boot"); + assertThat(row.getArtifactId()).isEqualTo("spring-boot-starter-parent"); + assertThat(row.getVersion()).isEqualTo("3.3.3"); + assertThat(row.getRelativePath()).isNull(); + } + )); + }, + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """, + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void directParentMatchesArtifactIdGlob() { + rewriteRun( + spec -> { + spec.recipe(new ParentPomInsight("org.springframework.boot", "spring-*-parent", null, null)); + spec.dataTable(ParentPomsInUse.Row.class, rows -> assertThat(rows) + .singleElement() + .satisfies(row -> { + assertThat(row.getProjectArtifactId()).isEqualTo("my-app"); + assertThat(row.getGroupId()).isEqualTo("org.springframework.boot"); + assertThat(row.getArtifactId()).isEqualTo("spring-boot-starter-parent"); + assertThat(row.getVersion()).isEqualTo("3.3.3"); + assertThat(row.getRelativePath()).isNull(); + } + )); + }, + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """, + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void indirectParentMatches() { + rewriteRun( + spec -> { + spec.recipe(new ParentPomInsight("org.springframework.boot", "spring-boot-dependencies", null, null)); + spec.dataTable(ParentPomsInUse.Row.class, rows -> assertThat(rows) + .singleElement() + .satisfies(row -> { + assertThat(row.getProjectArtifactId()).isEqualTo("my-app"); + assertThat(row.getGroupId()).isEqualTo("org.springframework.boot"); + assertThat(row.getArtifactId()).isEqualTo("spring-boot-dependencies"); + assertThat(row.getVersion()).isEqualTo("3.3.3"); + assertThat(row.getRelativePath()).isNull(); + } + )); + }, + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """, + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void indirectParentMatchesNonRecursive() { + rewriteRun( + spec -> spec.recipe(new ParentPomInsight("org.springframework.boot", "spring-boot-dependencies", null, false)), + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void indirectParentMatchesGAVPattern() { + rewriteRun( + spec -> { + spec.recipe(new ParentPomInsight("*.springframework.*", "spring-*-dependencies", "3.x", null)); + spec.dataTable(ParentPomsInUse.Row.class, rows -> assertThat(rows) + .singleElement() + .satisfies(row -> { + assertThat(row.getProjectArtifactId()).isEqualTo("my-app"); + assertThat(row.getGroupId()).isEqualTo("org.springframework.boot"); + assertThat(row.getArtifactId()).isEqualTo("spring-boot-dependencies"); + assertThat(row.getVersion()).isEqualTo("3.3.3"); + assertThat(row.getRelativePath()).isNull(); + } + )); + }, + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """, + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void multiModuleParentMatches() { + rewriteRun( + spec -> spec.dataTable(ParentPomsInUse.Row.class, rows -> assertThat(rows) + .singleElement() + .satisfies(row -> { + assertThat(row.getProjectArtifactId()).isEqualTo("child"); + assertThat(row.getGroupId()).isEqualTo("org.springframework.boot"); + assertThat(row.getArtifactId()).isEqualTo("spring-boot-starter-parent"); + assertThat(row.getVersion()).isEqualTo("3.3.3"); + assertThat(row.getRelativePath()).isNull(); + } + )), + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + child + + + com.mycompany.app + my-app + 1 + + """, + SourceSpec::skip + ), + mavenProject("child", + pomXml( + //language=xml + """ + + 4.0.0 + + + com.mycompany.app + my-app + 1 + ../ + + + child + + """, + //language=xml + """ + + 4.0.0 + + + com.mycompany.app + my-app + 1 + ../ + + + child + + """ + ) + ) + ); + } + + @Test + void groupIdDoesNotMatch() { + rewriteRun( + spec -> spec.recipe(new ParentPomInsight("org.springframework.invalid", "spring-boot-starter-parent", null, null)), + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void artifactIdDoesNotMatch() { + rewriteRun( + spec -> spec.recipe(new ParentPomInsight("org.springframework.boot", "spring-boot-starter-web", null, null)), + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void versionDoesNotMatch() { + rewriteRun( + spec -> spec.recipe(new ParentPomInsight("org.springframework.boot", "spring-boot-starter-parent", "3.3.4", null)), + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void minorVersionDoesNotMatch() { + rewriteRun( + spec -> spec.recipe(new ParentPomInsight("org.springframework.boot", "spring-boot-starter-parent", "3.3.x", null)), + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.0.5 + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void doesNotMatchGroupIdGlob() { + rewriteRun( + spec -> spec.recipe(new ParentPomInsight("org.invalid.*", "spring-boot-starter-parent", null, null)), + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void doesNotMatchArtifactIdGlob() { + rewriteRun( + spec -> spec.recipe(new ParentPomInsight("org.springframework.boot", "spring-boot-*-web", null, null)), + pomXml( + //language=xml + """ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } }