| /* |
| * 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 ">>") |
| * 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; |
| } |
| } |