diff --git a/CHANGES.md b/CHANGES.md index 9fc9a36075..9c2c464897 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Fixed - `shortenFullyQualifiedTypes` leaves a qualified name alone when the enclosing type extends or implements a type that is not in the same file, so an inherited member type cannot take the simple name. ([#3117](https://github.com/diffplug/spotless/issues/3117)) +- `expandWildcardImports` no longer drops the import of a type that is only used to qualify a static member (such as `Collections.sort(list)` or `TimeUnit.SECONDS`), or the static import of a field or enum constant (such as `PI` from `import static java.lang.Math.*`), which produced code that did not compile. ([#2833](https://github.com/diffplug/spotless/issues/2833)) ## [4.10.3] - 2026-09-25 diff --git a/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ExpandWildcardsFormatterFunc.java b/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ExpandWildcardsFormatterFunc.java index b981386b19..c563b7a4a2 100644 --- a/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ExpandWildcardsFormatterFunc.java +++ b/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ExpandWildcardsFormatterFunc.java @@ -37,8 +37,11 @@ import com.github.javaparser.ast.ImportDeclaration; import com.github.javaparser.ast.Node; import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.FieldAccessExpr; import com.github.javaparser.ast.expr.MarkerAnnotationExpr; import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.MethodReferenceExpr; +import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.expr.NormalAnnotationExpr; import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; import com.github.javaparser.ast.type.ClassOrInterfaceType; @@ -47,6 +50,7 @@ import com.github.javaparser.resolution.UnsolvedSymbolException; import com.github.javaparser.resolution.declarations.ResolvedAnnotationDeclaration; import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration; import com.github.javaparser.resolution.types.ResolvedType; import com.github.javaparser.symbolsolver.JavaSymbolSolver; import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; @@ -187,6 +191,65 @@ public void visit(final MethodCallExpr n, final Map> importMap) { + Optional value = resolveValue(n); + if (value.isPresent()) { + // static imports of fields and enum constants, e.g. `PI` or `SECONDS` + staticMemberName(value.get()).ifPresent(name -> matchTypeName(importMap, name, true)); + } else if (isQualifier(n)) { + // a type that is only used to qualify a member, e.g. `Collections.sort(list)` + resolveTypeName(n).ifPresent(name -> matchTypeName(importMap, name, false)); + } + super.visit(n, importMap); + } + + private static Optional resolveValue(NameExpr n) { + try { + return Optional.of(n.resolve()); + } catch (RuntimeException ex) { + // not a resolvable variable, parameter, field or enum constant; possibly the name of a type or package + return Optional.empty(); + } + } + + private static Optional staticMemberName(ResolvedValueDeclaration value) { + if (value.isField() && value.asField().isStatic()) { + return Optional.of(value.asField().declaringType().getQualifiedName() + "." + value.getName()); + } + if (value.isEnumConstant()) { + ResolvedType enumType = value.getType(); + if (enumType.isReferenceType()) { + return Optional.of(enumType.asReferenceType().getQualifiedName() + "." + value.getName()); + } + } + return Optional.empty(); + } + + private static boolean isQualifier(NameExpr n) { + Node parent = n.getParentNode().orElse(null); + if (parent instanceof MethodCallExpr call) { + return call.getScope().filter(scope -> scope == n).isPresent(); + } + if (parent instanceof FieldAccessExpr access) { + return access.getScope() == n; + } + if (parent instanceof MethodReferenceExpr reference) { + return reference.getScope() == n; + } + return false; + } + + private static Optional resolveTypeName(NameExpr n) { + try { + ResolvedType type = n.calculateResolvedType(); + return type.isReferenceType() ? Optional.of(type.asReferenceType().getQualifiedName()) : Optional.empty(); + } catch (RuntimeException ex) { + // e.g. the first segment of a fully qualified name, which needs no import + return Optional.empty(); + } + } + private static R wrapUnsolvedSymbolException(T node, Function func) { try { return func.apply(node); diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index ff9243124b..81baac8be5 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -14,6 +14,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Fixed - `shortenFullyQualifiedTypes` leaves a qualified name alone when the enclosing type extends or implements a type that is not in the same file, so an inherited member type cannot take the simple name. ([#3117](https://github.com/diffplug/spotless/issues/3117)) +- `expandWildcardImports` no longer drops the import of a type that is only used to qualify a static member (such as `Collections.sort(list)` or `TimeUnit.SECONDS`), or the static import of a field or enum constant (such as `PI` from `import static java.lang.Math.*`), which produced code that did not compile. ([#2833](https://github.com/diffplug/spotless/issues/2833)) ## [8.10.3] - 2026-09-25 ### Changes diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 98c013eaab..e0db083ca6 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -14,6 +14,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Fixed - `shortenFullyQualifiedTypes` leaves a qualified name alone when the enclosing type extends or implements a type that is not in the same file, so an inherited member type cannot take the simple name. ([#3117](https://github.com/diffplug/spotless/issues/3117)) +- `expandWildcardImports` no longer drops the import of a type that is only used to qualify a static member (such as `Collections.sort(list)` or `TimeUnit.SECONDS`), or the static import of a field or enum constant (such as `PI` from `import static java.lang.Math.*`), which produced code that did not compile. ([#2833](https://github.com/diffplug/spotless/issues/2833)) ### Fixed Migrate Plexus component injection to JSR-330 and Maven parameter. ([#3122](https://github.com/diffplug/spotless/pull/3122)) diff --git a/testlib/src/main/resources/java/expandwildcardimports/JavaUtilWildcardFormatted.test b/testlib/src/main/resources/java/expandwildcardimports/JavaUtilWildcardFormatted.test new file mode 100644 index 0000000000..a5809f59da --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/JavaUtilWildcardFormatted.test @@ -0,0 +1,7 @@ +package test; + +import java.util.List; + +public class Test { + private List items; +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/JavaUtilWildcardUnformatted.test b/testlib/src/main/resources/java/expandwildcardimports/JavaUtilWildcardUnformatted.test new file mode 100644 index 0000000000..6d355e13bb --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/JavaUtilWildcardUnformatted.test @@ -0,0 +1,7 @@ +package test; + +import java.util.*; + +public class Test { + private List items; +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/JdkXmlTypesFormatted.test b/testlib/src/main/resources/java/expandwildcardimports/JdkXmlTypesFormatted.test new file mode 100644 index 0000000000..bfdf5f36c6 --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/JdkXmlTypesFormatted.test @@ -0,0 +1,10 @@ +package test; + +import java.io.StringReader; +import org.xml.sax.InputSource; + +public class Test { + InputSource inputSource(String value) { + return new InputSource(new StringReader(value)); + } +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/JdkXmlTypesUnformatted.test b/testlib/src/main/resources/java/expandwildcardimports/JdkXmlTypesUnformatted.test new file mode 100644 index 0000000000..cfb2e355d7 --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/JdkXmlTypesUnformatted.test @@ -0,0 +1,10 @@ +package test; + +import java.io.*; +import org.xml.sax.InputSource; + +public class Test { + InputSource inputSource(String value) { + return new InputSource(new StringReader(value)); + } +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/QualifierTypesFormatted.test b/testlib/src/main/resources/java/expandwildcardimports/QualifierTypesFormatted.test new file mode 100644 index 0000000000..fb5ae428c1 --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/QualifierTypesFormatted.test @@ -0,0 +1,18 @@ +package test; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class Test { + List sorted(List values) { + Collections.sort(values); + var iterator = values.iterator(); + values.forEach(value -> System.out.println(value.length())); + return iterator.hasNext() ? values : Collections.emptyList(); + } + + long timeoutMillis() { + return TimeUnit.SECONDS.toMillis(1); + } +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/QualifierTypesUnformatted.test b/testlib/src/main/resources/java/expandwildcardimports/QualifierTypesUnformatted.test new file mode 100644 index 0000000000..3ef809fb6d --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/QualifierTypesUnformatted.test @@ -0,0 +1,17 @@ +package test; + +import java.util.*; +import java.util.concurrent.*; + +public class Test { + List sorted(List values) { + Collections.sort(values); + var iterator = values.iterator(); + values.forEach(value -> System.out.println(value.length())); + return iterator.hasNext() ? values : Collections.emptyList(); + } + + long timeoutMillis() { + return TimeUnit.SECONDS.toMillis(1); + } +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/StaticFieldsAndEnumConstantsFormatted.test b/testlib/src/main/resources/java/expandwildcardimports/StaticFieldsAndEnumConstantsFormatted.test new file mode 100644 index 0000000000..301ef94e42 --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/StaticFieldsAndEnumConstantsFormatted.test @@ -0,0 +1,15 @@ +package test; + +import static java.lang.Math.PI; +import static java.lang.Math.max; +import static java.util.concurrent.TimeUnit.SECONDS; + +public class Test { + double circleArea(double radius) { + return PI * radius * radius; + } + + long timeoutMillis() { + return SECONDS.toMillis(max(1, 2)); + } +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/StaticFieldsAndEnumConstantsUnformatted.test b/testlib/src/main/resources/java/expandwildcardimports/StaticFieldsAndEnumConstantsUnformatted.test new file mode 100644 index 0000000000..e4448ce810 --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/StaticFieldsAndEnumConstantsUnformatted.test @@ -0,0 +1,14 @@ +package test; + +import static java.lang.Math.*; +import static java.util.concurrent.TimeUnit.*; + +public class Test { + double circleArea(double radius) { + return PI * radius * radius; + } + + long timeoutMillis() { + return SECONDS.toMillis(max(1, 2)); + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/java/ExpandWildcardImportsStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/ExpandWildcardImportsStepTest.java index 723429081d..25ef9e2aab 100644 --- a/testlib/src/test/java/com/diffplug/spotless/java/ExpandWildcardImportsStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/java/ExpandWildcardImportsStepTest.java @@ -126,61 +126,29 @@ void expandWildcardImports_emptyClasspath() throws Exception { FormatterStep step = ExpandWildcardImportsStep.create(Collections.emptySet(), TestProvisioner.mavenCentral()); // Even with empty classpath, Java standard library imports should still be expanded - String simpleCode = """ - package test; - - import java.util.*; - - public class Test { - private List items; - } - """; - - String expectedOutput = """ - package test; - - import java.util.List; - - public class Test { - private List items; - } - """; - // The step should still expand java.util.* to java.util.List - StepHarness.forStep(step).test(simpleCode, expectedOutput); + StepHarness.forStep(step).testResource("java/expandwildcardimports/JavaUtilWildcardUnformatted.test", "java/expandwildcardimports/JavaUtilWildcardFormatted.test"); } @Test void expandWildcardImports_resolvesJdkXmlTypes() throws Exception { FormatterStep step = ExpandWildcardImportsStep.create(Collections.emptySet(), TestProvisioner.mavenCentral()); - String simpleCode = """ - package test; - - import java.io.*; - import org.xml.sax.InputSource; - - public class Test { - InputSource inputSource(String value) { - return new InputSource(new StringReader(value)); - } - } - """; + StepHarness.forStep(step).testResource("java/expandwildcardimports/JdkXmlTypesUnformatted.test", "java/expandwildcardimports/JdkXmlTypesFormatted.test"); + } - String expectedOutput = """ - package test; + @Test + void expandWildcardImports_keepsTypesOnlyUsedAsQualifiers() throws Exception { + FormatterStep step = ExpandWildcardImportsStep.create(Collections.emptySet(), TestProvisioner.mavenCentral()); - import java.io.StringReader; - import org.xml.sax.InputSource; + StepHarness.forStep(step).testResource("java/expandwildcardimports/QualifierTypesUnformatted.test", "java/expandwildcardimports/QualifierTypesFormatted.test"); + } - public class Test { - InputSource inputSource(String value) { - return new InputSource(new StringReader(value)); - } - } - """; + @Test + void expandWildcardImports_keepsStaticFieldsAndEnumConstants() throws Exception { + FormatterStep step = ExpandWildcardImportsStep.create(Collections.emptySet(), TestProvisioner.mavenCentral()); - StepHarness.forStep(step).test(simpleCode, expectedOutput); + StepHarness.forStep(step).testResource("java/expandwildcardimports/StaticFieldsAndEnumConstantsUnformatted.test", "java/expandwildcardimports/StaticFieldsAndEnumConstantsFormatted.test"); } @Test