From 892203d499685397c28e1d70a1c76dd554e3fdda Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 31 Dec 2019 22:39:02 -0800 Subject: [PATCH] Create a :spotlessRegisterDependencies task, and use it to resolve the Gradle 6+ deprecation warnings. --- .../gradle/spotless/FormatExtension.java | 15 ++ .../gradle/spotless/GradleProvisioner.java | 16 +- .../spotless/RegisterDependenciesInRoot.java | 182 ++++++++++++++++++ .../spotless/RegisterDependenciesTask.java | 82 ++++++++ .../gradle/spotless/SpotlessExtension.java | 14 ++ .../RegisterDependenciesInRootTest.java | 113 +++++++++++ 6 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesInRoot.java create mode 100644 plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java create mode 100644 plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesInRootTest.java diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java index 3723d89526..940f5b0041 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java @@ -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(); + } + /** * Sets which files will be excluded from formatting. Files to be formatted = (target - targetExclude). * @@ -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. */ diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java index 57ee601de9..8737a46ae1 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java @@ -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 { @@ -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()); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesInRoot.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesInRoot.java new file mode 100644 index 0000000000..60079a19fd --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesInRoot.java @@ -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 mavenCoords; + + public Request(boolean withTransitives, Collection 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 provisionWithTransitives(boolean withTransitives, Collection 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 provisionWithTransitives(boolean withTransitives, Collection mavenCoordinates) { + return doProvision(new Request(withTransitives, mavenCoordinates), true); + } + + private Map> 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 doProvision(Request req, boolean isRoot) { + Set 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 provisionForSub(Project project, boolean withTransitives, Collection mavenCoordinates) { + Request req = new Request(withTransitives, mavenCoordinates); + Set 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); + } +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java new file mode 100644 index 0000000000..7cdd7298b0 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java @@ -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 getSteps() { + List allSteps = new ArrayList<>(); + Set 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); + } +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java index 7d34a93caa..8b6421142b 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java @@ -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; @@ -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"; @@ -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). */ diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesInRootTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesInRootTest.java new file mode 100644 index 0000000000..d49f6ead2f --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesInRootTest.java @@ -0,0 +1,113 @@ +/* + * 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.IOException; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public class RegisterDependenciesInRootTest extends GradleIntegrationTest { + @Test + public void registerDependencies() throws IOException { + setFile("settings.gradle") + .toLines("include 'sub'"); + setFile("build.gradle").toLines( + "buildscript { repositories { mavenCentral() } }", + "plugins { id 'com.diffplug.gradle.spotless' }"); + setFile("sub/build.gradle").toLines( + "apply plugin: 'com.diffplug.gradle.spotless'", + "", + "spotless {", + " java {", + " target 'src/main/java/**/*.java'", + " googleJavaFormat('1.2')", + " }", + "}"); + + // works fine on old versions + String oldestSupported = gradleRunner() + .withArguments("spotlessCheck").build().getOutput(); + Assertions.assertThat(oldestSupported.replace("\r", "")).startsWith( + ":spotlessCheck UP-TO-DATE\n" + + ":sub:spotlessJava\n" + + ":sub:spotlessJavaCheck\n" + + ":sub:spotlessCheck\n" + + "\n" + + "BUILD SUCCESSFUL"); + + // generates a warning in 6.0 + setFile("gradle.properties").toLines(); + String warningStarts = gradleRunner().withGradleVersion("6.0") + .withArguments("spotlessCheck").build().getOutput(); + assertWarning(warningStarts, true); + + // we can make the old version generate the warning with spotless_register_dependencies_in_root + setFile("gradle.properties").toLines( + "spotless_register_dependencies_in_root=true"); + String oldestSupportedWithRegisterDependencies = gradleRunner() + .withArguments("spotlessCheck").build().getOutput(); + assertWarning(oldestSupportedWithRegisterDependencies, true); + + // fix the root project + setFile("build.gradle").toLines( + "buildscript { repositories { mavenCentral() } }", + "plugins { id 'com.diffplug.gradle.spotless' }", + "spotless {", + " java {", + " targetEmptyForDeclaration()", + " googleJavaFormat('1.2')", + " }", + "}"); + assertWarning(warningStarts, true); + + setFile("gradle.properties").toLines( + "spotless_register_dependencies_in_root=true"); + String oldestSupportedWithRegisterDependenciesFixed = gradleRunner() + .withArguments("spotlessCheck").build().getOutput(); + assertWarning(oldestSupportedWithRegisterDependenciesFixed, false); + + setFile("gradle.properties").toLines(); + String warningStartsFixed = gradleRunner().withGradleVersion("6.0") + .withArguments("spotlessCheck").build().getOutput(); + assertWarning(warningStartsFixed, false); + } + + private void assertWarning(String input, boolean isThere) { + String warning = "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: google-java-format\n" + + " requested: com.google.googlejavaformat:google-java-format:1.2 with transitives\n"; + if (isThere) { + Assertions.assertThat(input.replace("\r", "")).contains(warning); + } else { + Assertions.assertThat(input.replace("\r", "")).doesNotContain(warning); + } + } +}