/* * 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 java.io.File; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Incorporates the PaddedCell machinery into broader apply / check usage. * * Here's the general workflow: * * ### Identify that paddedCell is needed * * Initially, paddedCell is off. That's the default, and there's no need for users to know about it. * * If they encounter a scenario where `spotlessCheck` fails after calling `spotlessApply`, then they would * justifiably be frustrated. Luckily, every time `spotlessCheck` fails, it passes the failed files to * {@link #anyMisbehave(Formatter, List)}, which checks to see if any of the rules are causing a cycle * or some other kind of mischief. If they are, it can give the user a special error message instructing * them to turn on paddedCell. * * ### spotlessCheck with paddedCell on * * Spotless check behaves as normal, finding a list of problem files, but then passes that list * to {@link #check(File, File, Formatter, List)}. If there were no problem files, then `paddedCell` * is no longer necessary, so users might as well turn it off, so we give that info as a warning. */ public final class PaddedCellBulk { private static final Logger logger = Logger.getLogger(PaddedCellBulk.class.getName()); /** * Returns true if the formatter is misbehaving for any of the given files. * * If, after 500ms of searching, none are found that misbehave, it gives the * formatter the benefit of the doubt and returns false. The real point of this * check is to handle the case that a user just called spotlessApply, but spotlessCheck * is still failing. In that case, all of the problemFiles are guaranteed to * be misbehaving, so this time limit doesn't hurt correctness. * * If you call this method after every failed spotlessCheck, it can help you * tell the user about a misbehaving rule and alert her to how to enable * paddedCell mode, with minimal effort. */ public static boolean anyMisbehave(Formatter formatter, List<File> problemFiles) { return anyMisbehave(formatter, problemFiles, 500); } /** Same as {@link #anyMisbehave(Formatter, List)} but with a customizable timeout. */ public static boolean anyMisbehave(Formatter formatter, List<File> problemFiles, long timeoutMs) { Objects.requireNonNull(formatter, "formatter"); Objects.requireNonNull(problemFiles, "problemFiles"); long start = System.currentTimeMillis(); for (File problem : problemFiles) { PaddedCell padded = PaddedCell.check(formatter, problem); if (padded.misbehaved()) { return true; } if (timeoutMs > 0 && System.currentTimeMillis() - start > timeoutMs) { return false; } } return false; } /** * Performs a full check using PaddedCell logic on the given files with the given formatter. * If any are found which do not conform to the PaddedCell, a description of the error will * be written to the diagnose dir. * * @param rootDir The root directory, used to determine the relative paths of the problemFiles. * @param diagnoseDir Directory where problems are described (based on the relative paths determined based on rootDir). * @param formatter The formatter to apply. * @param problemFiles The files with which we have a problem. * @return A list of files which are failing because of paddedCell problems, but could be fixed. (specifically, the files for which spotlessApply would be effective) */ @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") public static List<File> check(File rootDir, File diagnoseDir, Formatter formatter, List<File> problemFiles) throws IOException { Objects.requireNonNull(rootDir, "rootDir"); Objects.requireNonNull(diagnoseDir, "diagnoseDir"); Objects.requireNonNull(formatter, "formatter"); Objects.requireNonNull(problemFiles, "problemFiles"); // "fake" Formatter which can use the already-computed result of a PaddedCell as FakeStep paddedCellStep = new FakeStep(); Formatter paddedFormatter = Formatter.builder() .lineEndingsPolicy(formatter.getLineEndingsPolicy()) .encoding(formatter.getEncoding()) .rootDir(formatter.getRootDir()) .steps(Collections.singletonList(paddedCellStep)) .exceptionPolicy(formatter.getExceptionPolicy()) .build(); // empty out the diagnose folder Path rootPath = rootDir.toPath(); Path diagnosePath = diagnoseDir.toPath(); cleanDir(diagnosePath); List<File> stillFailing = new ArrayList<>(); for (File problemFile : problemFiles) { logger.fine("Running padded cell check on " + problemFile); PaddedCell padded = PaddedCell.check(formatter, problemFile); if (!padded.misbehaved()) { logger.fine(" well-behaved."); } else { // the file is misbehaved, so we'll write all its steps to DIAGNOSE_DIR Path relative = rootPath.relativize(problemFile.toPath()); Path diagnoseFile = diagnosePath.resolve(relative); for (int i = 0; i < padded.steps().size(); ++i) { Path path = Paths.get(diagnoseFile + "." + padded.type().name().toLowerCase(Locale.ROOT) + i); Files.createDirectories(path.getParent()); String version = padded.steps().get(i); Files.write(path, version.getBytes(formatter.getEncoding())); } // dump the type of the misbehavior to console logger.finer(" " + relative + " " + padded.userMessage()); if (!padded.isResolvable()) { // if it's not resolvable, then there's // no point killing the build over it } else { // if the input is resolvable, we'll use that to try again at // determining if it's clean paddedCellStep.set(problemFile, padded.canonical()); if (!paddedFormatter.isClean(problemFile)) { stillFailing.add(problemFile); } } } } return stillFailing; } /** Helper for check(). */ @SuppressWarnings("serial") @SuppressFBWarnings("SE_NO_SERIALVERSIONID") static class FakeStep implements FormatterStep { private File file; private String formatted; void set(File file, String formatted) { this.file = file; this.formatted = formatted; } @Override public String format(String raw, File file) throws Exception { if (file.equals(this.file)) { this.file = null; return Objects.requireNonNull(formatted); } else { throw new IllegalArgumentException("Must call set() before each call to format."); } } @Override public String getName() { return "Padded cell result"; } } /** Performs the typical spotlessApply, but with PaddedCell handling of misbehaving FormatterSteps. */ public static void apply(Formatter formatter, File file) throws IOException { Objects.requireNonNull(formatter, "formatter"); Objects.requireNonNull(file, "file"); byte[] rawBytes = Files.readAllBytes(file.toPath()); String raw = new String(rawBytes, formatter.getEncoding()); String rawUnix = LineEnding.toUnix(raw); // enforce the format String formattedUnix = formatter.compute(rawUnix, file); // convert the line endings if necessary String formatted = formatter.computeLineEndings(formattedUnix, file); // if F(input) == input, then the formatter is well-behaving and the input is clean byte[] formattedBytes = formatted.getBytes(formatter.getEncoding()); if (Arrays.equals(rawBytes, formattedBytes)) { return; } // F(input) != input, so we'll do a padded check PaddedCell cell = PaddedCell.check(formatter, file, rawUnix); if (!cell.isResolvable()) { // nothing we can do, but check will warn and dump out the divergence path return; } // get the canonical bytes String canonicalUnix = cell.canonical(); String canonical = formatter.computeLineEndings(canonicalUnix, file); byte[] canonicalBytes = canonical.getBytes(formatter.getEncoding()); if (!Arrays.equals(rawBytes, canonicalBytes)) { // and write them to disk if needed Files.write(file.toPath(), canonicalBytes, StandardOpenOption.TRUNCATE_EXISTING); } } /** Does whatever it takes to turn this path into an empty folder. */ private static void cleanDir(Path folder) throws IOException { if (Files.exists(folder)) { if (Files.isDirectory(folder)) { Files.walkFileTree(folder, FOLDER_CLEANING_VISITOR); } else { Files.delete(folder); } } Files.createDirectories(folder); } private static final SimpleFileVisitor<Path> FOLDER_CLEANING_VISITOR = new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(final Path file, final IOException e) { return handleException(e); } private FileVisitResult handleException(final IOException e) { logger.log(Level.SEVERE, e.getMessage(), e); return FileVisitResult.TERMINATE; } @Override public FileVisitResult postVisitDirectory(final Path dir, final IOException e) throws IOException { if (e != null) { return handleException(e); } Files.delete(dir); return FileVisitResult.CONTINUE; } }; }