blob: 3e973958d8ce03bb484904e9332e53a528e52275 [file] [log] [blame]
/*
* 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 com.google.common.base.StandardSystemProperty.JAVA_CLASS_VERSION;
import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION;
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.lang.reflect.Method;
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 (getMajor() >= 14) {
try {
visitor =
Class.forName("com.google.googlejavaformat.java.java14.Java14InputAstVisitor")
.asSubclass(JavaInputAstVisitor.class)
.getConstructor(OpsBuilder.class, int.class)
.newInstance(builder, options.indentationMultiplier());
} catch (ReflectiveOperationException e) {
throw new LinkageError(e.getMessage(), e);
}
} 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();
}
// Runtime.Version was added in JDK 9, so use reflection to access it to preserve source
// compatibility with Java 8.
private static int getMajor() {
try {
Method versionMethod = Runtime.class.getMethod("version");
Object version = versionMethod.invoke(null);
return (int) version.getClass().getMethod("major").invoke(version);
} catch (Exception e) {
// continue below
}
int version = (int) Double.parseDouble(JAVA_CLASS_VERSION.value());
if (49 <= version && version <= 52) {
return version - (49 - 5);
}
throw new IllegalStateException("Unknown Java version: " + JAVA_SPECIFICATION_VERSION.value());
}
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).
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;
}
}