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
Original file line number Diff line number Diff line change
Expand Up @@ -15,43 +15,91 @@
*/
package org.openrewrite.java.migrate.search;

import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.Getter;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.ScanningRecipe;
import org.openrewrite.SourceFile;
import org.openrewrite.Tree;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaVisitor;
import org.openrewrite.java.marker.JavaProject;
import org.openrewrite.java.marker.JavaVersion;
import org.openrewrite.java.migrate.table.JavaVersionTable;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaSourceFile;
import org.openrewrite.marker.GitProvenance;
import org.openrewrite.marker.Markers;

import java.util.HashSet;
import java.util.Set;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

@EqualsAndHashCode(callSuper = false)
@Value
public class FindJavaVersion extends Recipe {
import static java.util.Collections.emptyList;

public class FindJavaVersion extends ScanningRecipe<Map<String, JavaVersionTable.Row>> {

transient JavaVersionTable table = new JavaVersionTable(this);
transient Set<JavaVersion> seen = new HashSet<>();

String displayName = "Find Java versions in use";
@Getter
final String displayName = "Find Java versions in use";

String description = "Finds Java versions in use.";
@Getter
final String description = "Finds Java versions in use, emitting one row per git repository " +
"(the lowest source/target compatibility across modules in that repository).";

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaVisitor<ExecutionContext>() {
public Map<String, JavaVersionTable.Row> getInitialValue(ExecutionContext ctx) {
return new LinkedHashMap<>();
}

@Override
public TreeVisitor<?, ExecutionContext> getScanner(Map<String, JavaVersionTable.Row> acc) {
return new TreeVisitor<Tree, ExecutionContext>() {
@Override
public J visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
cu.getMarkers().findFirst(JavaVersion.class)
.filter(seen::add)
.map(jv -> new JavaVersionTable.Row(
public Tree preVisit(Tree tree, ExecutionContext ctx) {
if (tree instanceof JavaSourceFile) {
Markers markers = tree.getMarkers();
markers.findFirst(JavaVersion.class).ifPresent(jv -> {
// Prefer the git origin as the dedup key: every module in a multi-module repo
// shares one GitProvenance, so they collapse to a single row. Fall back to the
// JavaProject UUID (one row per module) when no git provenance is available,
// and finally to the JavaVersion UUID so disconnected source files still
// produce one row per file rather than silently merging.
// Prefixes keep the three key namespaces from colliding.
String key = markers.findFirst(GitProvenance.class)
.map(GitProvenance::getOrigin)
.map(origin -> "origin:" + origin)
.orElseGet(() -> markers.findFirst(JavaProject.class)
.map(jp -> "project:" + jp.getId())
.orElseGet(() -> "version:" + jv.getId()));

JavaVersionTable.Row candidate = new JavaVersionTable.Row(
Integer.toString(jv.getMajorVersion()),
Integer.toString(jv.getMajorReleaseVersion())))
.ifPresent(row -> table.insertRow(ctx, row));
return cu;
Integer.toString(jv.getMajorReleaseVersion()));
acc.merge(key, candidate, FindJavaVersion::lower);
});
}
return tree;
}
};
}

@Override
public Collection<? extends SourceFile> generate(Map<String, JavaVersionTable.Row> acc, ExecutionContext ctx) {
for (JavaVersionTable.Row row : acc.values()) {
table.insertRow(ctx, row);
}
return emptyList();
}

// Lower target compatibility wins; tiebreak on lower source compatibility.
// The retained row is the migration floor for the repository.
private static JavaVersionTable.Row lower(JavaVersionTable.Row a, JavaVersionTable.Row b) {
int aTarget = Integer.parseInt(a.getTargetVersion());
int bTarget = Integer.parseInt(b.getTargetVersion());
if (aTarget != bTarget) {
return aTarget < bTarget ? a : b;
}
int aSource = Integer.parseInt(a.getSourceVersion());
int bSource = Integer.parseInt(b.getSourceVersion());
return aSource <= bSource ? a : b;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.java.marker.JavaProject;
import org.openrewrite.java.marker.JavaVersion;
import org.openrewrite.java.migrate.table.JavaVersionTable;
import org.openrewrite.marker.GitProvenance;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

Expand All @@ -33,30 +35,141 @@ public void defaults(RecipeSpec spec) {
spec.recipe(new FindJavaVersion());
}

private static GitProvenance gitProvenance(String origin) {
return new GitProvenance(randomId(), origin, "main", "abc123", null, null, null);
}

@DocumentExample
@Test
void twoClassesWithSameMarkerLeadToOneRow() {
void multiModuleRepositoryCollapsesToOneRow() {
// All modules in a multi-module repository share one GitProvenance, so the recipe
// emits a single row identifying the repository as a whole.
var git = gitProvenance("https://github.com/example/demo.git");
var moduleA = new JavaProject(randomId(), "module-a", null);
var moduleB = new JavaProject(randomId(), "module-b", null);
var jv = new JavaVersion(randomId(), "Sam", "Shelter", "17", "8");
rewriteRun(
spec -> spec.dataTable(JavaVersionTable.Row.class, rows ->
assertThat(rows).containsExactly(
new JavaVersionTable.Row("17", "8")
)),
//language=java
java(
"""
class A {
}
""",
spec -> spec.markers(git, moduleA, jv)),
//language=java
java(
"""
class B {
}
""",
spec -> spec.markers(git, moduleB, jv))
);
}

@Test
void heterogeneousVersionsInOneRepoPickLowestTarget() {
// When modules in the same repo target different JDKs, the row reports the lowest
// target — the migration floor for the repository.
var git = gitProvenance("https://github.com/example/mixed.git");
var legacy = new JavaProject(randomId(), "legacy-module", null);
var modern = new JavaProject(randomId(), "modern-module", null);
var java8 = new JavaVersion(randomId(), "Sam", "Shelter", "8", "8");
var java17 = new JavaVersion(randomId(), "Sam", "Shelter", "17", "17");
rewriteRun(
spec -> spec.dataTable(JavaVersionTable.Row.class, rows ->
assertThat(rows).containsExactly(
new JavaVersionTable.Row("8", "8")
)),
//language=java
java(
"""
class Legacy {
}
""",
spec -> spec.markers(git, legacy, java8)),
//language=java
java(
"""
class Modern {
}
""",
spec -> spec.markers(git, modern, java17))
);
}

@Test
void identicalJavaVersionMarkersAcrossRepositoriesAreEachReported() {
// Reproduces customer-requests#2409: across multiple repositories with the same JDK,
// every repository must contribute its own row in the data table. Previously a
// recipe-instance HashSet deduplicated by JavaVersion content, so identical markers
// in different repositories were silently dropped.
var gitA = gitProvenance("https://github.com/example/repo-a.git");
var gitB = gitProvenance("https://github.com/example/repo-b.git");
var gitC = gitProvenance("https://github.com/example/repo-c.git");
var project = new JavaProject(randomId(), "service", null);
var jv = new JavaVersion(randomId(), "Sam", "Shelter", "17", "8");
rewriteRun(
spec -> spec.dataTable(JavaVersionTable.Row.class, rows ->
assertThat(rows).containsExactly(
new JavaVersionTable.Row("17", "8"),
new JavaVersionTable.Row("17", "8"),
new JavaVersionTable.Row("17", "8")
)),
//language=java
java(
"""
class A {
}
""",
spec -> spec.markers(gitA, project, jv)),
//language=java
java(
"""
class B {
}
""",
spec -> spec.markers(gitB, project, jv)),
//language=java
java(
"""
class C {
}
""",
spec -> spec.markers(gitC, project, jv))
);
}

@Test
void withoutGitProvenanceFallsBackToPerProject() {
// When no GitProvenance is available (local non-git source trees, some test setups),
// the recipe falls back to one row per JavaProject so distinct modules are not silently merged.
var projectOne = new JavaProject(randomId(), "module-a", null);
var projectTwo = new JavaProject(randomId(), "module-b", null);
var jv = new JavaVersion(randomId(), "Sam", "Shelter", "17", "8");
rewriteRun(
spec -> spec.dataTable(JavaVersionTable.Row.class, rows -> {
assertThat(rows).containsExactly(
new JavaVersionTable.Row("17", "8")
);
}),
spec -> spec.dataTable(JavaVersionTable.Row.class, rows ->
assertThat(rows).containsExactly(
new JavaVersionTable.Row("17", "8"),
new JavaVersionTable.Row("17", "8")
)),
//language=java
java(
"""
class A {
}
""",
spec -> spec.markers(jv)),
spec -> spec.markers(projectOne, jv)),
//language=java
java(
"""
class B {
}
""",
spec -> spec.markers(jv))
spec -> spec.markers(projectTwo, jv))
);
}
}
Loading