Skip to content

Commit

Permalink
Create a :spotlessRegisterDependencies task, and use it to resolve th…
Browse files Browse the repository at this point in the history
…e Gradle 6+ deprecation warnings.
  • Loading branch information
nedtwigg committed Jan 1, 2020
1 parent 28d8962 commit 892203d
Show file tree
Hide file tree
Showing 6 changed files with 420 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ public void target(Object... targets) {
this.target = parseTargetsIsExclude(targets, false);
}

/** Sets the target to be empty without a warning. */
public void targetEmptyForDeclaration() {
this.target = getProject().files();
}

This comment has been minimized.

Copy link
@jbduncan

jbduncan Jan 1, 2020

Member

@nedtwigg If it were me, I'd be tempted to rename this method to noTarget() to make the target in the build file easier to read and understand.

WDYT?

This comment has been minimized.

Copy link
@jbduncan

jbduncan Jan 1, 2020

Member

That being said, I can understand why you named this method targetEmptyForDeclaration(), and I've just seen the warning message that will be raised to the user when the problem addressed by this commit pops up. For the given warning message, noTarget() might actually be more confusing.

This comment has been minimized.

Copy link
@nedtwigg

nedtwigg Jan 1, 2020

Author Member

I'm gonna use this to integrate with a few of my projects and see how it feels. I'd like to stick with something that starts with target for autocomplete-discovery with our other target-related methods. I agree that targetEmpty() or targetNone() would be simpler, but I don't see why anyone would ever have an empty target besides this root-project declaration thing.

Related is the "throw a warning for an empty target" idea (ala #437 and #111). It won't be part of this PR, but it's something that I'm thinking about to decide if targetEmptyForDeclaration() will throw an error if its applied anywhere besides the root project or not...

This comment has been minimized.

Copy link
@nedtwigg

nedtwigg Jan 1, 2020

Author Member

Ha! Moot point, figured out a way that we don't need it afterall.

This comment has been minimized.

Copy link
@jbduncan

jbduncan Jan 2, 2020

Member

Ooh fantastic. 👍


/**
* Sets which files will be excluded from formatting. Files to be formatted = (target - targetExclude).
*
Expand Down Expand Up @@ -603,6 +608,16 @@ protected void setupTask(SpotlessTask task) {
}
task.setSteps(steps);
task.setLineEndingsPolicy(getLineEndings().createPolicy(getProject().getProjectDir(), () -> task.target));
if (root.registerDependenciesTask != null) {
// if we have a register dependencies task
if (root.project == root.project.getRootProject()) {
// :spotlessRegisterDependencies depends on every SpotlessTask in the root
root.registerDependenciesTask.dependsOn(task);
} else {
// and every SpotlessTask in a subproject depends on :spotlessRegisterDependencies
task.dependsOn(root.registerDependenciesTask);
}
}
}

/** Returns the project that this extension is attached to. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ public class GradleProvisioner {
private GradleProvisioner() {}

public static Provisioner fromProject(Project project) {
RegisterDependenciesTask task = project.getPlugins().apply(SpotlessPlugin.class).getExtension().registerDependenciesTask;
if (task == null) {
return fromRootBuildscript(project);
} else {
if (project.getRootProject() == project) {
return task.rootProvisioner;
} else {
return new RegisterDependenciesInRoot.SubProvisioner(task.rootProvisioner, project);
}
}
}

static Provisioner fromRootBuildscript(Project project) {
Objects.requireNonNull(project);
return (withTransitives, mavenCoords) -> {
try {
Expand Down Expand Up @@ -60,6 +73,5 @@ public static Provisioner fromProject(Project project) {
};
}

private static final Logger logger = Logger.getLogger(GradleProvisioner.class.getName());

static final Logger logger = Logger.getLogger(GradleProvisioner.class.getName());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* Copyright 2016 DiffPlug
*
* 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.diffplug.gradle.spotless;

import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import javax.annotation.Nullable;

import org.gradle.api.Project;
import org.gradle.util.GradleVersion;

import com.diffplug.common.base.Preconditions;
import com.diffplug.common.collect.ImmutableList;
import com.diffplug.spotless.FormatterStep;
import com.diffplug.spotless.Provisioner;

class RegisterDependenciesInRoot {
static final GradleVersion STRICT_CONFIG_ACCESS_WARNING = GradleVersion.version("6.0");

static final String ENABLE_KEY = "spotless_register_dependencies_in_root";
static final String TASK_NAME = "spotlessRegisterDependencies";

/** Determines if the "spotless_register_dependencies_in_root" mode is enabled. */
public static boolean isEnabled(Project project) {
Object enable = project.getRootProject().findProperty(ENABLE_KEY);
if (Boolean.TRUE.equals(enable) || "true".equals(enable)) {
return true;
}
boolean onlyOneProjectInEntireBuild = project == project.getRootProject()
&& project.getChildProjects().isEmpty();
if (onlyOneProjectInEntireBuild) {
return false;
}
if (GradleVersion.current().compareTo(STRICT_CONFIG_ACCESS_WARNING) >= 0) {
return true;
}
return false;
}

/** Models a request to the provisioner. */
private static class Request {
final boolean withTransitives;
final ImmutableList<String> mavenCoords;

public Request(boolean withTransitives, Collection<String> mavenCoords) {
this.withTransitives = withTransitives;
this.mavenCoords = ImmutableList.copyOf(mavenCoords);
}

@Override
public int hashCode() {
return withTransitives ? mavenCoords.hashCode() : ~mavenCoords.hashCode();
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof Request) {
Request o = (Request) obj;
return o.withTransitives == withTransitives && o.mavenCoords.equals(mavenCoords);
} else {
return false;
}
}

@Override
public String toString() {
String coords = mavenCoords.toString();
StringBuilder builder = new StringBuilder();
builder.append(coords.substring(1, coords.length() - 1)); // strip off []
if (withTransitives) {
builder.append(" with transitives");
} else {
builder.append(" no transitives");
}
return builder.toString();
}
}

