/*
* 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.spotless;
import static com.diffplug.spotless.LibPreconditions.requireElementsNonNull;
import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
/**
* Models the result of applying a {@link Formatter} on a given {@link File}
* while characterizing various failure modes (slow convergence, cycles, and divergence).
*
* See {@link #check(Formatter, File)} as the entry point to this class.
*/
public final class PaddedCell {
/** The kind of result. */
public enum Type {
CONVERGE, CYCLE, DIVERGE;
/** Creates a PaddedCell with the given file and steps. */
PaddedCell create(File file, List<String> steps) {
return new PaddedCell(file, this, steps);
}
}
private final File file;
private final Type type;
private final List<String> steps;
private PaddedCell(File file, Type type, List<String> steps) {
this.file = Objects.requireNonNull(file, "file");
this.type = Objects.requireNonNull(type, "type");
// defensive copy
this.steps = new ArrayList<>(steps);
requireElementsNonNull(this.steps);
}
/** Returns the file which was tested. */
public File file() {
return file;
}
/** Returns the type of the result (either {@link Type#CONVERGE}, {@link Type#CYCLE}, or {@link Type#DIVERGE}). */
public Type type() {
return type;
}
/** Returns the steps it takes to get to the result. */
public List<String> steps() {
return Collections.unmodifiableList(steps);
}
/**
* Applies the given formatter to the given file, checking that
* F(F(input)) == F(input).
*
* If it meets this test, {@link #misbehaved()} will return false.
*
* If it fails the test, {@link #misbehaved()} will return true, and you can find
* out more about the misbehavior based on its {@link Type}.
*
*/
public static PaddedCell check(Formatter formatter, File file) {
Objects.requireNonNull(formatter, "formatter");
Objects.requireNonNull(file, "file");
byte[] rawBytes = ThrowingEx.get(() -> Files.readAllBytes(file.toPath()));
String raw = new String(rawBytes, formatter.getEncoding());
String original = LineEnding.toUnix(raw);
return check(formatter, file, original, MAX_CYCLE);
}
public static PaddedCell check(Formatter formatter, File file, String originalUnix) {
return check(
Objects.requireNonNull(formatter, "formatter"),
Objects.requireNonNull(file, "file"),
Objects.requireNonNull(originalUnix, "originalUnix"),
MAX_CYCLE);
}
private static final int MAX_CYCLE = 10;
private static PaddedCell check(Formatter formatter, File file, String original, int maxLength) {
if (maxLength < 2) {
throw new IllegalArgumentException("maxLength must be at least 2");
}
String appliedOnce = formatter.compute(original, file);
if (appliedOnce.equals(original)) {
return Type.CONVERGE.create(file, Collections.singletonList(appliedOnce));
}
String appliedTwice = formatter.compute(appliedOnce, file);
if (appliedOnce.equals(appliedTwice)) {
return Type.CONVERGE.create(file, Collections.singletonList(appliedOnce));
}
List<String> appliedN = new ArrayList<>();
appliedN.add(appliedOnce);
appliedN.add(appliedTwice);
String input = appliedTwice;
while (appliedN.size() < maxLength) {
String output = formatter.compute(input, file);
if (output.equals(input)) {
return Type.CONVERGE.create(file, appliedN);
} else {
int idx = appliedN.indexOf(output);
if (idx >= 0) {
return Type.CYCLE.create(file, appliedN.subList(idx, appliedN.size()));
} else {
appliedN.add(output);
input = output;
}
}
}
return Type.DIVERGE.create(file, appliedN);
}
/**
* Returns true iff the formatter misbehaved in any way
* (did not converge after a single iteration).
*/
public boolean misbehaved() {
boolean isWellBehaved = type == Type.CONVERGE && steps.size() <= 1;
return !isWellBehaved;
}
/** Any result which doesn't diverge can be resolved. */
public boolean isResolvable() {
return type != Type.DIVERGE;
}
/** Returns the "canonical" form for this particular result (only possible if isResolvable). */
public String canonical() {
// @formatter:off
switch (type) {
case CONVERGE: return steps.get(steps.size() - 1);
case CYCLE: return Collections.min(steps, Comparator.comparing(String::length).thenComparing(Function.identity()));
case DIVERGE: throw new IllegalArgumentException("No canonical form for a diverging result");
default: throw new IllegalArgumentException("Unknown type: " + type);
}
// @formatter:on
}
/** Returns a string which describes this result. */
public String userMessage() {
// @formatter:off
switch (type) {
case CONVERGE: return "converges after " + steps.size() + " steps";
case CYCLE: return "cycles between " + steps.size() + " steps";
case DIVERGE: return "diverges after " + steps.size() + " steps";
default: throw new IllegalArgumentException("Unknown type: " + type);
}
// @formatter:on
}
}