1 /* 2 * Copyright 2017 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 15 package com.google.googlejavaformat.java; 16 17 import static com.google.common.collect.ImmutableList.toImmutableList; 18 19 import com.google.common.base.CharMatcher; 20 import com.google.common.base.Preconditions; 21 import com.google.common.collect.DiscreteDomain; 22 import com.google.common.collect.ImmutableList; 23 import com.google.common.collect.Range; 24 import com.google.common.collect.RangeSet; 25 import com.google.common.collect.TreeRangeSet; 26 import java.util.ArrayList; 27 import java.util.List; 28 29 /** Formats a subset of a compilation unit. */ 30 public class SnippetFormatter { 31 32 /** The kind of snippet to format. */ 33 public enum SnippetKind { 34 COMPILATION_UNIT, 35 CLASS_BODY_DECLARATIONS, 36 STATEMENTS, 37 EXPRESSION 38 } 39 40 private class SnippetWrapper { 41 int offset; 42 final StringBuilder contents = new StringBuilder(); 43 append(String str)44 public SnippetWrapper append(String str) { 45 contents.append(str); 46 return this; 47 } 48 appendSource(String source)49 public SnippetWrapper appendSource(String source) { 50 this.offset = contents.length(); 51 contents.append(source); 52 return this; 53 } 54 closeBraces(int initialIndent)55 public void closeBraces(int initialIndent) { 56 for (int i = initialIndent; --i >= 0; ) { 57 contents.append("\n").append(createIndentationString(i)).append("}"); 58 } 59 } 60 } 61 62 private static final int INDENTATION_SIZE = 2; 63 private final Formatter formatter = new Formatter(); 64 private static final CharMatcher NOT_WHITESPACE = CharMatcher.whitespace().negate(); 65 createIndentationString(int indentationLevel)66 public String createIndentationString(int indentationLevel) { 67 Preconditions.checkArgument( 68 indentationLevel >= 0, 69 "Indentation level cannot be less than zero. Given: %s", 70 indentationLevel); 71 int spaces = indentationLevel * INDENTATION_SIZE; 72 StringBuilder buf = new StringBuilder(spaces); 73 for (int i = 0; i < spaces; i++) { 74 buf.append(' '); 75 } 76 return buf.toString(); 77 } 78 offsetRange(Range<Integer> range, int offset)79 private static Range<Integer> offsetRange(Range<Integer> range, int offset) { 80 range = range.canonical(DiscreteDomain.integers()); 81 return Range.closedOpen(range.lowerEndpoint() + offset, range.upperEndpoint() + offset); 82 } 83 offsetRanges(List<Range<Integer>> ranges, int offset)84 private static List<Range<Integer>> offsetRanges(List<Range<Integer>> ranges, int offset) { 85 List<Range<Integer>> result = new ArrayList<>(); 86 for (Range<Integer> range : ranges) { 87 result.add(offsetRange(range, offset)); 88 } 89 return result; 90 } 91 92 /** Runs the Google Java formatter on the given source, with only the given ranges specified. */ format( SnippetKind kind, String source, List<Range<Integer>> ranges, int initialIndent, boolean includeComments)93 public ImmutableList<Replacement> format( 94 SnippetKind kind, 95 String source, 96 List<Range<Integer>> ranges, 97 int initialIndent, 98 boolean includeComments) 99 throws FormatterException { 100 RangeSet<Integer> rangeSet = TreeRangeSet.create(); 101 for (Range<Integer> range : ranges) { 102 rangeSet.add(range); 103 } 104 if (includeComments) { 105 if (kind != SnippetKind.COMPILATION_UNIT) { 106 throw new IllegalArgumentException( 107 "comment formatting is only supported for compilation units"); 108 } 109 return formatter.getFormatReplacements(source, ranges); 110 } 111 SnippetWrapper wrapper = snippetWrapper(kind, source, initialIndent); 112 ranges = offsetRanges(ranges, wrapper.offset); 113 114 String replacement = formatter.formatSource(wrapper.contents.toString(), ranges); 115 replacement = 116 replacement.substring( 117 wrapper.offset, 118 replacement.length() - (wrapper.contents.length() - wrapper.offset - source.length())); 119 120 return toReplacements(source, replacement).stream() 121 .filter(r -> rangeSet.encloses(r.getReplaceRange())) 122 .collect(toImmutableList()); 123 } 124 125 /** 126 * Generates {@code Replacement}s rewriting {@code source} to {@code replacement}, under the 127 * assumption that they differ in whitespace alone. 128 */ toReplacements(String source, String replacement)129 private static List<Replacement> toReplacements(String source, String replacement) { 130 if (!NOT_WHITESPACE.retainFrom(source).equals(NOT_WHITESPACE.retainFrom(replacement))) { 131 throw new IllegalArgumentException( 132 "source = \"" + source + "\", replacement = \"" + replacement + "\""); 133 } 134 /* 135 * In the past we seemed to have problems touching non-whitespace text in the formatter, even 136 * just replacing some code with itself. Retrospective attempts to reproduce this have failed, 137 * but this may be an issue for future changes. 138 */ 139 List<Replacement> replacements = new ArrayList<>(); 140 int i = NOT_WHITESPACE.indexIn(source); 141 int j = NOT_WHITESPACE.indexIn(replacement); 142 if (i != 0 || j != 0) { 143 replacements.add(Replacement.create(0, i, replacement.substring(0, j))); 144 } 145 while (i != -1 && j != -1) { 146 int i2 = NOT_WHITESPACE.indexIn(source, i + 1); 147 int j2 = NOT_WHITESPACE.indexIn(replacement, j + 1); 148 if (i2 == -1 || j2 == -1) { 149 break; 150 } 151 if ((i2 - i) != (j2 - j) 152 || !source.substring(i + 1, i2).equals(replacement.substring(j + 1, j2))) { 153 replacements.add(Replacement.create(i + 1, i2, replacement.substring(j + 1, j2))); 154 } 155 i = i2; 156 j = j2; 157 } 158 return replacements; 159 } 160 snippetWrapper(SnippetKind kind, String source, int initialIndent)161 private SnippetWrapper snippetWrapper(SnippetKind kind, String source, int initialIndent) { 162 /* 163 * Synthesize a dummy class around the code snippet provided by Eclipse. The dummy class is 164 * correctly formatted -- the blocks use correct indentation, etc. 165 */ 166 switch (kind) { 167 case COMPILATION_UNIT: 168 { 169 SnippetWrapper wrapper = new SnippetWrapper(); 170 for (int i = 1; i <= initialIndent; i++) { 171 wrapper.append("class Dummy {\n").append(createIndentationString(i)); 172 } 173 wrapper.appendSource(source); 174 wrapper.closeBraces(initialIndent); 175 return wrapper; 176 } 177 case CLASS_BODY_DECLARATIONS: 178 { 179 SnippetWrapper wrapper = new SnippetWrapper(); 180 for (int i = 1; i <= initialIndent; i++) { 181 wrapper.append("class Dummy {\n").append(createIndentationString(i)); 182 } 183 wrapper.appendSource(source); 184 wrapper.closeBraces(initialIndent); 185 return wrapper; 186 } 187 case STATEMENTS: 188 { 189 SnippetWrapper wrapper = new SnippetWrapper(); 190 wrapper.append("class Dummy {\n").append(createIndentationString(1)); 191 for (int i = 2; i <= initialIndent; i++) { 192 wrapper.append("{\n").append(createIndentationString(i)); 193 } 194 wrapper.appendSource(source); 195 wrapper.closeBraces(initialIndent); 196 return wrapper; 197 } 198 case EXPRESSION: 199 { 200 SnippetWrapper wrapper = new SnippetWrapper(); 201 wrapper.append("class Dummy {\n").append(createIndentationString(1)); 202 for (int i = 2; i <= initialIndent; i++) { 203 wrapper.append("{\n").append(createIndentationString(i)); 204 } 205 wrapper.append("Object o = "); 206 wrapper.appendSource(source); 207 wrapper.append(";"); 208 wrapper.closeBraces(initialIndent); 209 return wrapper; 210 } 211 default: 212 throw new IllegalArgumentException("Unknown snippet kind: " + kind); 213 } 214 } 215 } 216