blob: 560ff0ccd8273f3e4225f3fafa3547417d1ceb1b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.maven.enforcer.rules.dependency;
import javax.inject.Inject;
import javax.inject.Named;
import java.text.ChoiceFormat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
import org.apache.maven.enforcer.rules.AbstractStandardEnforcerRule;
import org.apache.maven.enforcer.rules.utils.ArtifactMatcher;
import org.apache.maven.enforcer.rules.utils.ArtifactUtils;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.project.MavenProject;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.collection.DependencyCollectionException;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.graph.DependencyVisitor;
import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor;
import org.eclipse.aether.version.VersionConstraint;
/**
* This rule bans dependencies having a version which requires resolution (i.e. dynamic versions which might change with
* each build). Dynamic versions are either
* <ul>
* <li>version ranges,</li>
* <li>the special placeholders {@code LATEST} or {@code RELEASE} or</li>
* <li>versions ending with {@code -SNAPSHOT}.
* </ul>
*
* @since 3.2.0
*/
@Named("banDynamicVersions")
public final class BanDynamicVersions extends AbstractStandardEnforcerRule {
private static final String RELEASE = "RELEASE";
private static final String LATEST = "LATEST";
private static final String SNAPSHOT_SUFFIX = "-SNAPSHOT";
/**
* {@code true} if versions ending with {@code -SNAPSHOT} should be allowed
*/
private boolean allowSnapshots;
/**
* {@code true} if versions using {@code LATEST} should be allowed
*/
private boolean allowLatest;
/**
* {@code true} if versions using {@code RELEASE} should be allowed
*/
private boolean allowRelease;
/**
* {@code true} if version ranges should be allowed
*/
private boolean allowRanges;
/**
* {@code true} if ranges having the same upper and lower bound like {@code [1.0]} should be allowed.
* Only applicable if {@link #allowRanges} is not set to {@code true}.
*/
private boolean allowRangesWithIdenticalBounds;
/**
* {@code true} if optional dependencies should not be checked
*/
private boolean excludeOptionals;
/**
* the scopes of dependencies which should be excluded from this rule
*/
private List<String> excludedScopes = Collections.emptyList();
/**
* Specify the ignored dependencies. This can be a list of artifacts in the format
* <code>groupId[:artifactId[:version[:type[:scope:[classifier]]]]]</code>.
* Any of the sections can be a wildcard by using '*' (e.g. {@code group:*:1.0}).
* <br>
* Any of the ignored dependencies may have dynamic versions.
*/
private List<String> ignores = null;
private final ResolverUtil resolverUtil;
@Inject
public BanDynamicVersions(
MavenProject project, RepositorySystem repoSystem, MavenSession mavenSession, ResolverUtil resolverUtil) {
this.resolverUtil = Objects.requireNonNull(resolverUtil);
}
private final class BannedDynamicVersionCollector implements DependencyVisitor {
private final Deque<DependencyNode> nodeStack; // all intermediate nodes (without the root node)
private boolean isRoot = true;
private List<String> violations;
private final Predicate<DependencyNode> predicate;
public List<String> getViolations() {
return violations;
}
BannedDynamicVersionCollector(Predicate<DependencyNode> predicate) {
this.nodeStack = new ArrayDeque<>();
this.predicate = predicate;
this.isRoot = true;
this.violations = new ArrayList<>();
}
private boolean isBannedDynamicVersion(VersionConstraint versionConstraint) {
if (versionConstraint.getVersion() != null) {
if (versionConstraint.getVersion().toString().equals(LATEST)) {
return !allowLatest;
} else if (versionConstraint.getVersion().toString().equals(RELEASE)) {
return !allowRelease;
} else if (versionConstraint.getVersion().toString().endsWith(SNAPSHOT_SUFFIX)) {
return !allowSnapshots;
}
} else if (versionConstraint.getRange() != null) {
if (allowRangesWithIdenticalBounds
&& Objects.equals(
versionConstraint.getRange().getLowerBound(),
versionConstraint.getRange().getUpperBound())) {
return false;
}
return !allowRanges;
} else {
getLog().warn("Unexpected version constraint found: " + versionConstraint);
}
return false;
}
@Override
public boolean visitEnter(DependencyNode node) {
if (isRoot) {
isRoot = false;
} else {
getLog().debug("Found node " + node + " with version constraint " + node.getVersionConstraint());
if (predicate.test(node) && isBannedDynamicVersion(node.getVersionConstraint())) {
violations.add("Dependency "
+ node.getDependency()
+ dumpIntermediatePath(nodeStack)
+ " is referenced with a banned dynamic version "
+ node.getVersionConstraint());
return false;
}
nodeStack.addLast(node);
}
return true;
}
@Override
public boolean visitLeave(DependencyNode node) {
if (!nodeStack.isEmpty()) {
nodeStack.removeLast();
}
return true;
}
}
@Override
public void execute() throws EnforcerRuleException {
try {
DependencyNode rootDependency =
resolverUtil.resolveTransitiveDependencies(excludeOptionals, excludedScopes);
List<String> violations = collectDependenciesWithBannedDynamicVersions(rootDependency);
if (!violations.isEmpty()) {
ChoiceFormat dependenciesFormat = new ChoiceFormat("1#dependency|1<dependencies");
throw new EnforcerRuleException("Found " + violations.size() + " "
+ dependenciesFormat.format(violations.size())
+ " with dynamic versions." + System.lineSeparator()
+ String.join(System.lineSeparator(), violations));
}
} catch (DependencyCollectionException e) {
throw new EnforcerRuleException("Could not retrieve dependency metadata for project", e);
}
}
private static String dumpIntermediatePath(Collection<DependencyNode> path) {
if (path.isEmpty()) {
return "";
}
return " via " + path.stream().map(n -> n.getArtifact().toString()).collect(Collectors.joining(" -> "));
}
private static final class ExcludeArtifactPatternsPredicate implements Predicate<DependencyNode> {
private final ArtifactMatcher artifactMatcher;
ExcludeArtifactPatternsPredicate(List<String> excludes) {
this.artifactMatcher = new ArtifactMatcher(excludes, Collections.emptyList());
}
@Override
public boolean test(DependencyNode depNode) {
return artifactMatcher.match(ArtifactUtils.toArtifact(depNode));
}
}
private List<String> collectDependenciesWithBannedDynamicVersions(DependencyNode rootDependency)
throws DependencyCollectionException {
Predicate<DependencyNode> predicate;
if (ignores != null && !ignores.isEmpty()) {
predicate = new ExcludeArtifactPatternsPredicate(ignores);
} else {
predicate = d -> true;
}
BannedDynamicVersionCollector bannedDynamicVersionCollector = new BannedDynamicVersionCollector(predicate);
DependencyVisitor depVisitor = new TreeDependencyVisitor(bannedDynamicVersionCollector);
rootDependency.accept(depVisitor);
return bannedDynamicVersionCollector.getViolations();
}
@Override
public String toString() {
return String.format(
"BanDynamicVersions[allowSnapshots=%b, allowLatest=%b, allowRelease=%b, allowRanges=%b, allowRangesWithIdenticalBounds=%b, excludeOptionals=%b, excludedScopes=%s, ignores=%s]",
allowSnapshots,
allowLatest,
allowRelease,
allowRanges,
allowRangesWithIdenticalBounds,
excludeOptionals,
excludedScopes,
ignores);
}
}