/** The provisioner used for all sub-projects. */
static class SubProvisioner implements Provisioner {
private final RootProvisioner root;
private final Project project;

public SubProvisioner(RootProvisioner root, Project project) {
this.root = Objects.requireNonNull(root);
this.project = Objects.requireNonNull(project);
}

@Override
public Set<File> provisionWithTransitives(boolean withTransitives, Collection<String> mavenCoordinates) {
return root.provisionForSub(project, withTransitives, mavenCoordinates);
}
}

/** The provisioner used for the root project. */
static class RootProvisioner implements Provisioner {
private final Project rootProject;

RootProvisioner(Project rootProject) {
Preconditions.checkArgument(rootProject == rootProject.getRootProject());
this.rootProject = rootProject;
}

@Override
public Set<File> provisionWithTransitives(boolean withTransitives, Collection<String> mavenCoordinates) {
return doProvision(new Request(withTransitives, mavenCoordinates), true);
}

private Map<Request, Set<File>> cache = new HashMap<>();

/** Guaranteed to return non-null for internal requests, but might return null for an external request which isn't cached already. */
private synchronized @Nullable Set<File> doProvision(Request req, boolean isRoot) {
Set<File> result = cache.get(req);
if (result != null) {
return result;
}
if (isRoot) {
result = GradleProvisioner.fromRootBuildscript(rootProject).provisionWithTransitives(req.withTransitives, req.mavenCoords);
cache.put(req, result);
return result;
} else {
return null;
}
}

private Set<File> provisionForSub(Project project, boolean withTransitives, Collection<String> mavenCoordinates) {
Request req = new Request(withTransitives, mavenCoordinates);
Set<File> result = doProvision(req, false);
if (result != null) {
return result;
} else {
// if it wasn't cached, complain loudly and use the crappy workaround
GradleProvisioner.logger.severe(warningMsg(req));
return GradleProvisioner.fromRootBuildscript(project).provisionWithTransitives(withTransitives, mavenCoordinates);
}
}
}

