/*
* 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.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.SkipWhenEmpty;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.incremental.IncrementalTaskInputs;
import com.diffplug.spotless.FormatExceptionPolicy;
import com.diffplug.spotless.FormatExceptionPolicyStrict;
import com.diffplug.spotless.Formatter;
import com.diffplug.spotless.FormatterStep;
import com.diffplug.spotless.LineEnding;
import com.diffplug.spotless.PaddedCell;
import com.diffplug.spotless.PaddedCellBulk;
public class SpotlessTask extends DefaultTask {
// set by SpotlessExtension, but possibly overridden by FormatExtension
@Input
protected String encoding = "UTF-8";
public String getEncoding() {
return encoding;
}
public void setEncoding(String encoding) {
this.encoding = Objects.requireNonNull(encoding);
}
@Input
protected LineEnding.Policy lineEndingsPolicy = LineEnding.UNIX.createPolicy();
public LineEnding.Policy getLineEndingsPolicy() {
return lineEndingsPolicy;
}
public void setLineEndingsPolicy(LineEnding.Policy lineEndingsPolicy) {
this.lineEndingsPolicy = Objects.requireNonNull(lineEndingsPolicy);
}
// set by FormatExtension
@Input
protected boolean paddedCell = false;
public boolean isPaddedCell() {
return paddedCell;
}
public void setPaddedCell(boolean paddedCell) {
this.paddedCell = paddedCell;
}
@Input
protected FormatExceptionPolicy exceptionPolicy = new FormatExceptionPolicyStrict();
public void setExceptionPolicy(FormatExceptionPolicy exceptionPolicy) {
this.exceptionPolicy = Objects.requireNonNull(exceptionPolicy);
}
public FormatExceptionPolicy getExceptionPolicy() {
return exceptionPolicy;
}
@InputFiles
@SkipWhenEmpty
protected Iterable<File> target;
public Iterable<File> getTarget() {
return target;
}
public void setTarget(Iterable<File> target) {
this.target = requireElementsNonNull(target);
}
@Input
protected List<FormatterStep> steps = new ArrayList<>();
public List<FormatterStep> getSteps() {
return Collections.unmodifiableList(steps);
}
public void setSteps(List<FormatterStep> steps) {
this.steps = requireElementsNonNull(steps);
}
public boolean addStep(FormatterStep step) {
return this.steps.add(Objects.requireNonNull(step));
}
private boolean check = false;
private boolean apply = false;
public void setCheck() {
this.check = true;
}
public void setApply() {
this.apply = true;
}
/** Returns the name of this format. */
String formatName() {
String name = getName();
if (name.startsWith(SpotlessPlugin.EXTENSION)) {
return name.substring(SpotlessPlugin.EXTENSION.length()).toLowerCase(Locale.ROOT);
} else {
return name;
}
}
@TaskAction
public void performAction(IncrementalTaskInputs inputs) throws Exception {
if (target == null) {
throw new GradleException("You must specify 'Iterable<File> toFormat'");
}
if (!check && !apply) {
throw new GradleException("Don't call " + getName() + " directly, call " + getName() + SpotlessPlugin.CHECK + " or " + getName() + SpotlessPlugin.APPLY);
}
// create the formatter
Formatter formatter = Formatter.builder()
.lineEndingsPolicy(lineEndingsPolicy)
.encoding(Charset.forName(encoding))
.rootDir(getProject().getProjectDir().toPath())
.steps(steps)
.exceptionPolicy(exceptionPolicy)
.build();
// find the outOfDate files
List<File> outOfDate = new ArrayList<>();
inputs.outOfDate(inputDetails -> {
File file = inputDetails.getFile();
if (file.isFile()) {
outOfDate.add(file);
}
});
if (apply) {
apply(formatter, outOfDate);
}
if (check) {
check(formatter, outOfDate);
}
}
private void apply(Formatter formatter, List<File> outOfDate) throws Exception {
if (isPaddedCell()) {
for (File file : outOfDate) {
getLogger().debug("Applying format to " + file);
PaddedCellBulk.apply(formatter, file);
}
} else {
boolean anyMisbehave = false;
for (File file : outOfDate) {
getLogger().debug("Applying format to " + file);
String unixResultIfDirty = formatter.applyToAndReturnResultIfDirty(file);
// because apply will count as up-to-date, it's important
// that every call to apply will get a PaddedCell check
if (!anyMisbehave && unixResultIfDirty != null) {
String onceMore = formatter.compute(unixResultIfDirty, file);
// f(f(input) == f(input) for an idempotent function
if (!onceMore.equals(unixResultIfDirty)) {
// it's not idempotent. but, if it converges, then it's likely a glitch that won't reoccur,
// so there's no need to make a bunch of noise for the user
PaddedCell result = PaddedCell.check(formatter, file, onceMore);
if (result.type() == PaddedCell.Type.CONVERGE) {
String finalResult = formatter.computeLineEndings(result.canonical(), file);
Files.write(file.toPath(), finalResult.getBytes(formatter.getEncoding()), StandardOpenOption.TRUNCATE_EXISTING);
} else {
// it didn't converge, so the user is going to need padded cell mode
anyMisbehave = true;
}
}
}
}
if (anyMisbehave) {
throw PaddedCellGradle.youShouldTurnOnPaddedCell(this);
}
}
}
private void check(Formatter formatter, List<File> outOfDate) throws Exception {
List<File> problemFiles = new ArrayList<>();
for (File file : outOfDate) {
getLogger().debug("Checking format on " + file);
if (!formatter.isClean(file)) {
problemFiles.add(file);
}
}
if (paddedCell) {
PaddedCellGradle.check(this, formatter, problemFiles);
} else {
if (!problemFiles.isEmpty()) {
// if we're not in paddedCell mode, we'll check if maybe we should be
if (PaddedCellBulk.anyMisbehave(formatter, problemFiles)) {
throw PaddedCellGradle.youShouldTurnOnPaddedCell(this);
} else {
throw formatViolationsFor(formatter, problemFiles);
}
}
}
}
/** Returns an exception which indicates problem files nicely. */
GradleException formatViolationsFor(Formatter formatter, List<File> problemFiles) throws IOException {
return new GradleException(DiffMessageFormatter.messageFor(this, formatter, problemFiles));
}
}