From 411abb506cd95a7b18d26d9fd81fcb6c755bf00c Mon Sep 17 00:00:00 2001 From: Max Rydahl Andersen Date: Fri, 28 Aug 2026 17:06:41 +0200 Subject: [PATCH] shortenFullyQualifiedTypes: preserve unqualified type resolution --- .../ShortenQualifiedTypesFormatterFunc.java | 31 ++++++++++++------- .../ShortenFullyQualifiedTypesStepTest.java | 15 +++++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ShortenQualifiedTypesFormatterFunc.java b/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ShortenQualifiedTypesFormatterFunc.java index e0052e9353..b4a6196060 100644 --- a/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ShortenQualifiedTypesFormatterFunc.java +++ b/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ShortenQualifiedTypesFormatterFunc.java @@ -86,7 +86,13 @@ public String apply(String rawUnix) throws Exception { cu.findAll(EnumDeclaration.class).forEach(c -> declaredTypeNames.add(c.getNameAsString())); cu.findAll(RecordDeclaration.class).forEach(c -> declaredTypeNames.add(c.getNameAsString())); - // 4. Walk the AST to find outermost fully-qualified type nodes + // 4. Collect unqualified type references, which may resolve to types in the same package + Set unqualifiedTypeNames = new LinkedHashSet<>(); + cu.findAll(ClassOrInterfaceType.class).stream() + .filter(type -> type.getScope().isEmpty()) + .forEach(type -> unqualifiedTypeNames.add(type.getNameAsString())); + + // 5. Walk the AST to find outermost fully-qualified type nodes Map> simpleToFqns = new LinkedHashMap<>(); List qualifiedRefs = new ArrayList<>(); @@ -96,7 +102,7 @@ public String apply(String rawUnix) throws Exception { return rawUnix; } - // 5. Determine which FQNs are safe to shorten + // 6. Determine which FQNs are safe to shorten Set safeToShorten = new LinkedHashSet<>(); for (Map.Entry> entry : simpleToFqns.entrySet()) { String simple = entry.getKey(); @@ -109,8 +115,9 @@ public String apply(String rawUnix) throws Exception { if (existing != null && !existing.equals(fqn)) { continue; } - // Skip if simple name clashes with a type declared in this file - if (declaredTypeNames.contains(simple)) { + // Skip if simple name clashes with a type declared or already referenced in this file + if (declaredTypeNames.contains(simple) + || (existing == null && unqualifiedTypeNames.contains(simple) && !isImplicitlyImported(fqn, packageName))) { continue; } safeToShorten.add(fqn); @@ -120,7 +127,7 @@ public String apply(String rawUnix) throws Exception { return rawUnix; } - // 6. Convert line/column positions to string offsets and replace + // 7. Convert line/column positions to string offsets and replace // Build line-start offset table int[] lineOffsets = buildLineOffsets(rawUnix); @@ -147,14 +154,10 @@ public String apply(String rawUnix) throws Exception { sb.delete(removal[0], removal[1]); } - // 7. Add missing imports + // 8. Add missing imports Set newImports = new TreeSet<>(); for (String fqn : safeToShorten) { - if (fqn.startsWith("java.lang.") && fqn.indexOf('.', 10) == -1) { - continue; - } - if (!packageName.isEmpty() && fqn.startsWith(packageName + ".") - && fqn.indexOf('.', packageName.length() + 1) == -1) { + if (isImplicitlyImported(fqn, packageName)) { continue; } if (existingImportFqns.contains(fqn)) { @@ -242,6 +245,12 @@ private static boolean startsWithPackage(String rawName) { return !rawName.isEmpty() && Character.isLowerCase(rawName.charAt(0)); } + private static boolean isImplicitlyImported(String fqn, String packageName) { + return fqn.startsWith("java.lang.") && fqn.indexOf('.', 10) == -1 + || !packageName.isEmpty() && fqn.startsWith(packageName + ".") + && fqn.indexOf('.', packageName.length() + 1) == -1; + } + /** Builds an array where lineOffsets[line] is the char offset of the start of that line (1-indexed). */ private static int[] buildLineOffsets(String text) { List offsets = new ArrayList<>(); diff --git a/testlib/src/test/java/com/diffplug/spotless/java/ShortenFullyQualifiedTypesStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/ShortenFullyQualifiedTypesStepTest.java index 7e32b6d288..4293d0efae 100644 --- a/testlib/src/test/java/com/diffplug/spotless/java/ShortenFullyQualifiedTypesStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/java/ShortenFullyQualifiedTypesStepTest.java @@ -139,6 +139,7 @@ void alreadyImportedNotDuplicated() throws Exception { "", "public class Foo {", " java.util.List a;", + " List b;", "}", ""); String result = apply(before); @@ -329,6 +330,20 @@ void fqnCollisionWithInnerClassName() throws Exception { assertEquals(code, apply(code)); } + @Test + void fqnCollisionWithUnqualifiedSamePackageType() throws Exception { + // RandomAccessFile is declared in another file in this package; importing java.io.RandomAccessFile + // would silently change which type the unqualified superclass name resolves to + String code = String.join("\n", + "package test.reprod1;", + "", + "public class ClassA extends RandomAccessFile {", + " final java.io.RandomAccessFile file;", + "}", + ""); + assertEquals(code, apply(code)); + } + @Test void fqnNoCollisionWithDifferentSimpleName() throws Exception { // FQN whose simple name does NOT match the enclosing class — should still shorten