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 @@ -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(
Expand Down
51 changes: 3 additions & 48 deletions rewrite-java/src/main/java/org/openrewrite/java/ChangePackage.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -109,9 +108,9 @@ public TreeVisitor<?, ExecutionContext> 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) {
Expand Down Expand Up @@ -537,50 +536,6 @@ private boolean isTargetRecursivePackageName(String packageName) {

}

private static boolean hasJavadocReferenceToPackage(JavaSourceFile cu, String packageName, boolean recursive, String recursivePrefix) {
return new JavaIsoVisitor<AtomicBoolean>() {
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<AtomicBoolean> 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<AtomicBoolean>(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<Tree, ExecutionContext> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -49,6 +50,22 @@ public class TypesInUse {
private final Set<JavaType.Method> usedMethods;
private final Set<JavaType.Variable> variables;

/**
* Packages of the fully qualified types referenced only from documentation comments — Javadoc
* {@code {@link com.foo.Bar}}, C# {@code <see cref="..."/>}, 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.
* <p>
* This bucket is <em>transient</em>: 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<String> docReferencePackages;

/**
* Lazily-built prefix tree over every fully qualified name reachable via
* {@link TypeUtils#isAssignableTo(String, JavaType)} starting from any type referenced in this
Expand Down Expand Up @@ -83,7 +100,8 @@ public static TypesInUse build(JavaSourceFile cu) {
findTypesInUse.getTypes(),
findTypesInUse.getDeclaredMethods(),
findTypesInUse.getUsedMethods(),
findTypesInUse.getVariables());
findTypesInUse.getVariables(),
findTypesInUse.getDocReferencePackages());
}

/**
Expand All @@ -96,7 +114,23 @@ public static TypesInUse of(JavaSourceFile cu,
Set<JavaType.Method> declaredMethods,
Set<JavaType.Method> usedMethods,
Set<JavaType.Variable> 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;
}

/**
Expand Down Expand Up @@ -509,6 +543,7 @@ public static class FindTypesInUse extends JavaIsoVisitor<Integer> {
private final Set<JavaType.Method> declaredMethods = newSetFromMap(new IdentityHashMap<>());
private final Set<JavaType.Method> usedMethods = newSetFromMap(new IdentityHashMap<>());
private final Set<JavaType.Variable> variables = newSetFromMap(new IdentityHashMap<>());
private final Set<String> docReferencePackages = new HashSet<>();

@Override
public J.Import visitImport(J.Import _import, Integer p) {
Expand Down Expand Up @@ -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;
Expand Down