Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BaselineExactDependencies applies to all source sets #1262

Merged
merged 13 commits into from
Feb 28, 2020
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-1262.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: improvement
improvement:
description: BaselineExactDependencies applies to all source sets
links:
- https://github.com/palantir/gradle-baseline/pull/1262
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package com.palantir.baseline.plugins;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.palantir.baseline.tasks.CheckImplicitDependenciesTask;
import com.palantir.baseline.tasks.CheckUnusedDependenciesTask;
Expand All @@ -35,16 +37,19 @@
import org.apache.maven.shared.dependency.analyzer.asm.ASMDependencyAnalyzer;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ExcludeRule;
import org.gradle.api.artifacts.ModuleVersionIdentifier;
import org.gradle.api.artifacts.ResolvedArtifact;
import org.gradle.api.artifacts.ResolvedDependency;
import org.gradle.api.artifacts.component.ComponentIdentifier;
import org.gradle.api.artifacts.component.ProjectComponentIdentifier;
import org.gradle.api.attributes.Usage;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.util.GUtil;

/** Validates that java projects declare exactly the dependencies they rely on, no more and no less. */
public final class BaselineExactDependencies implements Plugin<Project> {
Expand All @@ -60,45 +65,100 @@ public final class BaselineExactDependencies implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPluginManager().withPlugin("java", plugin -> {
SourceSet mainSourceSet = project.getConvention()
TaskProvider<Task> checkUnusedDependencies = project.getTasks().register("checkUnusedDependencies");
TaskProvider<Task> checkImplicitDependencies = project.getTasks().register("checkImplicitDependencies");

project.getConvention()
.getPlugin(JavaPluginConvention.class)
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
Configuration compileClasspath =
project.getConfigurations().getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME);
Configuration compileOnly =
project.getConfigurations().getByName(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME);
Configuration justCompileOnlyResolvable = project.getConfigurations()
.create("baseline-exact-dependencies-compileOnly", conf -> {
conf.setVisible(false);
conf.setCanBeConsumed(false);
conf.extendsFrom(compileOnly);
// Important! this ensures we resolve 'compile' variants rather than 'runtime'
// This is the same attribute that's being set on compileClasspath
conf.getAttributes()
.attribute(
Usage.USAGE_ATTRIBUTE,
project.getObjects().named(Usage.class, Usage.JAVA_API));
});

project.getTasks().create("checkUnusedDependencies", CheckUnusedDependenciesTask.class, task -> {
task.dependsOn(JavaPlugin.CLASSES_TASK_NAME);
task.setSourceClasses(mainSourceSet.getOutput().getClassesDirs());
task.dependenciesConfiguration(compileClasspath);
task.sourceOnlyConfiguration(justCompileOnlyResolvable);

// this is liberally applied to ease the Java8 -> 11 transition
task.ignore("javax.annotation", "javax.annotation-api");
});
.all(sourceSet ->
configureSourceSet(project, sourceSet, checkUnusedDependencies, checkImplicitDependencies));
});
}

project.getTasks().create("checkImplicitDependencies", CheckImplicitDependenciesTask.class, task -> {
task.dependsOn(JavaPlugin.CLASSES_TASK_NAME);
task.setSourceClasses(mainSourceSet.getOutput().getClassesDirs());
task.dependenciesConfiguration(compileClasspath);
private static void configureSourceSet(
Project project,
SourceSet sourceSet,
TaskProvider<Task> checkUnusedDependencies,
TaskProvider<Task> checkImplicitDependencies) {
Configuration implementation =
project.getConfigurations().getByName(sourceSet.getImplementationConfigurationName());
Configuration compile = project.getConfigurations().getByName(sourceSet.getCompileConfigurationName());
Configuration compileClasspath =
project.getConfigurations().getByName(sourceSet.getCompileClasspathConfigurationName());

task.ignore("org.slf4j", "slf4j-api");
});
Configuration explicitCompile = project.getConfigurations()
.create("baseline-exact-dependencies-" + sourceSet.getName(), conf -> {
conf.setDescription(String.format(
"Tracks the explicit (not inherited) dependencies added to either %s or %s",
compile.toString(), implementation.toString()));
conf.setVisible(false);
conf.setCanBeConsumed(false);
// Important! this ensures we resolve 'compile' variants rather than 'runtime'
// This is the same attribute that's being set on compileClasspath
conf.getAttributes()
.attribute(
Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, Usage.JAVA_API));
});

// Figure out what our compile dependencies are while ignoring dependencies we've inherited from other source
// sets. For example, if we are `test`, some of our configurations extend from the `main` source set:
// testImplementation extendsFrom(implementation)
// \-- testCompile extendsFrom(compile)
// We therefore want to look at only the dependencies _directly_ declared in the implementation and compile
// configurations (belonging to our source set)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we write a test for this?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, wrote one

