• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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