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/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/build.gradle b/build.gradle index 0c05e307ce..074657924a 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,6 @@ apply from: rootProject.file('gradle/java-publish.gradle') apply from: rootProject.file('gradle/changelog.gradle') apply from: rootProject.file('gradle/rewrite.gradle') allprojects { - apply from: rootProject.file('gradle/error-prone.gradle') apply from: rootProject.file('gradle/spotless.gradle') } apply from: rootProject.file('gradle/spotless-freshmark.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' - ) - } -} 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..3e077cc2e4 --- /dev/null +++ b/lib/src/javaParser/java/com/diffplug/spotless/glue/javaParser/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.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 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..4556ff02bf --- /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.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 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 { + 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..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 0000000000..7e0764acc7 Binary files /dev/null and b/testlib/src/main/resources/java/expandwildcardimports/example-lib.jar differ diff --git a/testlib/src/test/java/com/diffplug/spotless/java/ExpandWildcardImportsStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/ExpandWildcardImportsStepTest.java new file mode 100644 index 0000000000..81862e9c2f --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/java/ExpandWildcardImportsStepTest.java @@ -0,0 +1,42 @@ +/* + * 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.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.StepHarnessWithFile; +import com.diffplug.spotless.TestProvisioner; + +public class ExpandWildcardImportsStepTest extends ResourceHarness { + + @Test + void expandWildCardImports() throws Exception { + newFile("src/foo/bar/baz/").mkdirs(); + Files.write(newFile("src/foo/bar/AnotherClassInSamePackage.java").toPath(), getTestResource("java/expandwildcardimports/AnotherClassInSamePackage.test").getBytes(StandardCharsets.UTF_8)); + Files.write(newFile("src/foo/bar/baz/AnotherImportedClass.java").toPath(), getTestResource("java/expandwildcardimports/AnotherImportedClass.test").getBytes(StandardCharsets.UTF_8)); + File dummyJar = new File(ResourceHarness.class.getResource("/java/expandwildcardimports/example-lib.jar").toURI()); + FormatterStep step = ExpandWildcardImportsStep.create(Set.of(newFile("src"), dummyJar), TestProvisioner.mavenCentral()); + StepHarnessWithFile.forStep(this, step).testResource("java/expandwildcardimports/JavaClassWithWildcardsUnformatted.test", "java/expandwildcardimports/JavaClassWithWildcardsFormatted.test"); + } + +}