Skip to content

Commit 78e3e4b

Browse files
author
Vincent Potucek
committed
new step to expand java wildcard imports #2744 #2594
1 parent fed07d9 commit 78e3e4b

File tree

18 files changed

+505
-15
lines changed

18 files changed

+505
-15
lines changed

CHANGES.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1111

1212
## [Unreleased]
1313
### Added
14+
- Add a `expandWildcardImports` API for java ([#2679](https://github.com/diffplug/spotless/issues/2594))
1415
- Add the ability to specify a wildcard version (`*`) for external formatter executables. ([#2757](https://github.com/diffplug/spotless/issues/2757))
1516
### Changes
16-
* Bump default `ktlint` version to latest `1.7.1` -> `1.8.0`. ([2763](https://github.com/diffplug/spotless/pull/2763))
17-
* Bump default `gherkin-utils` version to latest `9.2.0` -> `10.0.0`. ([#2619](https://github.com/diffplug/spotless/pull/2619))
17+
- Bump default `ktlint` version to latest `1.7.1` -> `1.8.0`. ([2763](https://github.com/diffplug/spotless/pull/2763))
18+
- Bump default `gherkin-utils` version to latest `9.2.0` -> `10.0.0`. ([#2619](https://github.com/diffplug/spotless/pull/2619))
1819

1920
## [4.1.0] - 2025-11-18
2021
### Changes

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ lib('java.GoogleJavaFormatStep') +'{{yes}} | {{yes}}
8585
lib('java.ImportOrderStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
8686
lib('java.PalantirJavaFormatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
8787
lib('java.RemoveUnusedImportsStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
88+
lib('java.ExpandWildcardImportsStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
8889
lib('java.ForbidWildcardImportsStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
8990
lib('java.ForbidModuleImportsStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
9091
extra('java.EclipseJdtFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
@@ -142,6 +143,7 @@ lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}}
142143
| [`java.ImportOrderStep`](lib/src/main/java/com/diffplug/spotless/java/ImportOrderStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
143144
| [`java.PalantirJavaFormatStep`](lib/src/main/java/com/diffplug/spotless/java/PalantirJavaFormatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
144145
| [`java.RemoveUnusedImportsStep`](lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
146+
| [`java.ExpandWildcardImportsStep`](lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
145147
| [`java.ForbidWildcardImportsStep`](lib/src/main/java/com/diffplug/spotless/java/ForbidWildcardImportsStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
146148
| [`java.ForbidModuleImportsStep`](lib/src/main/java/com/diffplug/spotless/java/ForbidModuleImportsStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
147149
| [`java.EclipseJdtFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: |

gradle/spotless.gradle

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ spotless {
33
if (project != rootProject) {
44
// the rootProject doesn't have any java
55
java {
6-
ratchetFrom 'origin/main'
76
bumpThisNumberIfACustomStepChanges(1)
8-
licenseHeaderFile rootProject.file('gradle/spotless.license')
9-
importOrderFile rootProject.file('gradle/spotless.importorder')
107
eclipse().configFile rootProject.file('gradle/spotless.eclipseformat.xml')
11-
trimTrailingWhitespace()
12-
removeUnusedImports()
13-
formatAnnotations()
8+
// expandWildcardImports() todo: use after release (No signature of method: com.diffplug.gradle.spotless.JavaExtension.expandWildcardImports() is applicable for argument types: () values: [])
149
forbidWildcardImports()
1510
forbidRegex('ForbidGradleInternal', 'import org\\.gradle\\.api\\.internal\\.(.*)', "Don't use Gradle's internal API")
11+
formatAnnotations()
12+
importOrderFile rootProject.file('gradle/spotless.importorder')
13+
licenseHeaderFile rootProject.file('gradle/spotless.license')
14+
ratchetFrom 'origin/main'
15+
removeUnusedImports()
16+
trimTrailingWhitespace()
1617
}
1718
}
1819
groovyGradle {

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: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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.java.parser;
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 final 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.isEmpty()) {
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+
int lastDot = qualifiedName.lastIndexOf('.');
139+
if (lastDot < 0) {
140+
return;
141+
}
142+
143+
String packageName = qualifiedName.substring(0, lastDot);
144+
for (var entry : importMap.entrySet()) {
145+
if (entry.getKey().isStatic() == isStatic
146+
&& packageName.equals(entry.getKey().getName().asString())) {
147+
entry.getValue().add(new ImportDeclaration(qualifiedName, isStatic, false));
148+
break;
149+
}
150+
}
151+
}
152+
153+
@Override
154+
public void visit(final MarkerAnnotationExpr n,
155+
final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
156+
visitAnnotation(n, importMap);
157+
super.visit(n, importMap);
158+
}
159+
160+
@Override
161+
public void visit(final SingleMemberAnnotationExpr n,
162+
final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
163+
visitAnnotation(n, importMap);
164+
super.visit(n, importMap);
165+
}
166+
167+
@Override
168+
public void visit(final NormalAnnotationExpr n,
169+
final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
170+
visitAnnotation(n, importMap);
171+
super.visit(n, importMap);
172+
}
173+
174+
private void visitAnnotation(final AnnotationExpr n,
175+
final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
176+
ResolvedAnnotationDeclaration resolvedType = wrapUnsolvedSymbolException(n, AnnotationExpr::resolve);
177+
matchTypeName(importMap, resolvedType.getQualifiedName(), false);
178+
}
179+
180+
@Override
181+
public void visit(final MethodCallExpr n, final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
182+
// static imports
183+
ResolvedMethodDeclaration resolved = wrapUnsolvedSymbolException(n, MethodCallExpr::resolve);
184+
if (resolved.isStatic()) {
185+
matchTypeName(importMap, resolved.getQualifiedName(), true);
186+
}
187+
super.visit(n, importMap);
188+
}
189+
190+
private static <T extends Node, R> R wrapUnsolvedSymbolException(T node, Function<T, R> func) {
191+
try {
192+
return func.apply(node);
193+
} catch (UnsolvedSymbolException ex) {
194+
if (node.getBegin().isPresent() && node.getEnd().isPresent()) {
195+
throw Lint.atLineRange(node.getBegin().get().line, node.getEnd().get().line, "UnsolvedSymbolException", ex.getMessage()).shortcut();
196+
}
197+
if (node.getBegin().isPresent()) {
198+
throw Lint.atLine(node.getBegin().get().line, "UnsolvedSymbolException", ex.getMessage()).shortcut();
199+
} else if (node.getEnd().isPresent()) {
200+
throw Lint.atLine(node.getEnd().get().line, "UnsolvedSymbolException", ex.getMessage()).shortcut();
201+
} else {
202+
throw Lint.atUndefinedLine("UnsolvedSymbolException", ex.getMessage()).shortcut();
203+
}
204+
}
205+
}
206+
207+
}
208+
209+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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.Serial;
20+
import java.io.Serializable;
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 final class ExpandWildcardImportsStep implements Serializable {
32+
@Serial
33+
private static final long serialVersionUID = 1L;
34+
35+
private static final String INCOMPATIBLE_ERROR_MESSAGE = "There was a problem interacting with Java-Parser; maybe you set an incompatible version?";
36+
private static final String MAVEN_COORDINATES = "com.github.javaparser:javaparser-symbol-solver-core";
37+
public static final String DEFAULT_VERSION = "3.27.1";
38+
39+
private final Collection<File> typeSolverClasspath;
40+
private final JarState.Promised jarState;
41+
42+
private ExpandWildcardImportsStep(Collection<File> typeSolverClasspath, JarState.Promised jarState) {
43+
this.typeSolverClasspath = typeSolverClasspath;
44+
this.jarState = jarState;
45+
}
46+
47+
public static FormatterStep create(Set<File> typeSolverClasspath, Provisioner provisioner) {
48+
Objects.requireNonNull(provisioner, "provisioner cannot be null");
49+
return FormatterStep.create("expandwildcardimports",
50+
new ExpandWildcardImportsStep(typeSolverClasspath,
51+
JarState.promise(() -> JarState.from(MAVEN_COORDINATES + ":" + DEFAULT_VERSION, provisioner))),
52+
ExpandWildcardImportsStep::equalityState,
53+
State::toFormatter);
54+
}
55+
56+
private State equalityState() {
57+
return new State(typeSolverClasspath, jarState.get());
58+
}
59+
60+
private static class State implements Serializable {
61+
@Serial
62+
private static final long serialVersionUID = 1L;
63+
64+
private final Collection<File> typeSolverClasspath;
65+
private final JarState jarState;
66+
67+
public State(Collection<File> typeSolverClasspath, JarState jarState) {
68+
this.typeSolverClasspath = typeSolverClasspath;
69+
this.jarState = jarState;
70+
}
71+
72+
FormatterFunc toFormatter() {
73+
try {
74+
return (FormatterFunc) jarState
75+
.getClassLoader()
76+
.loadClass("com.diffplug.spotless.glue.java.parser.ExpandWildcardsFormatterFunc")
77+
.getConstructor(Collection.class)
78+
.newInstance(typeSolverClasspath);
79+
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException
80+
| InstantiationException | IllegalAccessException | NoClassDefFoundError cause) {
81+
throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
82+
}
83+
}
84+
85+
}
86+
87+
}

lib/src/main/java/com/diffplug/spotless/java/ForbidWildcardImportsStep.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818
import com.diffplug.spotless.FormatterStep;
1919
import com.diffplug.spotless.generic.ReplaceRegexStep;
2020

21-
/** Forbids any wildcard import statements. */
21+
/**
22+
* Forbids any wildcard import statements.
23+
*
24+
* @deprecated Use {@link ExpandWildcardImportsStep}
25+
*/
26+
@Deprecated(forRemoval = true)
2227
public final class ForbidWildcardImportsStep {
2328

2429
/**

plugin-gradle/CHANGES.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
44

55
## [Unreleased]
66
### Added
7+
- Add a `expandWildcardImports` API for java ([#2679](https://github.com/diffplug/spotless/issues/2594))
78
- Add the ability to specify a wildcard version (`*`) for external formatter executables. ([#2757](https://github.com/diffplug/spotless/issues/2757))
89
### Fixed
910
- [fix] `NPE` due to workingTreeIterator being null for git ignored files. #911 ([#2771](https://github.com/diffplug/spotless/issues/2771))
1011
### Changes
11-
* Bump default `ktlint` version to latest `1.7.1` -> `1.8.0`. ([2763](https://github.com/diffplug/spotless/pull/2763))
12-
* Bump default `gherkin-utils` version to latest `9.2.0` -> `10.0.0`. ([#2619](https://github.com/diffplug/spotless/pull/2619))
12+
- Bump default `ktlint` version to latest `1.7.1` -> `1.8.0`. ([2763](https://github.com/diffplug/spotless/pull/2763))
13+
- Bump default `gherkin-utils` version to latest `9.2.0` -> `10.0.0`. ([#2619](https://github.com/diffplug/spotless/pull/2619))
1314

1415
## [8.1.0] - 2025-11-18
1516
### Changes

0 commit comments

Comments
 (0)