Skip to content

Commit 99b38d7

Browse files
committed
Record Javadoc reference packages in TypesInUse
`TypesInUse` deliberately excludes fully qualified Javadoc references from its import-retention set (#5738), since such references need no import. `ChangePackage` therefore carried its own LST walk to rediscover exactly those references for its precondition — duplicating the "is this a fully qualified Javadoc reference" predicate, which is how the regression fixed in #7284 originally crept in. Capture the packages of those references in a separate transient `javadocPackages` bucket during the single `FindTypesInUse` walk, and have `ChangePackage` query it via `hasJavadocReferenceInPackage`. Only the package is retained, since that is all package-renaming recipes ask. The import-retention set is unchanged, so `RemoveUnusedImports`/`UsesType`/the FQN trie behave exactly as before. The bucket is not serialized; `TypesInUse.of` leaves it empty.
1 parent a4b817d commit 99b38d7

3 files changed

Lines changed: 83 additions & 53 deletions

File tree

rewrite-java-test/src/test/java/org/openrewrite/java/internal/TypesInUseTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,44 @@ public class Foo {}
6565
);
6666
}
6767

68+
@Test
69+
void recordsFullyQualifiedJavadocReferencesSeparately() {
70+
rewriteRun(
71+
java(
72+
"""
73+
package org.openrewrite.other;
74+
public class Target {}
75+
"""
76+
),
77+
java(
78+
"""
79+
package com.example;
80+
/**
81+
* See {@link org.openrewrite.other.Target} for details.
82+
*/
83+
public class Bar {}
84+
""",
85+
spec -> spec.afterRecipe(cu -> {
86+
TypesInUse tiu = cu.getTypesInUse();
87+
88+
// Fully qualified Javadoc references stay out of the import-retention set (#5738)...
89+
assertThat(tiu.getTypesInUse().stream()
90+
.filter(t -> t instanceof JavaType.FullyQualified)
91+
.map(t -> ((JavaType.FullyQualified) t).getFullyQualifiedName()))
92+
.doesNotContain("org.openrewrite.other.Target");
93+
94+
// ...but their packages are recorded separately so package-renaming recipes can find them.
95+
assertThat(tiu.getJavadocPackages()).contains("org.openrewrite.other");
96+
97+
assertThat(tiu.hasJavadocReferenceInPackage("org.openrewrite.other", false)).isTrue();
98+
assertThat(tiu.hasJavadocReferenceInPackage("org.openrewrite", false)).isFalse();
99+
assertThat(tiu.hasJavadocReferenceInPackage("org.openrewrite", true)).isTrue();
100+
assertThat(tiu.hasJavadocReferenceInPackage("com.other", true)).isFalse();
101+
})
102+
)
103+
);
104+
}
105+
68106
@Test
69107
void publicFactoryReturnsInstanceWithSuppliedSets() {
70108
rewriteRun(

rewrite-java/src/main/java/org/openrewrite/java/ChangePackage.java

Lines changed: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929

3030
import java.nio.file.Paths;
3131
import java.util.*;
32-
import java.util.concurrent.atomic.AtomicBoolean;
3332

3433
import static java.util.Objects.requireNonNull;
3534
import static org.openrewrite.Tree.randomId;
@@ -109,9 +108,9 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
109108
}
110109
}
111110
}
112-
// Fully qualified javadoc references are excluded from TypesInUse
113-
// (they don't affect imports), but they still need package renaming.
114-
if (hasJavadocReferenceToPackage(cu, oldPackageName, recursive, recursivePackageNamePrefix)) {
111+
// Fully qualified javadoc references are excluded from TypesInUse's import-retention
112+
// set (they don't affect imports), but they still need package renaming.
113+
if (cu.getTypesInUse().hasJavadocReferenceInPackage(oldPackageName, recursive)) {
115114
return SearchResult.found(cu);
116115
}
117116
} else if (tree instanceof SourceFileWithReferences) {
@@ -537,50 +536,6 @@ private boolean isTargetRecursivePackageName(String packageName) {
537536

538537
}
539538

540-
private static boolean hasJavadocReferenceToPackage(JavaSourceFile cu, String packageName, boolean recursive, String recursivePrefix) {
541-
return new JavaIsoVisitor<AtomicBoolean>() {
542-
boolean inJavadocReference;
543-
544-
@Override
545-
public @Nullable J visit(@Nullable Tree tree, AtomicBoolean f) {
546-
// Once a matching reference is found, skip the rest of the traversal.
547-
return f.get() ? (J) tree : super.visit(tree, f);
548-
}
549-
550-
@Override
551-
protected JavadocVisitor<AtomicBoolean> getJavadocVisitor() {
552-
// Field accesses only carry a fully qualified package reference worth checking when
553-
// they appear inside a Javadoc reference, so let the Javadoc delegate flag that scope
554-
// rather than re-walking each field access's ancestors during the descent.
555-
return new JavadocVisitor<AtomicBoolean>(this) {
556-
@Override
557-
public Javadoc visitReference(Javadoc.Reference reference, AtomicBoolean f) {
558-
inJavadocReference = true;
559-
try {
560-
return super.visitReference(reference, f);
561-
} finally {
562-
inJavadocReference = false;
563-
}
564-
}
565-
};
566-
}
567-
568-
@Override
569-
public J.FieldAccess visitFieldAccess(J.FieldAccess fieldAccess, AtomicBoolean f) {
570-
if (inJavadocReference && !f.get()) {
571-
JavaType type = fieldAccess.getType();
572-
if (type instanceof JavaType.FullyQualified) {
573-
String pkg = ((JavaType.FullyQualified) type).getPackageName();
574-
if (pkg.equals(packageName) || recursive && pkg.startsWith(recursivePrefix)) {
575-
f.set(true);
576-
}
577-
}
578-
}
579-
return super.visitFieldAccess(fieldAccess, f);
580-
}
581-
}.reduce(cu, new AtomicBoolean()).get();
582-
}
583-
584539
@Value
585540
@EqualsAndHashCode(callSuper = false)
586541
private static class ReferenceChangePackageVisitor extends TreeVisitor<Tree, ExecutionContext> {

rewrite-java/src/main/java/org/openrewrite/java/internal/TypesInUse.java

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.openrewrite.java.tree.TypeUtils;
3131

3232
import java.util.Arrays;
33+
import java.util.Collections;
3334
import java.util.HashSet;
3435
import java.util.IdentityHashMap;
3536
import java.util.Iterator;
@@ -49,6 +50,19 @@ public class TypesInUse {
4950
private final Set<JavaType.Method> usedMethods;
5051
private final Set<JavaType.Variable> variables;
5152

53+
/**
54+
* Packages of the fully qualified types referenced only from Javadoc (e.g. {@code {@link com.foo.Bar}}
55+
* contributes {@code com.foo}). The types themselves are deliberately excluded from {@link #typesInUse}
56+
* because a fully qualified Javadoc reference needs no import, so it must not count toward import
57+
* retention (see #5738). Their packages are recorded here so that package-renaming recipes can still
58+
* discover them without re-walking the tree. Only the package is retained because that is all such
59+
* recipes query.
60+
* <p>
61+
* This bucket is <em>transient</em>: it is populated only by {@link #build(JavaSourceFile)} and is left
62+
* empty by {@link #of}, since a serialized type index carries no Javadoc references.
63+
*/
64+
private final Set<String> javadocPackages;
65+
5266
/**
5367
* Lazily-built prefix tree over every fully qualified name reachable via
5468
* {@link TypeUtils#isAssignableTo(String, JavaType)} starting from any type referenced in this
@@ -83,7 +97,8 @@ public static TypesInUse build(JavaSourceFile cu) {
8397
findTypesInUse.getTypes(),
8498
findTypesInUse.getDeclaredMethods(),
8599
findTypesInUse.getUsedMethods(),
86-
findTypesInUse.getVariables());
100+
findTypesInUse.getVariables(),
101+
findTypesInUse.getJavadocPackages());
87102
}
88103

89104
/**
@@ -96,7 +111,23 @@ public static TypesInUse of(JavaSourceFile cu,
96111
Set<JavaType.Method> declaredMethods,
97112
Set<JavaType.Method> usedMethods,
98113
Set<JavaType.Variable> variables) {
99-
return new TypesInUse(cu, typesInUse, declaredMethods, usedMethods, variables);
114+
return new TypesInUse(cu, typesInUse, declaredMethods, usedMethods, variables, Collections.emptySet());
115+
}
116+
117+
/**
118+
* Whether any fully qualified Javadoc reference in this compilation unit has a package equal to
119+
* {@code packageName}, or (when {@code recursive}) starts with {@code packageName + '.'}. Mirrors the
120+
* per-type package check that package-renaming recipes apply to {@link #typesInUse}, for the references
121+
* that {@link #typesInUse} deliberately omits.
122+
*/
123+
public boolean hasJavadocReferenceInPackage(String packageName, boolean recursive) {
124+
String recursivePrefix = packageName + ".";
125+
for (String pkg : javadocPackages) {
126+
if (pkg.equals(packageName) || recursive && pkg.startsWith(recursivePrefix)) {
127+
return true;
128+
}
129+
}
130+
return false;
100131
}
101132

102133
/**
@@ -509,6 +540,7 @@ public static class FindTypesInUse extends JavaIsoVisitor<Integer> {
509540
private final Set<JavaType.Method> declaredMethods = newSetFromMap(new IdentityHashMap<>());
510541
private final Set<JavaType.Method> usedMethods = newSetFromMap(new IdentityHashMap<>());
511542
private final Set<JavaType.Variable> variables = newSetFromMap(new IdentityHashMap<>());
543+
private final Set<String> javadocPackages = new HashSet<>();
512544

513545
@Override
514546
public J.Import visitImport(J.Import _import, Integer p) {
@@ -554,9 +586,14 @@ public J.Lambda.Parameters visitLambdaParameters(J.Lambda.Parameters parameters,
554586
usedMethods.add((JavaType.Method) javaType);
555587
}
556588
} else if (!(cursor.getValue() instanceof J.ClassDeclaration) &&
557-
!(cursor.getValue() instanceof J.Lambda) &&
558-
!isFullyQualifiedJavaDocReference(cursor)) {
559-
types.add(javaType);
589+
!(cursor.getValue() instanceof J.Lambda)) {
590+
if (isFullyQualifiedJavaDocReference(cursor)) {
591+
if (javaType instanceof JavaType.FullyQualified) {
592+
javadocPackages.add(((JavaType.FullyQualified) javaType).getPackageName());
593+
}
594+
} else {
595+
types.add(javaType);
596+
}
560597
}
561598
}
562599
return javaType;

0 commit comments

Comments
 (0)