/* * 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 java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.ListIterator; import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.EditList; import org.eclipse.jgit.diff.MyersDiff; import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.diff.RawTextComparator; import com.diffplug.common.base.CharMatcher; import com.diffplug.common.base.Preconditions; import com.diffplug.common.base.Splitter; import com.diffplug.spotless.Formatter; import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.PaddedCell; /** Formats the messages of failed spotlessCheck invocations with a nice diff message. */ final class DiffMessageFormatter { private static final int MAX_CHECK_MESSAGE_LINES = 50; static final int MAX_FILES_TO_LIST = 10; static String messageFor(SpotlessTask task, Formatter formatter, List<File> problemFiles) throws IOException { DiffMessageFormatter diffFormater = new DiffMessageFormatter(task, formatter, problemFiles); return "The following files had format violations:\n" + diffFormater.buffer + "Run 'gradlew " + SpotlessPlugin.EXTENSION + SpotlessPlugin.APPLY + "' to fix these violations."; } private final StringBuilder buffer = new StringBuilder(MAX_CHECK_MESSAGE_LINES * 64); private int numLines = 0; private DiffMessageFormatter(SpotlessTask task, Formatter formatter, List<File> problemFiles) throws IOException { Preconditions.checkArgument(!problemFiles.isEmpty(), "Problem files must not be empty"); Path rootDir = task.getProject().getRootDir().toPath(); ListIterator<File> problemIter = problemFiles.listIterator(); while (problemIter.hasNext() && numLines < MAX_CHECK_MESSAGE_LINES) { File file = problemIter.next(); addFile(rootDir.relativize(file.toPath()) + "\n" + DiffMessageFormatter.diff(task, formatter, file)); } if (problemIter.hasNext()) { int remainingFiles = problemFiles.size() - problemIter.nextIndex(); if (remainingFiles >= MAX_FILES_TO_LIST) { buffer.append("Violations also present in ").append(remainingFiles).append(" other files.\n"); } else { buffer.append("Violations also present in:\n"); while (problemIter.hasNext()) { addIntendedLine(NORMAL_INDENT, rootDir.relativize(problemIter.next().toPath()).toString()); } } } } private static final int MIN_LINES_PER_FILE = 4; private static final Splitter NEWLINE_SPLITTER = Splitter.on('\n'); private void addFile(String arg) { // at the very least, we'll print this about a file: // 0.txt // @@ -1,2 +1,2 @@, // -1\\r\\n, // -2\\r\\n, // ... (more lines that didn't fit) List<String> lines = NEWLINE_SPLITTER.splitToList(arg); if (!lines.isEmpty()) { addIntendedLine(NORMAL_INDENT, lines.get(0)); } for (int i = 1; i < Math.min(MIN_LINES_PER_FILE, lines.size()); ++i) { addIntendedLine(DIFF_INDENT, lines.get(i)); } // then we'll print the rest that can fit ListIterator<String> iter = lines.listIterator(Math.min(MIN_LINES_PER_FILE, lines.size())); while (iter.hasNext() && numLines < MAX_CHECK_MESSAGE_LINES) { addIntendedLine(DIFF_INDENT, iter.next()); } if (numLines >= MAX_CHECK_MESSAGE_LINES) { // we're out of space if (iter.hasNext()) { int linesLeft = lines.size() - iter.nextIndex(); addIntendedLine(NORMAL_INDENT, "... (" + linesLeft + " more lines that didn't fit)"); } } } private static final String NORMAL_INDENT = " "; private static final String DIFF_INDENT = NORMAL_INDENT + NORMAL_INDENT; private void addIntendedLine(String indent, String line) { buffer.append(indent); buffer.append(line); buffer.append('\n'); ++numLines; } /** * Returns a git-style diff between the contents of the given file and what those contents would * look like if formatted using the given formatter. Does not end with any newline * sequence (\n, \r, \r\n). */ private static String diff(SpotlessTask task, Formatter formatter, File file) throws IOException { String raw = new String(Files.readAllBytes(file.toPath()), formatter.getEncoding()); String rawUnix = LineEnding.toUnix(raw); String formattedUnix; if (task.isPaddedCell()) { formattedUnix = PaddedCell.check(formatter, file, rawUnix).canonical(); } else { formattedUnix = formatter.compute(rawUnix, file); } if (rawUnix.equals(formattedUnix)) { // the formatting is fine, so it's a line-ending issue String formatted = formatter.computeLineEndings(formattedUnix, file); return diffWhitespaceLineEndings(raw, formatted, false, true); } else { return diffWhitespaceLineEndings(rawUnix, formattedUnix, true, false); } } /** * Returns a git-style diff between the two unix strings. * * Output has no trailing newlines. * * Boolean args determine whether whitespace or line endings will be visible. */ private static String diffWhitespaceLineEndings(String dirty, String clean, boolean whitespace, boolean lineEndings) throws IOException { dirty = visibleWhitespaceLineEndings(dirty, whitespace, lineEndings); clean = visibleWhitespaceLineEndings(clean, whitespace, lineEndings); RawText a = new RawText(dirty.getBytes(StandardCharsets.UTF_8)); RawText b = new RawText(clean.getBytes(StandardCharsets.UTF_8)); EditList edits = new EditList(); edits.addAll(MyersDiff.INSTANCE.diff(RawTextComparator.DEFAULT, a, b)); ByteArrayOutputStream out = new ByteArrayOutputStream(); try (DiffFormatter formatter = new DiffFormatter(out)) { formatter.format(edits, a, b); } String formatted = out.toString(StandardCharsets.UTF_8.name()); // we don't need the diff to show this, since we display newlines ourselves formatted = formatted.replace("\\ No newline at end of file\n", ""); return NEWLINE_MATCHER.trimTrailingFrom(formatted); } private static final CharMatcher NEWLINE_MATCHER = CharMatcher.is('\n'); /** * Makes the whitespace and/or the lineEndings visible. * * MyersDiff wants inputs with only unix line endings. So this ensures that that is the case. */ private static String visibleWhitespaceLineEndings(String input, boolean whitespace, boolean lineEndings) { if (whitespace) { input = input.replace(' ', MIDDLE_DOT).replace("\t", "\\t"); } if (lineEndings) { input = input.replace("\n", "\\n\n").replace("\r", "\\r"); } else { // we want only \n, so if we didn't replace them above, we'll replace them here. input = input.replace("\r", ""); } return input; } private static final char MIDDLE_DOT = '\u00b7'; }