private static String warningMsg(Request requestedDeps) {
FormatterStep beingResolved = FormatterStep.lazyStepBeingResolvedInThisThread();
return String.format(
"This subproject is using a formatter that was not used in the root project. To enable%n" +
"performance optimzations (and avoid Gradle 7 deprecation warnings), you must declare%n" +
"all of your formatters within the root project. For example, if your subproject has%n" +
"a `java {}` block but your root project does not, just add a matching `java {}` block to%n" +
"your root project. If you want to make it clear that it is intentional that the target%n" +
"is empty, you can do this in your root build.gradle:%n" +
"%n" +
" spotless {%n" +
" java {%n" +
" targetEmptyForDeclaration()%n" +
" [...same steps as subproject...]%n" +
" }%n" +
" }%n" +
"%n" +
"To help you figure out which block is missing, the step you are missing is%n" +
" step name: %s%n" +
" requested: %s%n",
beingResolved == null ? "(unknown)" : beingResolved.getName(),
requestedDeps);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2016 DiffPlug
*
* 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.diffplug.gradle.spotless;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;

import com.diffplug.common.collect.Sets;
import com.diffplug.common.io.Files;
import com.diffplug.spotless.FormatterStep;

/**
* The minimal task required to force all SpotlessTasks in the root
* project to trigger their dependency resolution, so that they will
* be cached for subproject tasks to slurp from.
*/
public class RegisterDependenciesTask extends DefaultTask {
@Input
public List<FormatterStep> getSteps() {
List<FormatterStep> allSteps = new ArrayList<>();
Set<SpotlessTask> alreadyAdded = Sets.newIdentityHashSet();
for (Object dependsOn : getDependsOn()) {
// in Gradle 2.14, we can have a single task listed as a dep twice,
// and we can also have non-tasks listed as a dep
if (dependsOn instanceof SpotlessTask) {
SpotlessTask task = (SpotlessTask) dependsOn;
if (alreadyAdded.add(task)) {
allSteps.addAll(task.getSteps());
}
}
}
return allSteps;
}

File unitOutput;

@OutputFile
public File getUnitOutput() {
return unitOutput;
}

RegisterDependenciesInRoot.RootProvisioner rootProvisioner;

@Internal
public RegisterDependenciesInRoot.RootProvisioner getRootProvisioner() {
return rootProvisioner;
}

void setup() {
unitOutput = new File(getProject().getBuildDir(), "tmp/spotless-register-dependencies");
rootProvisioner = new RegisterDependenciesInRoot.RootProvisioner(getProject());
}

@TaskAction
public void trivialFunction() throws IOException {
Files.createParentDirs(unitOutput);
Files.write("unit", unitOutput, StandardCharsets.UTF_8);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import java.util.LinkedHashMap;
import java.util.Map;

import javax.annotation.Nullable;

import org.gradle.api.Action;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
Expand All @@ -39,6 +41,7 @@
public class SpotlessExtension {
final Project project;
final Task rootCheckTask, rootApplyTask;
final @Nullable RegisterDependenciesTask registerDependenciesTask;

static final String EXTENSION = "spotless";
static final String CHECK = "Check";
Expand All @@ -58,6 +61,17 @@ public SpotlessExtension(Project project) {
rootApplyTask = project.task(EXTENSION + APPLY);
rootApplyTask.setGroup(TASK_GROUP);
rootApplyTask.setDescription(APPLY_DESCRIPTION);
boolean registerDependenciesInRoot = RegisterDependenciesInRoot.isEnabled(project);
if (registerDependenciesInRoot) {
if (project.getRootProject() == project) {
registerDependenciesTask = project.getTasks().create(RegisterDependenciesInRoot.TASK_NAME, RegisterDependenciesTask.class);
registerDependenciesTask.setup();
} else {
registerDependenciesTask = project.getRootProject().getPlugins().apply(SpotlessPlugin.class).spotlessExtension.registerDependenciesTask;
}
} else {
registerDependenciesTask = null;
}
}

/** Line endings (if any). */
Expand Down

0 comments on commit 892203d

Please sign in to comment.