com.diffplug.gradle.spotless.FormatExtension Maven / Gradle / Ivy
Show all versions of spotless-plugin-gradle Show documentation
/*
* Copyright 2016-2024 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 static com.diffplug.gradle.spotless.SpotlessPluginRedirect.badSemver;
import static com.diffplug.gradle.spotless.SpotlessPluginRedirect.badSemverOfGradle;
import static java.util.Objects.requireNonNull;
import java.io.File;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.gradle.api.Action;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.file.ConfigurableFileTree;
import org.gradle.api.file.Directory;
import org.gradle.api.file.FileCollection;
import org.gradle.api.plugins.BasePlugin;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.util.GradleVersion;
import com.diffplug.common.base.Preconditions;
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.OnMatch;
import com.diffplug.spotless.Provisioner;
import com.diffplug.spotless.SerializedFunction;
import com.diffplug.spotless.biome.BiomeFlavor;
import com.diffplug.spotless.cpp.ClangFormatStep;
import com.diffplug.spotless.extra.EclipseBasedStepBuilder;
import com.diffplug.spotless.extra.wtp.EclipseWtpFormatterStep;
import com.diffplug.spotless.generic.EndWithNewlineStep;
import com.diffplug.spotless.generic.FenceStep;
import com.diffplug.spotless.generic.IndentStep;
import com.diffplug.spotless.generic.LicenseHeaderStep;
import com.diffplug.spotless.generic.LicenseHeaderStep.YearMode;
import com.diffplug.spotless.generic.NativeCmdStep;
import com.diffplug.spotless.generic.ReplaceRegexStep;
import com.diffplug.spotless.generic.ReplaceStep;
import com.diffplug.spotless.generic.TrimTrailingWhitespaceStep;
import com.diffplug.spotless.npm.NpmPathResolver;
import com.diffplug.spotless.npm.PrettierFormatterStep;
import groovy.lang.Closure;
/** Adds a {@code spotless{Name}Check} and {@code spotless{Name}Apply} task. */
public class FormatExtension {
final SpotlessExtension spotless;
final List> lazyActions = new ArrayList<>();
@Inject
public FormatExtension(SpotlessExtension spotless) {
this.spotless = requireNonNull(spotless);
}
protected final Provisioner provisioner() {
return spotless.getRegisterDependenciesTask().getTaskService().get().provisionerFor(spotless);
}
private String formatName() {
for (Map.Entry entry : spotless.formats.entrySet()) {
if (entry.getValue() == this) {
return entry.getKey();
}
}
throw new IllegalStateException("This format is not contained by any SpotlessExtension.");
}
LineEnding lineEndings;
/**
* Returns the line endings to use (defaults to
* {@link SpotlessExtensionImpl#getLineEndings()}.
*/
public LineEnding getLineEndings() {
return lineEndings == null ? spotless.getLineEndings() : lineEndings;
}
/**
* Sets the line endings to use (defaults to
* {@link SpotlessExtensionImpl#getLineEndings()}.
*/
public void setLineEndings(LineEnding lineEndings) {
this.lineEndings = requireNonNull(lineEndings);
}
Charset encoding;
/**
* Returns the encoding to use (defaults to
* {@link SpotlessExtensionImpl#getEncoding()}.
*/
public Charset getEncoding() {
return encoding == null ? spotless.getEncoding() : encoding;
}
/**
* Sets the encoding to use (defaults to
* {@link SpotlessExtensionImpl#getEncoding()}.
*/
public void setEncoding(String name) {
setEncoding(Charset.forName(requireNonNull(name)));
}
/**
* Sentinel to distinguish between "don't ratchet this format" and "use spotless
* parent format".
*/
private static final String RATCHETFROM_NOT_SET_AT_FORMAT_LEVEL = " not set at format level ";
private String ratchetFrom = RATCHETFROM_NOT_SET_AT_FORMAT_LEVEL;
/** @see #setRatchetFrom(String) */
public String getRatchetFrom() {
return ratchetFrom == RATCHETFROM_NOT_SET_AT_FORMAT_LEVEL ? spotless.getRatchetFrom() : ratchetFrom;
}
/**
* Allows you to override the value from the parent
* {@link SpotlessExtension#setRatchetFrom(String)} for this specific format.
*/
public void setRatchetFrom(String ratchetFrom) {
this.ratchetFrom = ratchetFrom;
}
/** @see #setRatchetFrom(String) */
public void ratchetFrom(String ratchetFrom) {
setRatchetFrom(ratchetFrom);
}
/**
* Sets the encoding to use (defaults to
* {@link SpotlessExtensionImpl#getEncoding()}.
*/
public void setEncoding(Charset charset) {
encoding = requireNonNull(charset);
}
final FormatExceptionPolicyStrict exceptionPolicy = new FormatExceptionPolicyStrict();
/** Ignores errors in the given step. */
public void ignoreErrorForStep(String stepName) {
exceptionPolicy.excludeStep(requireNonNull(stepName));
}
/** Ignores errors for the given relative path. */
public void ignoreErrorForPath(String relativePath) {
exceptionPolicy.excludePath(requireNonNull(relativePath));
}
/**
* Sets encoding to use (defaults to
* {@link SpotlessExtensionImpl#getEncoding()}).
*/
public void encoding(String charset) {
setEncoding(charset);
}
/** The files to be formatted = (target - targetExclude). */
protected FileCollection target, targetExclude;
/** The value from which files will be excluded if their content contain it. */
@Nullable
protected String targetExcludeContentPattern = null;
protected boolean isLicenseHeaderStep(FormatterStep formatterStep) {
String formatterStepName = formatterStep.getName();
if (formatterStepName.startsWith(LicenseHeaderStep.class.getName())) {
return true;
}
return false;
}
/**
* Sets which files should be formatted. Files to be formatted = (target -
* targetExclude).
*
* When this method is called multiple times, only the last call has any effect.
*
* FileCollections pass through raw. Strings are treated as the 'include' arg to
* fileTree, with project.rootDir as the dir. List are treated as the
* 'includes' arg to fileTree, with project.rootDir as the dir. Anything else
* gets passed to getProject().files().
*
* If you pass any strings that start with "**\/*", this method will
* automatically filter out "build", ".gradle", and ".git" folders.
*/
public void target(Object... targets) {
this.target = parseTargetsIsExclude(targets, false);
}
/**
* Sets which files will be excluded from formatting. Files to be formatted =
* (target - targetExclude).
*
* When this method is called multiple times, only the last call has any effect.
*
* FileCollections pass through raw. Strings are treated as the 'include' arg to
* fileTree, with project.rootDir as the dir. List are treated as the
* 'includes' arg to fileTree, with project.rootDir as the dir. Anything else
* gets passed to getProject().files().
*/
public void targetExclude(Object... targets) {
this.targetExclude = parseTargetsIsExclude(targets, true);
}
/**
* Excludes all files whose content contains {@code string}.
*
* When this method is called multiple times, only the last call has any effect.
*/
public void targetExcludeIfContentContains(String string) {
targetExcludeIfContentContainsRegex(Pattern.quote(string));
}
/**
* Excludes all files whose content contains the given regex.
*
* When this method is called multiple times, only the last call has any effect.
*/
public void targetExcludeIfContentContainsRegex(String regex) {
this.targetExcludeContentPattern = regex;
}
private FileCollection parseTargetsIsExclude(Object[] targets, boolean isExclude) {
requireElementsNonNull(targets);
if (targets.length == 0) {
return getProject().files();
} else if (targets.length == 1) {
return parseTargetIsExclude(targets[0], isExclude);
} else {
FileCollection union = getProject().files();
for (Object target : targets) {
union = union.plus(parseTargetIsExclude(target, isExclude));
}
return union;
}
}
/**
* FileCollections pass through raw. Strings are treated as the 'include' arg to
* fileTree, with project.rootDir as the dir. List are treated as the
* 'includes' arg to fileTree, with project.rootDir as the dir. Anything else
* gets passed to getProject().files().
*/
protected final FileCollection parseTarget(Object target) {
return parseTargetIsExclude(target, false);
}
private final FileCollection parseTargetIsExclude(Object target, boolean isExclude) {
if (target instanceof Collection) {
return parseTargetsIsExclude(((Collection>) target).toArray(), isExclude);
} else if (target instanceof FileCollection) {
return (FileCollection) target;
} else if (target instanceof String) {
File dir = getProject().getProjectDir();
ConfigurableFileTree matchedFiles = getProject().fileTree(dir);
String targetString = (String) target;
matchedFiles.include(targetString);
// 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
// but we only want to do that if they are *including* - if they are specifying
// what they want to exclude, we shouldn't filter at all
if (isExclude) {
return matchedFiles;
}
if (targetString.startsWith("**/") || targetString.startsWith("**\\")) {
List excludes = new ArrayList<>();
// no git
excludes.add(".git");
// no .gradle
if (getProject() == getProject().getRootProject()) {
excludes.add(".gradle");
}
// no build folders (flatInclude means that subproject might not be subfolders,
// see https://github.com/diffplug/spotless/issues/121)
relativizeIfSubdir(excludes, dir, getProject().getLayout().getBuildDirectory().getAsFile().get());
for (Project subproject : getProject().getSubprojects()) {
relativizeIfSubdir(excludes, dir, subproject.getLayout().getBuildDirectory().getAsFile().get());
}
matchedFiles.exclude(excludes);
}
return matchedFiles;
} else {
return getProject().files(target);
}
}
private static void relativizeIfSubdir(List relativePaths, File root, File dest) {
String relativized = relativize(root, dest);
if (relativized != null) {
relativePaths.add(relativized);
}
}
/**
* Returns the relative path between root and dest, or null if dest is not a
* child of root.
*/
static @Nullable String relativize(File root, File dest) {
String rootPath = root.getAbsolutePath();
String destPath = dest.getAbsolutePath();
if (!destPath.startsWith(rootPath)) {
return null;
} else {
return destPath.substring(rootPath.length());
}
}
/** The steps that need to be added. */
protected final List steps = new ArrayList<>();
/** Adds a new step. */
public void addStep(FormatterStep newStep) {
requireNonNull(newStep);
int existingIdx = getExistingStepIdx(newStep.getName());
if (existingIdx != -1) {
throw new GradleException(
"Multiple steps with name '" + newStep.getName() + "' for spotless format '" + formatName() + "'");
}
steps.add(newStep);
}
/** Adds a new step that requires a Provisioner. */
public void addStep(Function createStepFn) {
requireNonNull(createStepFn);
FormatterStep newStep = createStepFn.apply(provisioner());
addStep(newStep);
}
/**
* Returns the index of the existing step with the given name, or -1 if no such
* step exists.
*/
protected int getExistingStepIdx(String stepName) {
for (int i = 0; i < steps.size(); ++i) {
if (steps.get(i).getName().equals(stepName)) {
return i;
}
}
return -1;
}
/** Replaces the given step. */
protected void replaceStep(FormatterStep replacementStep) {
int existingIdx = getExistingStepIdx(replacementStep.getName());
if (existingIdx == -1) {
throw new GradleException("Cannot replace step '" + replacementStep.getName() + "' for spotless format '"
+ formatName() + "' because it hasn't been added yet.");
}
steps.set(existingIdx, replacementStep);
}
/** Clears all of the existing steps. */
public void clearSteps() {
steps.clear();
}
/**
* An optional performance optimization if you are using any of the
* {@code custom} methods. If you aren't explicitly calling {@code custom}, 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 the {@code custom} methods, then Gradle can never
* mark your files as {@code up-to-date}, because it can't know if perhaps the
* behavior of your custom function has changed.
*
* If you set {@code bumpThisNumberIfACustomStepChanges( )}, 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 {
private static final long serialVersionUID = 1L;
private static final Random RANDOM = new Random();
@Override
protected Integer calculateState() throws Exception {
return RANDOM.nextInt();
}
}
/**
* Adds a custom step. Receives a string with unix-newlines, must return a
* string with unix newlines.
*/
public void custom(String name, Closure formatter) {
requireNonNull(formatter, "formatter");
Closure dehydrated = formatter.dehydrate();
custom(name, new ClosureFormatterFunc(dehydrated));
}
static class ClosureFormatterFunc implements FormatterFunc, Serializable {
private final Closure closure;
ClosureFormatterFunc(Closure closure) {
this.closure = closure;
}
@Override
public String apply(String unixNewlines) {
return closure.call(unixNewlines);
}
}
/**
* Adds a custom step. Receives a string with unix-newlines, must return a
* string with unix newlines.
*/
public void custom(String name, FormatterFunc formatter) {
requireNonNull(formatter, "formatter");
if (badSemverOfGradle() < badSemver(SpotlessPlugin.VER_GRADLE_minVersionForCustom)) {
throw new GradleException("The 'custom' method is only available if you are using Gradle "
+ SpotlessPlugin.VER_GRADLE_minVersionForCustom
+ " or newer, this is "
+ GradleVersion.current().getVersion());
}
addStep(FormatterStep.createLazy(name, () -> globalState, SerializedFunction.alwaysReturns(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() {
addStep(IndentStep.Type.SPACE.create());
}
/** 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() {
addStep(IndentStep.Type.TAB.create());
}
/** Ensures formatting of files via native binary. */
public void nativeCmd(String name, String pathToExe, List arguments) {
addStep(NativeCmdStep.create(name, new File(pathToExe), arguments));
}
/**
* Created by {@link FormatExtension#licenseHeader(String, String)} or
* {@link FormatExtension#licenseHeaderFile(Object, String)}. For most
* language-specific formats (e.g. java, scala, etc.) you can omit the second
* {@code delimiter} argument, because it is supplied automatically
* ({@link HasBuiltinDelimiterForLicense}).
*/
public class LicenseHeaderConfig {
LicenseHeaderStep builder;
Boolean updateYearWithLatest = null;
public LicenseHeaderConfig named(String name) {
String existingStepName = builder.getName();
builder = builder.withName(name);
int existingStepIdx = getExistingStepIdx(existingStepName);
if (existingStepIdx != -1) {
steps.set(existingStepIdx, createStep());
} else {
addStep(createStep());
}
return this;
}
public LicenseHeaderConfig onlyIfContentMatches(String contentPattern) {
builder = builder.withContentPattern(contentPattern);
replaceStep(createStep());
return this;
}
public LicenseHeaderConfig(LicenseHeaderStep builder) {
this.builder = builder;
}
/**
* @param delimiter Spotless will look for a line that starts with this regular
* expression pattern to know what the "top" is.
*/
public LicenseHeaderConfig delimiter(String delimiter) {
builder = builder.withDelimiter(delimiter);
replaceStep(createStep());
return this;
}
/**
* @param yearSeparator The characters used to separate the first and last years
* in multi years patterns.
*/
public LicenseHeaderConfig yearSeparator(String yearSeparator) {
builder = builder.withYearSeparator(yearSeparator);
replaceStep(createStep());
return this;
}
public LicenseHeaderConfig skipLinesMatching(String skipLinesMatching) {
builder = builder.withSkipLinesMatching(skipLinesMatching);
replaceStep(createStep());
return this;
}
/**
* @param updateYearWithLatest Will turn {@code 2004} into {@code 2004-2020},
* and {@code 2004-2019} into {@code 2004-2020}
* Default value is false, unless
* {@link SpotlessExtensionImpl#ratchetFrom(String)}
* is used, in which case default value is true.
*/
public LicenseHeaderConfig updateYearWithLatest(boolean updateYearWithLatest) {
this.updateYearWithLatest = updateYearWithLatest;
replaceStep(createStep());
return this;
}
FormatterStep createStep() {
return builder.withYearModeLazy(() -> {
if ("true".equals(spotless.project
.findProperty(LicenseHeaderStep.FLAG_SET_LICENSE_HEADER_YEARS_FROM_GIT_HISTORY()))) {
return YearMode.SET_FROM_GIT;
} else {
boolean updateYear = updateYearWithLatest == null ? getRatchetFrom() != null : updateYearWithLatest;
return updateYear ? YearMode.UPDATE_TO_TODAY : YearMode.PRESERVE;
}
}).build();
}
}
/**
* @param licenseHeader Content that should be at the top of every file.
* @param delimiter Spotless will look for a line that starts with this
* regular expression pattern to know what the "top" is.
*/
public LicenseHeaderConfig licenseHeader(String licenseHeader, String delimiter) {
LicenseHeaderConfig config = new LicenseHeaderConfig(
LicenseHeaderStep.headerDelimiter(licenseHeader, delimiter));
addStep(config.createStep());
return config;
}
/**
* @param licenseHeaderFile Content that should be at the top of every file.
* @param delimiter Spotless will look for a line that starts with this
* regular expression pattern to know what the "top"
* is.
*/
public LicenseHeaderConfig licenseHeaderFile(Object licenseHeaderFile, String delimiter) {
LicenseHeaderConfig config = new LicenseHeaderConfig(LicenseHeaderStep.headerDelimiter(() -> {
File file = getProject().file(licenseHeaderFile);
byte[] data = Files.readAllBytes(file.toPath());
return new String(data, getEncoding());
}, delimiter));
addStep(config.createStep());
return config;
}
public abstract static class NpmStepConfig> {
public static final String SPOTLESS_NPM_INSTALL_CACHE_DEFAULT_NAME = "spotless-npm-install-cache";
@Nullable
protected Object npmFile;
@Nullable
protected Object nodeFile;
@Nullable
protected Object npmInstallCache;
@Nullable
protected Object npmrcFile;
protected Project project;
private Consumer replaceStep;
public NpmStepConfig(Project project, Consumer replaceStep) {
this.project = requireNonNull(project);
this.replaceStep = requireNonNull(replaceStep);
}
@SuppressWarnings("unchecked")
public T npmExecutable(final Object npmFile) {
this.npmFile = npmFile;
replaceStep();
return (T) this;
}
@SuppressWarnings("unchecked")
public T nodeExecutable(final Object nodeFile) {
this.nodeFile = nodeFile;
replaceStep();
return (T) this;
}
public T npmrc(final Object npmrcFile) {
this.npmrcFile = npmrcFile;
replaceStep();
return (T) this;
}
public T npmInstallCache(final Object npmInstallCache) {
this.npmInstallCache = npmInstallCache;
replaceStep();
return (T) this;
}
public T npmInstallCache() {
this.npmInstallCache = new File(project.getLayout().getBuildDirectory().getAsFile().get(),
SPOTLESS_NPM_INSTALL_CACHE_DEFAULT_NAME);
replaceStep();
return (T) this;
}
File npmFileOrNull() {
return fileOrNull(npmFile);
}
File nodeFileOrNull() {
return fileOrNull(nodeFile);
}
File npmrcFileOrNull() {
return fileOrNull(npmrcFile);
}
File npmModulesCacheOrNull() {
return fileOrNull(npmInstallCache);
}
private File fileOrNull(Object npmFile) {
return npmFile != null ? project.file(npmFile) : null;
}
protected void replaceStep() {
replaceStep.accept(createStep());
}
abstract protected FormatterStep createStep();
}
public class PrettierConfig extends NpmStepConfig {
@Nullable
Object prettierConfigFile;
@Nullable
Map prettierConfig;
final Map devDependencies;
PrettierConfig(Map devDependencies) {
super(getProject(), FormatExtension.this::replaceStep);
this.devDependencies = requireNonNull(devDependencies);
}
public PrettierConfig configFile(final Object prettierConfigFile) {
this.prettierConfigFile = prettierConfigFile;
replaceStep();
return this;
}
public PrettierConfig config(final Map prettierConfig) {
this.prettierConfig = new TreeMap<>(prettierConfig);
replaceStep();
return this;
}
@Override
protected FormatterStep createStep() {
final Project project = getProject();
return PrettierFormatterStep.create(devDependencies, provisioner(), project.getProjectDir(),
project.getLayout().getBuildDirectory().getAsFile().get(), npmModulesCacheOrNull(),
new NpmPathResolver(npmFileOrNull(), nodeFileOrNull(), npmrcFileOrNull(),
Arrays.asList(project.getProjectDir(), project.getRootDir())),
new com.diffplug.spotless.npm.PrettierConfig(
this.prettierConfigFile != null ? project.file(this.prettierConfigFile) : null,
this.prettierConfig));
}
}
/**
* Generic Biome formatter step that detects the language of the input file from
* the file name. It should be specified as a formatter step for a generic
* format{ ... }
.
*/
public class BiomeGeneric extends BiomeStepConfig {
@Nullable
String language;
/**
* Creates a new Biome config that downloads the Biome executable for the given
* version from the network.
*
* @param version Biome version to use. The default version is used when
* null
.
*/
public BiomeGeneric(String version) {
super(getProject(), FormatExtension.this::replaceStep, BiomeFlavor.BIOME, version);
}
/**
* Sets the language (syntax) of the input files to format. When
* null
or the empty string, the language is detected automatically
* from the file name. Currently the following languages are supported by Biome:
*
* - js (JavaScript)
* - jsx (JavaScript + JSX)
* - js? (JavaScript or JavaScript + JSX, depending on the file
* extension)
* - ts (TypeScript)
* - tsx (TypeScript + JSX)
* - ts? (TypeScript or TypeScript + JSX, depending on the file
* extension)
* - css (CSS, requires biome >= 1.9.0)
* - json (JSON)
* - jsonc (JSON + comments)
*
*
* @param language The language of the files to format.
* @return This step for further configuration.
*/
public BiomeGeneric language(String language) {
this.language = language;
replaceStep();
return this;
}
@Override
protected String getLanguage() {
return language;
}
@Override
protected BiomeGeneric getThis() {
return this;
}
}
/** Uses the default version of prettier. */
public PrettierConfig prettier() {
return prettier(PrettierFormatterStep.defaultDevDependencies());
}
/** Uses the specified version of prettier. */
public PrettierConfig prettier(String version) {
return prettier(PrettierFormatterStep.defaultDevDependenciesWithPrettier(version));
}
/** Uses exactly the npm packages specified in the map. */
public PrettierConfig prettier(Map devDependencies) {
PrettierConfig prettierConfig = new PrettierConfig(devDependencies);
addStep(prettierConfig.createStep());
return prettierConfig;
}
/**
* Defaults to downloading the default Biome version from the network. To work
* offline, you can specify the path to the Biome executable via
* {@code biome().pathToExe(...)}.
*/
public BiomeStepConfig> biome() {
return biome(null);
}
/** Downloads the given Biome version from the network. */
public BiomeStepConfig> biome(String version) {
var biomeConfig = new BiomeGeneric(version);
addStep(biomeConfig.createStep());
return biomeConfig;
}
/** Uses the default version of clang-format. */
public ClangFormatConfig clangFormat() {
return clangFormat(ClangFormatStep.defaultVersion());
}
/** Uses the specified version of clang-format. */
public ClangFormatConfig clangFormat(String version) {
return new ClangFormatConfig(version);
}
public class ClangFormatConfig {
ClangFormatStep stepCfg;
ClangFormatConfig(String version) {
this.stepCfg = ClangFormatStep.withVersion(version);
addStep(createStep());
}
/** Any of: LLVM, Google, Chromium, Mozilla, WebKit. */
public ClangFormatConfig style(String style) {
stepCfg = stepCfg.withStyle(style);
replaceStep(createStep());
return this;
}
public ClangFormatConfig pathToExe(String pathToBlack) {
stepCfg = stepCfg.withPathToExe(pathToBlack);
replaceStep(createStep());
return this;
}
private FormatterStep createStep() {
return stepCfg.create();
}
}
public class EclipseWtpConfig {
private final EclipseBasedStepBuilder builder;
EclipseWtpConfig(EclipseWtpFormatterStep type, String version) {
builder = type.createBuilder(provisioner());
builder.setVersion(version);
addStep(builder.build());
}
public void configFile(Object... configFiles) {
requireElementsNonNull(configFiles);
Project project = getProject();
builder.setPreferences(project.files(configFiles).getFiles());
replaceStep(builder.build());
}
}
public EclipseWtpConfig eclipseWtp(EclipseWtpFormatterStep type) {
return eclipseWtp(type, EclipseWtpFormatterStep.defaultVersion());
}
public EclipseWtpConfig eclipseWtp(EclipseWtpFormatterStep type, String version) {
return new EclipseWtpConfig(type, version);
}
/**
*
* spotless {
* format 'examples', {
* target '*.md'
* withinBlocks 'javascript examples', '\n```javascript\n', '\n```\n', {
* prettier().config(['parser': 'javascript'])
* }
* ...
*
*/
public void withinBlocks(String name, String open, String close, Action configure) {
withinBlocks(name, open, close, FormatExtension.class, configure);
}
/**
* Same as {@link #withinBlocks(String, String, String, Action)}, except you can
* specify any language-specific subclass of {@link FormatExtension} to get
* language-specific steps.
*
*
* spotless {
* format 'examples', {
* target '*.md'
* withinBlocks 'java examples', '\n```java\n', '\n```\n', com.diffplug.gradle.spotless.JavaExtension, {
* googleJavaFormat()
* formatAnnotations()
* }
* ...
*
*/
public void withinBlocks(String name, String open, String close, Class clazz,
Action configure) {
withinBlocksHelper(FenceStep.named(name).openClose(open, close), clazz, configure);
}
/**
* Same as {@link #withinBlocks(String, String, String, Action)}, except instead
* of an open/close pair, you specify a regex with exactly one capturing group.
*/
public void withinBlocksRegex(String name, String regex, Action configure) {
withinBlocksRegex(name, regex, FormatExtension.class, configure);
}
/**
* Same as {@link #withinBlocksRegex(String, String, Action)}, except you can
* specify any language-specific subclass of {@link FormatExtension} to get
* language-specific steps.
*/
public void withinBlocksRegex(String name, String regex, Class clazz,
Action configure) {
withinBlocksHelper(FenceStep.named(name).regex(regex), clazz, configure);
}
private void withinBlocksHelper(FenceStep fence, Class clazz,
Action configure) {
// create the sub-extension
T formatExtension = spotless.instantiateFormatExtension(clazz);
// configure it
configure.execute(formatExtension);
// create a step which applies all of those steps as sub-steps
FormatterStep step = fence.applyWithin(formatExtension.steps);
addStep(step);
}
/**
* Given a regex with *exactly one capturing group*, disables formatting inside
* that captured group.
*/
public void toggleOffOnRegex(String regex) {
this.toggleFence = FenceStep.named(FenceStep.defaultToggleName()).regex(regex);
}
/** Disables formatting between the given tags. */
public void toggleOffOn(String off, String on) {
this.toggleFence = FenceStep.named(FenceStep.defaultToggleName()).openClose(off, on);
}
/** Disables formatting between {@code spotless:off} and {@code spotless:on}. */
public void toggleOffOn() {
toggleOffOn(FenceStep.defaultToggleOff(), FenceStep.defaultToggleOn());
}
/**
* Undoes all previous calls to {@link #toggleOffOn()} and
* {@link #toggleOffOn(String, String)}.
*/
public void toggleOffOnDisable() {
this.toggleFence = null;
}
private @Nullable FenceStep toggleFence;
/** Sets up a format task according to the values in this extension. */
protected void setupTask(SpotlessTask task) {
task.setEncoding(getEncoding().name());
task.setExceptionPolicy(exceptionPolicy);
FileCollection totalTarget = targetExclude == null ? target : target.minus(targetExclude);
task.setTarget(totalTarget);
List steps;
if (toggleFence != null) {
steps = List.of(toggleFence.preserveWithin(this.steps));
} else {
steps = this.steps;
}
if (targetExcludeContentPattern != null) {
steps.replaceAll(
formatterStep -> formatterStep.filterByContent(OnMatch.EXCLUDE, targetExcludeContentPattern));
}
task.setSteps(steps);
Directory projectDir = getProject().getLayout().getProjectDirectory();
LineEnding lineEndings = getLineEndings();
task.setLineEndingsPolicy(
getProject().provider(() -> lineEndings.createPolicy(projectDir.getAsFile(), () -> totalTarget)));
spotless.getRegisterDependenciesTask().hookSubprojectTask(task);
task.setupRatchet(getRatchetFrom() != null ? getRatchetFrom() : "");
}
/** Returns the project that this extension is attached to. */
protected Project getProject() {
return spotless.project;
}
/** Eager version of {@link #createIndependentApplyTaskLazy(String)} */
public SpotlessApply createIndependentApplyTask(String taskName) {
return createIndependentApplyTaskLazy(taskName).get();
}
/**
* Creates an independent {@link SpotlessApply} for (very) unusual
* circumstances.
*
* Most users will not want this method. In the rare case that you want to
* create a {@code SpotlessApply} which is independent of the normal Spotless
* machinery, this will let you do that.
*
* 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 TaskProvider createIndependentApplyTaskLazy(String taskName) {
Preconditions.checkArgument(!taskName.endsWith(SpotlessExtension.APPLY),
"Task name must not end with " + SpotlessExtension.APPLY);
TaskProvider spotlessTask = spotless.project.getTasks()
.register(taskName + SpotlessTaskService.INDEPENDENT_HELPER, SpotlessTaskImpl.class, task -> {
task.init(spotless.getRegisterDependenciesTask().getTaskService());
setupTask(task);
// clean removes the SpotlessCache, so we have to run after clean
task.mustRunAfter(BasePlugin.CLEAN_TASK_NAME);
});
// create the apply task
TaskProvider applyTask = spotless.project.getTasks().register(taskName, SpotlessApply.class,
task -> {
task.dependsOn(spotlessTask);
task.init(spotlessTask.get());
});
return applyTask;
}
protected GradleException noDefaultTargetException() {
return new GradleException("Spotless failure, no target set! You must set a target for " + formatName());
}
}