diff --git a/CHANGES.md b/CHANGES.md index 40d40e1934..7773f2052c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +* `DiffMessageFormatter` can now generate messages based on a folder of cleaned files, as an alternative to a `Formatter` ([#982](https://github.com/diffplug/spotless/pull/982)). ## [2.19.2] - 2021-10-26 ### Changed diff --git a/gradle/spotless.gradle b/gradle/spotless.gradle index 1c59e64afa..96037b21a3 100644 --- a/gradle/spotless.gradle +++ b/gradle/spotless.gradle @@ -16,13 +16,13 @@ spotless { // the rootProject doesn't have any java java { ratchetFrom 'origin/main' - custom 'noInternalDeps', noInternalDepsClosure bumpThisNumberIfACustomStepChanges(1) licenseHeaderFile rootProject.file('gradle/spotless.license') importOrderFile rootProject.file('gradle/spotless.importorder') eclipse().configFile rootProject.file('gradle/spotless.eclipseformat.xml') trimTrailingWhitespace() removeUnusedImports() + custom 'noInternalDeps', noInternalDepsClosure } } groovyGradle { diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java index 8db5ebeb24..b5d5656198 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2021 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,10 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.ListIterator; import java.util.Objects; @@ -44,11 +46,74 @@ public static Builder builder() { return new Builder(); } + interface CleanProvider { + + Path getRootDir(); + + Charset getEncoding(); + + String getFormatted(File file, String rawUnix); + } + + private static class CleanProviderFormatter implements CleanProvider { + private final Formatter formatter; + + CleanProviderFormatter(Formatter formatter) { + this.formatter = Objects.requireNonNull(formatter); + } + + @Override + public Path getRootDir() { + return formatter.getRootDir(); + } + + @Override + public Charset getEncoding() { + return formatter.getEncoding(); + } + + @Override + public String getFormatted(File file, String rawUnix) { + String unix = PaddedCell.check(formatter, file, rawUnix).canonical(); + return formatter.computeLineEndings(unix, file); + } + } + + private static class CleanProviderFolder implements CleanProvider { + private final Path rootDir; + private final Path cleanDir; + private final Charset encoding; + + CleanProviderFolder(Path rootDir, Path cleanDir, String encoding) { + this.rootDir = rootDir; + this.cleanDir = cleanDir; + this.encoding = Charset.forName(encoding); + } + + @Override + public Path getRootDir() { + return rootDir; + } + + @Override + public Charset getEncoding() { + return encoding; + } + + @Override + public String getFormatted(File file, String rawUnix) { + Path relative = rootDir.relativize(file.toPath()); + Path clean = cleanDir.resolve(rootDir.relativize(file.toPath())); + byte[] content = Errors.rethrow().get(() -> Files.readAllBytes(clean)); + return new String(content, encoding); + } + } + public static class Builder { private Builder() {} private String runToFix; - private Formatter formatter; + private CleanProvider formatter; private List problemFiles; /** "Run 'gradlew spotlessApply' to fix these violations." */ @@ -58,7 +123,12 @@ public Builder runToFix(String runToFix) { } public Builder formatter(Formatter formatter) { - this.formatter = Objects.requireNonNull(formatter); + this.formatter = new CleanProviderFormatter(formatter); + return this; + } + + public Builder formatterFolder(Path rootDir, Path cleanDir, String encoding) { + this.formatter = new CleanProviderFolder(rootDir, cleanDir, encoding); return this; } @@ -164,12 +234,11 @@ private void addIntendedLine(String indent, String line) { private static String diff(Builder builder, File file) throws IOException { String raw = new String(Files.readAllBytes(file.toPath()), builder.formatter.getEncoding()); String rawUnix = LineEnding.toUnix(raw); - String formattedUnix; - formattedUnix = PaddedCell.check(builder.formatter, file, rawUnix).canonical(); + String formatted = builder.formatter.getFormatted(file, rawUnix); + String formattedUnix = LineEnding.toUnix(formatted); if (rawUnix.equals(formattedUnix)) { // the formatting is fine, so it's a line-ending issue - String formatted = builder.formatter.computeLineEndings(formattedUnix, file); return diffWhitespaceLineEndings(raw, formatted, false, true); } else { return diffWhitespaceLineEndings(rawUnix, formattedUnix, true, false); diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 7d4587fe47..f3c72a325e 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -5,6 +5,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Changed * **BREAKING** Previously, many projects required `buildscript { repositories { mavenCentral() }}` at the top of their root project, because Spotless resolved its dependencies using the buildscript repositories. Spotless now resolves its dependencies from the normal project repositories of the root project, which means that you can remove the `buildscript {}` block, but you still need `repositories { mavenCentral() }` (or similar) in the root project. +* **BREAKING** `createIndepentApplyTask(String taskName)` now requires that `taskName` does not end with `Apply` * Bump minimum required Gradle from `6.1` to `6.1.1`. ## [5.17.1] - 2021-10-26 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 92b9e20f3a..5c7b20b82f 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 @@ -40,6 +40,7 @@ import org.gradle.api.file.FileCollection; import org.gradle.api.plugins.BasePlugin; +import com.diffplug.common.base.Preconditions; import com.diffplug.spotless.FormatExceptionPolicyStrict; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; @@ -754,19 +755,22 @@ protected Project getProject() { * * The returned task will not be hooked up to the global {@code spotlessApply}, and there will be no corresponding {@code check} task. * + * The task name must not end with `Apply`. + * * NOTE: does not respect the rarely-used {@code spotlessFiles} property. */ public SpotlessApply createIndependentApplyTask(String taskName) { + Preconditions.checkArgument(!taskName.endsWith(SpotlessExtension.APPLY), "Task name must not end with " + SpotlessExtension.APPLY); // create and setup the task - SpotlessTask spotlessTask = spotless.project.getTasks().create(taskName + "Helper", SpotlessTaskImpl.class); + SpotlessTaskImpl spotlessTask = spotless.project.getTasks().create(taskName + SpotlessTaskService.INDEPENDENT_HELPER, SpotlessTaskImpl.class); + spotlessTask.init(spotless.getTaskService()); setupTask(spotlessTask); // enforce the clean ordering Task clean = spotless.project.getTasks().getByName(BasePlugin.CLEAN_TASK_NAME); spotlessTask.mustRunAfter(clean); // create the apply task SpotlessApply applyTask = spotless.project.getTasks().create(taskName, SpotlessApply.class); - applyTask.setSpotlessOutDirectory(spotlessTask.getOutputDirectory()); - applyTask.linkSource(spotlessTask); + applyTask.init(spotlessTask); applyTask.dependsOn(spotlessTask); return applyTask; diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GitRatchetGradle.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GitRatchetGradle.java index 5a7f402b89..7517018d66 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GitRatchetGradle.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GitRatchetGradle.java @@ -19,7 +19,6 @@ import javax.annotation.Nullable; -import org.gradle.api.Project; import org.gradle.api.services.BuildService; import org.gradle.api.services.BuildServiceParameters; import org.gradle.tooling.events.FinishEvent; @@ -28,15 +27,15 @@ import com.diffplug.spotless.extra.GitRatchet; /** Gradle implementation of GitRatchet. */ -public abstract class GitRatchetGradle extends GitRatchet implements BuildService, OperationCompletionListener { +public abstract class GitRatchetGradle extends GitRatchet implements BuildService, OperationCompletionListener { @Override - protected File getDir(Project project) { - return project.getProjectDir(); + protected File getDir(File project) { + return project; } @Override - protected @Nullable Project getParent(Project project) { - return project.getParent(); + protected @Nullable File getParent(File project) { + return project.getParentFile(); } @Override diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java index 9f2e213a17..02f60c21e6 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2021 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ private static void dumpIsClean() { System.err.println("IS CLEAN"); } - static void performHook(SpotlessTask spotlessTask) { + static void performHook(SpotlessTaskImpl spotlessTask) { String path = (String) spotlessTask.getProject().property(PROPERTY); File file = new File(path); if (!file.isAbsolute()) { @@ -43,7 +43,7 @@ static void performHook(SpotlessTask spotlessTask) { if (spotlessTask.getTarget().contains(file)) { try (Formatter formatter = spotlessTask.buildFormatter()) { if (spotlessTask.ratchet != null) { - if (spotlessTask.ratchet.isClean(spotlessTask.getProject(), spotlessTask.rootTreeSha, file)) { + if (spotlessTask.ratchet.isClean(spotlessTask.getProjectDir().get().getAsFile(), spotlessTask.rootTreeSha, file)) { dumpIsClean(); return; } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SerializableMisc.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SerializableMisc.java index 81db8adacc..c4ffbc4837 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SerializableMisc.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SerializableMisc.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2021 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ static void toFile(Serializable obj, File file) { } } - static T fromFile(Class clazz, File file) { + static T fromFile(Class clazz, File file) { try (InputStream input = Files.asByteSource(file).openBufferedStream()) { return fromStream(clazz, input); } catch (IOException e) { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessApply.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessApply.java index f96f2b1131..a85195fe9f 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessApply.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessApply.java @@ -20,38 +20,18 @@ import java.nio.file.Files; import java.nio.file.StandardCopyOption; -import org.gradle.api.DefaultTask; import org.gradle.api.file.ConfigurableFileTree; import org.gradle.api.file.FileVisitDetails; import org.gradle.api.file.FileVisitor; -import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.TaskAction; -public class SpotlessApply extends DefaultTask { - private SpotlessTask source; - - /** Bidirectional link between Apply and Spotless allows check to know if Apply ran or not. */ - void linkSource(SpotlessTask source) { - this.source = source; - source.applyTask = this; - } - - private File spotlessOutDirectory; - - @Internal - public File getSpotlessOutDirectory() { - return spotlessOutDirectory; - } - - public void setSpotlessOutDirectory(File spotlessOutDirectory) { - this.spotlessOutDirectory = spotlessOutDirectory; - } - +public abstract class SpotlessApply extends SpotlessTaskService.ClientTask { @TaskAction public void performAction() { - ConfigurableFileTree files = getProject().fileTree(spotlessOutDirectory); + getTaskService().get().registerApplyAlreadyRan(this); + ConfigurableFileTree files = getConfigCacheWorkaround().fileTree().from(getSpotlessOutDirectory().get()); if (files.isEmpty()) { - getState().setDidWork(source.getDidWork()); + getState().setDidWork(sourceDidWork()); } else { files.visit(new FileVisitor() { @Override @@ -62,7 +42,7 @@ public void visitDir(FileVisitDetails fileVisitDetails) { @Override public void visitFile(FileVisitDetails fileVisitDetails) { String path = fileVisitDetails.getPath(); - File originalSource = new File(getProject().getProjectDir(), path); + File originalSource = new File(getProjectDir().get().getAsFile(), path); try { getLogger().debug("Copying " + fileVisitDetails.getFile() + " to " + originalSource); Files.copy(fileVisitDetails.getFile().toPath(), originalSource.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessCheck.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessCheck.java index 3fb3e0e8c3..55336a00ad 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessCheck.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessCheck.java @@ -24,46 +24,36 @@ import java.util.Collections; import java.util.List; -import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.file.ConfigurableFileTree; import org.gradle.api.file.FileVisitDetails; import org.gradle.api.file.FileVisitor; +import org.gradle.api.provider.Property; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.TaskAction; import com.diffplug.spotless.FileSignature; -import com.diffplug.spotless.Formatter; import com.diffplug.spotless.ThrowingEx; import com.diffplug.spotless.extra.integration.DiffMessageFormatter; -public class SpotlessCheck extends DefaultTask { - SpotlessTask source; - private File spotlessOutDirectory; - +public abstract class SpotlessCheck extends SpotlessTaskService.ClientTask { @Internal - public File getSpotlessOutDirectory() { - return spotlessOutDirectory; - } - - public void setSpotlessOutDirectory(File spotlessOutDirectory) { - this.spotlessOutDirectory = spotlessOutDirectory; - } + public abstract Property getEncoding(); - public void performActionTest() throws Exception { + public void performActionTest() throws IOException { performAction(true); } @TaskAction - public void performAction() throws Exception { + public void performAction() throws IOException { performAction(false); } - private void performAction(boolean isTest) { - ConfigurableFileTree files = getProject().fileTree(spotlessOutDirectory); + private void performAction(boolean isTest) throws IOException { + ConfigurableFileTree files = getConfigCacheWorkaround().fileTree().from(getSpotlessOutDirectory().get()); if (files.isEmpty()) { - getState().setDidWork(source.getDidWork()); - } else if (!isTest && getProject().getGradle().getTaskGraph().hasTask(source.applyTask)) { + getState().setDidWork(sourceDidWork()); + } else if (!isTest && applyHasRun()) { // if our matching apply has already run, then we don't need to do anything getState().setDidWork(false); } else { @@ -77,7 +67,7 @@ public void visitDir(FileVisitDetails fileVisitDetails) { @Override public void visitFile(FileVisitDetails fileVisitDetails) { String path = fileVisitDetails.getPath(); - File originalSource = new File(getProject().getProjectDir(), path); + File originalSource = new File(getProjectDir().get().getAsFile(), path); try { // read the file on disk byte[] userFile = Files.readAllBytes(originalSource.toPath()); @@ -106,27 +96,32 @@ public void visitFile(FileVisitDetails fileVisitDetails) { } }); if (!problemFiles.isEmpty()) { - try (Formatter formatter = source.buildFormatter()) { - Collections.sort(problemFiles); - throw formatViolationsFor(formatter, problemFiles); - } + Collections.sort(problemFiles); + throw new GradleException(DiffMessageFormatter.builder() + .runToFix("Run '" + calculateGradleCommand() + " " + getTaskPathPrefix() + "spotlessApply' to fix these violations.") + .formatterFolder( + getProjectDir().get().getAsFile().toPath(), + getSpotlessOutDirectory().get().toPath(), + getEncoding().get()) + .problemFiles(problemFiles) + .getMessage()); } } } - /** Returns an exception which indicates problem files nicely. */ - private GradleException formatViolationsFor(Formatter formatter, List problemFiles) { - return new GradleException(DiffMessageFormatter.builder() - .runToFix("Run '" + calculateGradleCommand() + " " + getTaskPathPrefix() + "spotlessApply' to fix these violations.") - .formatter(formatter) - .problemFiles(problemFiles) - .getMessage()); + @Internal + abstract Property getProjectPath(); + + @Override + void init(SpotlessTaskImpl impl) { + super.init(impl); + getProjectPath().set(getProject().getPath()); + getEncoding().set(impl.getEncoding()); } private String getTaskPathPrefix() { - return getProject().getPath().equals(":") - ? ":" - : getProject().getPath() + ":"; + String path = getProjectPath().get(); + return path.equals(":") ? ":" : path + ":"; } private static String calculateGradleCommand() { 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 7959bdc71d..d89acdd061 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 @@ -27,6 +27,7 @@ import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.Project; +import org.gradle.api.provider.Provider; import com.diffplug.spotless.LineEnding; @@ -46,6 +47,8 @@ protected SpotlessExtension(Project project) { this.project = requireNonNull(project); } + abstract Provider getTaskService(); + abstract RegisterDependenciesTask getRegisterDependenciesTask(); /** Line endings (if any). */ diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java index a542b5a96d..1a2a66422a 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2021 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,13 @@ import org.gradle.api.Project; import org.gradle.api.plugins.BasePlugin; import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskProvider; public class SpotlessExtensionImpl extends SpotlessExtension { private final TaskProvider registerDependenciesTask; + private final Provider taskService; public SpotlessExtensionImpl(Project project) { super(project); @@ -52,6 +54,13 @@ public SpotlessExtensionImpl(Project project) { .configure(task -> task.dependsOn(rootCheckTask)); } }); + + taskService = project.getGradle().getSharedServices().registerIfAbsent("SpotlessTaskService", SpotlessTaskService.class, spec -> {}); + } + + @Override + Provider getTaskService() { + return taskService; } final TaskProvider rootCheckTask, rootApplyTask, rootDiagnoseTask; @@ -69,6 +78,7 @@ protected void createFormatTasks(String name, FormatExtension formatExtension) { // create the SpotlessTask String taskName = EXTENSION + SpotlessPlugin.capitalize(name); TaskProvider spotlessTask = tasks.register(taskName, SpotlessTaskImpl.class, task -> { + task.init(taskService); task.setEnabled(!isIdeHook); // clean removes the SpotlessCache, so we have to run after clean task.mustRunAfter(cleanTask); @@ -87,10 +97,9 @@ protected void createFormatTasks(String name, FormatExtension formatExtension) { // create the check and apply control tasks TaskProvider applyTask = tasks.register(taskName + APPLY, SpotlessApply.class, task -> { + task.init(spotlessTask.get()); task.setEnabled(!isIdeHook); task.dependsOn(spotlessTask); - task.setSpotlessOutDirectory(spotlessTask.get().getOutputDirectory()); - task.linkSource(spotlessTask.get()); }); rootApplyTask.configure(task -> { task.dependsOn(applyTask); @@ -102,10 +111,10 @@ protected void createFormatTasks(String name, FormatExtension formatExtension) { }); TaskProvider checkTask = tasks.register(taskName + CHECK, SpotlessCheck.class, task -> { + SpotlessTaskImpl source = spotlessTask.get(); + task.init(source); task.setEnabled(!isIdeHook); - task.dependsOn(spotlessTask); - task.setSpotlessOutDirectory(spotlessTask.get().getOutputDirectory()); - task.source = spotlessTask.get(); + task.dependsOn(source); // if the user runs both, make sure that apply happens first, task.mustRunAfter(applyTask); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java index bca839547a..8c997153ec 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 DiffPlug + * Copyright 2020-2021 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.eclipse.jgit.lib.ObjectId; import org.gradle.api.DefaultTask; +import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileCollection; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFiles; @@ -42,15 +43,7 @@ import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; -public class SpotlessTask extends DefaultTask { - SpotlessApply applyTask; - - /** Internal use only, allows coordination between check and apply when they are in the same build */ - @Internal - SpotlessApply getApplyTask() { - return applyTask; - } - +public abstract class SpotlessTask extends DefaultTask { // set by SpotlessExtension, but possibly overridden by FormatExtension protected String encoding = "UTF-8"; @@ -88,10 +81,14 @@ public void setLineEndingsPolicy(LineEnding.Policy lineEndingsPolicy) { public void setupRatchet(GitRatchetGradle gitRatchet, String ratchetFrom) { ratchet = gitRatchet; - rootTreeSha = gitRatchet.rootTreeShaOf(getProject(), ratchetFrom); - subtreeSha = gitRatchet.subtreeShaOf(getProject(), rootTreeSha); + File projectDir = getProjectDir().get().getAsFile(); + rootTreeSha = gitRatchet.rootTreeShaOf(projectDir, ratchetFrom); + subtreeSha = gitRatchet.subtreeShaOf(projectDir, rootTreeSha); } + @Internal + abstract DirectoryProperty getProjectDir(); + @Internal GitRatchetGradle getRatchet() { return ratchet; @@ -171,7 +168,7 @@ Formatter buildFormatter() { return Formatter.builder() .lineEndingsPolicy(lineEndingsPolicy) .encoding(Charset.forName(encoding)) - .rootDir(getProject().getRootDir().toPath()) + .rootDir(getProjectDir().get().getAsFile().toPath()) .steps(steps) .exceptionPolicy(exceptionPolicy) .build(); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java index b5351c701b..2fc37fd741 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java @@ -20,10 +20,16 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.Comparator; + +import javax.inject.Inject; import org.gradle.api.GradleException; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileSystemOperations; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.TaskAction; import org.gradle.work.ChangeType; import org.gradle.work.FileChange; @@ -34,16 +40,31 @@ import com.diffplug.spotless.PaddedCell; @CacheableTask -public class SpotlessTaskImpl extends SpotlessTask { +public abstract class SpotlessTaskImpl extends SpotlessTask { + @Internal + abstract Property getTaskService(); + + @Internal + abstract DirectoryProperty getProjectDir(); + + void init(Provider service) { + getTaskService().set(service); + getProjectDir().set(getProject().getProjectDir()); + } + + @Inject + protected abstract FileSystemOperations getFs(); + @TaskAction public void performAction(InputChanges inputs) throws Exception { + getTaskService().get().registerSourceAlreadyRan(this); if (target == null) { throw new GradleException("You must specify 'Iterable target'"); } if (!inputs.isIncremental()) { getLogger().info("Not incremental: removing prior outputs"); - getProject().delete(outputDirectory); + getFs().delete(d -> d.delete(outputDirectory)); Files.createDirectories(outputDirectory.toPath()); } @@ -65,7 +86,7 @@ private void processInputFile(Formatter formatter, File input) throws IOExceptio File output = getOutputFile(input); getLogger().debug("Applying format to " + input + " and writing to " + output); PaddedCell.DirtyState dirtyState; - if (ratchet != null && ratchet.isClean(getProject(), rootTreeSha, input)) { + if (ratchet != null && ratchet.isClean(getProjectDir().get().getAsFile(), rootTreeSha, input)) { dirtyState = PaddedCell.isClean(); } else { dirtyState = PaddedCell.calculateDirtyState(formatter, input); @@ -90,22 +111,20 @@ private void processInputFile(Formatter formatter, File input) throws IOExceptio private void deletePreviousResult(File input) throws IOException { File output = getOutputFile(input); if (output.isDirectory()) { - Files.walk(output.toPath()) - .sorted(Comparator.reverseOrder()) - .map(Path::toFile) - .forEach(File::delete); + getFs().delete(d -> d.delete(output)); } else { Files.deleteIfExists(output.toPath()); } } private File getOutputFile(File input) { - String outputFileName = FormatExtension.relativize(getProject().getProjectDir(), input); + File projectDir = getProjectDir().get().getAsFile(); + String outputFileName = FormatExtension.relativize(projectDir, input); if (outputFileName == null) { throw new IllegalArgumentException(StringPrinter.buildString(printer -> { - printer.println("Spotless error! All target files must be within the project root. In project " + getProject().getPath()); - printer.println(" root dir: " + getProject().getProjectDir().getAbsolutePath()); - printer.println(" target: " + input.getAbsolutePath()); + printer.println("Spotless error! All target files must be within the project dir."); + printer.println(" project dir: " + projectDir.getAbsolutePath()); + printer.println(" target: " + input.getAbsolutePath()); })); } return new File(outputDirectory, outputFileName); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java new file mode 100644 index 0000000000..a08e93bd12 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java @@ -0,0 +1,108 @@ +/* + * Copyright 2021 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.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Property; +import org.gradle.api.services.BuildService; +import org.gradle.api.services.BuildServiceParameters; +import org.gradle.api.tasks.Internal; + +import com.diffplug.common.base.Preconditions; +import com.diffplug.common.base.Unhandled; + +/** + * Allows the check and apply tasks to coordinate + * with each other (and the source task) to reduce + * duplicated work (e.g. no need for check to run if + * apply already did). + */ +public abstract class SpotlessTaskService implements BuildService { + private final Map apply = Collections.synchronizedMap(new HashMap<>()); + private final Map source = Collections.synchronizedMap(new HashMap<>()); + + public void registerSourceAlreadyRan(SpotlessTask task) { + source.put(task.getPath(), task); + } + + public void registerApplyAlreadyRan(SpotlessApply task) { + apply.put(task.sourceTaskPath(), task); + } + + static String INDEPENDENT_HELPER = "Helper"; + + static abstract class ClientTask extends DefaultTask { + @Internal + abstract Property getSpotlessOutDirectory(); + + @Internal + abstract Property getTaskService(); + + @Internal + abstract DirectoryProperty getProjectDir(); + + @Inject + protected abstract ObjectFactory getConfigCacheWorkaround(); + + void init(SpotlessTaskImpl impl) { + getSpotlessOutDirectory().set(impl.getOutputDirectory()); + getTaskService().set(impl.getTaskService()); + getProjectDir().set(impl.getProjectDir()); + } + + String sourceTaskPath() { + String path = getPath(); + if (this instanceof SpotlessApply) { + if (path.endsWith(SpotlessExtension.APPLY)) { + return path.substring(0, path.length() - SpotlessExtension.APPLY.length()); + } else { + return path + INDEPENDENT_HELPER; + } + } else if (this instanceof SpotlessCheck) { + Preconditions.checkArgument(path.endsWith(SpotlessExtension.CHECK)); + return path.substring(0, path.length() - SpotlessExtension.CHECK.length()); + } else { + throw Unhandled.classException(this); + } + } + + private SpotlessTaskService service() { + return getTaskService().get(); + } + + protected boolean sourceDidWork() { + SpotlessTask sourceTask = service().source.get(sourceTaskPath()); + if (sourceTask != null) { + return sourceTask.getDidWork(); + } else { + return false; + } + } + + protected boolean applyHasRun() { + return service().apply.containsKey(sourceTaskPath()); + } + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/DiffMessageFormatterTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/DiffMessageFormatterTest.java index d64bf91501..c46d398bf8 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/DiffMessageFormatterTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/DiffMessageFormatterTest.java @@ -25,6 +25,8 @@ import org.assertj.core.api.Assertions; import org.gradle.api.Project; +import org.gradle.api.provider.Provider; +import org.gradle.api.services.BuildServiceParameters; import org.junit.jupiter.api.Test; import com.diffplug.common.base.StringPrinter; @@ -39,6 +41,13 @@ class DiffMessageFormatterTest extends ResourceHarness { private class Bundle { Project project = TestProvisioner.gradleProject(rootFolder()); + Provider taskService = GradleIntegrationHarness.providerOf(new SpotlessTaskService() { + @Override + public BuildServiceParameters.None getParameters() { + return null; + } + }); + File file; SpotlessTaskImpl task; SpotlessCheck check; @@ -52,22 +61,21 @@ private class Bundle { private SpotlessTaskImpl createFormatTask(String name) { SpotlessTaskImpl task = project.getTasks().create("spotless" + SpotlessPlugin.capitalize(name), SpotlessTaskImpl.class); + task.init(taskService); task.setLineEndingsPolicy(LineEnding.UNIX.createPolicy()); task.setTarget(Collections.singletonList(file)); return task; } - private SpotlessCheck createCheckTask(String name, SpotlessTask source) { + private SpotlessCheck createCheckTask(String name, SpotlessTaskImpl source) { SpotlessCheck task = project.getTasks().create("spotless" + SpotlessPlugin.capitalize(name) + "Check", SpotlessCheck.class); - task.source = source; - task.setSpotlessOutDirectory(source.getOutputDirectory()); + task.init(source); return task; } - private SpotlessApply createApplyTask(String name, SpotlessTask source) { + private SpotlessApply createApplyTask(String name, SpotlessTaskImpl source) { SpotlessApply task = project.getTasks().create("spotless" + SpotlessPlugin.capitalize(name) + "Apply", SpotlessApply.class); - task.linkSource(this.task); - task.setSpotlessOutDirectory(source.getOutputDirectory()); + task.init(source); return task; } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FormatTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FormatTaskTest.java index 91ce4de704..8985cd7a14 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FormatTaskTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FormatTaskTest.java @@ -19,6 +19,7 @@ import java.util.Collections; import org.gradle.api.Project; +import org.gradle.api.services.BuildServiceParameters; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,6 +36,12 @@ void createTask() { Project project = TestProvisioner.gradleProject(rootFolder()); spotlessTask = project.getTasks().create("spotlessTaskUnderTest", SpotlessTaskImpl.class); spotlessTask.setLineEndingsPolicy(LineEnding.UNIX.createPolicy()); + spotlessTask.init(GradleIntegrationHarness.providerOf(new SpotlessTaskService() { + @Override + public BuildServiceParameters.None getParameters() { + return null; + } + })); } @Test diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIncrementalResolutionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIncrementalResolutionTest.java index 1de2ef2b4a..6235c246bd 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIncrementalResolutionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIncrementalResolutionTest.java @@ -24,6 +24,8 @@ import java.util.SortedSet; import java.util.TreeSet; +import org.assertj.core.api.AbstractStringAssert; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import com.diffplug.common.base.Errors; @@ -53,11 +55,18 @@ void failureDoesntTriggerAll() throws IOException { writeState("aBc"); assertState("aBc"); // check will run against all three the first time. - // Subsequent runs will only run the formatter on the bad file (in order to generate the failure message) checkRanAgainst("abc"); - checkRanAgainst("b"); - checkRanAgainst("b"); - + // Subsequent runs will use the cached error message + checkRanAgainstNoneButError().contains("> The following files had format violations:\n" + + " b.md\n" + + " @@ -1 +1 @@\n" + + " -B\n" + + " +b"); + checkRanAgainstNoneButError().contains("> The following files had format violations:\n" + + " b.md\n" + + " @@ -1 +1 @@\n" + + " -B\n" + + " +b"); // apply will simply copy outputs the first time: no formatters executed applyRanAgainst(""); // the second time, it will only run on the file that was changed by apply @@ -69,8 +78,12 @@ void failureDoesntTriggerAll() throws IOException { writeState("Abc"); // then check runs against just the changed file checkRanAgainst("a"); - // even after failing - checkRanAgainst("a"); + // even after failing once the error is still there + checkRanAgainstNoneButError().contains("> The following files had format violations:\n" + + " a.md\n" + + " @@ -1 +1 @@\n" + + " -A\n" + + " +a"); // and so does apply applyRanAgainst(); applyRanAgainst("a"); @@ -112,12 +125,17 @@ private void checkRanAgainst(String... ranAgainst) throws IOException { taskRanAgainst("spotlessCheck", ranAgainst); } - private void taskRanAgainst(String task, String... ranAgainst) throws IOException { + private AbstractStringAssert checkRanAgainstNoneButError() throws IOException { + String console = taskRanAgainst("spotlessCheck"); + return Assertions.assertThat(console); + } + + private String taskRanAgainst(String task, String... ranAgainst) throws IOException { pauseForFilesystem(); String console = StringPrinter.buildString(Errors.rethrow().wrap(printer -> { boolean expectFailure = task.equals("spotlessCheck") && !isClean(); if (expectFailure) { - gradleRunner().withArguments(task).forwardStdOutput(printer.toWriter()).buildAndFail(); + gradleRunner().withArguments(task).forwardStdOutput(printer.toWriter()).forwardStdError(printer.toWriter()).buildAndFail(); } else { gradleRunner().withArguments(task).forwardStdOutput(printer.toWriter()).build(); } @@ -130,6 +148,7 @@ private void taskRanAgainst(String task, String... ranAgainst) throws IOExceptio } } assertEquals(concat(Arrays.asList(ranAgainst)), concat(added)); + return console; } private String concat(Iterable iterable) { diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java index 2d92f27eff..89f7f25ffe 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java @@ -24,6 +24,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import org.gradle.api.provider.Provider; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.BuildTask; import org.gradle.testkit.runner.GradleRunner; @@ -59,6 +60,10 @@ public enum GradleVersionSupport { } } + public static Provider providerOf(T value) { + return org.gradle.api.internal.provider.Providers.of(value); + } + /** * Each test gets its own temp folder, and we create a gradle * build there and run it. diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PaddedCellTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PaddedCellTaskTest.java index f03018c697..bc7a2fdc96 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PaddedCellTaskTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PaddedCellTaskTest.java @@ -23,6 +23,8 @@ import java.util.Collections; import org.gradle.api.Project; +import org.gradle.api.provider.Provider; +import org.gradle.api.services.BuildServiceParameters; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -38,9 +40,15 @@ class PaddedCellTaskTest extends ResourceHarness { private class Bundle { String name; Project project = TestProvisioner.gradleProject(rootFolder()); + Provider taskService = GradleIntegrationHarness.providerOf(new SpotlessTaskService() { + @Override + public BuildServiceParameters.None getParameters() { + return null; + } + }); File file; File outputFile; - SpotlessTaskImpl task; + SpotlessTaskImpl source; SpotlessCheck check; SpotlessApply apply; @@ -48,31 +56,30 @@ private class Bundle { this.name = name; file = setFile("src/test." + name).toContent("CCC"); FormatterStep step = FormatterStep.createNeverUpToDate(name, function); - task = createFormatTask(name, step); - check = createCheckTask(name, task); - apply = createApplyTask(name, task); - outputFile = new File(task.getOutputDirectory() + "/src", file.getName()); + source = createFormatTask(name, step); + check = createCheckTask(name, source); + apply = createApplyTask(name, source); + outputFile = new File(source.getOutputDirectory() + "/src", file.getName()); } private SpotlessTaskImpl createFormatTask(String name, FormatterStep step) { SpotlessTaskImpl task = project.getTasks().create("spotless" + SpotlessPlugin.capitalize(name), SpotlessTaskImpl.class); + task.init(taskService); task.addStep(step); task.setLineEndingsPolicy(LineEnding.UNIX.createPolicy()); task.setTarget(Collections.singletonList(file)); return task; } - private SpotlessCheck createCheckTask(String name, SpotlessTask source) { + private SpotlessCheck createCheckTask(String name, SpotlessTaskImpl source) { SpotlessCheck task = project.getTasks().create("spotless" + SpotlessPlugin.capitalize(name) + "Check", SpotlessCheck.class); - task.source = source; - task.setSpotlessOutDirectory(source.getOutputDirectory()); + task.init(source); return task; } - private SpotlessApply createApplyTask(String name, SpotlessTask source) { + private SpotlessApply createApplyTask(String name, SpotlessTaskImpl source) { SpotlessApply task = project.getTasks().create("spotless" + SpotlessPlugin.capitalize(name) + "Apply", SpotlessApply.class); - task.linkSource(source); - task.setSpotlessOutDirectory(source.getOutputDirectory()); + task.init(source); return task; } @@ -87,21 +94,21 @@ String checkFailureMsg() { void diagnose() throws IOException { SpotlessDiagnoseTask diagnose = project.getTasks().create("spotless" + SpotlessPlugin.capitalize(name) + "Diagnose", SpotlessDiagnoseTask.class); - diagnose.source = task; + diagnose.source = source; diagnose.performAction(); } void format() throws Exception { - Tasks.execute(task); + Tasks.execute(source); } void apply() throws Exception { - Tasks.execute(task); + Tasks.execute(source); apply.performAction(); } void check() throws Exception { - Tasks.execute(task); + Tasks.execute(source); check.performActionTest(); } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskTest.java index 1b24201b12..3f5fa30c9b 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskTest.java @@ -45,8 +45,6 @@ void registerDependencies() throws IOException { "> Task :spotlessInternalRegisterDependencies\n") .contains("> Task :sub:spotlessJava\n" + "> Task :sub:spotlessJavaCheck\n" + - "> Task :sub:spotlessCheck\n" + - "\n" + - "BUILD SUCCESSFUL"); + "> Task :sub:spotlessCheck\n"); } }