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