001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.maven.enforcer.rules.dependency; 020 021import javax.inject.Inject; 022import javax.inject.Named; 023 024import java.text.ChoiceFormat; 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.List; 029import java.util.Objects; 030import java.util.function.Predicate; 031import java.util.stream.Collectors; 032 033import org.apache.maven.enforcer.rule.api.EnforcerRuleException; 034import org.apache.maven.enforcer.rules.AbstractStandardEnforcerRule; 035import org.apache.maven.enforcer.rules.utils.ArtifactMatcher; 036import org.apache.maven.enforcer.rules.utils.ArtifactUtils; 037import org.apache.maven.execution.MavenSession; 038import org.apache.maven.project.MavenProject; 039import org.eclipse.aether.RepositorySystem; 040import org.eclipse.aether.graph.DependencyFilter; 041import org.eclipse.aether.graph.DependencyNode; 042import org.eclipse.aether.util.graph.manager.DependencyManagerUtils; 043import org.eclipse.aether.util.graph.visitor.PathRecordingDependencyVisitor; 044import org.eclipse.aether.util.version.GenericVersionScheme; 045import org.eclipse.aether.version.InvalidVersionSpecificationException; 046import org.eclipse.aether.version.VersionConstraint; 047 048/** 049 * This rule bans dependencies having a version which requires resolution (i.e. dynamic versions which might change with 050 * each build). Dynamic versions are either 051 * <ul> 052 * <li>version ranges,</li> 053 * <li>the special placeholders {@code LATEST} or {@code RELEASE} or</li> 054 * <li>versions ending with {@code -SNAPSHOT}. 055 * </ul> 056 * 057 * @since 3.2.0 058 */ 059@Named("banDynamicVersions") 060public final class BanDynamicVersions extends AbstractStandardEnforcerRule { 061 062 private static final String RELEASE = "RELEASE"; 063 064 private static final String LATEST = "LATEST"; 065 066 private static final String SNAPSHOT_SUFFIX = "-SNAPSHOT"; 067 068 /** 069 * {@code true} if versions ending with {@code -SNAPSHOT} should be allowed 070 */ 071 private boolean allowSnapshots; 072 073 /** 074 * {@code true} if versions using {@code LATEST} should be allowed 075 */ 076 private boolean allowLatest; 077 078 /** 079 * {@code true} if versions using {@code RELEASE} should be allowed 080 */ 081 private boolean allowRelease; 082 083 /** 084 * {@code true} if version ranges should be allowed 085 */ 086 private boolean allowRanges; 087 088 /** 089 * {@code true} if ranges having the same upper and lower bound like {@code [1.0]} should be allowed. 090 * Only applicable if {@link #allowRanges} is not set to {@code true}. 091 */ 092 private boolean allowRangesWithIdenticalBounds; 093 094 /** 095 * {@code true} if optional dependencies should not be checked 096 */ 097 private boolean excludeOptionals; 098 099 /** 100 * the scopes of dependencies which should be excluded from this rule 101 */ 102 private List<String> excludedScopes = Collections.emptyList(); 103 104 /** 105 * Specify the ignored dependencies. This can be a list of artifacts in the format 106 * <code>groupId[:artifactId[:version[:type[:scope:[classifier]]]]]</code>. 107 * Any of the sections can be a wildcard by using '*' (e.g. {@code group:*:1.0}). 108 * <br> 109 * Any of the ignored dependencies may have dynamic versions. 110 */ 111 private List<String> ignores = null; 112 113 /** 114 * {@code true} if dependencies should be checked before Maven computes the final 115 * dependency tree. Setting this property will make the rule check dependencies 116 * before any conflicts are resolved. This is similar to the {@code verbose} 117 * parameter for the {@code tree} goal for {@code maven-dependency-plugin}. 118 */ 119 private boolean verbose; 120 121 private final ResolverUtil resolverUtil; 122 123 @Inject 124 public BanDynamicVersions( 125 MavenProject project, RepositorySystem repoSystem, MavenSession mavenSession, ResolverUtil resolverUtil) { 126 this.resolverUtil = Objects.requireNonNull(resolverUtil); 127 } 128 129 private final class BannedDynamicVersionCollector implements DependencyFilter { 130 131 private boolean isRoot = true; 132 133 private List<String> violations; 134 135 private final Predicate<DependencyNode> predicate; 136 137 private GenericVersionScheme versionScheme; 138 139 public List<String> getViolations() { 140 return violations; 141 } 142 143 BannedDynamicVersionCollector(Predicate<DependencyNode> predicate) { 144 this.predicate = predicate; 145 this.violations = new ArrayList<>(); 146 this.versionScheme = new GenericVersionScheme(); 147 } 148 149 private boolean isBannedDynamicVersion(VersionConstraint versionConstraint) { 150 if (versionConstraint.getVersion() != null) { 151 if (versionConstraint.getVersion().toString().equals(LATEST)) { 152 return !allowLatest; 153 } else if (versionConstraint.getVersion().toString().equals(RELEASE)) { 154 return !allowRelease; 155 } else if (versionConstraint.getVersion().toString().endsWith(SNAPSHOT_SUFFIX)) { 156 return !allowSnapshots; 157 } 158 } else if (versionConstraint.getRange() != null) { 159 if (allowRangesWithIdenticalBounds 160 && Objects.equals( 161 versionConstraint.getRange().getLowerBound(), 162 versionConstraint.getRange().getUpperBound())) { 163 return false; 164 } 165 return !allowRanges; 166 } else { 167 getLog().warn("Unexpected version constraint found: " + versionConstraint); 168 } 169 return false; 170 } 171 172 @Override 173 public boolean accept(DependencyNode node, List<DependencyNode> parents) { 174 if (isRoot) { 175 isRoot = false; 176 return false; 177 } 178 getLog().debug("Found node " + node + " with version constraint " + node.getVersionConstraint()); 179 if (!predicate.test(node)) { 180 return false; 181 } 182 VersionConstraint versionConstraint = node.getVersionConstraint(); 183 if (isBannedDynamicVersion(versionConstraint)) { 184 addViolation(versionConstraint, node, parents); 185 return true; 186 } 187 try { 188 if (verbose) { 189 String premanagedVersion = DependencyManagerUtils.getPremanagedVersion(node); 190 if (premanagedVersion != null) { 191 VersionConstraint premanagedContraint = versionScheme.parseVersionConstraint(premanagedVersion); 192 if (isBannedDynamicVersion(premanagedContraint)) { 193 addViolation(premanagedContraint, node, parents); 194 return true; 195 } 196 } 197 } 198 } catch (InvalidVersionSpecificationException ex) { 199 // This should never happen. 200 throw new RuntimeException("Failed to parse version for " + node, ex); 201 } 202 return false; 203 } 204 205 private void addViolation( 206 VersionConstraint versionContraint, DependencyNode node, List<DependencyNode> parents) { 207 List<DependencyNode> intermediatePath = new ArrayList<>(parents); 208 if (!intermediatePath.isEmpty()) { 209 // This project is also included in the path, but we do 210 // not want that in the report. 211 intermediatePath.remove(intermediatePath.size() - 1); 212 } 213 violations.add("Dependency " 214 + node.getDependency() 215 + dumpIntermediatePath(intermediatePath) 216 + " is referenced with a banned dynamic version " 217 + versionContraint); 218 } 219 } 220 221 @Override 222 public void execute() throws EnforcerRuleException { 223 DependencyNode rootDependency = 224 resolverUtil.resolveTransitiveDependencies(verbose, excludeOptionals, excludedScopes); 225 226 List<String> violations = collectDependenciesWithBannedDynamicVersions(rootDependency); 227 if (!violations.isEmpty()) { 228 ChoiceFormat dependenciesFormat = new ChoiceFormat("1#dependency|1<dependencies"); 229 throw new EnforcerRuleException("Found " + violations.size() + " " 230 + dependenciesFormat.format(violations.size()) 231 + " with dynamic versions." + System.lineSeparator() 232 + String.join(System.lineSeparator(), violations)); 233 } 234 } 235 236 private static String dumpIntermediatePath(Collection<DependencyNode> path) { 237 if (path.isEmpty()) { 238 return ""; 239 } 240 return " via " + path.stream().map(n -> n.getArtifact().toString()).collect(Collectors.joining(" -> ")); 241 } 242 243 private static final class ExcludeArtifactPatternsPredicate implements Predicate<DependencyNode> { 244 245 private final ArtifactMatcher artifactMatcher; 246 247 ExcludeArtifactPatternsPredicate(List<String> excludes) { 248 this.artifactMatcher = new ArtifactMatcher(excludes, Collections.emptyList()); 249 } 250 251 @Override 252 public boolean test(DependencyNode depNode) { 253 return !artifactMatcher.match(ArtifactUtils.toArtifact(depNode)); 254 } 255 } 256 257 private List<String> collectDependenciesWithBannedDynamicVersions(DependencyNode rootDependency) { 258 Predicate<DependencyNode> predicate; 259 if (ignores != null && !ignores.isEmpty()) { 260 predicate = new ExcludeArtifactPatternsPredicate(ignores); 261 } else { 262 predicate = d -> true; 263 } 264 BannedDynamicVersionCollector collector = new BannedDynamicVersionCollector(predicate); 265 rootDependency.accept(new PathRecordingDependencyVisitor(collector)); 266 return collector.getViolations(); 267 } 268 269 public void setVerbose(boolean verbose) { 270 this.verbose = verbose; 271 } 272 273 @Override 274 public String toString() { 275 return String.format( 276 "BanDynamicVersions[allowSnapshots=%b, allowLatest=%b, allowRelease=%b, allowRanges=%b, allowRangesWithIdenticalBounds=%b, excludeOptionals=%b, excludedScopes=%s, ignores=%s, verbose=%b]", 277 allowSnapshots, 278 allowLatest, 279 allowRelease, 280 allowRanges, 281 allowRangesWithIdenticalBounds, 282 excludeOptionals, 283 excludedScopes, 284 ignores, 285 verbose); 286 } 287}