-
Notifications
You must be signed in to change notification settings - Fork 515
Fix ChangeType inner class import handling when outer class import is present #7032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2505,7 +2505,8 @@ A test() { | |
| } | ||
| } | ||
| """, | ||
| """ | ||
| """ | ||
| import bar.A.Builder; | ||
| import foo.A; | ||
|
|
||
| class Test { | ||
|
|
@@ -2519,6 +2520,135 @@ A test() { | |
| ); | ||
| } | ||
|
|
||
| @Test | ||
| void renameInnerClassWithinSameOuterClass() { | ||
| rewriteRun( | ||
| spec -> spec.recipe(new ChangeType("foo.A$Builder", "foo.A$Creator", null)) | ||
| .parser(JavaParser.fromJavaVersion().dependsOn( | ||
| """ | ||
| package foo; | ||
|
|
||
| public class A { | ||
| public static class Builder {} | ||
| public static class Creator {} | ||
| } | ||
| """ | ||
| ) | ||
| ), | ||
| java( | ||
| """ | ||
| import foo.A; | ||
|
|
||
| class Test { | ||
| void test() { | ||
| A.Builder x = new A.Builder(); | ||
| } | ||
| } | ||
| """, | ||
| """ | ||
| import foo.A; | ||
|
|
||
| class Test { | ||
| void test() { | ||
| A.Creator x = new A.Creator(); | ||
| } | ||
| } | ||
| """ | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| @Test | ||
| void changeTypeOfInnerCompletely() { | ||
| rewriteRun( | ||
| spec -> spec.recipe(new ChangeType("foo.A$Builder", "bar.B$Builder", null)) | ||
| .parser(JavaParser.fromJavaVersion().dependsOn( | ||
| """ | ||
| package foo; | ||
|
|
||
| public class A { | ||
| public static class Builder {} | ||
| } | ||
| """, | ||
| """ | ||
| package bar; | ||
|
|
||
| public class B { | ||
| public static class Builder {} | ||
| } | ||
| """ | ||
| ) | ||
| ), | ||
| java( | ||
| """ | ||
| import foo.A; | ||
| import foo.A.Builder; | ||
|
Comment on lines
+2584
to
+2585
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we have both imports here in the test? I'd think we only need the first one right? |
||
|
|
||
| class Test { | ||
| void test() { | ||
| A.Builder x = new A.Builder(); | ||
| } | ||
| } | ||
| """, | ||
| """ | ||
| import bar.B; | ||
|
|
||
| class Test { | ||
| void test() { | ||
| B.Builder x = new B.Builder(); | ||
| } | ||
| } | ||
| """ | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| @Test | ||
| void changeTypeOfInnerClassImplicitly() { | ||
| rewriteRun( | ||
| spec -> spec.recipe(new ChangeType("foo.A", "bar.B", null)) | ||
| .parser(JavaParser.fromJavaVersion().dependsOn( | ||
| """ | ||
| package foo; | ||
|
|
||
| public class A { | ||
| public static class Builder {} | ||
| } | ||
| """, | ||
| """ | ||
| package bar; | ||
|
|
||
| public class B { | ||
| public static class Builder {} | ||
| } | ||
| """ | ||
|
Comment on lines
+2618
to
+2624
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't need this second depends on block right? As ChangeType synthesizes types for what it changes, and we only need to provide the correct types to compile the imports? |
||
| ) | ||
| ), | ||
| java( | ||
| """ | ||
| import foo.A; | ||
| import foo.A.Builder; | ||
|
|
||
| class Test { | ||
| void test() { | ||
| A.Builder x = new A.Builder(); | ||
| } | ||
| } | ||
| """, | ||
| """ | ||
| import bar.B; | ||
| import bar.B.Builder; | ||
|
|
||
| class Test { | ||
| void test() { | ||
| B.Builder x = new B.Builder(); | ||
| } | ||
| } | ||
| """ | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| @Test | ||
| void inheritedTypesUpdated() { | ||
| rewriteRun( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -193,12 +193,12 @@ public J visitImport(J.Import import_, ExecutionContext ctx) { | |
| return updateType(javaType); | ||
| } | ||
|
|
||
| private void addImport(JavaType.FullyQualified owningClass) { | ||
| private void addImport(JavaType.FullyQualified owningClass, boolean onlyIfUsed) { | ||
| if (importAlias != null) { | ||
| maybeAddImport(owningClass.getPackageName(), owningClass.getClassName(), null, importAlias.getSimpleName(), true); | ||
| maybeAddImport(owningClass.getPackageName(), owningClass.getClassName(), null, importAlias.getSimpleName(), onlyIfUsed); | ||
| } | ||
|
|
||
| maybeAddImport(owningClass.getPackageName(), owningClass.getClassName(), null, null, true); | ||
| maybeAddImport(owningClass.getPackageName(), owningClass.getClassName(), null, null, onlyIfUsed); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -225,6 +225,7 @@ private void addImport(JavaType.FullyQualified owningClass) { | |
| j = ((TypedTree) tree).withType(updateType(((TypedTree) tree).getType())); | ||
| } else if (tree instanceof JavaSourceFile) { | ||
| JavaSourceFile sf = (JavaSourceFile) tree; | ||
| boolean outerClassImportRemoved = false; | ||
| if (targetType instanceof JavaType.FullyQualified) { | ||
| for (J.Import anImport : sf.getImports()) { | ||
| if (anImport.isStatic()) { | ||
|
|
@@ -242,7 +243,10 @@ private void addImport(JavaType.FullyQualified owningClass) { | |
| if (originalType.getFullyQualifiedName().equals(type.getFullyQualifiedName())) { | ||
| sf = (JavaSourceFile) new RemoveImport<ExecutionContext>(originalType.getFullyQualifiedName()).visitNonNull(sf, ctx, getCursor().getParentOrThrow()); | ||
| } else if (originalType.getOwningClass() != null && originalType.getOwningClass().getFullyQualifiedName().equals(type.getFullyQualifiedName())) { | ||
| JavaSourceFile sfBefore = sf; | ||
| sf = (JavaSourceFile) new RemoveImport<ExecutionContext>(originalType.getOwningClass().getFullyQualifiedName()).visitNonNull(sf, ctx, getCursor().getParentOrThrow()); | ||
| // Track whether the outer class import was actually removed (not retained because still in use) | ||
| outerClassImportRemoved |= (sf != sfBefore); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -253,13 +257,21 @@ private void addImport(JavaType.FullyQualified owningClass) { | |
| if (fullyQualifiedTarget != null) { | ||
| JavaType.FullyQualified owningClass = fullyQualifiedTarget.getOwningClass(); | ||
| if (!topLevelClassnames.contains(getTopLevelClassName(fullyQualifiedTarget).getFullyQualifiedName())) { | ||
| if (hasNoConflictingImport(sf)) { | ||
| if (owningClass != null && !"java.lang".equals(fullyQualifiedTarget.getPackageName())) { | ||
| addImport(owningClass); | ||
| } | ||
| if (!"java.lang".equals(fullyQualifiedTarget.getPackageName())) { | ||
| addImport(fullyQualifiedTarget); | ||
| if (!"java.lang".equals(fullyQualifiedTarget.getPackageName()) && hasNoConflictingImport(sf)) { | ||
| if (owningClass != null) { | ||
| // Force-add the outer class import when the original outer class import was | ||
| // explicitly removed (meaning code used the outer class by name, e.g. "B" in "B.Builder"). | ||
| // When the outer class import was absent, the code uses only the simple inner class | ||
| // name (e.g. just "Y"), so AddImport can decide based on actual references. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In our comments we first use B, then switch to Y here; that seems needlessly confusing, or was that intentional? |
||
| addImport(owningClass, !outerClassImportRemoved); | ||
| } | ||
| // Force-add the inner class import only when the outer class was NOT removed: | ||
| // if the outer class was removed and is being re-added, `import bar.B` already | ||
| // makes `B.Builder` accessible — adding `import bar.B.Builder` would be redundant. | ||
| // When only the inner class import existed (e.g. code uses simple name "Y"), | ||
| // force-add when the owning class changed so FindTypes can locate the new type. | ||
| boolean forceAddInnerImport = !outerClassImportRemoved && !owningClassSame(owningClass); | ||
| addImport(fullyQualifiedTarget, !forceAddInnerImport); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -278,6 +290,16 @@ private void addImport(JavaType.FullyQualified owningClass) { | |
| return j; | ||
| } | ||
|
|
||
| private boolean owningClassSame(JavaType.@Nullable FullyQualified owningClass) { | ||
| JavaType.FullyQualified originalOwningClass = originalType.getOwningClass(); | ||
| if (originalOwningClass == null || owningClass == null) { | ||
| return true; | ||
| } | ||
|
|
||
| return originalOwningClass.getClassName().equals(owningClass.getClassName()) && | ||
| originalOwningClass.getPackageName().equals(owningClass.getPackageName()); | ||
| } | ||
|
|
||
| @Override | ||
| public J visitFieldAccess(J.FieldAccess fieldAccess, ExecutionContext ctx) { | ||
| if (fieldAccess.isFullyQualifiedClassReference(originalType.getFullyQualifiedName())) { | ||
|
|
@@ -400,7 +422,8 @@ public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) | |
| method.getSimpleName().equals(anImport.getQualid().getSimpleName())) { | ||
| JavaType.FullyQualified targetFqn = (JavaType.FullyQualified) targetType; | ||
|
|
||
| addImport(targetFqn); | ||
| // onlyIfUsed=true: the static import already proves the type is referenced. | ||
| addImport(targetFqn, true); | ||
| maybeAddImport((targetFqn).getFullyQualifiedName(), method.getName().getSimpleName()); | ||
| break; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now we're testing the fairly common case of A.B, but should we also test the variants for A.B.C ? I wonder if those are handled correctly for all the permutations of imported as A, A.B or A.B.C, and then changed as A, A$B or A$B$C.