From cd525f2e3d9eb9d3127c2e65b6f1c6f573c258e2 Mon Sep 17 00:00:00 2001 From: Max Rydahl Andersen Date: Sun, 23 Aug 2026 11:04:57 +0200 Subject: [PATCH] shortenFullyQualifiedTypes: skip FQNs clashing with types declared in the same file Skip shortening when the simple name of a fully-qualified type matches a class, interface, enum, or record declared in the same compilation unit. Fixes compilation errors in projects like jbang where e.g. dev.jbang.cli.Alias intentionally uses dev.jbang.catalog.Alias as a FQN to avoid clashing with the enclosing class name. --- .../ShortenQualifiedTypesFormatterFunc.java | 21 +++++++-- .../ShortenFullyQualifiedTypesStepTest.java | 43 +++++++++++++++++++ 2 files changed, 60 insertions(+), 4 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 510c205d81..e0052e9353 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 @@ -33,6 +33,9 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.ImportDeclaration; import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.RecordDeclaration; import com.github.javaparser.ast.type.ClassOrInterfaceType; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; @@ -77,7 +80,13 @@ public String apply(String rawUnix) throws Exception { existingImportsBySimple.put(simple, fqn); } - // 3. Walk the AST to find outermost fully-qualified type nodes + // 3. Collect type names declared in this file (top-level + nested) + Set declaredTypeNames = new LinkedHashSet<>(); + cu.findAll(ClassOrInterfaceDeclaration.class).forEach(c -> declaredTypeNames.add(c.getNameAsString())); + 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 Map> simpleToFqns = new LinkedHashMap<>(); List qualifiedRefs = new ArrayList<>(); @@ -87,7 +96,7 @@ public String apply(String rawUnix) throws Exception { return rawUnix; } - // 4. Determine which FQNs are safe to shorten + // 5. Determine which FQNs are safe to shorten Set safeToShorten = new LinkedHashSet<>(); for (Map.Entry> entry : simpleToFqns.entrySet()) { String simple = entry.getKey(); @@ -100,6 +109,10 @@ 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)) { + continue; + } safeToShorten.add(fqn); } @@ -107,7 +120,7 @@ public String apply(String rawUnix) throws Exception { return rawUnix; } - // 5. Convert line/column positions to string offsets and replace + // 6. Convert line/column positions to string offsets and replace // Build line-start offset table int[] lineOffsets = buildLineOffsets(rawUnix); @@ -134,7 +147,7 @@ public String apply(String rawUnix) throws Exception { sb.delete(removal[0], removal[1]); } - // 6. Add missing imports + // 7. Add missing imports Set newImports = new TreeSet<>(); for (String fqn : safeToShorten) { if (fqn.startsWith("java.lang.") && fqn.indexOf('.', 10) == -1) { 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 c82bc839e4..7e32b6d288 100644 --- a/testlib/src/test/java/com/diffplug/spotless/java/ShortenFullyQualifiedTypesStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/java/ShortenFullyQualifiedTypesStepTest.java @@ -301,6 +301,49 @@ void lambdaParameterTypes() throws Exception { assertTrue(result.contains("import java.util.List;"), "should import List"); } + @Test + void fqnCollisionWithEnclosingClassName() throws Exception { + // dev.jbang.cli.Alias intentionally uses dev.jbang.catalog.Alias as FQN + // because the simple name "Alias" would clash with the enclosing class + String code = String.join("\n", + "package dev.jbang.cli;", + "", + "public class Alias {", + " dev.jbang.catalog.Alias catalogAlias;", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void fqnCollisionWithInnerClassName() throws Exception { + // FQN whose simple name matches an inner class declared in the same file + String code = String.join("\n", + "package com.example;", + "", + "public class Outer {", + " static class Conflict {}", + " com.other.Conflict externalConflict;", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void fqnNoCollisionWithDifferentSimpleName() throws Exception { + // FQN whose simple name does NOT match the enclosing class — should still shorten + String before = String.join("\n", + "package dev.jbang.cli;", + "", + "public class Alias {", + " java.util.List items;", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.util.List"), "non-conflicting FQN should be shortened"); + assertTrue(result.contains("import java.util.List;"), "should import List"); + } + @Test void multipleAnnotationsWithFqn() throws Exception { // FQNs used as annotation types should NOT be treated as type references