diff --git a/plugin-maven/README.md b/plugin-maven/README.md index aa855dd4b9..6bb834dcae 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -232,6 +232,7 @@ any other maven phase (i.e. compile) then it can be configured as below; + @@ -257,6 +258,14 @@ any other maven phase (i.e. compile) then it can be configured as below; ``` +### expandWildcardImports + +Automatically replaces wildcard imports (e.g., `import java.util.*`) with explicit imports for the classes actually used in the code. This step analyzes your source code and project dependencies to determine which specific classes are needed and generates the appropriate import statements. + +```xml + +``` + ### forbidModuleImports ```xml diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java index 3952a8de70..e138cbd157 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java @@ -397,7 +397,7 @@ private FormatterConfig getFormatterConfig() { FileLocator fileLocator = getFileLocator(); final Optional optionalRatchetFrom = Optional.ofNullable(this.ratchetFrom) .filter(ratchet -> !RATCHETFROM_NONE.equals(ratchet)); - return new FormatterConfig(baseDir, encoding, lineEndings, optionalRatchetFrom, provisioner, fileLocator, formatterStepFactories, Optional.ofNullable(setLicenseHeaderYearsFromGitHistory), lintSuppressions); + return new FormatterConfig(baseDir, encoding, lineEndings, optionalRatchetFrom, provisioner, fileLocator, formatterStepFactories, Optional.ofNullable(setLicenseHeaderYearsFromGitHistory), lintSuppressions, project, repositorySystem, repositorySystemSession); } private FileLocator getFileLocator() { diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java index 842d4da68e..9477d36725 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java @@ -21,6 +21,10 @@ import java.util.List; import java.util.Optional; +import org.apache.maven.project.MavenProject; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession; + import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.LintSuppression; import com.diffplug.spotless.Provisioner; @@ -35,9 +39,12 @@ public class FormatterConfig { private final List globalStepFactories; private final Optional spotlessSetLicenseHeaderYearsFromGitHistory; private final List lintSuppressions; + private final MavenProject project; + private final RepositorySystem repositorySystem; + private final RepositorySystemSession repositorySystemSession; public FormatterConfig(File baseDir, String encoding, LineEnding lineEndings, Optional ratchetFrom, Provisioner provisioner, - FileLocator fileLocator, List globalStepFactories, Optional spotlessSetLicenseHeaderYearsFromGitHistory, List lintSuppressions) { + FileLocator fileLocator, List globalStepFactories, Optional spotlessSetLicenseHeaderYearsFromGitHistory, List lintSuppressions, MavenProject project, RepositorySystem repositorySystem, RepositorySystemSession repositorySystemSession) { this.encoding = encoding; this.lineEndings = lineEndings; this.ratchetFrom = ratchetFrom; @@ -46,6 +53,9 @@ public FormatterConfig(File baseDir, String encoding, LineEnding lineEndings, Op this.globalStepFactories = globalStepFactories; this.spotlessSetLicenseHeaderYearsFromGitHistory = spotlessSetLicenseHeaderYearsFromGitHistory; this.lintSuppressions = lintSuppressions; + this.project = project; + this.repositorySystem = repositorySystem; + this.repositorySystemSession = repositorySystemSession; } public String getEncoding() { @@ -79,4 +89,16 @@ public FileLocator getFileLocator() { public List getLintSuppressions() { return unmodifiableList(lintSuppressions); } + + public MavenProject getProject() { + return project; + } + + public RepositorySystem getRepositorySystem() { + return repositorySystem; + } + + public RepositorySystemSession getRepositorySystemSession() { + return repositorySystemSession; + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java index 6c7cc0d349..1e8e24915f 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java @@ -185,7 +185,7 @@ Optional ratchetFrom(FormatterConfig config) { } private FormatterStepConfig stepConfig(Charset encoding, FormatterConfig config) { - return new FormatterStepConfig(encoding, licenseHeaderDelimiter(), ratchetFrom(config), config.getProvisioner(), config.getFileLocator(), config.getSpotlessSetLicenseHeaderYearsFromGitHistory()); + return new FormatterStepConfig(encoding, licenseHeaderDelimiter(), ratchetFrom(config), config.getProvisioner(), config.getFileLocator(), config.getSpotlessSetLicenseHeaderYearsFromGitHistory(), config.getProject(), config.getRepositorySystem(), config.getRepositorySystemSession()); } private static List gatherStepFactories(List allGlobal, List allConfigured) { diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterStepConfig.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterStepConfig.java index a1f2e52b41..38dce91eef 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterStepConfig.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterStepConfig.java @@ -18,6 +18,10 @@ import java.nio.charset.Charset; import java.util.Optional; +import org.apache.maven.project.MavenProject; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession; + import com.diffplug.spotless.Provisioner; public class FormatterStepConfig { @@ -28,14 +32,20 @@ public class FormatterStepConfig { private final Provisioner provisioner; private final FileLocator fileLocator; private final Optional spotlessSetLicenseHeaderYearsFromGitHistory; + private final MavenProject project; + private final RepositorySystem repositorySystem; + private final RepositorySystemSession repositorySystemSession; - public FormatterStepConfig(Charset encoding, String licenseHeaderDelimiter, Optional ratchetFrom, Provisioner provisioner, FileLocator fileLocator, Optional spotlessSetLicenseHeaderYearsFromGitHistory) { + public FormatterStepConfig(Charset encoding, String licenseHeaderDelimiter, Optional ratchetFrom, Provisioner provisioner, FileLocator fileLocator, Optional spotlessSetLicenseHeaderYearsFromGitHistory, MavenProject project, RepositorySystem repositorySystem, RepositorySystemSession repositorySystemSession) { this.encoding = encoding; this.licenseHeaderDelimiter = licenseHeaderDelimiter; this.ratchetFrom = ratchetFrom; this.provisioner = provisioner; this.fileLocator = fileLocator; this.spotlessSetLicenseHeaderYearsFromGitHistory = spotlessSetLicenseHeaderYearsFromGitHistory; + this.project = project; + this.repositorySystem = repositorySystem; + this.repositorySystemSession = repositorySystemSession; } public Charset getEncoding() { @@ -61,4 +71,16 @@ public FileLocator getFileLocator() { public Optional spotlessSetLicenseHeaderYearsFromGitHistory() { return spotlessSetLicenseHeaderYearsFromGitHistory; } + + public MavenProject getProject() { + return project; + } + + public RepositorySystem getRepositorySystem() { + return repositorySystem; + } + + public RepositorySystemSession getRepositorySystemSession() { + return repositorySystemSession; + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/ExpandWildcardImports.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/ExpandWildcardImports.java new file mode 100644 index 0000000000..11e8440a43 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/ExpandWildcardImports.java @@ -0,0 +1,118 @@ +/* + * 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.maven.java; + +import java.io.File; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.maven.project.MavenProject; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.resolution.ArtifactResult; +import org.eclipse.aether.resolution.DependencyRequest; +import org.eclipse.aether.resolution.DependencyResolutionException; +import org.eclipse.aether.resolution.DependencyResult; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.java.ExpandWildcardImportsStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +public class ExpandWildcardImports implements FormatterStepFactory { + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + MavenProject project = config.getProject(); + Set typeSolverClasspath = new HashSet<>(); + + // Add all main source roots + project.getCompileSourceRoots().stream() + .map(File::new) + .filter(File::exists) + .forEach(typeSolverClasspath::add); + + // Add all test source roots + project.getTestCompileSourceRoots().stream() + .map(File::new) + .filter(File::exists) + .forEach(typeSolverClasspath::add); + + // Resolve dependencies using Maven's DependencyResolver API + // This will properly handle reactor dependencies by including their target/classes directories + // via the WorkspaceReader in the RepositorySystemSession + typeSolverClasspath.addAll(resolveDependencies(project, config.getRepositorySystem(), config.getRepositorySystemSession())); + + return ExpandWildcardImportsStep.create(typeSolverClasspath, config.getProvisioner()); + } + + private Set resolveDependencies(MavenProject project, RepositorySystem repositorySystem, RepositorySystemSession session) { + try { + // Use the project's already-resolved artifacts (which includes transitives) + // and convert them to Aether dependencies for re-resolution + // This allows the WorkspaceReader to map reactor modules to target/classes + List dependencies = project.getArtifacts().stream() + .map(artifact -> new Dependency( + new org.eclipse.aether.artifact.DefaultArtifact( + artifact.getGroupId(), + artifact.getArtifactId(), + artifact.getClassifier(), + artifact.getType(), + artifact.getVersion()), + artifact.getScope())) + .collect(Collectors.toList()); + + // Create a collect request with all dependencies + CollectRequest collectRequest = new CollectRequest(); + collectRequest.setDependencies(dependencies); + collectRequest.setRepositories(project.getRemoteProjectRepositories()); + + // Create a dependency request to resolve all artifacts + DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, null); + + // Resolve dependencies - this will use the WorkspaceReader in the session + // to resolve reactor modules to their target/classes directories + DependencyResult result = repositorySystem.resolveDependencies(session, dependencyRequest); + + // Extract the resolved artifact files + return result.getArtifactResults().stream() + .map(ArtifactResult::getArtifact) + .map(Artifact::getFile) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } catch (DependencyResolutionException e) { + // If resolution fails, fall back to using the artifacts already attached to the project + // This ensures the build doesn't fail, but reactor dependencies may not be properly resolved + // Note: Using System.err as this is a FormatterStepFactory without access to Maven's logger + System.err.println("Warning: Failed to resolve dependencies using RepositorySystem, " + + "falling back to project artifacts. Reactor dependencies may not be properly resolved: " + e.getMessage()); + return getFallbackArtifacts(project); + } + } + + private Set getFallbackArtifacts(MavenProject project) { + return project.getArtifacts().stream() + .map(org.apache.maven.artifact.Artifact::getFile) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Java.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Java.java index acc97647d7..53068fead5 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Java.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Java.java @@ -88,6 +88,10 @@ public void addFormatAnnotations(FormatAnnotations formatAnnotations) { addStepFactory(formatAnnotations); } + public void addExpandWildcardImports(ExpandWildcardImports expandWildcardImports) { + addStepFactory(expandWildcardImports); + } + public void addCleanthat(CleanthatJava cleanthat) { addStepFactory(cleanthat); } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/ExpandWildcardImportsStepTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/ExpandWildcardImportsStepTest.java new file mode 100644 index 0000000000..17670df238 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/ExpandWildcardImportsStepTest.java @@ -0,0 +1,128 @@ +/* + * 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.maven.java; + +import static java.util.Collections.emptyMap; + +import java.io.IOException; +import java.util.Map; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +class ExpandWildcardImportsStepTest extends MavenIntegrationHarness { + + @Test + void testExpandWildcardImports() throws Exception { + writePomWithJavaSteps(""); + + // Create the supporting classes needed for import resolution + 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"); + + // Create the annotation class that's used in the test + setFile("src/main/java/org/example/SomeAnnotation.java") + .toContent("package org.example;\n\npublic @interface SomeAnnotation {}\n"); + + // Set the main file to format + String path = "src/main/java/foo/bar/JavaClassWithWildcards.java"; + setFile(path).toResource("java/expandwildcardimports/JavaClassWithWildcardsUnformatted.test"); + + // Run spotless:apply + mavenRunner().withArguments("spotless:apply").runNoError(); + + // Verify the wildcards were expanded + assertFile(path).sameAsResource("java/expandwildcardimports/JavaClassWithWildcardsFormatted.test"); + } + + @Test + void testExpandWildcardImportsMultiModule() throws Exception { + /* + Create a multi-module project with the following structure: + + /junit-tmp-dir + ├── common + │ ├── pom.xml + │ └── src/main/java + │ ├── foo/bar/AnotherClassInSamePackage.java + │ ├── foo/bar/baz/AnotherImportedClass.java + │ └── org/example/SomeAnnotation.java + ├── app + │ ├── pom.xml + │ └── src/main/java + │ └── foo/bar/JavaClassWithWildcards.java (depends on common module) + ├── pom.xml (parent) + ├── .mvn + ├── mvnw + └── mvnw.cmd + */ + + // Create parent POM + String[] modules = new String[]{"common", "app"}; + String[] configuration = new String[]{ + "", + " ", + "" + }; + Map parentPomParams = buildPomXmlParams(null, null, null, configuration, modules, null, null); + setFile("pom.xml").toContent(createPomXmlContent("/multi-module/pom-parent.xml.mustache", parentPomParams)); + + // Create common module with supporting classes + setFile("common/pom.xml").toContent(createChildPom("common")); + setFile("common/src/main/java/foo/bar/AnotherClassInSamePackage.java") + .toResource("java/expandwildcardimports/AnotherClassInSamePackage.test"); + setFile("common/src/main/java/foo/bar/baz/AnotherImportedClass.java") + .toResource("java/expandwildcardimports/AnotherImportedClass.test"); + setFile("common/src/main/java/org/example/SomeAnnotation.java") + .toContent("package org.example;\n\npublic @interface SomeAnnotation {}\n"); + + // Create app module that depends on common + setFile("app/pom.xml").toContent(createChildPomWithDependency("app", "common")); + String path = "app/src/main/java/foo/bar/JavaClassWithWildcards.java"; + setFile(path).toResource("java/expandwildcardimports/JavaClassWithWildcardsUnformatted.test"); + + // Format all files in the multi-module project without requiring 'mvn install' first + // This tests that spotless:apply works in a reactor build without pre-installing dependencies + mavenRunner().withArguments("spotless:apply").runNoError(); + + // Verify the wildcards were expanded in app module + assertFile(path).sameAsResource("java/expandwildcardimports/JavaClassWithWildcardsFormatted.test"); + } + + private String createChildPom(String childId) throws IOException { + return createPomXmlContent("/multi-module/pom-child.xml.mustache", Map.of("childId", childId)); + } + + private String createChildPomWithDependency(String childId, String dependencyModule) throws IOException { + String childPom = createChildPom(childId); + // Add dependency to the common module + String dependency = """ + + + com.diffplug.spotless + spotless-maven-plugin-tests-child-%s + 1.0.0-SNAPSHOT + + + """.formatted(dependencyModule); + // Insert before + return childPom.replace("", dependency + "\n"); + } +}