Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
36 changes: 34 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ under the License.
</contributors>

<prerequisites>
<maven>3.6.3</maven>
<maven>${mavenVersion}</maven>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please don't change

</prerequisites>

<scm>
Expand Down Expand Up @@ -95,6 +95,9 @@ under the License.
<resolverVersion>1.9.24</resolverVersion>
<slf4jVersion>1.7.36</slf4jVersion>
<jettyVersion>9.4.58.v20250814</jettyVersion>
<jettyVersion>9.4.57.v20241219</jettyVersion>
<johnzonVersion>1.2.21</johnzonVersion>
<glassfishVersion>2.0.1</glassfishVersion>
<mockito.version>4.11.0</mockito.version>
<plexus-archiver.version>4.10.1</plexus-archiver.version>
<pluginTestingVersion>3.3.0</pluginTestingVersion>
Expand Down Expand Up @@ -152,6 +155,11 @@ under the License.
<artifactId>doxia-sink-api</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.8</version>
</dependency>

<!-- reporting -->
<dependency>
Expand Down Expand Up @@ -327,7 +335,14 @@ under the License.
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.json</artifactId>
<version>2.0.1</version>
<version>${glassfishVersion}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.johnzon</groupId>
<artifactId>johnzon-core</artifactId>
<version>${johnzonVersion}</version>
<classifier>jakarta</classifier>
<scope>test</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -406,6 +421,18 @@ under the License.
<version>${slf4jVersion}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.tomitribe</groupId>
<artifactId>tomitribe-util</artifactId>
<version>1.3.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-classworlds</artifactId>
<version>2.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -470,6 +497,11 @@ under the License.
<configuration>
<!-- Need more heap space in order to run the unit tests !-->
<argLine>-Xmx512m</argLine>
<systemPropertyVariables>
<slf4jVersion>${slf4jVersion}</slf4jVersion>
<johnzonVersion>${johnzonVersion}</johnzonVersion>
<glassfishVersion>${glassfishVersion}</glassfishVersion>
</systemPropertyVariables>
<rerunFailingTestsCount>2</rerunFailingTestsCount>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration>
Expand Down
8 changes: 7 additions & 1 deletion src/it/projects/analyze/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,18 @@
<artifactId>maven-model</artifactId>
<version>2.0.6</version>
</dependency>
<!-- MDEP-957 slf4j-simple is unused but should not be reported -->
<!-- MDEP-957 slf4j-simple incorrectly appears unused, and should not be reported -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.16</version>
</dependency>
<!-- MDEP-964 javax.json incorrectly appears unused, and should not be reported -->
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.1.4</version>
</dependency>
</dependencies>

