Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')
50 changes: 0 additions & 50 deletions gradle/error-prone.gradle

This file was deleted.

3 changes: 3 additions & 0 deletions lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def NEEDS_GLUE = [
'googleJavaFormat',
'gson',
'jackson',
'javaParser',
'ktfmt',
'ktlint',
'palantirJavaFormat',
Expand Down Expand Up @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/*
* Copyright 2023-2025 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.spotless.glue.javaparser;

import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.regex.Pattern;
import javassist.ClassPool;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.MarkerAnnotationExpr;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import com.github.javaparser.resolution.SymbolResolver;
import com.github.javaparser.resolution.UnsolvedSymbolException;
import com.github.javaparser.resolution.declarations.ResolvedAnnotationDeclaration;
import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration;
import com.github.javaparser.resolution.types.ResolvedType;
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;

import com.diffplug.spotless.FormatterFunc;
import com.diffplug.spotless.LineEnding;
import com.diffplug.spotless.Lint;

public class ExpandWildcardsFormatterFunc implements FormatterFunc.NeedsFile {

private 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<File> 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<CompilationUnit> parseResult = parser.parse(rawUnix).getResult();
if (parseResult.isEmpty()) {
return rawUnix;
}
CompilationUnit cu = parseResult.get();
Map<ImportDeclaration, Set<ImportDeclaration>> 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<ImportDeclaration> findWildcardImports(CompilationUnit cu) {
List<ImportDeclaration> wildcardImports = new ArrayList<>();
for (ImportDeclaration importDeclaration : cu.getImports()) {
if (importDeclaration.isAsterisk()) {
wildcardImports.add(importDeclaration);
}
}
return wildcardImports;
}

private static final class CollectImportedTypesVisitor
extends VoidVisitorAdapter<Map<ImportDeclaration, Set<ImportDeclaration>>> {

@Override
public void visit(final ClassOrInterfaceType n,
final Map<ImportDeclaration, Set<ImportDeclaration>> 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<ImportDeclaration, Set<ImportDeclaration>> importMap, String qualifiedName,
boolean isStatic) {
for (var entry : importMap.entrySet()) {
if (entry.getKey().isStatic() == isStatic
&& qualifiedName.startsWith(entry.getKey().getName().asString())) {
entry.getValue().add(new ImportDeclaration(qualifiedName, isStatic, false));
break;
}
}
}

@Override
public void visit(final MarkerAnnotationExpr n,
final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
visitAnnotation(n, importMap);
super.visit(n, importMap);
}

@Override
public void visit(final SingleMemberAnnotationExpr n,
final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
visitAnnotation(n, importMap);
super.visit(n, importMap);
}

@Override
public void visit(final NormalAnnotationExpr n,
final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
visitAnnotation(n, importMap);
super.visit(n, importMap);
}

private void visitAnnotation(final AnnotationExpr n,
final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
ResolvedAnnotationDeclaration resolvedType = wrapUnsolvedSymbolException(n, AnnotationExpr::resolve);
matchTypeName(importMap, resolvedType.getQualifiedName(), false);
}

@Override
public void visit(final MethodCallExpr n, final Map<ImportDeclaration, Set<ImportDeclaration>> importMap) {
// static imports
ResolvedMethodDeclaration resolved = wrapUnsolvedSymbolException(n, MethodCallExpr::resolve);
if (resolved.isStatic()) {
matchTypeName(importMap, resolved.getQualifiedName(), true);
}
super.visit(n, importMap);
}

private static <T extends Node, R> R wrapUnsolvedSymbolException(T node, Function<T, R> 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();
}
}
}

}

}
Original file line number Diff line number Diff line change
@@ -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<File> typeSolverClasspath;
private final JarState.Promised jarState;

private ExpandWildcardImportsStep(Collection<File> typeSolverClasspath, JarState.Promised jarState) {
this.typeSolverClasspath = typeSolverClasspath;
this.jarState = jarState;
}

public static FormatterStep create(Set<File> 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<File> typeSolverClasspath;
private final JarState jarState;

public State(Collection<File> 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);
}
}

}

}
2 changes: 2 additions & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading