You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by kw...@apache.org on 2022/06/08 12:37:21 UTC

[sling-maven-enforcer-rules] branch master updated: SLING-11369 add flags for optional and direct dependencies

This is an automated email from the ASF dual-hosted git repository.

kwin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-maven-enforcer-rules.git


The following commit(s) were added to refs/heads/master by this push:
     new d66bcfb  SLING-11369 add flags for optional and direct dependencies
d66bcfb is described below

commit d66bcfb9a32dc923f5c692b4c23bd4641c50a00d
Author: Konrad Windszus <kw...@apache.org>
AuthorDate: Wed Jun 8 14:37:16 2022 +0200

    SLING-11369 add flags for optional and direct dependencies
    
    make exclusions also exclude their dependencies
    extend IT
---
 README.md                                          |   9 +-
 pom.xml                                            |  16 +-
 .../invoker.properties                             |   0
 .../pom.xml                                        |  23 ++-
 .../verify.groovy                                  |   0
 ...uireProvidedDependenciesInRuntimeClasspath.java | 189 ++++++++++++++++-----
 6 files changed, 189 insertions(+), 48 deletions(-)

diff --git a/README.md b/README.md
index d353fda..bf2612a 100644
--- a/README.md
+++ b/README.md
@@ -25,11 +25,14 @@ As those are not transitively inherited they need to be declared explicitly in t
 
 #### Parameters
 
- * `excludes` - a list of dependencies to skip. Their transitive dependencies are not evaluated either. The format is `groupId[:artifactId][:version][:type][:scope][:classifier]` where `artifactId`, `version`, `type`, `scope` and `classifier` are optional. Wild cards (`*` for arbitrarily many characters or `?` for an arbitrary single character) may be used to replace an entire or just parts of a section. *Examples*: 
+All parameters are optional.
+
+ * `excludes` - a list of dependencies to skip. Their transitive dependencies are not evaluated either. The format is `<groupId>[:<artifactId>[:<extension>[:<classifier>]]]`. Wild cards (`*`) may be used to replace an entire part of a section. *Examples*: 
      * `org.apache.maven` (everything with the given group)
      * `org.apache.maven:myArtifact`
-     * `org.apache.maven:*:1.2` (exclude version 1.2 and above, equivalent to [1.2,) )
-     * `org.apache.maven:*:[1.2]` (explicit exclude of version 1.2)
+     * `org.apache.maven:*:jar`
+ * `includeOptionalDependencies` - whether to include optional dependencies in the check. Either `true` or `false`. By default no optional dependencies are checked.
+ * `includeDirectDependencies` - whether to include direct (provided) dependencies in the check. Either `true` or `false`. By default no direct provided dependencies are checked.
 
 #### Sample Plugin Configuration:
 
diff --git a/pom.xml b/pom.xml
index ef0a356..1c2f544 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
     <parent>
         <groupId>org.apache.sling</groupId>
         <artifactId>sling</artifactId>
-        <version>47</version>
+        <version>48</version>
     </parent>
     <artifactId>maven-enforcer-rules</artifactId>
     <version>0.0.1-SNAPSHOT</version>
@@ -40,7 +40,7 @@
     <properties>
         <sling.java.version>8</sling.java.version>
         <api.version>3.0.0</api.version>
-        <maven.version>3.3.1</maven.version>
+        <maven.version>3.1.1</maven.version><!-- must be the same version as used by enforcer-api due to shared classloader -->
         <project.build.outputTimestamp>10</project.build.outputTimestamp>
     </properties>
 
