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