From 08b02f752fdf99b458e8da374fd3df226fbe0802 Mon Sep 17 00:00:00 2001 From: Vincent Potucek Date: Wed, 3 Dec 2025 16:22:26 +0100 Subject: [PATCH] new step to expand java wildcard imports #2744 #2594 --- CHANGES.md | 5 +- README.md | 2 + gradle/spotless.gradle | 13 +- lib/build.gradle | 3 + .../parser/ExpandWildcardsFormatterFunc.java | 209 ++++++++++++++++++ .../java/ExpandWildcardImportsStep.java | 87 ++++++++ .../java/ForbidWildcardImportsStep.java | 7 +- plugin-gradle/CHANGES.md | 5 +- plugin-gradle/README.md | 16 +- .../gradle/spotless/JavaExtension.java | 12 + .../com/diffplug/gradle/spotless/JvmLang.java | 11 +- .../spotless/JavaDefaultTargetTest.java | 34 +++ .../AnotherClassInSamePackage.test | 3 + .../AnotherImportedClass.test | 3 + .../JavaClassWithWildcardsFormatted.test | 36 +++ .../JavaClassWithWildcardsUnformatted.test | 33 +++ .../expandwildcardimports/example-lib.jar | Bin 0 -> 758 bytes .../java/ExpandWildcardImportsStepTest.java | 42 ++++ 18 files changed, 505 insertions(+), 16 deletions(-) create mode 100644 lib/src/javaParser/java/com/diffplug/spotless/glue/java/parser/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 d3bb7eacf1..c092ced4b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,10 +11,11 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Added +- Add a `expandWildcardImports` API for java ([#2679](https://github.com/diffplug/spotless/issues/2594)) - Add the ability to specify a wildcard version (`*`) for external formatter executables. ([#2757](https://github.com/diffplug/spotless/issues/2757)) ### Changes -* Bump default `ktlint` version to latest `1.7.1` -> `1.8.0`. ([2763](https://github.com/diffplug/spotless/pull/2763)) -* Bump default `gherkin-utils` version to latest `9.2.0` -> `10.0.0`. ([#2619](https://github.com/diffplug/spotless/pull/2619)) +- Bump default `ktlint` version to latest `1.7.1` -> `1.8.0`. ([2763](https://github.com/diffplug/spotless/pull/2763)) +- Bump default `gherkin-utils` version to latest `9.2.0` -> `10.0.0`. ([#2619](https://github.com/diffplug/spotless/pull/2619)) ## [4.1.0] - 2025-11-18 ### Changes 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: | diff --git a/gradle/spotless.gradle b/gradle/spotless.gradle index 4202d3e62b..3eb431af87 100644 --- a/gradle/spotless.gradle +++ b/gradle/spotless.gradle @@ -3,16 +3,17 @@ spotless { if (project != rootProject) { // the rootProject doesn't have any java java { - ratchetFrom 'origin/main' bumpThisNumberIfACustomStepChanges(1) - licenseHeaderFile rootProject.file('gradle/spotless.license') - importOrderFile rootProject.file('gradle/spotless.importorder') eclipse().configFile rootProject.file('gradle/spotless.eclipseformat.xml') - trimTrailingWhitespace() - removeUnusedImports() - formatAnnotations() + // expandWildcardImports() todo: use after release (No signature of method: com.diffplug.gradle.spotless.JavaExtension.expandWildcardImports() is applicable for argument types: () values: []) forbidWildcardImports() forbidRegex('ForbidGradleInternal', 'import org\\.gradle\\.api\\.internal\\.(.*)', "Don't use Gradle's internal API") + formatAnnotations() + importOrderFile rootProject.file('gradle/spotless.importorder') + licenseHeaderFile rootProject.file('gradle/spotless.license') + ratchetFrom 'origin/main' + removeUnusedImports() + trimTrailingWhitespace() } } groovyGradle { diff --git a/lib/build.gradle b/lib/build.gradle index 84b84e157e..b8575984e1 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/java/parser/ExpandWildcardsFormatterFunc.java b/lib/src/javaParser/java/com/diffplug/spotless/glue/java/parser/ExpandWildcardsFormatterFunc.java new file mode 100644 index 0000000000..07f4338054 --- /dev/null +++ b/lib/src/javaParser/java/com/diffplug/spotless/glue/java/parser/ExpandWildcardsFormatterFunc.java @@ -0,0 +1,209 @@ +/* + * 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.java.parser; + +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 final 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.isEmpty()) { + 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) { + int lastDot = qualifiedName.lastIndexOf('.'); + if (lastDot < 0) { + return; + } + + String packageName = qualifiedName.substring(0, lastDot); + for (var entry : importMap.entrySet()) { + if (entry.getKey().isStatic() == isStatic + && packageName.equals(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..1b29ef14b7 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java @@ -0,0 +1,87 @@ +/* + * 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.Serial; +import java.io.Serializable; +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 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?"; + 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 { + @Serial + private static final long serialVersionUID = 1L; + + private final Collection typeSolverClasspath; + private final JarState jarState; + + public State(Collection typeSolverClasspath, JarState jarState) { + this.typeSolverClasspath = typeSolverClasspath; + this.jarState = jarState; + } + + FormatterFunc toFormatter() { + try { + return (FormatterFunc) jarState + .getClassLoader() + .loadClass("com.diffplug.spotless.glue.java.parser.ExpandWildcardsFormatterFunc") + .getConstructor(Collection.class) + .newInstance(typeSolverClasspath); + } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException + | InstantiationException | IllegalAccessException | NoClassDefFoundError cause) { + throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause); + } + } + + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/java/ForbidWildcardImportsStep.java b/lib/src/main/java/com/diffplug/spotless/java/ForbidWildcardImportsStep.java index 71116768f6..646172d901 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/ForbidWildcardImportsStep.java +++ b/lib/src/main/java/com/diffplug/spotless/java/ForbidWildcardImportsStep.java @@ -18,7 +18,12 @@ import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.generic.ReplaceRegexStep; -/** Forbids any wildcard import statements. */ +/** + * Forbids any wildcard import statements. + * + * @deprecated Use {@link ExpandWildcardImportsStep} + */ +@Deprecated(forRemoval = true) public final class ForbidWildcardImportsStep { /** diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index b9de11b3a1..9ba52b275d 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -4,12 +4,13 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Added +- Add a `expandWildcardImports` API for java ([#2679](https://github.com/diffplug/spotless/issues/2594)) - Add the ability to specify a wildcard version (`*`) for external formatter executables. ([#2757](https://github.com/diffplug/spotless/issues/2757)) ### Fixed - [fix] `NPE` due to workingTreeIterator being null for git ignored files. #911 ([#2771](https://github.com/diffplug/spotless/issues/2771)) ### Changes -* Bump default `ktlint` version to latest `1.7.1` -> `1.8.0`. ([2763](https://github.com/diffplug/spotless/pull/2763)) -* Bump default `gherkin-utils` version to latest `9.2.0` -> `10.0.0`. ([#2619](https://github.com/diffplug/spotless/pull/2619)) +- Bump default `ktlint` version to latest `1.7.1` -> `1.8.0`. ([2763](https://github.com/diffplug/spotless/pull/2763)) +- Bump default `gherkin-utils` version to latest `9.2.0` -> `10.0.0`. ([#2619](https://github.com/diffplug/spotless/pull/2619)) ## [8.1.0] - 2025-11-18 ### Changes diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index fe808a043c..9c9722c770 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() // @Deprecated; use 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..5cc7352054 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,14 @@ import javax.inject.Inject; import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; import org.gradle.api.tasks.SourceSet; 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 +170,15 @@ public void forbidModuleImports() { addStep(ForbidModuleImportsStep.create()); } + public void expandWildcardImports() { + expandWildcardImportsOn(getSourceSets(getProject(), "expansion of wildcards requires the 'java' plugin to be applied").stream().flatMap(s -> s.getAllJava().getSrcDirs().stream()).collect(toSet())); + } + + private void expandWildcardImportsOn(final Set typeSolverClasspath) { + 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..b0379fc0e6 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,14 +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(); - for (SourceSet sourceSet : sourceSets) { + return javaPluginExtension.getSourceSets(); + } + + default FileCollection getSources(Project project, String message, Function sourceSetSourceDirectory, Spec filterSpec) { + FileCollection union = project.files(); + for (SourceSet sourceSet : getSourceSets(project, message)) { union = union.plus(sourceSetSourceDirectory.apply(sourceSet).filter(filterSpec)); } return union; 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..b2b19c3c6e --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsFormatted.test @@ -0,0 +1,36 @@ +package foo.bar; + +import java.util.Collection; +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; + +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(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 new file mode 100644 index 0000000000..f1aab02c1d --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/JavaClassWithWildcardsUnformatted.test @@ -0,0 +1,33 @@ +package foo.bar; + +import java.util.*; +import java.util.concurrent.Callable; +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(Callable callable) { + 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