@@ -54,6 +54,12 @@
             <groupId>org.apache.maven.enforcer</groupId>
             <artifactId>enforcer-rules</artifactId>
             <version>${api.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>*</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
         <dependency>
             <groupId>org.apache.maven</groupId>
@@ -67,6 +73,11 @@
             <version>${maven.version}</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+              <groupId>org.apache.maven.shared</groupId>
+              <artifactId>maven-shared-utils</artifactId>
+              <version>3.3.4</version>
+        </dependency>
         <!-- as Maven Enforcer API still uses JSR305 null annotations we cannot yet switch to Jetbrains null annotations -->
         <dependency>
             <groupId>com.google.code.findbugs</groupId>
@@ -90,6 +101,7 @@
                     </pomIncludes>
                     <postBuildHookScript>verify.groovy</postBuildHookScript>
                     <streamLogsOnFailures>true</streamLogsOnFailures>
+                    <debug>true</debug>
                 </configuration>
                 <executions>
                     <execution>
diff --git a/src/it/require-transitive-provided-deps-in-runtime-classpath/invoker.properties b/src/it/require-provided-deps-in-runtime-classpath/invoker.properties
similarity index 100%
rename from src/it/require-transitive-provided-deps-in-runtime-classpath/invoker.properties
rename to src/it/require-provided-deps-in-runtime-classpath/invoker.properties
diff --git a/src/it/require-transitive-provided-deps-in-runtime-classpath/pom.xml b/src/it/require-provided-deps-in-runtime-classpath/pom.xml
similarity index 78%
rename from src/it/require-transitive-provided-deps-in-runtime-classpath/pom.xml
rename to src/it/require-provided-deps-in-runtime-classpath/pom.xml
index 61a85de..de5d56d 100644
--- a/src/it/require-transitive-provided-deps-in-runtime-classpath/pom.xml
+++ b/src/it/require-provided-deps-in-runtime-classpath/pom.xml
@@ -22,7 +22,7 @@
     <modelVersion>4.0.0</modelVersion>
 
     <groupId>org.apache.sling.maven.enforcer.it</groupId>
-    <artifactId>require-transitive-provided-deps-in-runtime-classpath</artifactId>
+    <artifactId>require-provided-deps-in-runtime-classpath</artifactId>
     <packaging>pom</packaging>
     <version>@project.version@</version>
 
@@ -52,8 +52,10 @@
                                     <excludes>
                                         <exclude>*:h2</exclude>
                                         <exclude>org.codehaus.woodstox</exclude>
-                                        <exclude>javax.servlet:javax.servlet-api:3.1.0</exclude>
+                                        <exclude>javax.servlet:javax.servlet-api</exclude>
+                                        <exclude>*:org.apache.felix.scr.annotations</exclude>
                                     </excludes>
+                                    <includeDirectDependencies>true</includeDirectDependencies>
                                 </requireProvidedDependenciesInRuntimeClasspath>
                             </rules>
                             <fail>true</fail>
@@ -71,12 +73,27 @@
             <artifactId>vault-cli</artifactId>
             <version>3.6.0</version>
         </dependency>
-        <!-- direct provided dependency -->
+        <!-- direct provided dependency excluded by default -->
         <dependency>
             <groupId>com.google.code.findbugs</groupId>
             <artifactId>jsr305</artifactId>
             <version>3.0.2</version>
             <scope>provided</scope>
         </dependency>
+        <!-- provided but excluded dependency with further transitive dependencies -->
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.scr.annotations</artifactId>
+            <version>1.9.8</version>
+            <scope>provided</scope>
+        </dependency>
+        <!-- test scope is excluded by default -->
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest</artifactId>
+            <version>2.2</version>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 </project>
\ No newline at end of file
diff --git a/src/it/require-transitive-provided-deps-in-runtime-classpath/verify.groovy b/src/it/require-provided-deps-in-runtime-classpath/verify.groovy
similarity index 100%
rename from src/it/require-transitive-provided-deps-in-runtime-classpath/verify.groovy
rename to src/it/require-provided-deps-in-runtime-classpath/verify.groovy
diff --git a/src/main/java/org/apache/sling/maven/enforcer/RequireProvidedDependenciesInRuntimeClasspath.java b/src/main/java/org/apache/sling/maven/enforcer/RequireProvidedDependenciesInRuntimeClasspath.java
index 545238e..9f6a37e 100644
--- a/src/main/java/org/apache/sling/maven/enforcer/RequireProvidedDependenciesInRuntimeClasspath.java
+++ b/src/main/java/org/apache/sling/maven/enforcer/RequireProvidedDependenciesInRuntimeClasspath.java
@@ -21,6 +21,7 @@ package org.apache.sling.maven.enforcer;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedList;
@@ -41,12 +42,17 @@ import org.apache.maven.plugin.logging.Log;
 import org.apache.maven.plugins.enforcer.AbstractNonCacheableEnforcerRule;
 import org.apache.maven.plugins.enforcer.utils.ArtifactUtils;
 import org.apache.maven.project.MavenProject;
+import org.apache.maven.shared.utils.logging.MessageBuilder;
+import org.apache.maven.shared.utils.logging.MessageUtils;
 import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
 import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
 import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.eclipse.aether.RepositorySystem;
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.graph.Dependency;
 import org.eclipse.aether.graph.DependencyFilter;
 import org.eclipse.aether.graph.DependencyNode;
 import org.eclipse.aether.graph.DependencyVisitor;
@@ -56,13 +62,11 @@ 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 org.eclipse.aether.util.filter.OrDependencyFilter;
-import org.eclipse.aether.util.filter.PatternInclusionsDependencyFilter;
-import org.eclipse.aether.util.filter.ScopeDependencyFilter;
 import org.eclipse.aether.util.graph.selector.AndDependencySelector;
 import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector;
 import org.eclipse.aether.util.graph.selector.OptionalDependencySelector;
 import org.eclipse.aether.util.graph.selector.ScopeDependencySelector;
+import org.eclipse.aether.util.graph.selector.StaticDependencySelector;
 import org.eclipse.aether.util.graph.visitor.PathRecordingDependencyVisitor;
 import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor;
 
@@ -77,13 +81,28 @@ import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor;
 public class RequireProvidedDependenciesInRuntimeClasspath
         extends AbstractNonCacheableEnforcerRule implements EnforcerRule2 {
 
-    /** Specify the banned dependencies. This can be a list of artifacts in the format <code>groupId[:artifactId][:version]</code>. Any of
-     * the sections can be a wildcard by using '*' (ie group:*:1.0) <br>
-     * The rule will fail if any dependency matches any exclude, unless it also matches an include rule.
+    /** 
+     * Specify the banned dependencies. This is a list of artifacts in format {@code <groupId>[:<artifactId>[:<extension>[:<classifier>]]]}. 
+     * Excluded dependencies are not traversed, i.e. their transitive dependencies are not considered.
      * 
      * @see {@link #setExcludes(List)} */
     private List<String> excludes = null;
 
+    /**
+     * Whether to include optional dependencies in the check. Default = false.
+     * 
+     * @see {@link #setIncludeOptionalDependencies(boolean)}
+     */
+    private boolean includeOptionals;
+
+    /**
+     * Whether to include direct (i.e. non transitive) provided dependencies in the check. Default = false.
+     * 
+     * @see {@link #setIncludeDirectDependencies(boolean)}
+     */
+    private boolean includeDirects;
+
+    @SuppressWarnings("unchecked")
     @Override
     public void execute(@Nonnull EnforcerRuleHelper helper) throws EnforcerRuleException {
         MavenProject project;
@@ -103,31 +122,48 @@ public class RequireProvidedDependenciesInRuntimeClasspath
             throw new EnforcerRuleException("Unable to retrieve component RepositorySystem", cle);
         }
         Log log = helper.getLog();
-        // make sure to transitively also collect dependencies of provided scope
-        newRepoSession.setDependencySelector(new AndDependencySelector(
-                new OptionalDependencySelector(),
-                new ScopeDependencySelector("test"),
-                new ExclusionDependencySelector(Collections.singleton(new Exclusion("*", "*", "*", "pom")))));
+        
+        Collection<DependencySelector> depSelectors = new ArrayList<>();
+        depSelectors.add(new ScopeDependencySelector("test")); // exclude transitive and direct "test" dependencies of the rootDependency (i.e. the current project)
+        // add also the exclude patterns
+        if (excludes != null && !excludes.isEmpty()) {
+            Collection<Exclusion> exclusions = excludes.stream().map(RequireProvidedDependenciesInRuntimeClasspath::convertPatternToExclusion).collect(Collectors.toCollection(LinkedList::new));
+            exclusions.add(new Exclusion("*", "*", "*", "pom"));
+            depSelectors.add(new ExclusionDependencySelector(exclusions));
+        }
+        if (!includeOptionals) {
+            depSelectors.add(new OptionalDependencySelector());
+        }
+        if (!includeDirects) {
+            depSelectors.add(new LevelAndScopeExclusionSelector(1, "provided"));
+        }
+        newRepoSession.setDependencySelector(new AndDependencySelector(depSelectors));
+
         // use the ones for https://maven.apache.org/guides/mini/guide-maven-classloading.html#3-plugin-classloaders
+        @SuppressWarnings("deprecation")
         List<Artifact> runtimeArtifacts = project.getRuntimeArtifacts();
+        if (log.isDebugEnabled()) {
+            log.debug("Collected " + runtimeArtifacts.size()+ " runtime dependencies ");
+            for (Artifact runtimeArtifact : runtimeArtifacts) {
+                log.debug(runtimeArtifact.toString());
+            }
+        }
 
+        Dependency rootDependency = RepositoryUtils.toDependency(project.getArtifact(), null);
         try {
             Map<org.eclipse.aether.artifact.Artifact, List<List<DependencyNode>>> artifactMap = collectTransitiveDependencies(
-                    RepositoryUtils.toDependency(project.getArtifact(), null),
-                    repoSystem, newRepoSession, remoteRepositories);
+                    rootDependency, repoSystem, newRepoSession, remoteRepositories, log);
             if (log.isDebugEnabled()) {
                 log.debug("Collected " + artifactMap.size()+ " transitive dependencies: ");
                 for (Entry<org.eclipse.aether.artifact.Artifact, List<List<DependencyNode>>> artifactResult : artifactMap.entrySet()) {
                     log.debug(artifactResult.getKey().toString()
                             + " (" + dumpPaths(artifactResult.getValue()) + ")");
                 }
-                
             }
             // collect all violations
-            Collection<String> violationMessages = new LinkedList<>();
-            checkForMissingArtifacts(artifactMap, runtimeArtifacts, violationMessages, log);
-            if (!violationMessages.isEmpty()) {
-                throw new EnforcerRuleException("Found " + violationMessages.size() + " missing runtime dependencies:" + System.lineSeparator() + String.join(System.lineSeparator(), violationMessages));
+            int numViolations = checkForMissingArtifacts(artifactMap, runtimeArtifacts, log);
+            if (numViolations > 0) {
+                throw new EnforcerRuleException("Found " + numViolations + " missing runtime dependencies. Look at the errors emitted above for the details.");
             }
         } catch (DependencyResolutionException e) {
             // draw graph
@@ -138,10 +174,74 @@ public class RequireProvidedDependenciesInRuntimeClasspath
             throw new EnforcerRuleException("Could not retrieve dependency metadata for project  : "
                     + e.getMessage() + ". Partial dependency tree: " + writer.toString(), e);
         }
+    }
+
+
+    /**
+     * 
+     * @param pattern string in in the format {@code <groupId>[:<artifactId>[:<extension>[:<classifier>]]]}
+     * @return the exclusion
+     */
+    private static Exclusion convertPatternToExclusion(String pattern) {
+        String[] parts = pattern.split(":");
+        if (parts.length > 4) {
+            throw new IllegalArgumentException("Pattern must contain at most three colons, but contains " + parts + ": " + pattern);
+        }
+        String groupId = parts[0];
+        String artifactId = "*";
+        String extension = "*";
+        String classifier = "*";
+        if (parts.length > 1) {
+            artifactId = parts[1];
+        }
+        if (parts.length > 2) {
+            extension = parts[2];
+        }
+        if (parts.length > 3) {
+            classifier = parts[3];
+        }
+        return new Exclusion(groupId, artifactId, classifier, extension);
+    }
+
+    private static final class LevelAndScopeExclusionSelector implements DependencySelector {
+
+        private final int targetLevel;
+        private final String targetScope;
+        private final int currentLevel;
+
+        private static final DependencySelector ALL_SELECTOR = new StaticDependencySelector(true);
+
+        public LevelAndScopeExclusionSelector(int targetLevel, String targetScope) {
+            this(targetLevel, targetScope, 0);
+        }
+
+        private LevelAndScopeExclusionSelector(int targetLevel, String targetScope, int currentLevel) {
+            this.targetLevel = targetLevel;
+            this.targetScope = Objects.requireNonNull(targetScope);
+            this.currentLevel = currentLevel;
+        }
 
+        @Override
+        public boolean selectDependency(Dependency dependency) {
+            if (currentLevel == targetLevel) {
+                return !targetScope.equals(dependency.getScope());
+            }
+            return true;
+        }
+
+        @Override
+        public DependencySelector deriveChildSelector(DependencyCollectionContext context) {
+            if (currentLevel < targetLevel) {
+                return new LevelAndScopeExclusionSelector(targetLevel, targetScope, currentLevel+1);
+            } else {
+                // org.eclipse.aether:aether-util:jar:0.9.0.M2 used at runtime doesn't yet support null for no restrictions
+                return ALL_SELECTOR;
+            }
+        }
     }
 
-    private final class DependencyVisitorPrinter implements DependencyVisitor {
+    
+    private static final class DependencyVisitorPrinter implements DependencyVisitor {
         private final PrintWriter printWriter;
         private String indent = "";
 
@@ -169,19 +269,23 @@ public class RequireProvidedDependenciesInRuntimeClasspath
         }
     }
 
-    protected void checkForMissingArtifacts(Map<org.eclipse.aether.artifact.Artifact, List<List<DependencyNode>>> artifactMap, List<Artifact> runtimeArtifacts,
-            Collection<String> violationMessages, Log log) throws EnforcerRuleException {
+    protected int checkForMissingArtifacts(Map<org.eclipse.aether.artifact.Artifact, List<List<DependencyNode>>> artifactMap, List<Artifact> runtimeArtifacts,
+            Log log) throws EnforcerRuleException {
+        int numViolations = 0;
+        
         for (Entry<org.eclipse.aether.artifact.Artifact, List<List<DependencyNode>>> artifactResult : artifactMap.entrySet()) {
             Artifact dependency = RepositoryUtils.toArtifact(artifactResult.getKey());
             if (ArtifactUtils.checkDependencies(Collections.singleton(dependency), excludes) == null) {
                 if (!isArtifactContainedInList(dependency, runtimeArtifacts)) {
-                    violationMessages.add("Provided dependency " + dependency
-                            + " (" + dumpPaths(artifactResult.getValue()) + ") not found as runtime dependency!");
+                    MessageBuilder msgBuilder = MessageUtils.buffer();
+                    log.error(msgBuilder.a("Provided dependency ").strong(dependency).mojo(" (" + dumpPaths(artifactResult.getValue()) + ")").a(" not found as runtime dependency!").toString());
+                    numViolations++;
                 }
             } else {
                 log.debug("Skip excluded dependency " + dependency);
             }
         }
+        return numViolations;
     }
 
     private static String dumpPaths(List<List<DependencyNode>> paths) {
@@ -196,6 +300,9 @@ public class RequireProvidedDependenciesInRuntimeClasspath
     }
 
     private static String dumpPath(List<DependencyNode> path) {
+        if (path.size() <= 2) {
+            return "";
+        }
         return path.stream()
                 .skip(1) // first entry is the project itself
                 .limit(path.size() - 2l)  // last entry is the dependency (which is logged separately)
@@ -217,22 +324,22 @@ public class RequireProvidedDependenciesInRuntimeClasspath
         return false;
     }
 
-    protected static Map<org.eclipse.aether.artifact.Artifact, List<List<DependencyNode>>> collectTransitiveDependencies(
+    protected Map<org.eclipse.aether.artifact.Artifact, List<List<DependencyNode>>> collectTransitiveDependencies(
             org.eclipse.aether.graph.Dependency rootDependency,
             RepositorySystem repoSystem, RepositorySystemSession repoSession,
-            List<RemoteRepository> remoteRepositories)
+            List<RemoteRepository> remoteRepositories, Log log)
             throws DependencyResolutionException {
         CollectRequest collectRequest = new CollectRequest(rootDependency, remoteRepositories);
-        // project itself (independent of scope) + transitive provided ones
-        DependencyFilter depFilter = new OrDependencyFilter(
-                new PatternInclusionsDependencyFilter(
-                        rootDependency.getArtifact().getGroupId() + ":"
-                                + rootDependency.getArtifact().getArtifactId()),
-                new ScopeDependencyFilter(Collections.singleton("provided"),
-                        Collections.<String> emptySet()));
-        // filtering applied only for DependencyResult.getArtifactResults() but not for DependencyResult.getRoot()!
-        DependencyRequest req = new DependencyRequest(collectRequest, depFilter);
+        DependencyRequest req = new DependencyRequest(collectRequest, null);
         DependencyResult resolutionResult = repoSystem.resolveDependencies(repoSession, req);
+        if (log.isDebugEnabled()) {
+            // draw full dependency graph
+            StringWriter writer = new StringWriter();
+            DependencyVisitor depVisitor = new TreeDependencyVisitor(
+                    new DependencyVisitorPrinter(new PrintWriter(writer)));
+            resolutionResult.getRoot().accept(depVisitor);
+            log.debug("dependency tree: " + writer.toString());
+        }
         // generate a map with key = artifact, value = all paths to it
         return resolutionResult.getArtifactResults().stream()
                 .filter(a -> !a.getArtifact().toString().equals(rootDependency.getArtifact().toString())) // remove rootDependency itself
@@ -245,7 +352,7 @@ public class RequireProvidedDependenciesInRuntimeClasspath
         return visitor.getPaths();
     }
 
-    static final class SingleArtifactFilter implements DependencyFilter {
+    private static final class SingleArtifactFilter implements DependencyFilter {
         private final org.eclipse.aether.artifact.Artifact artifact;
 
         public SingleArtifactFilter(org.eclipse.aether.artifact.Artifact artifact) {
@@ -257,13 +364,15 @@ public class RequireProvidedDependenciesInRuntimeClasspath
         }
     }
 
-    /** Specify the banned dependencies. This can be a list of artifacts in the format <code>groupId[:artifactId][:version]</code>. Any of
-     * the sections can be a wildcard by using '*' (ie group:*:1.0) <br>
-     * The rule will fail if any dependency matches any exclude, unless it also matches an include rule.
-     * 
-     * @param theExcludes the excludes to set */
     public void setExcludes(List<String> theExcludes) {
         this.excludes = theExcludes;
     }
 
+    public void setIncludeOptionalDependencies(boolean includeOptionals) {
+        this.includeOptionals = includeOptionals;
+    }
+
+    public void setIncludeDirectDependencies(boolean includeDirects) {
+        this.includeDirects = includeDirects;
+    }
 }
\ No newline at end of file