From 66b57589eef540b6e6963547598841f1341f8b4f Mon Sep 17 00:00:00 2001 From: Christian Grubert Date: Sun, 16 Nov 2025 13:04:13 +0100 Subject: [PATCH 1/6] new step to expand java wildcard imports --- CHANGES.md | 2 + lib/build.gradle | 3 + .../ExpandWildcardsFormatterFunc.java | 203 ++++++++++++++++++ .../java/ExpandWildcardImportsStep.java | 82 +++++++ plugin-gradle/CHANGES.md | 2 + plugin-gradle/README.md | 16 +- .../gradle/spotless/JavaExtension.java | 11 + .../com/diffplug/gradle/spotless/JvmLang.java | 10 +- .../spotless/JavaDefaultTargetTest.java | 34 +++ .../AnotherClassInSamePackage.test | 3 + .../AnotherImportedClass.test | 3 + .../JavaClassWithWildcardsFormatted.test | 35 +++ .../JavaClassWithWildcardsUnformatted.test | 32 +++ .../expandwildcardimports/example-lib.jar | Bin 0 -> 758 bytes .../java/ExpandWildcardImportsStepTest.java | 42 ++++ 15 files changed, 474 insertions(+), 4 deletions(-) create mode 100644 lib/src/javaParser/java/com/diffplug/spotless/glue/javaParser/ExpandWildcardsFormatterFunc.java create mode 100644 lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java create mode 100644 testlib/src/main/resources/java/expandwildcardimports/AnotherClassInSamePackage.test create mode 100644 testlib/src/main/resources/java/expandwildcardimports/AnotherImportedClass.test create mode 100644 testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsFormatted.test create mode 100644 testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsUnformatted.test create mode 100644 testlib/src/main/resources/java/expandwildcardimports/example-lib.jar create mode 100644 testlib/src/test/java/com/diffplug/spotless/java/ExpandWildcardImportsStepTest.java diff --git a/CHANGES.md b/CHANGES.md index 8079135a57..75e4ef2bf7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +- Add a `expandWildcardImports` API for java ([#2679](https://github.com/diffplug/spotless/issues/2594)) ## [4.1.0] - 2025-11-18 ### Changes diff --git a/lib/build.gradle b/lib/build.gradle index 517bb72cd6..8acddc0f16 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -18,6 +18,7 @@ def NEEDS_GLUE = [ 'googleJavaFormat', 'gson', 'jackson', + 'javaParser', 'ktfmt', 'ktlint', 'palantirJavaFormat', @@ -100,6 +101,8 @@ dependencies { String VER_JACKSON='2.20.1' jacksonCompileOnly "com.fasterxml.jackson.core:jackson-databind:$VER_JACKSON" jacksonCompileOnly "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$VER_JACKSON" + // javaParser + javaParserCompileOnly "com.github.javaparser:javaparser-symbol-solver-core:3.27.1" // ktfmt ktfmtCompileOnly "com.facebook:ktfmt:0.59" ktfmtCompileOnly("com.google.googlejavaformat:google-java-format") { 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 new file mode 100644 index 0000000000..2b6a5df469 --- /dev/null +++ b/lib/src/javaParser/java/com/diffplug/spotless/glue/javaParser/ExpandWildcardsFormatterFunc.java @@ -0,0 +1,203 @@ +/* + * Copyright 2023-2025 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.glue.javaParser; + +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toMap; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Function; +import java.util.regex.Pattern; +import javassist.ClassPool; + +import com.github.javaparser.JavaParser; +import com.github.javaparser.ast.CompilationUnit; +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.MarkerAnnotationExpr; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.NormalAnnotationExpr; +import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; +import com.github.javaparser.resolution.SymbolResolver; +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.types.ResolvedType; +import com.github.javaparser.symbolsolver.JavaSymbolSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.Lint; + +public class ExpandWildcardsFormatterFunc implements FormatterFunc.NeedsFile { + + private JavaParser parser; + static { + // If ClassPool is allowed to cache class files, it does not free the file-lock + ClassPool.cacheOpenedJarFile = false; + } + + public ExpandWildcardsFormatterFunc(Collection typeSolverClasspath) throws IOException { + this.parser = new JavaParser(); + + CombinedTypeSolver combinedTypeSolver = new CombinedTypeSolver(); + combinedTypeSolver.add(new ReflectionTypeSolver()); + for (File element : typeSolverClasspath) { + if (element.isFile()) { + combinedTypeSolver.add(new JarTypeSolver(element)); + } else if (element.isDirectory()) { + combinedTypeSolver.add(new JavaParserTypeSolver(element)); + } // gracefully ignore non-existing src-directories + } + + SymbolResolver symbolSolver = new JavaSymbolSolver(combinedTypeSolver); + parser.getParserConfiguration().setSymbolResolver(symbolSolver); + } + + @Override + public String applyWithFile(String rawUnix, File file) throws Exception { + Optional parseResult = parser.parse(rawUnix).getResult(); + if (!parseResult.isPresent()) { + return rawUnix; + } + CompilationUnit cu = parseResult.get(); + Map> importMap = findWildcardImports(cu) + .stream() + .collect(toMap(Function.identity(), + t -> new TreeSet<>(Comparator.comparing(ImportDeclaration::getNameAsString)))); + if (importMap.isEmpty()) { + // No wildcards found => do not change anything + return rawUnix; + } + + cu.accept(new CollectImportedTypesVisitor(), importMap); + for (var entry : importMap.entrySet()) { + String pattern = Pattern.quote(LineEnding.toUnix(entry.getKey().toString())); + String replacement = entry.getValue().stream().map(ImportDeclaration::toString).collect(joining()); + rawUnix = rawUnix.replaceAll(pattern, replacement); + } + + return rawUnix; + } + + private List findWildcardImports(CompilationUnit cu) { + List wildcardImports = new ArrayList<>(); + for (ImportDeclaration importDeclaration : cu.getImports()) { + if (importDeclaration.isAsterisk()) { + wildcardImports.add(importDeclaration); + } + } + return wildcardImports; + } + + private static final class CollectImportedTypesVisitor + extends VoidVisitorAdapter>> { + + @Override + public void visit(final ClassOrInterfaceType n, + final Map> importMap) { + // default imports + ResolvedType resolvedType = wrapUnsolvedSymbolException(n, ClassOrInterfaceType::resolve); + if (resolvedType.isReference()) { + matchTypeName(importMap, resolvedType.asReferenceType().getQualifiedName(), false); + } + super.visit(n, importMap); + } + + private void matchTypeName(Map> importMap, String qualifiedName, + boolean isStatic) { + for (var entry : importMap.entrySet()) { + if (entry.getKey().isStatic() == isStatic + && qualifiedName.startsWith(entry.getKey().getName().asString())) { + entry.getValue().add(new ImportDeclaration(qualifiedName, isStatic, false)); + break; + } + } + } + + @Override + public void visit(final MarkerAnnotationExpr n, + final Map> importMap) { + visitAnnotation(n, importMap); + super.visit(n, importMap); + } + + @Override + public void visit(final SingleMemberAnnotationExpr n, + final Map> importMap) { + visitAnnotation(n, importMap); + super.visit(n, importMap); + } + + @Override + public void visit(final NormalAnnotationExpr n, + final Map> importMap) { + visitAnnotation(n, importMap); + super.visit(n, importMap); + } + + private void visitAnnotation(final AnnotationExpr n, + final Map> importMap) { + ResolvedAnnotationDeclaration resolvedType = wrapUnsolvedSymbolException(n, AnnotationExpr::resolve); + matchTypeName(importMap, resolvedType.getQualifiedName(), false); + } + + @Override + public void visit(final MethodCallExpr n, final Map> importMap) { + // static imports + ResolvedMethodDeclaration resolved = wrapUnsolvedSymbolException(n, MethodCallExpr::resolve); + if (resolved.isStatic()) { + matchTypeName(importMap, resolved.getQualifiedName(), true); + } + super.visit(n, importMap); + } + + private static R wrapUnsolvedSymbolException(T node, Function func) { + try { + return func.apply(node); + } catch (UnsolvedSymbolException ex) { + if (node.getBegin().isPresent() && node.getEnd().isPresent()) { + throw Lint.atLineRange(node.getBegin().get().line, node.getEnd().get().line, "UnsolvedSymbolException", ex.getMessage()).shortcut(); + } + if (node.getBegin().isPresent()) { + throw Lint.atLine(node.getBegin().get().line, "UnsolvedSymbolException", ex.getMessage()).shortcut(); + } else if (node.getEnd().isPresent()) { + throw Lint.atLine(node.getEnd().get().line, "UnsolvedSymbolException", ex.getMessage()).shortcut(); + } else { + throw Lint.atUndefinedLine("UnsolvedSymbolException", ex.getMessage()).shortcut(); + } + } + } + + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java b/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java new file mode 100644 index 0000000000..a2e7a6b8d5 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java @@ -0,0 +1,82 @@ +/* + * Copyright 2025 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.java; + +import java.io.File; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Provisioner; + +public class ExpandWildcardImportsStep implements Serializable { + + private static final String INCOMPATIBLE_ERROR_MESSAGE = "There was a problem interacting with Java-Parser; maybe you set an incompatible version?"; + private static final String MAVEN_COORDINATES = "com.github.javaparser:javaparser-symbol-solver-core"; + public static final String DEFAULT_VERSION = "3.27.1"; + + private final Collection typeSolverClasspath; + private final JarState.Promised jarState; + + private ExpandWildcardImportsStep(Collection typeSolverClasspath, JarState.Promised jarState) { + this.typeSolverClasspath = typeSolverClasspath; + this.jarState = jarState; + } + + public static FormatterStep create(Set typeSolverClasspath, Provisioner provisioner) { + Objects.requireNonNull(provisioner, "provisioner cannot be null"); + return FormatterStep.create("expandwildcardimports", + new ExpandWildcardImportsStep(typeSolverClasspath, + JarState.promise(() -> JarState.from(MAVEN_COORDINATES + ":" + DEFAULT_VERSION, provisioner))), + ExpandWildcardImportsStep::equalityState, + State::toFormatter); + } + + private State equalityState() { + return new State(typeSolverClasspath, jarState.get()); + } + + private static class State implements Serializable { + + private final Collection typeSolverClasspath; + private final JarState jarState; + + public State(Collection typeSolverClasspath, JarState jarState) { + this.typeSolverClasspath = typeSolverClasspath; + this.jarState = jarState; + } + + FormatterFunc toFormatter() { + try { + Class formatterFunc = jarState.getClassLoader() + .loadClass("com.diffplug.spotless.glue.javaParser.ExpandWildcardsFormatterFunc"); + Constructor constructor = formatterFunc.getConstructor(Collection.class); + return (FormatterFunc) constructor.newInstance(typeSolverClasspath); + } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException + | InstantiationException | IllegalAccessException | NoClassDefFoundError cause) { + throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause); + } + } + + } + +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index ff792de588..61be32f5a2 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Added +- Add a `expandWildcardImports` API for java ([#2679](https://github.com/diffplug/spotless/issues/2594)) ## [8.1.0] - 2025-11-18 ### Changes diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 2ed91844c9..88d374513c 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -207,7 +207,7 @@ spotless { importOrderFile('eclipse-import-order.txt') // import order file as exported from eclipse removeUnusedImports() - forbidWildcardImports() + forbidWildcardImports() // or expandWildcardImports, see below forbidModuleImports() // Cleanthat will refactor your code, but it may break your style: apply it before your formatter @@ -259,6 +259,20 @@ spotless { } ``` +### expandWildcardImports + +This step expands all wildcard imports to single class imports. +To do this, [JavaParser](https://javaparser.org/) is used to parse the complete sourcecode and resolve the full qualified name of all used classes and static methods. +This operation can be resource intensive when formatting many source files, so you may want to change to `forbidWildcardImports` when your codebase is cleaned and stable. + +``` +spotless { + java { + expandWildcardImports() + } +} +``` + ### forbidModuleImports ``` diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java index f74761a019..3ba91b92ed 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java @@ -16,6 +16,7 @@ package com.diffplug.gradle.spotless; import static com.diffplug.gradle.spotless.PluginGradlePreconditions.requireElementsNonNull; +import static java.util.stream.Collectors.toSet; import java.io.File; import java.util.ArrayList; @@ -30,12 +31,15 @@ import javax.inject.Inject; import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.extra.java.EclipseJdtFormatterStep; import com.diffplug.spotless.generic.LicenseHeaderStep; import com.diffplug.spotless.java.CleanthatJavaStep; +import com.diffplug.spotless.java.ExpandWildcardImportsStep; import com.diffplug.spotless.java.ForbidModuleImportsStep; import com.diffplug.spotless.java.ForbidWildcardImportsStep; import com.diffplug.spotless.java.FormatAnnotationsStep; @@ -167,6 +171,13 @@ public void forbidModuleImports() { addStep(ForbidModuleImportsStep.create()); } + public void expandWildcardImports() { + SourceSetContainer sourceSets = getSourceSets(getProject(), "expansion of wildcards requires the 'java' plugin to be applied"); + Set typeSolverClasspath = sourceSets.stream().flatMap(s -> s.getAllJava().getSrcDirs().stream()).collect(toSet()); + getProject().getConfigurations().stream().filter(Configuration::isCanBeResolved).flatMap(c -> c.getFiles().stream()).forEach(typeSolverClasspath::add); + addStep(ExpandWildcardImportsStep.create(typeSolverClasspath, provisioner())); + } + /** Uses the google-java-format jar to format source code. */ public GoogleJavaFormatConfig googleJavaFormat() { return googleJavaFormat(GoogleJavaFormatStep.defaultVersion()); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JvmLang.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JvmLang.java index 99311deb1f..2a072a18aa 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JvmLang.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JvmLang.java @@ -29,13 +29,17 @@ interface JvmLang { - default FileCollection getSources(Project project, String message, Function sourceSetSourceDirectory, Spec filterSpec) { - FileCollection union = project.files(); + default SourceSetContainer getSourceSets(Project project, String message) { final JavaPluginExtension javaPluginExtension = project.getExtensions().findByType(JavaPluginExtension.class); if (javaPluginExtension == null) { throw new GradleException(message); } - final SourceSetContainer sourceSets = javaPluginExtension.getSourceSets(); + return javaPluginExtension.getSourceSets(); + } + + default FileCollection getSources(Project project, String message, Function sourceSetSourceDirectory, Spec filterSpec) { + FileCollection union = project.files(); + final SourceSetContainer sourceSets = getSourceSets(project, message); for (SourceSet sourceSet : sourceSets) { union = union.plus(sourceSetSourceDirectory.apply(sourceSet).filter(filterSpec)); } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavaDefaultTargetTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavaDefaultTargetTest.java index d24b5685c1..169ef2df6c 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavaDefaultTargetTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavaDefaultTargetTest.java @@ -16,6 +16,7 @@ package com.diffplug.gradle.spotless; import java.io.IOException; +import java.nio.file.Files; import org.junit.jupiter.api.Test; @@ -121,6 +122,39 @@ void forbidModuleImports() throws IOException { assertFile("test.java").sameAsResource("java/forbidmoduleimports/JavaCodeModuleImportsFormatted.test"); } + @Test + void expandWildCardImports() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "", + "repositories {", + " mavenCentral()", + " flatDir{dirs('libs')}", + "}", + "", + "dependencies {", + " implementation(':example-lib:1.0.0')", + "}", + "", + "spotless {", + " java {", + " target file('src/main/java/foo/bar/JavaCodeWildcardsUnformatted.java')", + " expandWildcardImports()", + " }", + "}"); + + newFile("libs").mkdirs(); + Files.write(newFile("libs/example-lib-1.0.0.jar").toPath(), getClass().getResourceAsStream("/java/expandwildcardimports/example-lib.jar").readAllBytes()); + setFile("src/main/java/foo/bar/AnotherClassInSamePackage.java").toResource("java/expandwildcardimports/AnotherClassInSamePackage.test"); + setFile("src/main/java/foo/bar/baz/AnotherImportedClass.java").toResource("java/expandwildcardimports/AnotherImportedClass.test"); + setFile("src/main/java/foo/bar/JavaCodeWildcardsUnformatted.java").toResource("java/expandwildcardimports/JavaClassWithWildcardsUnformatted.test"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/java/foo/bar/JavaCodeWildcardsUnformatted.java").sameAsResource("java/expandwildcardimports/JavaClassWithWildcardsFormatted.test"); + } + /** * Triggers the special case in {@link FormatExtension#setupTask(SpotlessTask)} with {@code toggleFence} and * {@code targetExcludeContentPattern} both being not {@code null}. diff --git a/testlib/src/main/resources/java/expandwildcardimports/AnotherClassInSamePackage.test b/testlib/src/main/resources/java/expandwildcardimports/AnotherClassInSamePackage.test new file mode 100644 index 0000000000..29bbfbccd0 --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/AnotherClassInSamePackage.test @@ -0,0 +1,3 @@ +package foo.bar; + +public class AnotherClassInSamePackage {} diff --git a/testlib/src/main/resources/java/expandwildcardimports/AnotherImportedClass.test b/testlib/src/main/resources/java/expandwildcardimports/AnotherImportedClass.test new file mode 100644 index 0000000000..40ec17d82c --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/AnotherImportedClass.test @@ -0,0 +1,3 @@ +package foo.bar.baz; + +public class AnotherImportedClass {} diff --git a/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsFormatted.test b/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsFormatted.test new file mode 100644 index 0000000000..7cfe130c14 --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsFormatted.test @@ -0,0 +1,35 @@ +package foo.bar; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import foo.bar.baz.AnotherImportedClass; +import org.example.SomeAnnotation; + +import static java.util.stream.Collectors.toList; + +@SomeAnnotation +public class JavaClassWithWildcards { + + private List prop; // This is a comment that should not be removed + + public JavaClassWithWildcards(Map param) { + // Another comment + Collection localVariable = param.values(); + localVariable.stream().collect(toList()); + } + + + + /** + * Some JavaDoc + */ + public Optional testMethod() { + AnotherClassInSamePackage test1 = null; + AnotherImportedClass test2 = null; + return Optional.empty(); + } + +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsUnformatted.test b/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsUnformatted.test new file mode 100644 index 0000000000..bc21e61da8 --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsUnformatted.test @@ -0,0 +1,32 @@ +package foo.bar; + +import java.util.*; +import foo.bar.baz.*; +import org.example.*; +import java.io.*; + +import static java.util.stream.Collectors.*; + +@SomeAnnotation +public class JavaClassWithWildcards { + + private List prop; // This is a comment that should not be removed + + public JavaClassWithWildcards(Map param) { + // Another comment + Collection localVariable = param.values(); + localVariable.stream().collect(toList()); + } + + + + /** + * Some JavaDoc + */ + public Optional testMethod() { + AnotherClassInSamePackage test1 = null; + AnotherImportedClass test2 = null; + return Optional.empty(); + } + +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/example-lib.jar b/testlib/src/main/resources/java/expandwildcardimports/example-lib.jar new file mode 100644 index 0000000000000000000000000000000000000000..7e0764acc7831dd74b688d11304993672534af80 GIT binary patch literal 758 zcmWIWW@Zs#VBp|jU|?_rVg?2#Fv-Bc38Z~pLmYKI{oM4K83IrgZmMT`Ck<2}3B*9{ z2!+0mex7cw!6ACSZl8V6oc8h7)w{^+t*dqJ%=yhh23L$9Jbm<(3C%E|ez>hHKz4pn zI);VFYIvY(QY#X33vyC1)M^+Ue!CK=<~a~6ps5Yc&rNmA%gZlGEXmBz(@V}tEG`c1 zH{?3xAmI96;&E0;z?MxLg>&yPN3b68+HivN+J?54v+Iude#jA%JK)@zoV-EeM}0;8 zkIz;1^B9tPSgl%GR~xN-ziN+^u5kFP8&S`vo}VlFLP)JOYl_LKv`-~^C9hbkT2gn1 z+Q)4Rbh@DtoPDTfujRIaN2~9oSs&Vx`u*3UtjC-RE}g4ahsYNlzId~K?%VHYx@K@y zIQ?tfG2dbS-Ga7@A@4_zC2 Date: Thu, 20 Nov 2025 10:17:26 -0800 Subject: [PATCH 2/6] Remove error-prone from our build. --- build.gradle | 1 - gradle/error-prone.gradle | 50 --------------------------------------- 2 files changed, 51 deletions(-) delete mode 100644 gradle/error-prone.gradle diff --git a/build.gradle b/build.gradle index a13a8bdb9c..3f64ed8381 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,6 @@ repositories { apply from: rootProject.file('gradle/java-publish.gradle') apply from: rootProject.file('gradle/changelog.gradle') allprojects { - apply from: rootProject.file('gradle/error-prone.gradle') apply from: rootProject.file('gradle/rewrite.gradle') apply from: rootProject.file('gradle/spotless.gradle') } diff --git a/gradle/error-prone.gradle b/gradle/error-prone.gradle deleted file mode 100644 index a3aff16cfa..0000000000 --- a/gradle/error-prone.gradle +++ /dev/null @@ -1,50 +0,0 @@ -apply plugin: 'net.ltgt.errorprone' - -dependencies { - errorprone('com.google.errorprone:error_prone_core:2.42.0') - errorprone('tech.picnic.error-prone-support:error-prone-contrib:0.26.0') -} - -tasks.withType(JavaCompile).configureEach { - options.errorprone { - disable( // consider fix, or reasoning. - 'FunctionalInterfaceMethodChanged', - 'JavaxInjectOnAbstractMethod', - 'OverridesJavaxInjectableMethod', - ) - error( - 'AmbiguousJsonCreator', - 'AssertJNullnessAssertion', - 'AutowiredConstructor', - 'CanonicalAnnotationSyntax', - 'CollectorMutability', - 'ConstantNaming', - 'DirectReturn', - 'EmptyMethod', - 'ExplicitArgumentEnumeration', - 'ExplicitEnumOrdering', - 'IdentityConversion', - 'ImmutablesSortedSetComparator', - 'IsInstanceLambdaUsage', - 'MockitoMockClassReference', - 'MockitoStubbing', - 'NestedOptionals', - 'PrimitiveComparison', - 'RedundantStringConversion', - 'RedundantStringEscape', - 'ReturnValueIgnored', - 'SelfAssignment', - 'StringJoin', - 'StringJoining', - 'UnnecessarilyFullyQualified', - 'UnnecessaryLambda', - ) - // bug: this only happens when the file is dirty. - // might be an up2date (caching) issue, as file is currently in corrupt state. - // ForbidGradleInternal(import org.gradle.api.internal.project.ProjectInternal;) - errorproneArgs.add('-XepExcludedPaths:' + - '.*/SelfTest.java|' + - '.*/GradleIntegrationHarness.java' - ) - } -} From 9085e20c51579dc78d243c1358871c48b6282d36 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 20 Nov 2025 10:25:36 -0800 Subject: [PATCH 3/6] Add missing serialVersionUID. --- .../com/diffplug/spotless/java/ExpandWildcardImportsStep.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java b/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java index a2e7a6b8d5..7a3521069c 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java +++ b/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java @@ -29,6 +29,7 @@ import com.diffplug.spotless.Provisioner; public class ExpandWildcardImportsStep implements Serializable { + private static final long serialVersionUID = 1L; private static final String INCOMPATIBLE_ERROR_MESSAGE = "There was a problem interacting with Java-Parser; maybe you set an incompatible version?"; private static final String MAVEN_COORDINATES = "com.github.javaparser:javaparser-symbol-solver-core"; @@ -56,6 +57,7 @@ private State equalityState() { } private static class State implements Serializable { + private static final long serialVersionUID = 1L; private final Collection typeSolverClasspath; private final JarState jarState; From c5febe3c49fdfd37f8dd72155e712b2eba1a623e Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 20 Nov 2025 14:18:30 -0800 Subject: [PATCH 4/6] rewriteRun --- .../glue/javaParser/ExpandWildcardsFormatterFunc.java | 6 +++--- .../diffplug/spotless/java/ExpandWildcardImportsStep.java | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) 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 2b6a5df469..a084bf46a4 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 @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless.glue.javaParser; +package com.diffplug.spotless.glue.javaparser; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toMap; @@ -60,7 +60,7 @@ public class ExpandWildcardsFormatterFunc implements FormatterFunc.NeedsFile { - private JavaParser parser; + private final JavaParser parser; static { // If ClassPool is allowed to cache class files, it does not free the file-lock ClassPool.cacheOpenedJarFile = false; @@ -86,7 +86,7 @@ public ExpandWildcardsFormatterFunc(Collection typeSolverClasspath) throws @Override public String applyWithFile(String rawUnix, File file) throws Exception { Optional parseResult = parser.parse(rawUnix).getResult(); - if (!parseResult.isPresent()) { + if (parseResult.isEmpty()) { return rawUnix; } CompilationUnit cu = parseResult.get(); diff --git a/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java b/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java index 7a3521069c..78aa67501d 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java +++ b/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java @@ -16,6 +16,7 @@ package com.diffplug.spotless.java; import java.io.File; +import java.io.Serial; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -28,7 +29,8 @@ import com.diffplug.spotless.JarState; import com.diffplug.spotless.Provisioner; -public class ExpandWildcardImportsStep implements Serializable { +public final class ExpandWildcardImportsStep implements Serializable { + @Serial private static final long serialVersionUID = 1L; private static final String INCOMPATIBLE_ERROR_MESSAGE = "There was a problem interacting with Java-Parser; maybe you set an incompatible version?"; @@ -57,6 +59,7 @@ private State equalityState() { } private static class State implements Serializable { + @Serial private static final long serialVersionUID = 1L; private final Collection typeSolverClasspath; From a7e043c13ee37452d0d7d762b52ccac1be886991 Mon Sep 17 00:00:00 2001 From: Christian Grubert Date: Sat, 22 Nov 2025 14:51:46 +0100 Subject: [PATCH 5/6] add ExpandWildcardImportsStep to feature matrix --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 064509f3f5..824082f7e5 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ lib('java.GoogleJavaFormatStep') +'{{yes}} | {{yes}} lib('java.ImportOrderStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', lib('java.PalantirJavaFormatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('java.RemoveUnusedImportsStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', +lib('java.ExpandWildcardImportsStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |', lib('java.ForbidWildcardImportsStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', lib('java.ForbidModuleImportsStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', extra('java.EclipseJdtFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', @@ -142,6 +143,7 @@ lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}} | [`java.ImportOrderStep`](lib/src/main/java/com/diffplug/spotless/java/ImportOrderStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`java.PalantirJavaFormatStep`](lib/src/main/java/com/diffplug/spotless/java/PalantirJavaFormatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`java.RemoveUnusedImportsStep`](lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java) | :+1: | :+1: | :+1: | :white_large_square: | +| [`java.ExpandWildcardImportsStep`](lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | | [`java.ForbidWildcardImportsStep`](lib/src/main/java/com/diffplug/spotless/java/ForbidWildcardImportsStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`java.ForbidModuleImportsStep`](lib/src/main/java/com/diffplug/spotless/java/ForbidModuleImportsStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`java.EclipseJdtFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: | From bf766433a9ffc90a177362caabc5aaf30633218b Mon Sep 17 00:00:00 2001 From: Christian Grubert Date: Sat, 22 Nov 2025 15:09:03 +0100 Subject: [PATCH 6/6] prevent false resolving of wildcard subpackages --- .../glue/javaParser/ExpandWildcardsFormatterFunc.java | 8 +++++++- .../diffplug/spotless/java/ExpandWildcardImportsStep.java | 2 +- .../JavaClassWithWildcardsFormatted.test | 3 ++- .../JavaClassWithWildcardsUnformatted.test | 3 ++- 4 files changed, 12 insertions(+), 4 deletions(-) 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 a084bf46a4..3e077cc2e4 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 @@ -135,9 +135,15 @@ public void visit(final ClassOrInterfaceType n, private void matchTypeName(Map> importMap, String qualifiedName, boolean isStatic) { + int lastDot = qualifiedName.lastIndexOf('.'); + if (lastDot < 0) { + return; + } + + String packageName = qualifiedName.substring(0, lastDot); for (var entry : importMap.entrySet()) { if (entry.getKey().isStatic() == isStatic - && qualifiedName.startsWith(entry.getKey().getName().asString())) { + && packageName.equals(entry.getKey().getName().asString())) { entry.getValue().add(new ImportDeclaration(qualifiedName, isStatic, false)); break; } diff --git a/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java b/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java index 78aa67501d..4556ff02bf 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java +++ b/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java @@ -73,7 +73,7 @@ public State(Collection typeSolverClasspath, JarState jarState) { FormatterFunc toFormatter() { try { Class formatterFunc = jarState.getClassLoader() - .loadClass("com.diffplug.spotless.glue.javaParser.ExpandWildcardsFormatterFunc"); + .loadClass("com.diffplug.spotless.glue.javaparser.ExpandWildcardsFormatterFunc"); Constructor constructor = formatterFunc.getConstructor(Collection.class); return (FormatterFunc) constructor.newInstance(typeSolverClasspath); } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException diff --git a/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsFormatted.test b/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsFormatted.test index 7cfe130c14..b2b19c3c6e 100644 --- a/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsFormatted.test +++ b/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsFormatted.test @@ -5,6 +5,7 @@ import java.util.Date; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.Callable; import foo.bar.baz.AnotherImportedClass; import org.example.SomeAnnotation; @@ -26,7 +27,7 @@ public class JavaClassWithWildcards { /** * Some JavaDoc */ - public Optional testMethod() { + public Optional testMethod(Callable callable) { AnotherClassInSamePackage test1 = null; AnotherImportedClass test2 = null; return Optional.empty(); diff --git a/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsUnformatted.test b/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsUnformatted.test index bc21e61da8..f1aab02c1d 100644 --- a/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsUnformatted.test +++ b/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsUnformatted.test @@ -1,6 +1,7 @@ package foo.bar; import java.util.*; +import java.util.concurrent.Callable; import foo.bar.baz.*; import org.example.*; import java.io.*; @@ -23,7 +24,7 @@ public class JavaClassWithWildcards { /** * Some JavaDoc */ - public Optional testMethod() { + public Optional testMethod(Callable callable) { AnotherClassInSamePackage test1 = null; AnotherImportedClass test2 = null; return Optional.empty();