Skip to content

Commit 66b5758

Browse files
committed
new step to expand java wildcard imports
1 parent b39368a commit 66b5758

File tree

15 files changed

+474
-4
lines changed

15 files changed

+474
-4
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This document is intended for Spotless developers.
1010
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
1111

1212
## [Unreleased]
13+
### Added
14+
- Add a `expandWildcardImports` API for java ([#2679](https://github.com/diffplug/spotless/issues/2594))
1315

1416
## [4.1.0] - 2025-11-18
1517
### Changes

lib/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def NEEDS_GLUE = [
1818
'googleJavaFormat',
1919
'gson',
2020
'jackson',
21+
'javaParser',
2122
'ktfmt',
2223
'ktlint',
2324
'palantirJavaFormat',
@@ -100,6 +101,8 @@ dependencies {
100101
String VER_JACKSON='2.20.1'
101102
jacksonCompileOnly "com.fasterxml.jackson.core:jackson-databind:$VER_JACKSON"
102103
jacksonCompileOnly "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$VER_JACKSON"
104+
// javaParser
105+
javaParserCompileOnly "com.github.javaparser:javaparser-symbol-solver-core:3.27.1"
103106
// ktfmt
104107
ktfmtCompileOnly "com.facebook:ktfmt:0.59"
105108
ktfmtCompileOnly("com.google.googlejavaformat:google-java-format") {
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
* Copyright 2023-2025 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.glue.javaParser;
17+
18+
import static java.util.stream.Collectors.joining;
19+
import static java.util.stream.Collectors.toMap;
20+
21+
import java.io.File;
22+
import java.io.IOException;
23+
import java.util.ArrayList;
24+
import java.util.Collection;
25+
import java.util.Comparator;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.Optional;
29+
import java.util.Set;
30+
import java.util.TreeSet;
31+
import java.util.function.Function;
32+
import java.util.regex.Pattern;
33+
import javassist.ClassPool;
34+
35+
import com.github.javaparser.JavaParser;
36+
import com.github.javaparser.ast.CompilationUnit;
37+
import com.github.javaparser.ast.ImportDeclaration;
38+
import com.github.javaparser.ast.Node;
39+
import com.github.javaparser.ast.expr.AnnotationExpr;
40+
import com.github.javaparser.ast.expr.MarkerAnnotationExpr;
41+
import com.github.javaparser.ast.expr.MethodCallExpr;
42+
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
43+
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
44+
import com.github.javaparser.ast.type.ClassOrInterfaceType;
45+
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
46+
import com.github.javaparser.resolution.SymbolResolver;
47+
import com.github.javaparser.resolution.UnsolvedSymbolException;
48+
import com.github.javaparser.resolution.declarations.ResolvedAnnotationDeclaration;
49+
import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration;
50+
import com.github.javaparser.resolution.types.ResolvedType;
51+
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
52+
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
53+
import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver;
54+
import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver;
55+
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
56+
57+
import com.diffplug.spotless.FormatterFunc;
58+
import com.diffplug.spotless.LineEnding;
59+
import com.diffplug.spotless.Lint;
60+
61+
public class ExpandWildcardsFormatterFunc implements FormatterFunc.NeedsFile {
62+
63+
private JavaParser parser;
64+
static {
65+
// If ClassPool is allowed to cache class files, it does not free the file-lock
66+
ClassPool.cacheOpenedJarFile = false;
67+
}
68+
69+
public ExpandWildcardsFormatterFunc(Collection<File> typeSolverClasspath) throws IOException {
70+
this.parser = new JavaParser();
71+
72+
CombinedTypeSolver combinedTypeSolver = new CombinedTypeSolver();
73+
combinedTypeSolver.add(new ReflectionTypeSolver());
74+
for (File element : typeSolverClasspath) {
75+
if (element.isFile()) {
76+
combinedTypeSolver.add(new JarTypeSolver(element));
77+
} else if (element.isDirectory()) {
78+
combinedTypeSolver.add(new JavaParserTypeSolver(element));
79+
} // gracefully ignore non-existing src-directories
80+
}
81+
82+
SymbolResolver symbolSolver = new JavaSymbolSolver(combinedTypeSolver);
83+
parser.getParserConfiguration().setSymbolResolver(symbolSolver);
84+
}
85+
86+
@Override
87+
public String applyWithFile(String rawUnix, File file) throws Exception {
88+
Optional<CompilationUnit> parseResult = parser.parse(rawUnix).getResult();
89+
if (!parseResult.isPresent()) {
90+
return rawUnix;
91+
}
92+
CompilationUnit cu = parseResult.get();
93+
Map<ImportDeclaration, Set<ImportDeclaration>> importMap = findWildcardImports(cu)
94+
.stream()
95+
.collect(toMap(Function.identity(),
96+
t -> new TreeSet<>(Comparator.comparing(ImportDeclaration::getNameAsString))));
97+
if (importMap.isEmpty()) {
98+
// No wildcards found => do not change anything
99+
return rawUnix;
100+
}
101+
102+
cu.accept(new CollectImportedTypesVisitor(), importMap);
103+
for (var entry : importMap.entrySet()) {
104+
String pattern = Pattern.quote(LineEnding.toUnix(entry.getKey().toString()));
105+
String replacement = entry.getValue().stream().map(ImportDeclaration::toString).collect(joining());
106+
rawUnix = rawUnix.replaceAll(pattern, replacement);
107+
}
108+
109+
return rawUnix;
110+
}
111+
112+
private List<ImportDeclaration> findWildcardImports(CompilationUnit cu) {
113+
List<ImportDeclaration> wildcardImports = new ArrayList<>();
114+
for (ImportDeclaration importDeclaration : cu.getImports()) {
115+
if (importDeclaration.isAsterisk()) {
116+
wildcardImports.add(importDeclaration);
117+
}
118+
}
119+
return wildcardImports;
120+
}
121+
122+
private static final class CollectImportedTypesVisitor
123+
extends VoidVisitorAdapter<Map<ImportDeclaration, Set<ImportDeclaration>>> {
124+
125+
@Override
126+
public void visit(final ClassOrInterfaceType n,
127+
final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
128+
// default imports
129+
ResolvedType resolvedType = wrapUnsolvedSymbolException(n, ClassOrInterfaceType::resolve);
130+
if (resolvedType.isReference()) {
131+
matchTypeName(importMap, resolvedType.asReferenceType().getQualifiedName(), false);
132+
}
133+
super.visit(n, importMap);
134+
}
135+
136+
private void matchTypeName(Map<ImportDeclaration, Set<ImportDeclaration>> importMap, String qualifiedName,
137+
boolean isStatic) {
138+
for (var entry : importMap.entrySet()) {
139+
if (entry.getKey().isStatic() == isStatic
140+
&& qualifiedName.startsWith(entry.getKey().getName().asString())) {
141+
entry.getValue().add(new ImportDeclaration(qualifiedName, isStatic, false));
142+
break;
143+
}
144+
}
145+
}
146+
147+
@Override
148+
public void visit(final MarkerAnnotationExpr n,
149+
final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
150+
visitAnnotation(n, importMap);
151+
super.visit(n, importMap);
152+
}
153+
154+
@Override
155+
public void visit(final SingleMemberAnnotationExpr n,
156+
final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
157+
visitAnnotation(n, importMap);
158+
super.visit(n, importMap);
159+
}
160+
161+
@Override
162+
public void visit(final NormalAnnotationExpr n,
163+
final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
164+
visitAnnotation(n, importMap);
165+
super.visit(n, importMap);
166+
}
167+
168+
private void visitAnnotation(final AnnotationExpr n,
169+
final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
170+
ResolvedAnnotationDeclaration resolvedType = wrapUnsolvedSymbolException(n, AnnotationExpr::resolve);
171+
matchTypeName(importMap, resolvedType.getQualifiedName(), false);
172+
}
173+
174+
@Override
175+
public void visit(final MethodCallExpr n, final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
176+
// static imports
177+
ResolvedMethodDeclaration resolved = wrapUnsolvedSymbolException(n, MethodCallExpr::resolve);
178+
if (resolved.isStatic()) {
179+
matchTypeName(importMap, resolved.getQualifiedName(), true);
180+
}
181+
super.visit(n, importMap);
182+
}
183+
184+
private static <T extends Node, R> R wrapUnsolvedSymbolException(T node, Function<T, R> func) {
185+
try {
186+
return func.apply(node);
187+
} catch (UnsolvedSymbolException ex) {
188+
if (node.getBegin().isPresent() && node.getEnd().isPresent()) {
189+
throw Lint.atLineRange(node.getBegin().get().line, node.getEnd().get().line, "UnsolvedSymbolException", ex.getMessage()).shortcut();
190+
}
191+
if (node.getBegin().isPresent()) {
192+
throw Lint.atLine(node.getBegin().get().line, "UnsolvedSymbolException", ex.getMessage()).shortcut();
193+
} else if (node.getEnd().isPresent()) {
194+
throw Lint.atLine(node.getEnd().get().line, "UnsolvedSymbolException", ex.getMessage()).shortcut();
195+
} else {
196+
throw Lint.atUndefinedLine("UnsolvedSymbolException", ex.getMessage()).shortcut();
197+
}
198+
}
199+
}
200+
201+
}
202+
203+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2025 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.java;
17+
18+
import java.io.File;
19+
import java.io.Serializable;
20+
import java.lang.reflect.Constructor;
21+
import java.lang.reflect.InvocationTargetException;
22+
import java.util.Collection;
23+
import java.util.Objects;
24+
import java.util.Set;
25+
26+
import com.diffplug.spotless.FormatterFunc;
27+
import com.diffplug.spotless.FormatterStep;
28+
import com.diffplug.spotless.JarState;
29+
import com.diffplug.spotless.Provisioner;
30+
31+
public class ExpandWildcardImportsStep implements Serializable {
32+
33+
private static final String INCOMPATIBLE_ERROR_MESSAGE = "There was a problem interacting with Java-Parser; maybe you set an incompatible version?";
34+
private static final String MAVEN_COORDINATES = "com.github.javaparser:javaparser-symbol-solver-core";
35+
public static final String DEFAULT_VERSION = "3.27.1";
36+
37+
private final Collection<File> typeSolverClasspath;
38+
private final JarState.Promised jarState;
39+
40+
private ExpandWildcardImportsStep(Collection<File> typeSolverClasspath, JarState.Promised jarState) {
41+
this.typeSolverClasspath = typeSolverClasspath;
42+
this.jarState = jarState;
43+
}
44+
45+
public static FormatterStep create(Set<File> typeSolverClasspath, Provisioner provisioner) {
46+
Objects.requireNonNull(provisioner, "provisioner cannot be null");
47+
return FormatterStep.create("expandwildcardimports",
48+
new ExpandWildcardImportsStep(typeSolverClasspath,
49+
JarState.promise(() -> JarState.from(MAVEN_COORDINATES + ":" + DEFAULT_VERSION, provisioner))),
50+
ExpandWildcardImportsStep::equalityState,
51+
State::toFormatter);
52+
}
53+
54+
private State equalityState() {
55+
return new State(typeSolverClasspath, jarState.get());
56+
}
57+
58+
private static class State implements Serializable {
59+
60+
private final Collection<File> typeSolverClasspath;
61+
private final JarState jarState;
62+
63+
public State(Collection<File> typeSolverClasspath, JarState jarState) {
64+
this.typeSolverClasspath = typeSolverClasspath;
65+
this.jarState = jarState;
66+
}
67+
68+
FormatterFunc toFormatter() {
69+
try {
70+
Class<?> formatterFunc = jarState.getClassLoader()
71+
.loadClass("com.diffplug.spotless.glue.javaParser.ExpandWildcardsFormatterFunc");
72+
Constructor<?> constructor = formatterFunc.getConstructor(Collection.class);
73+
return (FormatterFunc) constructor.newInstance(typeSolverClasspath);
74+
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException
75+
| InstantiationException | IllegalAccessException | NoClassDefFoundError cause) {
76+
throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
77+
}
78+
}
79+
80+
}
81+
82+
}

plugin-gradle/CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).
44

55
## [Unreleased]
6+
### Added
7+
- Add a `expandWildcardImports` API for java ([#2679](https://github.com/diffplug/spotless/issues/2594))
68

79
## [8.1.0] - 2025-11-18
810
### Changes

plugin-gradle/README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ spotless {
207207
importOrderFile('eclipse-import-order.txt') // import order file as exported from eclipse
208208
209209
removeUnusedImports()
210-
forbidWildcardImports()
210+
forbidWildcardImports() // or expandWildcardImports, see below
211211
forbidModuleImports()
212212
213213
// Cleanthat will refactor your code, but it may break your style: apply it before your formatter
@@ -259,6 +259,20 @@ spotless {
259259
}
260260
```
261261
262+
### expandWildcardImports
263+
264+
This step expands all wildcard imports to single class imports.
265+
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.
266+
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.
267+
268+
```
269+
spotless {
270+
java {
271+
expandWildcardImports()
272+
}
273+
}
274+
```
275+
262276
### forbidModuleImports
263277
264278
```

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.diffplug.gradle.spotless;
1717

1818
import static com.diffplug.gradle.spotless.PluginGradlePreconditions.requireElementsNonNull;
19+
import static java.util.stream.Collectors.toSet;
1920

2021
import java.io.File;
2122
import java.util.ArrayList;
@@ -30,12 +31,15 @@
3031
import javax.inject.Inject;
3132

3233
import org.gradle.api.Project;
34+
import org.gradle.api.artifacts.Configuration;
3335
import org.gradle.api.tasks.SourceSet;
36+
import org.gradle.api.tasks.SourceSetContainer;
3437

3538
import com.diffplug.spotless.FormatterStep;
3639
import com.diffplug.spotless.extra.java.EclipseJdtFormatterStep;
3740
import com.diffplug.spotless.generic.LicenseHeaderStep;
3841
import com.diffplug.spotless.java.CleanthatJavaStep;
42+
import com.diffplug.spotless.java.ExpandWildcardImportsStep;
3943
import com.diffplug.spotless.java.ForbidModuleImportsStep;
4044
import com.diffplug.spotless.java.ForbidWildcardImportsStep;
4145
import com.diffplug.spotless.java.FormatAnnotationsStep;
@@ -167,6 +171,13 @@ public void forbidModuleImports() {
167171
addStep(ForbidModuleImportsStep.create());
168172
}
169173

174+
public void expandWildcardImports() {
175+
SourceSetContainer sourceSets = getSourceSets(getProject(), "expansion of wildcards requires the 'java' plugin to be applied");
176+
Set<File> typeSolverClasspath = sourceSets.stream().flatMap(s -> s.getAllJava().getSrcDirs().stream()).collect(toSet());
177+
getProject().getConfigurations().stream().filter(Configuration::isCanBeResolved).flatMap(c -> c.getFiles().stream()).forEach(typeSolverClasspath::add);
178+
addStep(ExpandWildcardImportsStep.create(typeSolverClasspath, provisioner()));
179+
}
180+
170181
/** Uses the <a href="https://github.com/google/google-java-format">google-java-format</a> jar to format source code. */
171182
public GoogleJavaFormatConfig googleJavaFormat() {
172183
return googleJavaFormat(GoogleJavaFormatStep.defaultVersion());

0 commit comments

Comments
 (0)