/* * 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 static com.diffplug.gradle.spotless.PluginGradlePreconditions.requireElementsNonNull; import java.io.File; import java.io.Serializable; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Random; import java.util.stream.Stream; import javax.annotation.Nullable; import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.file.FileCollection; import org.gradle.api.internal.file.UnionFileCollection; import com.diffplug.spotless.FormatExceptionPolicyStrict; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LazyForwardingEquality; import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.ThrowingEx; import com.diffplug.spotless.generic.EndWithNewlineStep; import com.diffplug.spotless.generic.IndentStep; import com.diffplug.spotless.generic.LicenseHeaderStep; import com.diffplug.spotless.generic.ReplaceRegexStep; import com.diffplug.spotless.generic.ReplaceStep; import com.diffplug.spotless.generic.TrimTrailingWhitespaceStep; import groovy.lang.Closure; /** Adds a `spotless{Name}Check` and `spotless{Name}Apply` task. */ public class FormatExtension { final SpotlessExtension root; public FormatExtension(SpotlessExtension root) { this.root = Objects.requireNonNull(root); } private String formatName() { for (Map.Entry<String, FormatExtension> entry : root.formats.entrySet()) { if (entry.getValue() == this) { return entry.getKey(); } } throw new IllegalStateException("This format is not contained by any SpotlessExtension."); } boolean paddedCell = false; /** Enables paddedCell mode. @see <a href="https://github.com/diffplug/spotless/blob/master/PADDEDCELL.md">Padded cell</a> */ public void paddedCell() { paddedCell(true); } /** Enables paddedCell mode. @see <a href="https://github.com/diffplug/spotless/blob/master/PADDEDCELL.md">Padded cell</a> */ public void paddedCell(boolean paddedCell) { this.paddedCell = paddedCell; } LineEnding lineEndings; /** Returns the line endings to use (defaults to {@link SpotlessExtension#getLineEndings()}. */ public LineEnding getLineEndings() { return lineEndings == null ? root.getLineEndings() : lineEndings; } /** Sets the line endings to use (defaults to {@link SpotlessExtension#getLineEndings()}. */ public void setLineEndings(LineEnding lineEndings) { this.lineEndings = Objects.requireNonNull(lineEndings); } Charset encoding; /** Returns the encoding to use (defaults to {@link SpotlessExtension#getEncoding()}. */ public Charset getEncoding() { return encoding == null ? root.getEncoding() : encoding; } /** Sets the encoding to use (defaults to {@link SpotlessExtension#getEncoding()}. */ public void setEncoding(String name) { setEncoding(Charset.forName(Objects.requireNonNull(name))); } /** Sets the encoding to use (defaults to {@link SpotlessExtension#getEncoding()}. */ public void setEncoding(Charset charset) { encoding = Objects.requireNonNull(charset); } final FormatExceptionPolicyStrict exceptionPolicy = new FormatExceptionPolicyStrict(); /** Ignores errors in the given step. */ public void ignoreErrorForStep(String stepName) { exceptionPolicy.excludeStep(Objects.requireNonNull(stepName)); } /** Ignores errors for the given relative path. */ public void ignoreErrorForPath(String relativePath) { exceptionPolicy.excludePath(Objects.requireNonNull(relativePath)); } /** Sets encoding to use (defaults to {@link SpotlessExtension#getEncoding()}). */ public void encoding(String charset) { setEncoding(charset); } /** The files that need to be formatted. */ protected FileCollection target; /** * FileCollections pass through raw. * Strings are treated as the 'include' arg to fileTree, with project.rootDir as the dir. * List<String> are treated as the 'includes' arg to fileTree, with project.rootDir as the dir. * Anything else gets passed to getProject().files(). */ public void target(Object... targets) { requireElementsNonNull(targets); if (targets.length == 0) { this.target = getProject().files(); } else if (targets.length == 1) { this.target = parseTarget(targets[0]); } else { if (Stream.of(targets).allMatch(o -> o instanceof String)) { this.target = parseTarget(Arrays.asList(targets)); } else { UnionFileCollection union = new UnionFileCollection(); for (Object target : targets) { union.add(parseTarget(target)); } this.target = union; } } } @SuppressWarnings("unchecked") protected FileCollection parseTarget(Object target) { if (target instanceof FileCollection) { return (FileCollection) target; } else if (target instanceof String || (target instanceof List && ((List<?>) target).stream().allMatch(o -> o instanceof String))) { // since people are likely to do '**/*.md', we want to make sure to exclude folders // they don't want to format which will slow down the operation greatly File dir = getProject().getProjectDir(); List<String> excludes = new ArrayList<>(); // no git excludes.add(".git"); // no .gradle if (getProject() == getProject().getRootProject()) { excludes.add(".gradle"); } // no build folders excludes.add(relativize(dir, getProject().getBuildDir())); for (Project subproject : getProject().getSubprojects()) { excludes.add(relativize(dir, subproject.getBuildDir())); } if (target instanceof String) { return (FileCollection) getProject().fileTree(dir).include((String) target).exclude(excludes); } else { // target can only be a List<String> at this point return (FileCollection) getProject().fileTree(dir).include((List<String>) target).exclude(excludes); } } else { return getProject().files(target); } } static String relativize(File root, File dest) { String rootPath = root.getAbsolutePath(); String destPath = dest.getAbsolutePath(); if (!destPath.startsWith(rootPath)) { throw new IllegalArgumentException(dest + " is not a child of " + root); } else { return destPath.substring(rootPath.length()); } } /** The steps that need to be added. */ protected final List<FormatterStep> steps = new ArrayList<>(); /** Adds a new step. */ public void addStep(FormatterStep newStep) { Objects.requireNonNull(newStep); FormatterStep existing = getExistingStep(newStep.getName()); if (existing != null) { throw new GradleException("Multiple steps with name '" + newStep.getName() + "' for spotless format '" + formatName() + "'"); } steps.add(newStep); } /** Returns the existing step with the given name, if any. */ protected @Nullable FormatterStep getExistingStep(String stepName) { return steps.stream() // .filter(step -> stepName.equals(step.getName())) // .findFirst() // .orElse(null); } /** Replaces the given step. */ protected void replaceStep(FormatterStep replacementStep) { FormatterStep existing = getExistingStep(replacementStep.getName()); if (existing == null) { throw new GradleException("Cannot replace step '" + replacementStep.getName() + "' for spotless format '" + formatName() + "' because it hasn't been added yet."); } int index = steps.indexOf(existing); steps.set(index, replacementStep); } /** Clears all of the existing steps. */ public void clearSteps() { steps.clear(); } /** * An optional performance optimization if you are using any of the `custom` or `customLazy` * methods. If you aren't explicitly calling `custom` or `customLazy`, then this method * has no effect. * * Spotless tracks what files have changed from run to run, so that it can run faster * by only checking files which have changed, or whose formatting steps have changed. * If you use either the `custom` or `customLazy` methods, then gradle can never mark * your files as `up-to-date`, because it can't know if perhaps the behavior of your * custom function has changed. * * If you set `bumpThisNumberIfACustomStepChanges( <some number> )`, then spotless will * assume that the custom rules have not changed if the number has not changed. If a * custom rule does change, then you must bump the number so that spotless will know * that it must recheck the files it has already checked. */ public void bumpThisNumberIfACustomStepChanges(int number) { globalState = number; } private Serializable globalState = new NeverUpToDateBetweenRuns(); static class NeverUpToDateBetweenRuns extends LazyForwardingEquality<Integer> { private static final long serialVersionUID = 1L; private static final Random RANDOM = new Random(); @Override protected Integer calculateState() throws Exception { return RANDOM.nextInt(); } } /** * Adds the given custom step, which is constructed lazily for performance reasons. * * The resulting function will receive a string with unix-newlines, and it must return a string unix newlines. * * If you're getting errors about `closure cannot be cast to com.diffplug.common.base.Throwing$Function`, then use * {@link #customLazyGroovy(String, ThrowingEx.Supplier)}. */ public void customLazy(String name, ThrowingEx.Supplier<FormatterFunc> formatterSupplier) { Objects.requireNonNull(name, "name"); Objects.requireNonNull(formatterSupplier, "formatterSupplier"); addStep(FormatterStep.createLazy(name, () -> globalState, unusedState -> formatterSupplier.get())); } /** Same as {@link #customLazy(String, ThrowingEx.Supplier)}, but for Groovy closures. */ public void customLazyGroovy(String name, ThrowingEx.Supplier<Closure<String>> formatterSupplier) { Objects.requireNonNull(formatterSupplier, "formatterSupplier"); customLazy(name, () -> formatterSupplier.get()::call); } /** Adds a custom step. Receives a string with unix-newlines, must return a string with unix newlines. */ public void custom(String name, Closure<String> formatter) { Objects.requireNonNull(formatter, "formatter"); custom(name, formatter::call); } /** Adds a custom step. Receives a string with unix-newlines, must return a string with unix newlines. */ public void custom(String name, FormatterFunc formatter) { Objects.requireNonNull(formatter, "formatter"); customLazy(name, () -> formatter); } /** Highly efficient find-replace char sequence. */ public void replace(String name, CharSequence original, CharSequence after) { addStep(ReplaceStep.create(name, original, after)); } /** Highly efficient find-replace regex. */ public void replaceRegex(String name, String regex, String replacement) { addStep(ReplaceRegexStep.create(name, regex, replacement)); } /** Removes trailing whitespace. */ public void trimTrailingWhitespace() { addStep(TrimTrailingWhitespaceStep.create()); } /** Ensures that files end with a single newline. */ public void endWithNewline() { addStep(EndWithNewlineStep.create()); } /** Ensures that the files are indented using spaces. */ public void indentWithSpaces(int numSpacesPerTab) { addStep(IndentStep.Type.SPACE.create(numSpacesPerTab)); } /** Ensures that the files are indented using spaces. */ public void indentWithSpaces() { indentWithSpaces(4); } /** Ensures that the files are indented using tabs. */ public void indentWithTabs(int tabToSpaces) { addStep(IndentStep.Type.TAB.create(tabToSpaces)); } /** Ensures that the files are indented using tabs. */ public void indentWithTabs() { indentWithTabs(4); } /** * @param licenseHeader * Content that should be at the top of every file * @param delimiter * Spotless will look for a line that starts with this to know what the "top" is. */ public void licenseHeader(String licenseHeader, String delimiter) { addStep(LicenseHeaderStep.createFromHeader(licenseHeader, delimiter)); } /** * @param licenseHeaderFile * Content that should be at the top of every file * @param delimiter * Spotless will look for a line that starts with this to know what the "top" is. */ public void licenseHeaderFile(Object licenseHeaderFile, String delimiter) { Objects.requireNonNull(licenseHeaderFile, "licenseHeaderFile"); Objects.requireNonNull(delimiter, "delimiter"); addStep(LicenseHeaderStep.createFromFile(getProject().file(licenseHeaderFile), getEncoding(), delimiter)); } /** Sets up a format task according to the values in this extension. */ protected void setupTask(SpotlessTask task) { task.setPaddedCell(paddedCell); task.setEncoding(getEncoding().name()); task.setExceptionPolicy(exceptionPolicy); task.setTarget(target); task.setSteps(steps); task.setLineEndingsPolicy(getLineEndings().createPolicy(getProject().getProjectDir(), () -> task.target)); } /** Returns the project that this extension is attached to. */ protected Project getProject() { return root.project; } }