<build>
Expand Down
3 changes: 0 additions & 3 deletions src/it/projects/analyze/verify.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,5 @@ assert buildLog.contains( '[WARNING] org.apache.maven:maven-repository-metada
assert buildLog.contains( '[WARNING] class org.apache.maven.artifact.repository.metadata.Metadata')
assert buildLog.contains( '[WARNING] Unused declared dependencies found:')
assert buildLog.contains( '[WARNING] org.apache.maven:maven-project:jar:2.0.6:compile')
assert buildLog.contains( '[INFO] Ignored unused declared dependencies:')
assert buildLog.contains( '[INFO] org.slf4j:slf4j-simple:jar:2.0.16:compile')
assert !buildLog.contains( '[WARNING] org.slf4j:slf4j-simple:jar:2.0.16:compile')

return true
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,23 @@
package org.apache.maven.plugins.dependency.analyze;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Stream;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
Expand All @@ -43,6 +51,17 @@
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.ModuleVisitor;
import org.objectweb.asm.Type;

import static java.util.Collections.list;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.objectweb.asm.Opcodes.ASM9;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;

/**
* Analyzes the dependencies of this project and determines which are: used and declared; used and undeclared; unused
Expand Down Expand Up @@ -214,16 +233,11 @@ public abstract class AbstractAnalyzeMojo extends AbstractMojo {
* <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> matches all snapshot artifacts.
* </p>
*
* <p>Certain dependencies that are known to be used and loaded by reflection
* are always ignored. This includes {@code org.slf4j:slf4j-simple::}.</p>
*
* @since 2.10
*/
@Parameter
private String[] ignoredUnusedDeclaredDependencies = new String[0];

private String[] unconditionallyIgnoredDeclaredDependencies = {"org.slf4j:slf4j-simple::"};

/**
* List of dependencies that are ignored if they are in not test scope but are only used in test classes.
* The filter syntax is:
Expand All @@ -241,7 +255,7 @@ public abstract class AbstractAnalyzeMojo extends AbstractMojo {
*
* @since 3.3.0
*/
@Parameter(defaultValue = "org.slf4j:slf4j-simple::")
@Parameter
private String[] ignoredNonTestScopedDependencies;

/**
Expand Down Expand Up @@ -366,7 +380,6 @@ private boolean checkDependencies() throws MojoExecutionException {

ignoredUnusedDeclared.addAll(filterDependencies(unusedDeclared, ignoredDependencies));
ignoredUnusedDeclared.addAll(filterDependencies(unusedDeclared, ignoredUnusedDeclaredDependencies));
ignoredUnusedDeclared.addAll(filterDependencies(unusedDeclared, unconditionallyIgnoredDeclaredDependencies));

if (ignoreAllNonTestScoped) {
ignoredNonTestScope.addAll(filterDependencies(nonTestScope, new String[] {"*"}));
Expand Down Expand Up @@ -398,11 +411,15 @@ private boolean checkDependencies() throws MojoExecutionException {
}

if (!unusedDeclared.isEmpty()) {
logDependencyWarning("Unused declared dependencies found:");

logArtifacts(unusedDeclared, true);
reported = true;
warning = true;
final Set<String> declaredSpi = scanForSpiUsage(usedDeclared, usedUndeclaredWithClasses);
cleanupUnused(declaredSpi, unusedDeclared);
if (!unusedDeclared.isEmpty()) {
logDependencyWarning("Unused declared dependencies found:");

logArtifacts(unusedDeclared, true);
reported = true;
warning = true;
}
}

if (!nonTestScope.isEmpty()) {
Expand Down Expand Up @@ -449,6 +466,125 @@ private boolean checkDependencies() throws MojoExecutionException {
return warning;
}

// todo: enhance analyzer (dependency) to do it since it already visits classes
// will save some time
private Set<String> scanForSpiUsage(
final Set<Artifact> usedDeclared, final Map<Artifact, Set<String>> usedUndeclaredWithClasses) {
return Stream.concat(
usedDeclared.stream().flatMap(this::findUsedSpi),
usedUndeclaredWithClasses.keySet().stream().flatMap(this::findUsedSpi))
.collect(toSet());
}

private Stream<String> findUsedSpi(final Artifact artifact) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we extends a maven-dependency-analyzer with such feature?

try (JarFile jar = new JarFile(artifact.getFile())) {
return list(jar.entries()).stream()
.filter(entry -> entry.getName().endsWith(".class"))
.flatMap(entry -> {
final ClassReader classReader;
try {
classReader = new ClassReader(jar.getInputStream(entry));
} catch (IOException e) {
return Stream.empty();
}
final Set<String> spi = new HashSet<>();
classReader.accept(
new ClassVisitor(ASM9) {
@Override
public MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
return new MethodVisitor(ASM9) {
private Type lastType = null;

@Override
public void visitLdcInsn(final Object value) {
if (value instanceof Type) {
lastType = (Type) value;
}
}

@Override
public void visitMethodInsn(
final int opcode,
final String owner,
final String name,
final String descriptor,
final boolean isInterface) {
if (opcode == INVOKESTATIC
&& Objects.equals(owner, "java/util/ServiceLoader")
&& Objects.equals(name, "load")) {
spi.add(lastType.getClassName());
}
lastType = null;
}
};
}
},
0);
return spi.stream();
})
.collect(toList()) // materialize before closing the jar
.stream();
} catch (final IOException ioe) {
return Stream.empty();
}
}

// here we try to detect "well-known" indirect patterns to remove false warnings
private void cleanupUnused(final Set<String> spi, final Set<Artifact> unusedDeclared) {
unusedDeclared.removeIf(this::isSlf4jBinding);
unusedDeclared.removeIf(it -> hasUsedSPIImpl(spi, it));
}

// mainly for v1.x line but doesn't hurt much for v2
// TODO: enhance to ensure there is a single binding else just log a warning for all
// and maybe even handle version?
private boolean isSlf4jBinding(final Artifact artifact) {
try (JarFile file = new JarFile(artifact.getFile())) {
return file.getEntry("org/slf4j/impl/StaticLoggerBinder.class") != null;
} catch (final IOException e) {
return false;
}
}

private boolean hasUsedSPIImpl(final Set<String> usedSpi, final Artifact artifact) {
final Set<String> spi;
try (JarFile file = new JarFile(artifact.getFile())) {
spi = list(file.entries()).stream()
.filter(it -> it.getName().startsWith("META-INF/services/") && !it.isDirectory())
.map(it -> it.getName().substring("META-INF/services/".length()))
.collect(toSet());

// java >= 9
final JarEntry moduleEntry = file.getJarEntry("module-info.class");
if (moduleEntry != null) {
try (InputStream in = file.getInputStream(moduleEntry)) {
final ClassReader cr = new ClassReader(in);
cr.accept(
new ClassVisitor(ASM9) {
@Override
public ModuleVisitor visitModule(String name, int access, String version) {
return new ModuleVisitor(ASM9) {
@Override
public void visitProvide(final String service, final String[] providers) {
spi.add(service.replace('/', '.'));
}
};
}
},
0);
}
}
} catch (final IOException e) {
return false;
}
return usedSpi.stream().anyMatch(spi::contains);
}

private void filterArtifactsByScope(Set<Artifact> artifacts, String scope) {
artifacts.removeIf(artifact -> artifact.getScope().equals(scope));
}
Expand Down Expand Up @@ -559,7 +695,8 @@ private void writeScriptableOutput(Set<Artifact> artifacts) {
}

private List<Artifact> filterDependencies(Set<Artifact> artifacts, String[] excludes) {
ArtifactFilter filter = new StrictPatternExcludesArtifactFilter(Arrays.asList(excludes));
ArtifactFilter filter = new StrictPatternExcludesArtifactFilter(
excludes == null ? Collections.emptyList() : Arrays.asList(excludes));
List<Artifact> result = new ArrayList<>();

for (Iterator<Artifact> it = artifacts.iterator(); it.hasNext(); ) {
Expand Down
Loading
Loading