com.palantir.baseline.tasks.CheckUnusedDependenciesTask Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of gradle-baseline-java Show documentation
Show all versions of gradle-baseline-java Show documentation
A Gradle plugin for applying Baseline-recommended build and IDE settings
/*
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
*
* 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.palantir.baseline.tasks;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.palantir.baseline.plugins.BaselineExactDependencies;
import com.palantir.gradle.failurereports.exceptions.ExceptionWithSuggestion;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.gradle.api.DefaultTask;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ResolvedArtifact;
import org.gradle.api.artifacts.ResolvedDependency;
import org.gradle.api.file.FileCollection;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;
public class CheckUnusedDependenciesTask extends DefaultTask {
private final ListProperty dependenciesConfigurations;
private final ListProperty sourceOnlyConfigurations;
private final Property sourceClasses;
private final SetProperty ignore;
public CheckUnusedDependenciesTask() {
setGroup("Verification");
setDescription("Ensures no extraneous dependencies are declared");
dependenciesConfigurations = getProject().getObjects().listProperty(Configuration.class);
dependenciesConfigurations.set(Collections.emptyList());
sourceOnlyConfigurations = getProject().getObjects().listProperty(Configuration.class);
sourceOnlyConfigurations.set(Collections.emptyList());
sourceClasses = getProject().getObjects().property(FileCollection.class);
ignore = getProject().getObjects().setProperty(String.class);
ignore.set(Collections.emptySet());
getOutputs().upToDateWhen(_task -> true);
}
@TaskAction
public final void checkUnusedDependencies() {
Set declaredDependencies = dependenciesConfigurations.get().stream()
.map(Configuration::getResolvedConfiguration)
.flatMap(resolved -> resolved.getFirstLevelModuleDependencies().stream())
.collect(Collectors.toSet());
BaselineExactDependencies.INDEXES.populateIndexes(declaredDependencies);
Set declaredArtifacts = declaredDependencies.stream()
.flatMap(dependency -> dependency.getModuleArtifacts().stream())
.filter(dependency ->
BaselineExactDependencies.VALID_ARTIFACT_EXTENSIONS.contains(dependency.getExtension()))
.collect(Collectors.toSet());
excludeSourceOnlyDependencies();
Set necessaryArtifacts = Streams.stream(
sourceClasses.get().iterator())
.flatMap(BaselineExactDependencies::referencedClasses)
.flatMap(BaselineExactDependencies.INDEXES::classToArtifacts)
.collect(Collectors.toSet());
Set possiblyUnused = Sets.difference(declaredArtifacts, necessaryArtifacts);
getLogger()
.debug(
"Possibly unused dependencies: {}",
possiblyUnused.stream()
.map(BaselineExactDependencies::asString)
.sorted()
.collect(Collectors.toList()));
List declaredButUnused = possiblyUnused.stream()
.filter(artifact -> !shouldIgnore(artifact))
.sorted(Comparator.comparing(BaselineExactDependencies::asString))
.collect(Collectors.toList());
if (!declaredButUnused.isEmpty()) {
// TODO(dfox): don't print warnings for jars that define service loaded classes (e.g. meta-inf)
StringBuilder builder = new StringBuilder();
builder.append(String.format(
"Found %s dependencies unused during compilation, please delete them from '%s' or choose one of "
+ "the suggested fixes:\n",
declaredButUnused.size(), buildFile()));
for (ResolvedArtifact resolvedArtifact : declaredButUnused) {
builder.append('\t')
.append(BaselineExactDependencies.asDependencyStringWithName(resolvedArtifact))
.append('\n');
// Suggest fixes by looking at all transitive classes, filtering the ones we have declarations on,
// and mapping the remaining ones back to the jars they came from.
ResolvedDependency dependency =
BaselineExactDependencies.INDEXES.artifactsFromDependency(resolvedArtifact);
Set didYouMean = dependency.getAllModuleArtifacts().stream()
.filter(artifact ->
BaselineExactDependencies.VALID_ARTIFACT_EXTENSIONS.contains(artifact.getExtension()))
.flatMap(BaselineExactDependencies.INDEXES::classesFromArtifact)
.filter(referencedClasses()::contains)
.flatMap(BaselineExactDependencies.INDEXES::classToArtifacts)
.filter(artifact -> !declaredArtifacts.contains(artifact))
.collect(Collectors.toSet());
if (!didYouMean.isEmpty()) {
builder.append("\t\tDid you mean:\n");
didYouMean.stream()
.map(BaselineExactDependencies::asDependencyStringWithoutName)
.sorted()
.forEach(dependencyString -> builder.append("\t\t\t")
.append(dependencyString)
.append("\n"));
}
}
throw new ExceptionWithSuggestion(builder.toString(), buildFile().toString());
}
}
/**
* Excludes any source only dependencies configured by the user, as they would be incorrectly flagged as unused by
* this task due to BaselineExactDependencies use of
* {@link org.apache.maven.shared.dependency.analyzer.asm.ASMDependencyAnalyzer} which only looks at the
* dependencies of the generated byte-code, not the union of compile + runtime dependencies.
*/
private void excludeSourceOnlyDependencies() {
sourceOnlyConfigurations
.get()
.forEach(config -> config.getResolvedConfiguration().getFirstLevelModuleDependencies().stream()
.flatMap(dependency -> dependency.getModuleArtifacts().stream())
.forEach(artifact -> ignoreDependency(config, artifact)));
}
private void ignoreDependency(Configuration config, ResolvedArtifact artifact) {
String dependencyId = BaselineExactDependencies.asString(artifact);
getLogger().info("Ignoring {} dependency: {}", config.getName(), dependencyId);
ignore.add(dependencyId);
}
/** All classes which are mentioned in this project's source code. */
private Set referencedClasses() {
return Streams.stream(sourceClasses.get().iterator())
.flatMap(BaselineExactDependencies::referencedClasses)
.collect(Collectors.toSet());
}
private Path buildFile() {
return getProject()
.getRootDir()
.toPath()
.relativize(getProject().getBuildFile().toPath());
}
private boolean shouldIgnore(ResolvedArtifact artifact) {
return ignore.get().contains(BaselineExactDependencies.asString(artifact));
}
@Classpath
public final ListProperty getDependenciesConfigurations() {
return dependenciesConfigurations;
}
public final void dependenciesConfiguration(Configuration dependenciesConfiguration) {
this.dependenciesConfigurations.add(Objects.requireNonNull(dependenciesConfiguration));
}
@Input
public final Provider> getSourceOnlyConfigurations() {
return sourceOnlyConfigurations;
}
/**
* Don't use this unless this configuration is resolvable.
*
* @deprecated This task only looks at directly declared compile dependencies that also appear in the
* runtime classpath, so there's no need to exclude anything like {@code compileOnly} anymore.
*/
@Deprecated
public final void sourceOnlyConfiguration(Configuration configuration) {
Preconditions.checkNotNull(configuration, "This method requires a non-null configuration");
Preconditions.checkArgument(
configuration.isCanBeResolved(),
"May only add sourceOnlyConfiguration if it is resolvable: %s",
configuration);
this.sourceOnlyConfigurations.add(Objects.requireNonNull(configuration));
}
@Classpath
public final Provider getSourceClasses() {
return sourceClasses;
}
public final void setSourceClasses(FileCollection newClasses) {
this.sourceClasses.set(getProject().files(newClasses));
}
public final void ignore(Provider> value) {
ignore.addAll(value);
}
public final void ignore(String group, String name) {
ignore.add(BaselineExactDependencies.ignoreCoordinate(group, name));
}
@Input
public final Provider> getIgnored() {
return ignore;
}
}