aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
blob: 5aa7a1233c642c345a36100a7ca261265b75d1a1 (plain)
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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
/*
 * 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.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.IOError;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
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 javac parser to generate an AST. Because the 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 "&gt;&gt;")
 * 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 {

  public static final int MAX_LINE_LENGTH = 100;

  static final Range<Integer> EMPTY_RANGE = Range.closedOpen(-1, -1);

  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)
      throws FormatterException {
    Context context = new Context();
    DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
    context.put(DiagnosticListener.class, diagnostics);
    Options.instance(context).put("allowStringFolding", "false");
    Options.instance(context).put("--enable-preview", "true");
    JCCompilationUnit unit;
    JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
    try {
      fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.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(), Formatter::errorDiagnostic);
    if (!Iterables.isEmpty(errorDiagnostics)) {
      throw FormatterException.fromJavacDiagnostics(errorDiagnostics);
    }
    OpsBuilder builder = new OpsBuilder(javaInput, javaOutput);
    // Output the compilation unit.
    JavaInputAstVisitor visitor;
    if (Runtime.version().feature() >= 21) {
      visitor =
          createVisitor(
              "com.google.googlejavaformat.java.java21.Java21InputAstVisitor", builder, options);
    } else if (Runtime.version().feature() >= 17) {
      visitor =
          createVisitor(
              "com.google.googlejavaformat.java.java17.Java17InputAstVisitor", builder, options);
    } else {
      visitor = new JavaInputAstVisitor(builder, options.indentationMultiplier());
    }
    visitor.scan(unit, null);
    builder.sync(javaInput.getText().length());
    builder.drain();
    Doc doc = new DocBuilder().withOps(builder.build()).build();
    doc.computeBreaks(javaOutput.getCommentsHelper(), MAX_LINE_LENGTH, new Doc.State(+0, 0));
    doc.write(javaOutput);
    javaOutput.flush();
  }

  private static JavaInputAstVisitor createVisitor(
      final String className, final OpsBuilder builder, final JavaFormatterOptions options) {
    try {
      return Class.forName(className)
          .asSubclass(JavaInputAstVisitor.class)
          .getConstructor(OpsBuilder.class, int.class)
          .newInstance(builder, options.indentationMultiplier());
    } catch (ReflectiveOperationException e) {
      throw new LinkageError(e.getMessage(), e);
    }
  }

  static boolean errorDiagnostic(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;
  }

  /**
   * 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.
   *
   * <p>Leaves import statements untouched.
   *
   * @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, ImmutableList.of(Range.closedOpen(0, input.length())));
  }

  /**
   * Formats an input string (a Java compilation unit) and fixes imports.
   *
   * <p>Fixing imports includes ordering, spacing, and removal of unused import statements.
   *
   * @param input the input string
   * @return the output string
   * @throws FormatterException if the input string cannot be parsed
   * @see <a
   *     href="https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing">
   *     Google Java Style Guide - 3.3.3 Import ordering and spacing</a>
   */
  public String formatSourceAndFixImports(String input) throws FormatterException {
    input = ImportOrderer.reorderImports(input, options.style());
    input = RemoveUnusedImports.removeUnusedImports(input);
    String formatted = formatSource(input);
    formatted = StringWrapper.wrap(formatted, this);
    return formatted;
  }

  /**
   * 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 {
    JavaInput javaInput = new JavaInput(input);

    // 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).
    if (options.reorderModifiers()) {
      javaInput = ModifierOrderer.reorderModifiers(javaInput, characterRanges);
    }

    String lineSeparator = Newlines.guessLineSeparator(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;
  }
}