diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/internal/TypesInUseTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/internal/TypesInUseTest.java index 6c97352cbc7..eb2b573e6f6 100755 --- a/rewrite-java-test/src/test/java/org/openrewrite/java/internal/TypesInUseTest.java +++ b/rewrite-java-test/src/test/java/org/openrewrite/java/internal/TypesInUseTest.java @@ -65,6 +65,42 @@ public class Foo {} ); } + @Test + void recordsFullyQualifiedJavadocReferencesSeparately() { + rewriteRun( + java( + """ + package org.openrewrite.other; + public class Target {} + """ + ), + java( + """ + package com.example; + /** + * See {@link org.openrewrite.other.Target} for details. + */ + public class Bar {} + """, + spec -> spec.afterRecipe(cu -> { + TypesInUse tiu = cu.getTypesInUse(); + + // Fully qualified Javadoc references stay out of the import-retention set (#5738)... + assertThat(tiu.getTypesInUse().stream() + .filter(t -> t instanceof JavaType.FullyQualified) + .map(t -> ((JavaType.FullyQualified) t).getFullyQualifiedName())) + .doesNotContain("org.openrewrite.other.Target"); + + // ...but their packages are recorded separately so package-renaming recipes can find them. + assertThat(tiu.hasDocReferenceInPackage("org.openrewrite.other", false)).isTrue(); + assertThat(tiu.hasDocReferenceInPackage("org.openrewrite", false)).isFalse(); + assertThat(tiu.hasDocReferenceInPackage("org.openrewrite", true)).isTrue(); + assertThat(tiu.hasDocReferenceInPackage("com.other", true)).isFalse(); + }) + ) + ); + } + @Test void publicFactoryReturnsInstanceWithSuppliedSets() { rewriteRun( diff --git a/rewrite-java/src/main/java/org/openrewrite/java/ChangePackage.java b/rewrite-java/src/main/java/org/openrewrite/java/ChangePackage.java index dcb822fa6ee..d824e672986 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/ChangePackage.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/ChangePackage.java @@ -29,7 +29,6 @@ import java.nio.file.Paths; import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; import static java.util.Objects.requireNonNull; import static org.openrewrite.Tree.randomId; @@ -109,9 +108,9 @@ public TreeVisitor getVisitor() { } } } - // Fully qualified javadoc references are excluded from TypesInUse - // (they don't affect imports), but they still need package renaming. - if (hasJavadocReferenceToPackage(cu, oldPackageName, recursive, recursivePackageNamePrefix)) { + // Fully qualified documentation-comment references are excluded from TypesInUse's + // import-retention set (they don't affect imports), but they still need package renaming. + if (cu.getTypesInUse().hasDocReferenceInPackage(oldPackageName, recursive)) { return SearchResult.found(cu); } } else if (tree instanceof SourceFileWithReferences) { @@ -537,50 +536,6 @@ private boolean isTargetRecursivePackageName(String packageName) { } - private static boolean hasJavadocReferenceToPackage(JavaSourceFile cu, String packageName, boolean recursive, String recursivePrefix) { - return new JavaIsoVisitor() { - boolean inJavadocReference; - - @Override - public @Nullable J visit(@Nullable Tree tree, AtomicBoolean f) { - // Once a matching reference is found, skip the rest of the traversal. - return f.get() ? (J) tree : super.visit(tree, f); - } - - @Override - protected JavadocVisitor getJavadocVisitor() { - // Field accesses only carry a fully qualified package reference worth checking when - // they appear inside a Javadoc reference, so let the Javadoc delegate flag that scope - // rather than re-walking each field access's ancestors during the descent. - return new JavadocVisitor(this) { - @Override - public Javadoc visitReference(Javadoc.Reference reference, AtomicBoolean f) { - inJavadocReference = true; - try { - return super.visitReference(reference, f); - } finally { - inJavadocReference = false; - } - } - }; - } - - @Override - public J.FieldAccess visitFieldAccess(J.FieldAccess fieldAccess, AtomicBoolean f) { - if (inJavadocReference && !f.get()) { - JavaType type = fieldAccess.getType(); - if (type instanceof JavaType.FullyQualified) { - String pkg = ((JavaType.FullyQualified) type).getPackageName(); - if (pkg.equals(packageName) || recursive && pkg.startsWith(recursivePrefix)) { - f.set(true); - } - } - } - return super.visitFieldAccess(fieldAccess, f); - } - }.reduce(cu, new AtomicBoolean()).get(); - } - @Value @EqualsAndHashCode(callSuper = false) private static class ReferenceChangePackageVisitor extends TreeVisitor { diff --git a/rewrite-java/src/main/java/org/openrewrite/java/internal/TypesInUse.java b/rewrite-java/src/main/java/org/openrewrite/java/internal/TypesInUse.java index 886332ec9a3..835699b938a 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/internal/TypesInUse.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/internal/TypesInUse.java @@ -30,6 +30,7 @@ import org.openrewrite.java.tree.TypeUtils; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; @@ -49,6 +50,22 @@ public class TypesInUse { private final Set usedMethods; private final Set variables; + /** + * Packages of the fully qualified types referenced only from documentation comments — Javadoc + * {@code {@link com.foo.Bar}}, C# {@code }, etc. — each contributing its package + * (e.g. {@code com.foo}). The types themselves are deliberately excluded from {@link #typesInUse} + * because a fully qualified doc reference needs no import, so it must not count toward import + * retention (see #5738). Their packages are recorded here so that package-renaming recipes can still + * discover them without re-walking the tree. Only the package is retained because that is all such + * recipes query. + *

+ * This bucket is transient: it is populated only by {@link #build(JavaSourceFile)} and is left + * empty by {@link #of}, since a serialized type index carries no doc references. It is queried only via + * {@link #hasDocReferenceInPackage}; the raw set is intentionally not exposed. + */ + @Getter(AccessLevel.NONE) + private final Set docReferencePackages; + /** * Lazily-built prefix tree over every fully qualified name reachable via * {@link TypeUtils#isAssignableTo(String, JavaType)} starting from any type referenced in this @@ -83,7 +100,8 @@ public static TypesInUse build(JavaSourceFile cu) { findTypesInUse.getTypes(), findTypesInUse.getDeclaredMethods(), findTypesInUse.getUsedMethods(), - findTypesInUse.getVariables()); + findTypesInUse.getVariables(), + findTypesInUse.getDocReferencePackages()); } /** @@ -96,7 +114,23 @@ public static TypesInUse of(JavaSourceFile cu, Set declaredMethods, Set usedMethods, Set variables) { - return new TypesInUse(cu, typesInUse, declaredMethods, usedMethods, variables); + return new TypesInUse(cu, typesInUse, declaredMethods, usedMethods, variables, Collections.emptySet()); + } + + /** + * Whether any fully qualified documentation-comment reference in this compilation unit has a package + * equal to {@code packageName}, or (when {@code recursive}) starts with {@code packageName + '.'}. + * Mirrors the per-type package check that package-renaming recipes apply to {@link #typesInUse}, for + * the references that {@link #typesInUse} deliberately omits. + */ + public boolean hasDocReferenceInPackage(String packageName, boolean recursive) { + String recursivePrefix = packageName + "."; + for (String pkg : docReferencePackages) { + if (pkg.equals(packageName) || recursive && pkg.startsWith(recursivePrefix)) { + return true; + } + } + return false; } /** @@ -509,6 +543,7 @@ public static class FindTypesInUse extends JavaIsoVisitor { private final Set declaredMethods = newSetFromMap(new IdentityHashMap<>()); private final Set usedMethods = newSetFromMap(new IdentityHashMap<>()); private final Set variables = newSetFromMap(new IdentityHashMap<>()); + private final Set docReferencePackages = new HashSet<>(); @Override public J.Import visitImport(J.Import _import, Integer p) { @@ -554,9 +589,14 @@ public J.Lambda.Parameters visitLambdaParameters(J.Lambda.Parameters parameters, usedMethods.add((JavaType.Method) javaType); } } else if (!(cursor.getValue() instanceof J.ClassDeclaration) && - !(cursor.getValue() instanceof J.Lambda) && - !isFullyQualifiedJavaDocReference(cursor)) { - types.add(javaType); + !(cursor.getValue() instanceof J.Lambda)) { + if (isFullyQualifiedJavaDocReference(cursor)) { + if (javaType instanceof JavaType.FullyQualified) { + docReferencePackages.add(((JavaType.FullyQualified) javaType).getPackageName()); + } + } else { + types.add(javaType); + } } } return javaType;