Skip to content

Commit 4ae067a

Browse files
committed
Open source 'snippet' formatting logic
MOE_MIGRATED_REVID=146805663
1 parent 40fd87b commit 4ae067a

File tree

2 files changed

+325
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)