-
Notifications
You must be signed in to change notification settings - Fork 843
/
Formatter.java
269 lines (250 loc) · 11.1 KB
/
Formatter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
/*
* Copyright 2015 Google Inc.
*
* 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.google.googlejavaformat.java;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;
import com.google.common.io.CharSink;
import com.google.common.io.CharSource;
import com.google.errorprone.annotations.Immutable;
import com.google.googlejavaformat.Doc;
import com.google.googlejavaformat.DocBuilder;
import com.google.googlejavaformat.FormattingError;
import com.google.googlejavaformat.Newlines;
import com.google.googlejavaformat.Op;
import com.google.googlejavaformat.OpsBuilder;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.parser.JavacParser;
import com.sun.tools.javac.parser.ParserFactory;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Log;
import com.sun.tools.javac.util.Options;
import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.DiagnosticListener;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardLocation;
/**
* This is google-java-format, a new Java formatter that follows the Google Java Style Guide quite
* precisely---to the letter and to the spirit.
*
* <p>This formatter uses the Eclipse parser to generate an AST. Because the Eclipse AST loses
* information about the non-tokens in the input (including newlines, comments, etc.), and even some
* tokens (e.g., optional commas or semicolons), this formatter lexes the input again and follows
* along in the resulting list of tokens. Its lexer splits all multi-character operators (like ">>")
* into multiple single-character operators. Each non-token is assigned to a token---non-tokens
* following a token on the same line go with that token; those following go with the next token---
* and there is a final EOF token to hold final comments.
*
* <p>The formatter walks the AST to generate a Greg Nelson/Derek Oppen-style list of formatting
* {@link Op}s [1--2] that then generates a structured {@link Doc}. Each AST node type has a visitor
* to emit a sequence of {@link Op}s for the node.
*
* <p>Some data-structure operations are easier in the list of {@link Op}s, while others become
* easier in the {@link Doc}. The {@link Op}s are walked to attach the comments. As the {@link Op}s
* are generated, missing input tokens are inserted and incorrect output tokens are dropped,
* ensuring that the output matches the input even in the face of formatter errors. Finally, the
* formatter walks the {@link Doc} to format it in the given width.
*
* <p>This formatter also produces data structures of which tokens and comments appear where on the
* input, and on the output, to help output a partial reformatting of a slightly edited input.
*
* <p>Instances of the formatter are immutable and thread-safe.
*
* <p>[1] Nelson, Greg, and John DeTreville. Personal communication.
*
* <p>[2] Oppen, Derek C. "Prettyprinting". ACM Transactions on Programming Languages and Systems,
* Volume 2 Issue 4, Oct. 1980, pp. 465–483.
*/
@Immutable
public final class Formatter {
static final Range<Integer> EMPTY_RANGE = Range.closedOpen(-1, -1);
private static final Predicate<Diagnostic<?>> ERROR_DIAGNOSTIC =
new Predicate<Diagnostic<?>>() {
@Override
public boolean apply(Diagnostic<?> input) {
if (input.getKind() != Diagnostic.Kind.ERROR) {
return false;
}
switch (input.getCode()) {
case "compiler.err.invalid.meth.decl.ret.type.req":
// accept constructor-like method declarations that don't match the name of their
// enclosing class
return false;
default:
break;
}
return true;
}
};
private final JavaFormatterOptions options;
/** A new Formatter instance with default options. */
public Formatter() {
this(JavaFormatterOptions.defaultOptions());
}
public Formatter(JavaFormatterOptions options) {
this.options = options;
}
/**
* Construct a {@code Formatter} given a Java compilation unit. Parses the code; builds a {@link
* JavaInput} and the corresponding {@link JavaOutput}.
*
* @param javaInput the input, a Java compilation unit
* @param javaOutput the {@link JavaOutput}
* @param options the {@link JavaFormatterOptions}
*/
static void format(
final JavaInput javaInput, JavaOutput javaOutput, JavaFormatterOptions options) {
Context context = new Context();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
context.put(DiagnosticListener.class, diagnostics);
Options.instance(context).put("allowStringFolding", "false");
JCCompilationUnit unit;
JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
try {
fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.<File>of());
} catch (IOException e) {
// impossible
throw new IOError(e);
}
SimpleJavaFileObject source =
new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return javaInput.getText();
}
};
Log.instance(context).useSource(source);
ParserFactory parserFactory = ParserFactory.instance(context);
JavacParser parser =
parserFactory.newParser(
javaInput.getText(),
/*keepDocComments=*/ true,
/*keepEndPos=*/ true,
/*keepLineMap=*/ true);
unit = parser.parseCompilationUnit();
unit.sourcefile = source;
javaInput.setCompilationUnit(unit);
Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
Iterables.filter(diagnostics.getDiagnostics(), ERROR_DIAGNOSTIC);
if (!Iterables.isEmpty(errorDiagnostics)) {
throw FormattingError.fromJavacDiagnostics(errorDiagnostics);
}
OpsBuilder builder = new OpsBuilder(javaInput, javaOutput);
// Output the compilation unit.
new JavaInputAstVisitor(builder, options.indentationMultiplier()).scan(unit, null);
builder.sync(javaInput.getText().length());
builder.drain();
Doc doc = new DocBuilder().withOps(builder.build()).build();
doc.computeBreaks(
javaOutput.getCommentsHelper(), options.maxLineLength(), new Doc.State(+0, 0));
doc.write(javaOutput);
javaOutput.flush();
}
/**
* Format the given input (a Java compilation unit) into the output stream.
*
* @throws FormatterException if the input cannot be parsed
*/
public void formatSource(CharSource input, CharSink output)
throws FormatterException, IOException {
// TODO(cushon): proper support for streaming input/output. Input may
// not be feasible (parsing) but output should be easier.
output.write(formatSource(input.read()));
}
/**
* Format an input string (a Java compilation unit) into an output string.
*
* @param input the input string
* @return the output string
* @throws FormatterException if the input string cannot be parsed
*/
public String formatSource(String input) throws FormatterException {
return formatSource(input, Collections.singleton(Range.closedOpen(0, input.length())));
}
/**
* Format an input string (a Java compilation unit), for only the specified character ranges.
* These ranges are extended as necessary (e.g., to encompass whole lines).
*
* @param input the input string
* @param characterRanges the character ranges to be reformatted
* @return the output string
* @throws FormatterException if the input string cannot be parsed
*/
public String formatSource(String input, Collection<Range<Integer>> characterRanges)
throws FormatterException {
return JavaOutput.applyReplacements(input, getFormatReplacements(input, characterRanges));
}
/**
* Emit a list of {@link Replacement}s to convert from input to output.
*
* @param input the input compilation unit
* @param characterRanges the character ranges to reformat
* @return a list of {@link Replacement}s, sorted from low index to high index, without overlaps
* @throws FormatterException if the input string cannot be parsed
*/
public ImmutableList<Replacement> getFormatReplacements(
String input, Collection<Range<Integer>> characterRanges) throws FormatterException {
// TODO(cushon): this is only safe because the modifier ordering doesn't affect whitespace,
// and doesn't change the replacements that are output. This is not true in general for
// 'de-linting' changes (e.g. import ordering).
input = ModifierOrderer.reorderModifiers(input, characterRanges);
String lineSeparator = Newlines.guessLineSeparator(input);
JavaInput javaInput = new JavaInput(input);
JavaOutput javaOutput =
new JavaOutput(lineSeparator, javaInput, new JavaCommentsHelper(lineSeparator, options));
try {
format(javaInput, javaOutput, options);
} catch (FormattingError e) {
throw new FormatterException(e.diagnostics());
}
RangeSet<Integer> tokenRangeSet = javaInput.characterRangesToTokenRanges(characterRanges);
return javaOutput.getFormatReplacements(tokenRangeSet);
}
/**
* Converts zero-indexed, [closed, open) line ranges in the given source file to character ranges.
*/
public static RangeSet<Integer> lineRangesToCharRanges(
String input, RangeSet<Integer> lineRanges) {
List<Integer> lines = new ArrayList<>();
Iterators.addAll(lines, Newlines.lineOffsetIterator(input));
lines.add(input.length() + 1);
final RangeSet<Integer> characterRanges = TreeRangeSet.create();
for (Range<Integer> lineRange :
lineRanges.subRangeSet(Range.closedOpen(0, lines.size() - 1)).asRanges()) {
int lineStart = lines.get(lineRange.lowerEndpoint());
// Exclude the trailing newline. This isn't strictly necessary, but handling blank lines
// as empty ranges is convenient.
int lineEnd = lines.get(lineRange.upperEndpoint()) - 1;
Range<Integer> range = Range.closedOpen(lineStart, lineEnd);
characterRanges.add(range);
}
return characterRanges;
}
}