explicitCompile.withDependencies(deps -> {
Configuration implCopy = implementation.copy();
Configuration compileCopy = compile.copy();
// Without these, explicitCompile will successfully resolve 0 files and you'll waste 1 hour trying
// to figure out why.
project.getConfigurations().add(implCopy);
project.getConfigurations().add(compileCopy);

explicitCompile.extendsFrom(implCopy, compileCopy);
// Mirror the transitive constraints form compileClasspath in order to pick up GCV locks.
// We should really do this with an addAllLater but that would require Gradle 6, or a hacky workaround.
explicitCompile.getDependencyConstraints().addAll(compileClasspath.getAllDependencyConstraints());
// Inherit the excludes from compileClasspath too (that get aggregated from all its super-configurations).
compileClasspath.getExcludeRules().forEach(rule -> explicitCompile.exclude(excludeRuleAsMap(rule)));
});

TaskProvider<CheckUnusedDependenciesTask> sourceSetUnusedDependencies = project.getTasks()
.register(
GUtil.toLowerCamelCase("checkUnusedDependencies " + sourceSet.getName()),
CheckUnusedDependenciesTask.class,
task -> {
task.dependsOn(sourceSet.getClassesTaskName());
task.setSourceClasses(sourceSet.getOutput().getClassesDirs());
task.dependenciesConfiguration(explicitCompile);

// this is liberally applied to ease the Java8 -> 11 transition
task.ignore("javax.annotation", "javax.annotation-api");
});
checkUnusedDependencies.configure(task -> task.dependsOn(sourceSetUnusedDependencies));
TaskProvider<CheckImplicitDependenciesTask> sourceSetCheckImplicitDependencies = project.getTasks()
.register(
GUtil.toLowerCamelCase("checkImplicitDependencies " + sourceSet.getName()),
CheckImplicitDependenciesTask.class,
task -> {
task.dependsOn(sourceSet.getClassesTaskName());
task.setSourceClasses(sourceSet.getOutput().getClassesDirs());
task.dependenciesConfiguration(compileClasspath);

task.ignore("org.slf4j", "slf4j-api");
});
checkImplicitDependencies.configure(task -> task.dependsOn(sourceSetCheckImplicitDependencies));
}

private static Map<String, String> excludeRuleAsMap(ExcludeRule rule) {
Builder<String, String> excludeRule = ImmutableMap.builder();
if (rule.getGroup() != null) {
excludeRule.put("group", rule.getGroup());
}
if (rule.getModule() != null) {
excludeRule.put("module", rule.getModule());
}
return excludeRule.build();
}

/** Given a {@code com/palantir/product/Foo.class} file, what other classes does it import/reference. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

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;
Expand Down Expand Up @@ -190,6 +191,11 @@ public final Provider<List<Configuration>> getSourceOnlyConfigurations() {
}

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);
Copy link
Contributor

Choose a reason for hiding this comment

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

To be fair this method shouldn't exist, because I don't know in what situation users might actually use it.
The name is confusing now too since it implies source-only configurations need to be added, but we already add (ignore) all compileOnly configurations from all source sets.

But for now, let's at least throw eagerly if the user gave us a bad configuration.

this.sourceOnlyConfigurations.add(Objects.requireNonNull(configuration));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class BaselineExactDependenciesTest extends AbstractPluginTest {
then:
BuildResult result = with('checkUnusedDependencies', '--stacktrace').buildAndFail()
result.task(':classes').getOutcome() == TaskOutcome.SUCCESS
result.task(':checkUnusedDependencies').getOutcome() == TaskOutcome.FAILED
result.task(':checkUnusedDependenciesMain').getOutcome() == TaskOutcome.FAILED
result.output.contains("Found 1 dependencies unused during compilation")
}

Expand All @@ -109,6 +109,7 @@ class BaselineExactDependenciesTest extends AbstractPluginTest {
BuildResult result = with('checkUnusedDependencies', '--stacktrace').build()
result.task(':classes').getOutcome() == TaskOutcome.SUCCESS
result.task(':checkUnusedDependencies').getOutcome() == TaskOutcome.SUCCESS
result.task(':checkUnusedDependenciesMain').getOutcome() == TaskOutcome.SUCCESS
}

def 'checkImplicitDependencies fails when a class is imported without being declared as a dependency'() {
Expand All @@ -134,7 +135,7 @@ class BaselineExactDependenciesTest extends AbstractPluginTest {
then:
BuildResult result = with('checkImplicitDependencies', '--stacktrace').buildAndFail()
result.task(':classes').getOutcome() == TaskOutcome.SUCCESS
result.task(':checkImplicitDependencies').getOutcome() == TaskOutcome.FAILED
result.task(':checkImplicitDependenciesMain').getOutcome() == TaskOutcome.FAILED
result.output.contains("Found 1 implicit dependencies")
}

Expand All @@ -156,7 +157,7 @@ class BaselineExactDependenciesTest extends AbstractPluginTest {
then:
BuildResult result = with('checkImplicitDependencies', '--stacktrace').withDebug(true).buildAndFail()
result.task(':classes').getOutcome() == TaskOutcome.SUCCESS
result.task(':checkImplicitDependencies').getOutcome() == TaskOutcome.FAILED
result.task(':checkImplicitDependenciesMain').getOutcome() == TaskOutcome.FAILED
result.output.contains("Found 1 implicit dependencies")
result.output.contains("implementation project(':sub-project-no-deps')")
}
Expand Down