fix(google-java-format): update to v1.18.1

This commit is contained in:
Akash Yadav
2023-10-20 20:02:23 +05:30
parent c76b1b2539
commit d74627a131
41 changed files with 5541 additions and 5221 deletions
@@ -14,6 +14,10 @@
package com.google.googlejavaformat;
import com.google.googlejavaformat.Input.Tok;
import java.util.Optional;
import java.util.regex.Pattern;
/**
* Rewrite comments. This interface is implemented by {@link
* com.google.googlejavaformat.java.JavaCommentsHelper JavaCommentsHelper}.
@@ -28,4 +32,19 @@ public interface CommentsHelper {
* @return the rewritten comment
*/
String rewrite(Input.Tok tok, int maxWidth, int column0);
static Optional<String> reformatParameterComment(Tok tok) {
if (!tok.isSlashStarComment()) {
return Optional.empty();
}
var match = PARAMETER_COMMENT.matcher(tok.getOriginalText());
if (!match.matches()) {
return Optional.empty();
}
return Optional.of(String.format("/* %s= */", match.group(1)));
}
Pattern PARAMETER_COMMENT =
Pattern.compile(
"/\\*\\s*(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\Q...\\E)?)\\s*=\\s*\\*/");
}
@@ -15,14 +15,16 @@
package com.google.googlejavaformat;
import static com.google.common.collect.Iterables.getLast;
import static com.google.googlejavaformat.CommentsHelper.reformatParameterComment;
import static java.lang.Math.max;
import com.google.common.base.MoreObjects;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.DiscreteDomain;
import com.google.common.collect.Iterators;
import com.google.common.collect.Range;
import com.google.googlejavaformat.Output.BreakTag;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -37,91 +39,6 @@ import java.util.Optional;
* representing single spaces; and {@link Break}s, which represent optional line-breaks.
*/
public abstract class Doc {
private static final Range<Integer> EMPTY_RANGE = Range.closedOpen(-1, -1);
private static final DiscreteDomain<Integer> INTEGERS = DiscreteDomain.integers();
// Memoized width; Float.POSITIVE_INFINITY if contains forced breaks.
private boolean widthComputed = false;
private float width = 0.0F;
// Memoized flat; not defined (and never computed) if contains forced breaks.
private boolean flatComputed = false;
private String flat = "";
// Memoized Range.
private boolean rangeComputed = false;
private Range<Integer> range = EMPTY_RANGE;
/**
* Make breaking decisions for a {@code Doc}.
*
* @param maxWidth the maximum line width
* @param state the current output state
* @return the new output state
*/
public abstract State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state);
/** Write a {@code Doc} to an {@link Output}, after breaking decisions have been made. */
public abstract void write(Output output);
/**
* Return the width of a {@code Doc}, or {@code Float.POSITIVE_INFINITY} if it must be broken.
*
* @return the width
*/
final float getWidth() {
if (!widthComputed) {
width = computeWidth();
widthComputed = true;
}
return width;
}
/**
* Compute the {@code Doc}'s width.
*
* @return the width, or {@code Float.POSITIVE_INFINITY} if it must be broken
*/
abstract float computeWidth();
/**
* Return a {@code Doc}'s flat-string value; not defined (and never called) if the (@code Doc}
* contains forced breaks.
*
* @return the flat-string value
*/
final String getFlat() {
if (!flatComputed) {
flat = computeFlat();
flatComputed = true;
}
return flat;
}
/**
* Compute the {@code Doc}'s flat value. Not defined (and never called) if contains forced breaks.
*
* @return the flat value
*/
abstract String computeFlat();
/**
* Return the {@link Range} of a {@code Doc}.
*
* @return the {@code Doc}'s {@link Range}
*/
final Range<Integer> range() {
if (!rangeComputed) {
range = computeRange();
rangeComputed = true;
}
return range;
}
/**
* Compute the {@code Doc}'s {@link Range} of {@link Input.Token}s.
*
* @return the {@link Range}
*/
abstract Range<Integer> computeRange();
/**
* Each {@link Break} in a {@link Level} is either {@link FillMode#UNIFIED} or {@link
* FillMode#INDEPENDENT}.
@@ -153,10 +70,6 @@ public abstract class Doc {
final int column;
final boolean mustBreak;
public State(int indent0, int column0) {
this(indent0, indent0, column0, false);
}
State(int lastIndent, int indent, int column, boolean mustBreak) {
this.lastIndent = lastIndent;
this.indent = indent;
@@ -164,6 +77,18 @@ public abstract class Doc {
this.mustBreak = mustBreak;
}
public State(int indent0, int column0) {
this(indent0, indent0, column0, false);
}
State withColumn(int column) {
return new State(lastIndent, indent, column, mustBreak);
}
State withMustBreak(boolean mustBreak) {
return new State(lastIndent, indent, column, mustBreak);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
@@ -173,286 +98,85 @@ public abstract class Doc {
.add("mustBreak", mustBreak)
.toString();
}
State withColumn(int column) {
return new State(lastIndent, indent, column, mustBreak);
}
State withMustBreak(boolean mustBreak) {
return new State(lastIndent, indent, column, mustBreak);
}
}
/** A leaf {@link Doc} for a token. */
public static final class Token extends Doc implements Op {
private final Input.Token token;
private final RealOrImaginary realOrImaginary;
private final Indent plusIndentCommentsBefore;
private final Optional<Indent> breakAndIndentTrailingComment;
private static final Range<Integer> EMPTY_RANGE = Range.closedOpen(-1, -1);
private static final DiscreteDomain<Integer> INTEGERS = DiscreteDomain.integers();
private Token(
Input.Token token,
RealOrImaginary realOrImaginary,
Indent plusIndentCommentsBefore,
Optional<Indent> breakAndIndentTrailingComment) {
this.token = token;
this.realOrImaginary = realOrImaginary;
this.plusIndentCommentsBefore = plusIndentCommentsBefore;
this.breakAndIndentTrailingComment = breakAndIndentTrailingComment;
}
// Memoized width; Float.POSITIVE_INFINITY if contains forced breaks.
private final Supplier<Float> width = Suppliers.memoize(this::computeWidth);
/**
* Make a {@code Token}.
*
* @param token the {@link Input.Token} to wrap
* @param realOrImaginary did this {@link Input.Token} appear in the input, or was it generated
* incorrectly?
* @param plusIndentCommentsBefore extra {@code plusIndent} for comments just before this token
* @return the new {@code Token}
*/
static Op make(
Input.Token token,
Doc.Token.RealOrImaginary realOrImaginary,
Indent plusIndentCommentsBefore,
Optional<Indent> breakAndIndentTrailingComment) {
return new Token(
token, realOrImaginary, plusIndentCommentsBefore, breakAndIndentTrailingComment);
}
// Memoized flat; not defined (and never computed) if contains forced breaks.
private final Supplier<String> flat = Suppliers.memoize(this::computeFlat);
@Override
public void add(DocBuilder builder) {
builder.add(this);
}
// Memoized Range.
private final Supplier<Range<Integer>> range = Suppliers.memoize(this::computeRange);
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("token", token)
.add("realOrImaginary", realOrImaginary)
.add("plusIndentCommentsBefore", plusIndentCommentsBefore)
.toString();
}
/**
* How much extra to indent comments before the {@code Token}.
*
* @return the extra indent
*/
Indent getPlusIndentCommentsBefore() {
return plusIndentCommentsBefore;
}
/** Force a line break and indent trailing javadoc or block comments. */
Optional<Indent> breakAndIndentTrailingComment() {
return breakAndIndentTrailingComment;
}
/**
* Return the wrapped {@link Input.Token}.
*
* @return the {@link Input.Token}
*/
Input.Token getToken() {
return token;
}
/**
* Is the token good? That is, does it match an {@link Input.Token}?
*
* @return whether the @code Token} is good
*/
RealOrImaginary realOrImaginary() {
return realOrImaginary;
}
@Override
float computeWidth() {
return token.getTok().length();
}
@Override
String computeFlat() {
return token.getTok().getOriginalText();
}
@Override
Range<Integer> computeRange() {
return Range.singleton(token.getTok().getIndex()).canonical(INTEGERS);
}
@Override
public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
String text = token.getTok().getOriginalText();
return state.withColumn(state.column + text.length());
}
@Override
public void write(Output output) {
String text = token.getTok().getOriginalText();
output.append(text, range());
}
/** Is a Token a real token, or imaginary (e.g., a token generated incorrectly, or an EOF)? */
public enum RealOrImaginary {
REAL,
IMAGINARY;
boolean isReal() {
return this == REAL;
}
}
/**
* Return the width of a {@code Doc}, or {@code Float.POSITIVE_INFINITY} if it must be broken.
*
* @return the width
*/
final float getWidth() {
return width.get();
}
/** A leaf node in a {@link Doc} for an optional break. */
public static final class Break extends Doc implements Op {
private final FillMode fillMode;
private final String flat;
private final Indent plusIndent;
private final Optional<BreakTag> optTag;
/** Was this break taken? */
boolean broken;
/** New indent after this break. */
int newIndent;
private Break(FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optTag) {
this.fillMode = fillMode;
this.flat = flat;
this.plusIndent = plusIndent;
this.optTag = optTag;
}
/**
* Make a {@code Break}.
*
* @param fillMode the {@link FillMode}
* @param flat the text when not broken
* @param plusIndent extra indent if taken
* @param optTag an optional tag for remembering whether the break was taken
* @return the new {@code Break}
*/
public static Break make(
FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optTag) {
return new Break(fillMode, flat, plusIndent, optTag);
}
/**
* Make a forced {@code Break}.
*
* @return the new forced {@code Break}
*/
public static Break makeForced() {
return make(FillMode.FORCED, "", Indent.Const.ZERO);
}
/**
* Make a {@code Break}.
*
* @param fillMode the {@link FillMode}
* @param flat the text when not broken
* @param plusIndent extra indent if taken
* @return the new {@code Break}
*/
public static Break make(FillMode fillMode, String flat, Indent plusIndent) {
return new Break(fillMode, flat, plusIndent, /* optTag= */ Optional.empty());
}
@Override
public void add(DocBuilder builder) {
builder.breakDoc(this);
}
public State computeBreaks(State state, int lastIndent, boolean broken) {
if (optTag.isPresent()) {
optTag.get().recordBroken(broken);
}
if (broken) {
this.broken = true;
this.newIndent = max(lastIndent + plusIndent.eval(), 0);
return state.withColumn(newIndent);
} else {
this.broken = false;
this.newIndent = -1;
return state.withColumn(state.column + flat.length());
}
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("fillMode", fillMode)
.add("flat", flat)
.add("plusIndent", plusIndent)
.add("optTag", optTag)
.toString();
}
/**
* Return the {@code Break}'s extra indent.
*
* @return the extra indent
*/
int getPlusIndent() {
return plusIndent.eval();
}
@Override
float computeWidth() {
return isForced() ? Float.POSITIVE_INFINITY : (float) flat.length();
}
/**
* Is the {@code Break} forced?
*
* @return whether the {@code Break} is forced
*/
boolean isForced() {
return fillMode == FillMode.FORCED;
}
@Override
String computeFlat() {
return flat;
}
@Override
Range<Integer> computeRange() {
return EMPTY_RANGE;
}
@Override
public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
// Updating the state for {@link Break}s requires deciding if the break
// should be taken.
// TODO(cushon): this hierarchy is wrong, create a separate interface
// for unbreakable Docs?
throw new UnsupportedOperationException("Did you mean computeBreaks(State, int, boolean)?");
}
@Override
public void write(Output output) {
if (broken) {
output.append("\n", EMPTY_RANGE);
output.indent(newIndent);
} else {
output.append(flat, range());
}
}
/**
* Return a {@code Doc}'s flat-string value; not defined (and never called) if the (@code Doc}
* contains forced breaks.
*
* @return the flat-string value
*/
final String getFlat() {
return flat.get();
}
/**
* Return the {@link Range} of a {@code Doc}.
*
* @return the {@code Doc}'s {@link Range}
*/
final Range<Integer> range() {
return range.get();
}
/**
* Compute the {@code Doc}'s width.
*
* @return the width, or {@code Float.POSITIVE_INFINITY} if it must be broken
*/
abstract float computeWidth();
/**
* Compute the {@code Doc}'s flat value. Not defined (and never called) if contains forced breaks.
*
* @return the flat value
*/
abstract String computeFlat();
/**
* Compute the {@code Doc}'s {@link Range} of {@link Input.Token}s.
*
* @return the {@link Range}
*/
abstract Range<Integer> computeRange();
/**
* Make breaking decisions for a {@code Doc}.
*
* @param maxWidth the maximum line width
* @param state the current output state
* @return the new output state
*/
public abstract State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state);
/** Write a {@code Doc} to an {@link Output}, after breaking decisions have been made. */
public abstract void write(Output output);
/** A {@code Level} inside a {@link Doc}. */
static final class Level extends Doc {
private final Indent plusIndent; // The extra indent following breaks.
private final List<Doc> docs = new ArrayList<>(); // The elements of the level.
/** True if the entire {@link Level} fits on one line. */
boolean oneLine = false;
/**
* Groups of {@link Doc}s that are children of the current {@link Level}, separated by {@link
* Break}s.
*/
List<List<Doc>> splits = new ArrayList<>();
/** {@link Break}s between {@link Doc}s in the current {@link Level}. */
List<Break> breaks = new ArrayList<>();
private Level(Indent plusIndent) {
this.plusIndent = plusIndent;
@@ -468,18 +192,6 @@ public abstract class Doc {
return new Level(plusIndent);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("plusIndent", plusIndent)
.add("docs", docs)
.toString();
}
// State that needs to be preserved between calculating breaks and
// writing output.
// TODO(cushon): represent phases as separate immutable data.
/**
* Add a {@link Doc} to the {@code Level}.
*
@@ -516,6 +228,22 @@ public abstract class Doc {
return docRange;
}
// State that needs to be preserved between calculating breaks and
// writing output.
// TODO(cushon): represent phases as separate immutable data.
/** True if the entire {@link Level} fits on one line. */
boolean oneLine = false;
/**
* Groups of {@link Doc}s that are children of the current {@link Level}, separated by {@link
* Break}s.
*/
List<List<Doc>> splits = new ArrayList<>();
/** {@link Break}s between {@link Doc}s in the current {@link Level}. */
List<Break> breaks = new ArrayList<>();
@Override
public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
float thisWidth = getWidth();
@@ -529,6 +257,20 @@ public abstract class Doc {
return state.withColumn(broken.column);
}
private static void splitByBreaks(List<Doc> docs, List<List<Doc>> splits, List<Break> breaks) {
splits.clear();
breaks.clear();
splits.add(new ArrayList<>());
for (Doc doc : docs) {
if (doc instanceof Break) {
breaks.add((Break) doc);
splits.add(new ArrayList<>());
} else {
getLast(splits).add(doc);
}
}
}
/** Compute breaks for a {@link Level} that spans multiple lines. */
private State computeBroken(CommentsHelper commentsHelper, int maxWidth, State state) {
splitByBreaks(docs, splits, breaks);
@@ -546,20 +288,6 @@ public abstract class Doc {
return state;
}
private static void splitByBreaks(List<Doc> docs, List<List<Doc>> splits, List<Break> breaks) {
splits.clear();
breaks.clear();
splits.add(new ArrayList<>());
for (Doc doc : docs) {
if (doc instanceof Break) {
breaks.add((Break) doc);
splits.add(new ArrayList<>());
} else {
getLast(splits).add(doc);
}
}
}
/** Lay out a Break-separated group of Docs in the current Level. */
private static State computeBreakAndSplit(
CommentsHelper commentsHelper,
@@ -593,20 +321,6 @@ public abstract class Doc {
return state;
}
/**
* Get the width of a sequence of {@link Doc}s.
*
* @param docs the {@link Doc}s
* @return the width, or {@code Float.POSITIVE_INFINITY} if any {@link Doc} must be broken
*/
static float getWidth(List<Doc> docs) {
float width = 0.0F;
for (Doc doc : docs) {
width += doc.getWidth();
}
return width;
}
@Override
public void write(Output output) {
if (oneLine) {
@@ -630,9 +344,151 @@ public abstract class Doc {
}
}
/**
* Get the width of a sequence of {@link Doc}s.
*
* @param docs the {@link Doc}s
* @return the width, or {@code Float.POSITIVE_INFINITY} if any {@link Doc} must be broken
*/
static float getWidth(List<Doc> docs) {
float width = 0.0F;
for (Doc doc : docs) {
width += doc.getWidth();
}
return width;
}
private static Range<Integer> union(Range<Integer> x, Range<Integer> y) {
return x.isEmpty() ? y : y.isEmpty() ? x : x.span(y).canonical(INTEGERS);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("plusIndent", plusIndent)
.add("docs", docs)
.toString();
}
}
/** A leaf {@link Doc} for a token. */
public static final class Token extends Doc implements Op {
/** Is a Token a real token, or imaginary (e.g., a token generated incorrectly, or an EOF)? */
public enum RealOrImaginary {
REAL,
IMAGINARY;
boolean isReal() {
return this == REAL;
}
}
private final Input.Token token;
private final RealOrImaginary realOrImaginary;
private final Indent plusIndentCommentsBefore;
private final Optional<Indent> breakAndIndentTrailingComment;
private Token(
Input.Token token,
RealOrImaginary realOrImaginary,
Indent plusIndentCommentsBefore,
Optional<Indent> breakAndIndentTrailingComment) {
this.token = token;
this.realOrImaginary = realOrImaginary;
this.plusIndentCommentsBefore = plusIndentCommentsBefore;
this.breakAndIndentTrailingComment = breakAndIndentTrailingComment;
}
/**
* How much extra to indent comments before the {@code Token}.
*
* @return the extra indent
*/
Indent getPlusIndentCommentsBefore() {
return plusIndentCommentsBefore;
}
/** Force a line break and indent trailing javadoc or block comments. */
Optional<Indent> breakAndIndentTrailingComment() {
return breakAndIndentTrailingComment;
}
/**
* Make a {@code Token}.
*
* @param token the {@link Input.Token} to wrap
* @param realOrImaginary did this {@link Input.Token} appear in the input, or was it generated
* incorrectly?
* @param plusIndentCommentsBefore extra {@code plusIndent} for comments just before this token
* @return the new {@code Token}
*/
static Op make(
Input.Token token,
Doc.Token.RealOrImaginary realOrImaginary,
Indent plusIndentCommentsBefore,
Optional<Indent> breakAndIndentTrailingComment) {
return new Token(
token, realOrImaginary, plusIndentCommentsBefore, breakAndIndentTrailingComment);
}
/**
* Return the wrapped {@link Input.Token}.
*
* @return the {@link Input.Token}
*/
Input.Token getToken() {
return token;
}
/**
* Is the token good? That is, does it match an {@link Input.Token}?
*
* @return whether the @code Token} is good
*/
RealOrImaginary realOrImaginary() {
return realOrImaginary;
}
@Override
public void add(DocBuilder builder) {
builder.add(this);
}
@Override
float computeWidth() {
return token.getTok().length();
}
@Override
String computeFlat() {
return token.getTok().getOriginalText();
}
@Override
Range<Integer> computeRange() {
return Range.singleton(token.getTok().getIndex()).canonical(INTEGERS);
}
@Override
public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
String text = token.getTok().getOriginalText();
return state.withColumn(state.column + text.length());
}
@Override
public void write(Output output) {
String text = token.getTok().getOriginalText();
output.append(text, range());
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("token", token)
.add("realOrImaginary", realOrImaginary)
.add("plusIndentCommentsBefore", plusIndentCommentsBefore)
.toString();
}
}
/** A Leaf node in a {@link Doc} for a non-breaking space. */
@@ -655,11 +511,6 @@ public abstract class Doc {
builder.add(this);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).toString();
}
@Override
float computeWidth() {
return 1.0F;
@@ -684,12 +535,155 @@ public abstract class Doc {
public void write(Output output) {
output.append(" ", range());
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).toString();
}
}
/** A leaf node in a {@link Doc} for an optional break. */
public static final class Break extends Doc implements Op {
private final FillMode fillMode;
private final String flat;
private final Indent plusIndent;
private final Optional<BreakTag> optTag;
private Break(FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optTag) {
this.fillMode = fillMode;
this.flat = flat;
this.plusIndent = plusIndent;
this.optTag = optTag;
}
/**
* Make a {@code Break}.
*
* @param fillMode the {@link FillMode}
* @param flat the text when not broken
* @param plusIndent extra indent if taken
* @return the new {@code Break}
*/
public static Break make(FillMode fillMode, String flat, Indent plusIndent) {
return new Break(fillMode, flat, plusIndent, /* optTag= */ Optional.empty());
}
/**
* Make a {@code Break}.
*
* @param fillMode the {@link FillMode}
* @param flat the text when not broken
* @param plusIndent extra indent if taken
* @param optTag an optional tag for remembering whether the break was taken
* @return the new {@code Break}
*/
public static Break make(
FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optTag) {
return new Break(fillMode, flat, plusIndent, optTag);
}
/**
* Make a forced {@code Break}.
*
* @return the new forced {@code Break}
*/
public static Break makeForced() {
return make(FillMode.FORCED, "", Indent.Const.ZERO);
}
/**
* Return the {@code Break}'s extra indent.
*
* @return the extra indent
*/
int getPlusIndent() {
return plusIndent.eval();
}
/**
* Is the {@code Break} forced?
*
* @return whether the {@code Break} is forced
*/
boolean isForced() {
return fillMode == FillMode.FORCED;
}
@Override
public void add(DocBuilder builder) {
builder.breakDoc(this);
}
@Override
float computeWidth() {
return isForced() ? Float.POSITIVE_INFINITY : (float) flat.length();
}
@Override
String computeFlat() {
return flat;
}
@Override
Range<Integer> computeRange() {
return EMPTY_RANGE;
}
/** Was this break taken? */
boolean broken;
/** New indent after this break. */
int newIndent;
public State computeBreaks(State state, int lastIndent, boolean broken) {
if (optTag.isPresent()) {
optTag.get().recordBroken(broken);
}
if (broken) {
this.broken = true;
this.newIndent = max(lastIndent + plusIndent.eval(), 0);
return state.withColumn(newIndent);
} else {
this.broken = false;
this.newIndent = -1;
return state.withColumn(state.column + flat.length());
}
}
@Override
public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
// Updating the state for {@link Break}s requires deciding if the break
// should be taken.
// TODO(cushon): this hierarchy is wrong, create a separate interface
// for unbreakable Docs?
throw new UnsupportedOperationException("Did you mean computeBreaks(State, int, boolean)?");
}
@Override
public void write(Output output) {
if (broken) {
output.append("\n", EMPTY_RANGE);
output.indent(newIndent);
} else {
output.append(flat, range());
}
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("fillMode", fillMode)
.add("flat", flat)
.add("plusIndent", plusIndent)
.add("optTag", optTag)
.toString();
}
}
/** A leaf node in a {@link Doc} for a non-token. */
static final class Tok extends Doc implements Op {
private final Input.Tok tok;
String text;
private Tok(Input.Tok tok) {
this.tok = tok;
@@ -710,11 +704,6 @@ public abstract class Doc {
builder.add(this);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("tok", tok).toString();
}
@Override
float computeWidth() {
int idx = Newlines.firstBreak(tok.getOriginalText());
@@ -726,7 +715,7 @@ public abstract class Doc {
// Account for line comments with missing spaces, see computeFlat.
return tok.length() + 1;
} else {
return tok.length();
return reformatParameterComment(tok).map(String::length).orElse(tok.length());
}
}
return idx != -1 ? Float.POSITIVE_INFINITY : (float) tok.length();
@@ -734,15 +723,13 @@ public abstract class Doc {
@Override
String computeFlat() {
// TODO(cushon): commentsHelper.rewrite doesn't get called for spans that fit in a
// single
// line. That's fine for multi-line comment reflowing, but problematic for adding
// missing
// TODO(cushon): commentsHelper.rewrite doesn't get called for spans that fit in a single
// line. That's fine for multi-line comment reflowing, but problematic for adding missing
// spaces in line comments.
if (tok.isSlashSlashComment() && !tok.getOriginalText().startsWith("// ")) {
return "// " + tok.getOriginalText().substring("//".length());
}
return tok.getOriginalText();
return reformatParameterComment(tok).orElse(tok.getOriginalText());
}
@Override
@@ -750,6 +737,8 @@ public abstract class Doc {
return Range.singleton(tok.getIndex()).canonical(INTEGERS);
}
String text;
@Override
public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
text = commentsHelper.rewrite(tok, maxWidth, state.column);
@@ -761,5 +750,10 @@ public abstract class Doc {
public void write(Output output) {
output.append(text, range());
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("tok", tok).toString();
}
}
}
@@ -15,7 +15,6 @@
package com.google.googlejavaformat;
import com.google.common.base.MoreObjects;
import java.util.ArrayDeque;
import java.util.List;
@@ -60,24 +59,6 @@ public final class DocBuilder {
return this;
}
/**
* Return the {@link Doc}.
*
* @return the {@link Doc}
*/
public Doc build() {
return base;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("base", base)
.add("stack", stack)
.add("appendLevel", appendLevel)
.toString();
}
/**
* Open a new {@link Doc.Level}.
*
@@ -112,4 +93,22 @@ public final class DocBuilder {
appendLevel = stack.peekLast();
appendLevel.add(breakDoc);
}
/**
* Return the {@link Doc}.
*
* @return the {@link Doc}
*/
public Doc build() {
return base;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("base", base)
.add("stack", stack)
.add("appendLevel", appendLevel)
.toString();
}
}
@@ -23,12 +23,6 @@ public class FormatterDiagnostic {
private final String message;
private final int column;
private FormatterDiagnostic(int lineNumber, int column, String message) {
this.lineNumber = lineNumber;
this.column = column;
this.message = message;
}
public static FormatterDiagnostic create(String message) {
return new FormatterDiagnostic(-1, -1, message);
}
@@ -40,6 +34,12 @@ public class FormatterDiagnostic {
return new FormatterDiagnostic(lineNumber, column, message);
}
private FormatterDiagnostic(int lineNumber, int column, String message) {
this.lineNumber = lineNumber;
this.column = column;
this.message = message;
}
/**
* Returns the line number on which the error occurred, or {@code -1} if the error does not have a
* line number.
@@ -67,8 +67,7 @@ public class FormatterDiagnostic {
sb.append(lineNumber).append(':');
}
if (column >= 0) {
// internal column numbers are 0-based, but diagnostics use 1-based indexing by
// convention
// internal column numbers are 0-based, but diagnostics use 1-based indexing by convention
sb.append(column + 1).append(':');
}
if (lineNumber >= 0 || column >= 0) {
@@ -28,9 +28,10 @@ public abstract class Indent {
/** A constant function, returning a constant indent. */
public static final class Const extends Indent {
public static final Const ZERO = new Const(+0);
private final int n;
public static final Const ZERO = new Const(+0);
private Const(int n) {
this.n = n;
}
@@ -40,13 +41,13 @@ public abstract class Indent {
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("n", n).toString();
int eval() {
return n;
}
@Override
int eval() {
return n;
public String toString() {
return MoreObjects.toStringHelper(this).add("n", n).toString();
}
}
@@ -66,6 +67,11 @@ public abstract class Indent {
return new If(condition, thenIndent, elseIndent);
}
@Override
int eval() {
return (condition.wasBreakTaken() ? thenIndent : elseIndent).eval();
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
@@ -74,10 +80,5 @@ public abstract class Indent {
.add("elseIndent", elseIndent)
.toString();
}
@Override
int eval() {
return (condition.wasBreakTaken() ? thenIndent : elseIndent).eval();
}
}
}
@@ -21,54 +21,6 @@ import com.google.common.collect.ImmutableRangeMap;
/** An input to the formatter. */
public abstract class Input extends InputOutput {
/**
* Get the input tokens.
*
* @return the input tokens
*/
public abstract ImmutableList<? extends Token> getTokens();
/** A map from [start, end] position ranges to {@link Token}s. */
public abstract ImmutableRangeMap<Integer, ? extends Token> getPositionTokenMap();
public abstract ImmutableMap<Integer, Integer> getPositionToColumnMap();
public abstract String getText();
/**
* Get the number of toks.
*
* @return the number of toks, excluding the EOF tok
*/
public abstract int getkN();
/**
* Get the Token by index.
*
* @param k the Tok index
*/
public abstract Token getToken(int k);
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("super", super.toString()).toString();
}
/**
* Construct a diagnostic. Populates the input filename, and converts character offsets to
* numbers.
*/
public FormatterDiagnostic createDiagnostic(int inputPosition, String message) {
return FormatterDiagnostic.create(
getLineNumber(inputPosition), getColumnNumber(inputPosition), message);
}
/** Converts a character offset in the input to a line number. */
public abstract int getLineNumber(int inputPosition);
/** Converts a character offset in the input to a 0-based column number. */
public abstract int getColumnNumber(int inputPosition);
/**
* A {@code Tok} ("tock") is a token, or a comment, or a newline, or a maximal string of blanks. A
* token {@code Tok} underlies a {@link Token}, and each other {@code Tok} is attached to a single
@@ -144,4 +96,52 @@ public abstract class Input extends InputOutput {
*/
ImmutableList<? extends Tok> getToksAfter();
}
/**
* Get the input tokens.
*
* @return the input tokens
*/
public abstract ImmutableList<? extends Token> getTokens();
/** A map from [start, end] position ranges to {@link Token}s. */
public abstract ImmutableRangeMap<Integer, ? extends Token> getPositionTokenMap();
public abstract ImmutableMap<Integer, Integer> getPositionToColumnMap();
public abstract String getText();
/**
* Get the number of toks.
*
* @return the number of toks, excluding the EOF tok
*/
public abstract int getkN();
/**
* Get the Token by index.
*
* @param k the Tok index
*/
public abstract Token getToken(int k);
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("super", super.toString()).toString();
}
/** Converts a character offset in the input to a line number. */
public abstract int getLineNumber(int inputPosition);
/** Converts a character offset in the input to a 0-based column number. */
public abstract int getColumnNumber(int inputPosition);
/**
* Construct a diagnostic. Populates the input filename, and converts character offsets to
* numbers.
*/
public FormatterDiagnostic createDiagnostic(int inputPosition, String message) {
return FormatterDiagnostic.create(
getLineNumber(inputPosition), getColumnNumber(inputPosition), message);
}
}
@@ -17,7 +17,6 @@ package com.google.googlejavaformat;
import com.google.common.collect.DiscreteDomain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Range;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -25,12 +24,60 @@ import java.util.Map;
/** This interface defines methods common to an {@link Input} or an {@link Output}. */
public abstract class InputOutput {
private ImmutableList<String> lines = ImmutableList.of();
protected static final Range<Integer> EMPTY_RANGE = Range.closedOpen(-1, -1);
private static final DiscreteDomain<Integer> INTEGERS = DiscreteDomain.integers();
/** Set the lines. */
protected final void setLines(ImmutableList<String> lines) {
this.lines = lines;
}
/**
* Get the line count.
*
* @return the line count
*/
public final int getLineCount() {
return lines.size();
}
/**
* Get a line.
*
* @param lineI the line number
* @return the line
*/
public final String getLine(int lineI) {
return lines.get(lineI);
}
/** The {@link Range}s of the tokens or comments lying on each line, in any part. */
protected final List<Range<Integer>> ranges = new ArrayList<>();
private ImmutableList<String> lines = ImmutableList.of();
private static void addToRanges(List<Range<Integer>> ranges, int i, int k) {
while (ranges.size() <= i) {
ranges.add(EMPTY_RANGE);
}
Range<Integer> oldValue = ranges.get(i);
ranges.set(i, Range.closedOpen(oldValue.isEmpty() ? k : oldValue.lowerEndpoint(), k + 1));
}
protected final void computeRanges(List<? extends Input.Tok> toks) {
int lineI = 0;
for (Input.Tok tok : toks) {
String txt = tok.getOriginalText();
int lineI0 = lineI;
lineI += Newlines.count(txt);
int k = tok.getIndex();
if (k >= 0) {
for (int i = lineI0; i <= lineI; i++) {
addToRanges(ranges, i, k);
}
}
}
}
/**
* Given an {@code InputOutput}, compute the map from tok indices to line ranges.
@@ -55,15 +102,6 @@ public abstract class InputOutput {
return map;
}
/**
* Get the line count.
*
* @return the line count
*/
public final int getLineCount() {
return lines.size();
}
/**
* Get the {@link Range} of {@link Input.Tok}s lying in any part on a line.
*
@@ -74,46 +112,8 @@ public abstract class InputOutput {
return 0 <= lineI && lineI < ranges.size() ? ranges.get(lineI) : EMPTY_RANGE;
}
/**
* Get a line.
*
* @param lineI the line number
* @return the line
*/
public final String getLine(int lineI) {
return lines.get(lineI);
}
@Override
public String toString() {
return "InputOutput{" + "lines=" + lines + ", ranges=" + ranges + '}';
}
/** Set the lines. */
protected final void setLines(ImmutableList<String> lines) {
this.lines = lines;
}
protected final void computeRanges(List<? extends Input.Tok> toks) {
int lineI = 0;
for (Input.Tok tok : toks) {
String txt = tok.getOriginalText();
int lineI0 = lineI;
lineI += Newlines.count(txt);
int k = tok.getIndex();
if (k >= 0) {
for (int i = lineI0; i <= lineI; i++) {
addToRanges(ranges, i, k);
}
}
}
}
private static void addToRanges(List<Range<Integer>> ranges, int i, int k) {
while (ranges.size() <= i) {
ranges.add(EMPTY_RANGE);
}
Range<Integer> oldValue = ranges.get(i);
ranges.set(i, Range.closedOpen(oldValue.isEmpty() ? k : oldValue.lowerEndpoint(), k + 1));
}
}
@@ -17,25 +17,17 @@ package com.google.googlejavaformat;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterators;
import java.util.Iterator;
import java.util.NoSuchElementException;
/** Platform-independent newline handling. */
public class Newlines {
private static final ImmutableSet<String> BREAKS = ImmutableSet.of("\r\n", "\n", "\r");
/** Returns the number of line breaks in the input. */
public static int count(String input) {
return Iterators.size(lineOffsetIterator(input)) - 1;
}
/** Returns an iterator over the start offsets of lines in the input. */
public static Iterator<Integer> lineOffsetIterator(String input) {
return new LineOffsetIterator(input);
}
/** Returns the index of the first break in the input, or {@code -1}. */
public static int firstBreak(String input) {
Iterator<Integer> it = lineOffsetIterator(input);
@@ -43,6 +35,8 @@ public class Newlines {
return it.hasNext() ? it.next() : -1;
}
private static final ImmutableSet<String> BREAKS = ImmutableSet.of("\r\n", "\n", "\r");
/** Returns true if the entire input string is a recognized line break. */
public static boolean isNewline(String input) {
return BREAKS.contains(input);
@@ -98,6 +92,11 @@ public class Newlines {
return CharMatcher.anyOf("\n\r").matchesAnyOf(text);
}
/** Returns an iterator over the start offsets of lines in the input. */
public static Iterator<Integer> lineOffsetIterator(String input) {
return new LineOffsetIterator(input);
}
/** Returns an iterator over lines in the input, including trailing whitespace. */
public static Iterator<String> lineIterator(String input) {
return new LineIterator(input);
@@ -105,9 +104,9 @@ public class Newlines {
private static class LineOffsetIterator implements Iterator<Integer> {
private final String input;
private int curr = 0;
private int idx = 0;
private final String input;
private LineOffsetIterator(String input) {
this.input = input;
@@ -156,11 +155,12 @@ public class Newlines {
private static class LineIterator implements Iterator<String> {
private final String input;
private final Iterator<Integer> indices;
int idx;
String curr;
private final String input;
private final Iterator<Integer> indices;
private LineIterator(String input) {
this.input = input;
this.indices = lineOffsetIterator(input);
@@ -28,7 +28,6 @@ import com.google.googlejavaformat.Indent.Const;
import com.google.googlejavaformat.Input.Tok;
import com.google.googlejavaformat.Input.Token;
import com.google.googlejavaformat.Output.BreakTag;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -39,31 +38,7 @@ import java.util.Optional;
*/
public final class OpsBuilder {
private static final Indent.Const ZERO = Indent.Const.ZERO;
private static final Doc.Space SPACE = Doc.Space.make();
private final Input input;
private final List<Op> ops = new ArrayList<>();
private final Output output;
private int tokenI = 0;
private int inputPosition = Integer.MIN_VALUE;
private int lastPartialFormatBoundary = -1;
/** The number of unclosed open ops in the input stream. */
int depth = 0;
/**
* The {@code OpsBuilder} constructor.
*
* @param input the {@link Input}, used for retrieve information from the AST
* @param output the {@link Output}, used here only to record blank-line information
*/
public OpsBuilder(Input input, Output output) {
this.input = input;
this.output = output;
}
/**
* @return the actual size of the AST node at position, including comments.
*/
/** @return the actual size of the AST node at position, including comments. */
public int actualSize(int position, int length) {
Token startToken = input.getPositionTokenMap().get(position);
int start = startToken.getTok().getPosition();
@@ -82,9 +57,7 @@ public final class OpsBuilder {
return end - start;
}
/**
* @return the start column of the token at {@code position}, including leading comments.
*/
/** @return the start column of the token at {@code position}, including leading comments. */
public Integer actualStartColumn(int position) {
Token startToken = input.getPositionTokenMap().get(position);
int start = startToken.getTok().getPosition();
@@ -100,15 +73,93 @@ public final class OpsBuilder {
return start;
}
/** Add a list of {@link Op}s. */
public final void addAll(List<Op> ops) {
for (Op op : ops) {
add(op);
/** A request to add or remove a blank line in the output. */
public abstract static class BlankLineWanted {
/** Always emit a blank line. */
public static final BlankLineWanted YES = new SimpleBlankLine(Optional.of(true));
/** Never emit a blank line. */
public static final BlankLineWanted NO = new SimpleBlankLine(Optional.of(false));
/**
* Explicitly preserve blank lines from the input (e.g. before the first member in a class
* declaration). Overrides conditional blank lines.
*/
public static final BlankLineWanted PRESERVE =
new SimpleBlankLine(/* wanted= */ Optional.empty());
/** Is the blank line wanted? */
public abstract Optional<Boolean> wanted();
/** Merge this blank line request with another. */
public abstract BlankLineWanted merge(BlankLineWanted wanted);
/** Emit a blank line if the given break is taken. */
public static BlankLineWanted conditional(BreakTag breakTag) {
return new ConditionalBlankLine(ImmutableList.of(breakTag));
}
private static final class SimpleBlankLine extends BlankLineWanted {
private final Optional<Boolean> wanted;
SimpleBlankLine(Optional<Boolean> wanted) {
this.wanted = wanted;
}
@Override
public Optional<Boolean> wanted() {
return wanted;
}
@Override
public BlankLineWanted merge(BlankLineWanted other) {
return this;
}
}
private static final class ConditionalBlankLine extends BlankLineWanted {
private final ImmutableList<BreakTag> tags;
ConditionalBlankLine(Iterable<BreakTag> tags) {
this.tags = ImmutableList.copyOf(tags);
}
@Override
public Optional<Boolean> wanted() {
for (BreakTag tag : tags) {
if (tag.wasBreakTaken()) {
return Optional.of(true);
}
}
return Optional.empty();
}
@Override
public BlankLineWanted merge(BlankLineWanted other) {
if (!(other instanceof ConditionalBlankLine)) {
return other;
}
return new ConditionalBlankLine(
Iterables.concat(this.tags, ((ConditionalBlankLine) other).tags));
}
}
}
private final Input input;
private final List<Op> ops = new ArrayList<>();
private final Output output;
private static final Indent.Const ZERO = Indent.Const.ZERO;
private int tokenI = 0;
private int inputPosition = Integer.MIN_VALUE;
/** The number of unclosed open ops in the input stream. */
int depth = 0;
/** Add an {@link Op}, and record open/close ops for later validation of unclosed levels. */
private void add(Op op) {
public final void add(Op op) {
if (op instanceof OpenOp) {
depth++;
} else if (op instanceof CloseOp) {
@@ -120,6 +171,24 @@ public final class OpsBuilder {
ops.add(op);
}
/** Add a list of {@link Op}s. */
public final void addAll(List<Op> ops) {
for (Op op : ops) {
add(op);
}
}
/**
* The {@code OpsBuilder} constructor.
*
* @param input the {@link Input}, used for retrieve information from the AST
* @param output the {@link Output}, used here only to record blank-line information
*/
public OpsBuilder(Input input, Output output) {
this.input = input;
this.output = output;
}
/** Get the {@code OpsBuilder}'s {@link Input}. */
public final Input getInput() {
return input;
@@ -130,6 +199,22 @@ public final class OpsBuilder {
return depth;
}
/**
* Checks that all open ops in the op stream have matching close ops.
*
* @throws FormattingError if any ops were unclosed
*/
public void checkClosed(int previous) {
if (depth != previous) {
throw new FormattingError(diagnostic(String.format("saw %d unclosed ops", depth)));
}
}
/** Create a {@link FormatterDiagnostic} at the current position. */
public FormatterDiagnostic diagnostic(String message) {
return input.createDiagnostic(inputPosition, message);
}
/**
* Sync to position in the input. If we've skipped outputting any tokens that were present in the
* input tokens, output them here and optionally complain.
@@ -150,11 +235,6 @@ public final class OpsBuilder {
}
}
/** Create a {@link FormatterDiagnostic} at the current position. */
public FormatterDiagnostic diagnostic(String message) {
return input.createDiagnostic(inputPosition, message);
}
/** Output any remaining tokens from the input stream (e.g. terminal whitespace). */
public final void drain() {
int inputPosition = input.getText().length() + 1;
@@ -175,17 +255,6 @@ public final class OpsBuilder {
checkClosed(0);
}
/**
* Checks that all open ops in the op stream have matching close ops.
*
* @throws FormattingError if any ops were unclosed
*/
public void checkClosed(int previous) {
if (depth != previous) {
throw new FormattingError(diagnostic(String.format("saw %d unclosed ops", depth)));
}
}
/**
* Open a new level by emitting an {@link OpenOp}.
*
@@ -200,6 +269,20 @@ public final class OpsBuilder {
add(CloseOp.make());
}
/** Return the text of the next {@link Input.Token}, or absent if there is none. */
public final Optional<String> peekToken() {
return peekToken(0);
}
/** Return the text of an upcoming {@link Input.Token}, or absent if there is none. */
public final Optional<String> peekToken(int skip) {
ImmutableList<? extends Input.Token> tokens = input.getTokens();
int idx = tokenI + skip;
return idx < tokens.size()
? Optional.of(tokens.get(idx).getTok().getOriginalText())
: Optional.empty();
}
/**
* Returns the {@link Input.Tok}s starting at the current source position, which are satisfied by
* the given predicate.
@@ -264,20 +347,6 @@ public final class OpsBuilder {
}
}
/** Return the text of the next {@link Input.Token}, or absent if there is none. */
public final Optional<String> peekToken() {
return peekToken(0);
}
/** Return the text of an upcoming {@link Input.Token}, or absent if there is none. */
public final Optional<String> peekToken(int skip) {
ImmutableList<? extends Input.Token> tokens = input.getTokens();
int idx = tokenI + skip;
return idx < tokens.size()
? Optional.of(tokens.get(idx).getTok().getOriginalText())
: Optional.empty();
}
/**
* Emit a single- or multi-character op by breaking it into single-character {@link Doc.Token}s.
*
@@ -304,30 +373,6 @@ public final class OpsBuilder {
breakOp(Doc.FillMode.UNIFIED, "", ZERO);
}
/**
* Emit a generic {@link Doc.Break}.
*
* @param fillMode the {@link Doc.FillMode}
* @param flat the {@link Doc.Break} when not broken
* @param plusIndent extra indent if taken
*/
public final void breakOp(Doc.FillMode fillMode, String flat, Indent plusIndent) {
breakOp(fillMode, flat, plusIndent, /* optionalTag= */ Optional.empty());
}
/**
* Emit a generic {@link Doc.Break}.
*
* @param fillMode the {@link Doc.FillMode}
* @param flat the {@link Doc.Break} when not broken
* @param plusIndent extra indent if taken
* @param optionalTag an optional tag for remembering whether the break was taken
*/
public final void breakOp(
Doc.FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optionalTag) {
add(Doc.Break.make(fillMode, flat, plusIndent, optionalTag));
}
/**
* Emit a {@link Doc.Break}.
*
@@ -374,6 +419,50 @@ public final class OpsBuilder {
breakOp(Doc.FillMode.INDEPENDENT, flat, ZERO);
}
/**
* Emit a generic {@link Doc.Break}.
*
* @param fillMode the {@link Doc.FillMode}
* @param flat the {@link Doc.Break} when not broken
* @param plusIndent extra indent if taken
*/
public final void breakOp(Doc.FillMode fillMode, String flat, Indent plusIndent) {
breakOp(fillMode, flat, plusIndent, /* optionalTag= */ Optional.empty());
}
/**
* Emit a generic {@link Doc.Break}.
*
* @param fillMode the {@link Doc.FillMode}
* @param flat the {@link Doc.Break} when not broken
* @param plusIndent extra indent if taken
* @param optionalTag an optional tag for remembering whether the break was taken
*/
public final void breakOp(
Doc.FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optionalTag) {
add(Doc.Break.make(fillMode, flat, plusIndent, optionalTag));
}
private int lastPartialFormatBoundary = -1;
/**
* Make the boundary of a region that can be partially formatted. The boundary will be included in
* the following region, e.g.: [[boundary0, boundary1), [boundary1, boundary2), ...].
*/
public void markForPartialFormat() {
if (lastPartialFormatBoundary == -1) {
lastPartialFormatBoundary = tokenI;
return;
}
if (tokenI == lastPartialFormatBoundary) {
return;
}
Token start = input.getTokens().get(lastPartialFormatBoundary);
Token end = input.getTokens().get(tokenI - 1);
output.markForPartialFormat(start, end);
lastPartialFormatBoundary = tokenI;
}
/**
* Force or suppress a blank line here in the output.
*
@@ -392,6 +481,8 @@ public final class OpsBuilder {
return token.getTok().getIndex();
}
private static final Doc.Space SPACE = Doc.Space.make();
/**
* Build a list of {@link Op}s from the {@code OpsBuilder}.
*
@@ -452,8 +543,7 @@ public final class OpsBuilder {
}
}
if (allowBlankAfterLastComment && newlines > 1) {
// Force a line break after two newlines in a row following a line or block
// comment
// Force a line break after two newlines in a row following a line or block comment
output.blankLine(token.getTok().getIndex(), BlankLineWanted.YES);
}
if (lastWasComment && newlines > 0) {
@@ -545,24 +635,6 @@ public final class OpsBuilder {
return newOps.build();
}
/**
* Make the boundary of a region that can be partially formatted. The boundary will be included in
* the following region, e.g.: [[boundary0, boundary1), [boundary1, boundary2), ...].
*/
public void markForPartialFormat() {
if (lastPartialFormatBoundary == -1) {
lastPartialFormatBoundary = tokenI;
return;
}
if (tokenI == lastPartialFormatBoundary) {
return;
}
Token start = input.getTokens().get(lastPartialFormatBoundary);
Token end = input.getTokens().get(tokenI - 1);
output.markForPartialFormat(start, end);
lastPartialFormatBoundary = tokenI;
}
private static boolean isForcedBreak(Op op) {
return op instanceof Doc.Break && ((Doc.Break) op).isForced();
}
@@ -583,78 +655,4 @@ public final class OpsBuilder {
.add("inputPosition", inputPosition)
.toString();
}
/** A request to add or remove a blank line in the output. */
public abstract static class BlankLineWanted {
/** Always emit a blank line. */
public static final BlankLineWanted YES = new SimpleBlankLine(Optional.of(true));
/** Never emit a blank line. */
public static final BlankLineWanted NO = new SimpleBlankLine(Optional.of(false));
/**
* Explicitly preserve blank lines from the input (e.g. before the first member in a class
* declaration). Overrides conditional blank lines.
*/
public static final BlankLineWanted PRESERVE =
new SimpleBlankLine(/* wanted= */ Optional.empty());
/** Emit a blank line if the given break is taken. */
public static BlankLineWanted conditional(BreakTag breakTag) {
return new ConditionalBlankLine(ImmutableList.of(breakTag));
}
/** Is the blank line wanted? */
public abstract Optional<Boolean> wanted();
/** Merge this blank line request with another. */
public abstract BlankLineWanted merge(BlankLineWanted wanted);
private static final class SimpleBlankLine extends BlankLineWanted {
private final Optional<Boolean> wanted;
SimpleBlankLine(Optional<Boolean> wanted) {
this.wanted = wanted;
}
@Override
public Optional<Boolean> wanted() {
return wanted;
}
@Override
public BlankLineWanted merge(BlankLineWanted other) {
return this;
}
}
private static final class ConditionalBlankLine extends BlankLineWanted {
private final ImmutableList<BreakTag> tags;
ConditionalBlankLine(Iterable<BreakTag> tags) {
this.tags = ImmutableList.copyOf(tags);
}
@Override
public Optional<Boolean> wanted() {
for (BreakTag tag : tags) {
if (tag.wasBreakTaken()) {
return Optional.of(true);
}
}
return Optional.empty();
}
@Override
public BlankLineWanted merge(BlankLineWanted other) {
if (!(other instanceof ConditionalBlankLine)) {
return other;
}
return new ConditionalBlankLine(
Iterables.concat(this.tags, ((ConditionalBlankLine) other).tags));
}
}
}
}
@@ -17,11 +17,29 @@ package com.google.googlejavaformat;
import com.google.common.base.MoreObjects;
import com.google.common.collect.Range;
import com.google.googlejavaformat.OpsBuilder.BlankLineWanted;
import java.util.Optional;
/** An output from the formatter. */
public abstract class Output extends InputOutput {
/** Unique identifier for a break. */
public static final class BreakTag {
Optional<Boolean> taken = Optional.empty();
public void recordBroken(boolean broken) {
// TODO(cushon): enforce invariants.
// Currently we rely on setting Breaks multiple times, e.g. when deciding
// whether a Level should be flowed. Using separate data structures
// instead of mutation or adding an explicit 'reset' step would allow
// a useful invariant to be enforced here.
taken = Optional.of(broken);
}
public boolean wasBreakTaken() {
return taken.orElse(false);
}
}
/**
* Indent by outputting {@code indent} spaces.
*
@@ -59,23 +77,4 @@ public abstract class Output extends InputOutput {
public String toString() {
return MoreObjects.toStringHelper(this).add("super", super.toString()).toString();
}
/** Unique identifier for a break. */
public static final class BreakTag {
Optional<Boolean> taken = Optional.empty();
public void recordBroken(boolean broken) {
// TODO(cushon): enforce invariants.
// Currently we rely on setting Breaks multiple times, e.g. when deciding
// whether a Level should be flowed. Using separate data structures
// instead of mutation or adding an explicit 'reset' step would allow
// a useful invariant to be enforced here.
taken = Optional.of(broken);
}
public boolean wasBreakTaken() {
return taken.orElse(false);
}
}
}
@@ -16,7 +16,6 @@ package com.google.googlejavaformat.java;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableRangeSet;
import java.util.Optional;
/**
@@ -82,10 +81,6 @@ final class CommandLineOptions {
this.formatJavadoc = formatJavadoc;
}
static Builder builder() {
return new Builder();
}
/** The files to format. */
ImmutableList<String> files() {
return files;
@@ -96,6 +91,21 @@ final class CommandLineOptions {
return inPlace;
}
/** Line ranges to format. */
ImmutableRangeSet<Integer> lines() {
return lines;
}
/** Character offsets for partial formatting, paired with {@code lengths}. */
ImmutableList<Integer> offsets() {
return offsets;
}
/** Partial formatting region lengths, paired with {@code offsets}. */
ImmutableList<Integer> lengths() {
return lengths;
}
/** Use AOSP style instead of Google Style (4-space indentation). */
boolean aosp() {
return aosp;
@@ -157,25 +167,14 @@ final class CommandLineOptions {
return !lines().isEmpty() || !offsets().isEmpty() || !lengths().isEmpty();
}
/** Line ranges to format. */
ImmutableRangeSet<Integer> lines() {
return lines;
}
/** Character offsets for partial formatting, paired with {@code lengths}. */
ImmutableList<Integer> offsets() {
return offsets;
}
/** Partial formatting region lengths, paired with {@code offsets}. */
ImmutableList<Integer> lengths() {
return lengths;
}
boolean formatJavadoc() {
return formatJavadoc;
}
static Builder builder() {
return new Builder();
}
static class Builder {
private final ImmutableList.Builder<String> files = ImmutableList.builder();
@@ -20,7 +20,6 @@ import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableRangeSet;
import com.google.common.collect.Range;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
@@ -38,81 +37,6 @@ final class CommandLineOptionsParser {
private static final Splitter ARG_SPLITTER =
Splitter.on(CharMatcher.breakingWhitespace()).omitEmptyStrings().trimResults();
private static Integer parseInteger(Iterator<String> it, String flag, String value) {
try {
return Integer.valueOf(getValue(flag, it, value));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
String.format("invalid integer value for %s: %s", flag, value), e);
}
}
private static String getValue(String flag, Iterator<String> it, String value) {
if (value != null) {
return value;
}
if (!it.hasNext()) {
throw new IllegalArgumentException("required value was not provided for: " + flag);
}
return it.next();
}
/**
* Parse multiple --lines flags, like {"1:12,14,20:36", "40:45,50"}. Multiple ranges can be given
* with multiple --lines flags or separated by commas. A single line can be set by a single
* number. Line numbers are {@code 1}-based, but are converted to the {@code 0}-based numbering
* used internally by google-java-format.
*/
private static void parseRangeSet(ImmutableRangeSet.Builder<Integer> result, String ranges) {
for (String range : COMMA_SPLITTER.split(ranges)) {
result.add(parseRange(range));
}
}
/**
* Parse a range, as in "1:12" or "42". Line numbers provided are {@code 1}-based, but are
* converted here to {@code 0}-based.
*/
private static Range<Integer> parseRange(String arg) {
List<String> args = COLON_SPLITTER.splitToList(arg);
switch (args.size()) {
case 1:
int line = Integer.parseInt(args.get(0)) - 1;
return Range.closedOpen(line, line + 1);
case 2:
int line0 = Integer.parseInt(args.get(0)) - 1;
int line1 = Integer.parseInt(args.get(1)) - 1;
return Range.closedOpen(line0, line1 + 1);
default:
throw new IllegalArgumentException(arg);
}
}
/**
* Pre-processes an argument list, expanding arguments of the form {@code @filename} by reading
* the content of the file and appending whitespace-delimited options to {@code arguments}.
*/
private static void expandParamsFiles(Iterable<String> args, List<String> expanded) {
for (String arg : args) {
if (arg.isEmpty()) {
continue;
}
if (!arg.startsWith("@")) {
expanded.add(arg);
} else if (arg.startsWith("@@")) {
expanded.add(arg.substring(1));
} else {
Path path = Paths.get(arg.substring(1));
try {
String sequence = new String(Files.readAllBytes(path), UTF_8);
expandParamsFiles(ARG_SPLITTER.split(sequence), expanded);
} catch (IOException e) {
throw new UncheckedIOException(path + ": could not read file: " + e.getMessage(), e);
}
}
}
}
/** Parses {@link CommandLineOptions}. */
static CommandLineOptions parse(Iterable<String> options) {
CommandLineOptions.Builder optionsBuilder = CommandLineOptions.builder();
@@ -207,4 +131,79 @@ final class CommandLineOptionsParser {
}
return optionsBuilder.build();
}
private static Integer parseInteger(Iterator<String> it, String flag, String value) {
try {
return Integer.valueOf(getValue(flag, it, value));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
String.format("invalid integer value for %s: %s", flag, value), e);
}
}
private static String getValue(String flag, Iterator<String> it, String value) {
if (value != null) {
return value;
}
if (!it.hasNext()) {
throw new IllegalArgumentException("required value was not provided for: " + flag);
}
return it.next();
}
/**
* Parse multiple --lines flags, like {"1:12,14,20:36", "40:45,50"}. Multiple ranges can be given
* with multiple --lines flags or separated by commas. A single line can be set by a single
* number. Line numbers are {@code 1}-based, but are converted to the {@code 0}-based numbering
* used internally by google-java-format.
*/
private static void parseRangeSet(ImmutableRangeSet.Builder<Integer> result, String ranges) {
for (String range : COMMA_SPLITTER.split(ranges)) {
result.add(parseRange(range));
}
}
/**
* Parse a range, as in "1:12" or "42". Line numbers provided are {@code 1}-based, but are
* converted here to {@code 0}-based.
*/
private static Range<Integer> parseRange(String arg) {
List<String> args = COLON_SPLITTER.splitToList(arg);
switch (args.size()) {
case 1:
int line = Integer.parseInt(args.get(0)) - 1;
return Range.closedOpen(line, line + 1);
case 2:
int line0 = Integer.parseInt(args.get(0)) - 1;
int line1 = Integer.parseInt(args.get(1)) - 1;
return Range.closedOpen(line0, line1 + 1);
default:
throw new IllegalArgumentException(arg);
}
}
/**
* Pre-processes an argument list, expanding arguments of the form {@code @filename} by reading
* the content of the file and appending whitespace-delimited options to {@code arguments}.
*/
private static void expandParamsFiles(Iterable<String> args, List<String> expanded) {
for (String arg : args) {
if (arg.isEmpty()) {
continue;
}
if (!arg.startsWith("@")) {
expanded.add(arg);
} else if (arg.startsWith("@@")) {
expanded.add(arg.substring(1));
} else {
Path path = Paths.get(arg.substring(1));
try {
String sequence = new String(Files.readAllBytes(path), UTF_8);
expandParamsFiles(ARG_SPLITTER.split(sequence), expanded);
} catch (IOException e) {
throw new UncheckedIOException(path + ": could not read file: " + e.getMessage(), e);
}
}
}
}
}
@@ -15,12 +15,11 @@
package com.google.googlejavaformat.java;
import com.google.common.collect.ImmutableList;
import openjdk.source.tree.AnnotatedTypeTree;
import openjdk.source.tree.AnnotationTree;
import openjdk.source.tree.ArrayTypeTree;
import openjdk.source.tree.Tree;
import openjdk.tools.javac.tree.JCTree;
import com.sun.source.tree.AnnotatedTypeTree;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ArrayTypeTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.tree.JCTree;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
@@ -37,6 +36,35 @@ import java.util.List;
*/
class DimensionHelpers {
/** The array dimension specifiers (including any type annotations) associated with a type. */
static class TypeWithDims {
final Tree node;
final ImmutableList<List<AnnotationTree>> dims;
public TypeWithDims(Tree node, ImmutableList<List<AnnotationTree>> dims) {
this.node = node;
this.dims = dims;
}
}
enum SortedDims {
YES,
NO
}
/** Returns a (possibly re-ordered) {@link TypeWithDims} for the given type. */
static TypeWithDims extractDims(Tree node, SortedDims sorted) {
Deque<List<AnnotationTree>> builder = new ArrayDeque<>();
node = extractDims(builder, node);
Iterable<List<AnnotationTree>> dims;
if (sorted == SortedDims.YES) {
dims = reorderBySourcePosition(builder);
} else {
dims = builder;
}
return new TypeWithDims(node, ImmutableList.copyOf(dims));
}
/**
* Rotate the list of dimension specifiers until all dimensions with type annotations appear in
* source order.
@@ -93,33 +121,4 @@ class DimensionHelpers {
return node;
}
}
/** Returns a (possibly re-ordered) {@link TypeWithDims} for the given type. */
static TypeWithDims extractDims(Tree node, SortedDims sorted) {
Deque<List<AnnotationTree>> builder = new ArrayDeque<>();
node = extractDims(builder, node);
Iterable<List<AnnotationTree>> dims;
if (sorted == SortedDims.YES) {
dims = reorderBySourcePosition(builder);
} else {
dims = builder;
}
return new TypeWithDims(node, ImmutableList.copyOf(dims));
}
enum SortedDims {
YES,
NO
}
/** The array dimension specifiers (including any type annotations) associated with a type. */
static class TypeWithDims {
final Tree node;
final ImmutableList<List<AnnotationTree>> dims;
public TypeWithDims(Tree node, ImmutableList<List<AnnotationTree>> dims) {
this.node = node;
this.dims = dims;
}
}
}
@@ -14,41 +14,73 @@
package com.google.googlejavaformat.java;
import com.google.auto.value.AutoValue;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;
import java.nio.file.Path;
import java.util.concurrent.Callable;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Encapsulates information about a file to be formatted, including which parts of the file to
* format.
*/
class FormatFileCallable implements Callable<String> {
class FormatFileCallable implements Callable<FormatFileCallable.Result> {
@AutoValue
abstract static class Result {
abstract @Nullable Path path();
abstract String input();
abstract @Nullable String output();
boolean changed() {
return !input().equals(output());
}
abstract @Nullable FormatterException exception();
static Result create(
@Nullable Path path,
String input,
@Nullable String output,
@Nullable FormatterException exception) {
return new AutoValue_FormatFileCallable_Result(path, input, output, exception);
}
}
private final Path path;
private final String input;
private final CommandLineOptions parameters;
private final JavaFormatterOptions options;
public FormatFileCallable(
CommandLineOptions parameters, String input, JavaFormatterOptions options) {
CommandLineOptions parameters, Path path, String input, JavaFormatterOptions options) {
this.path = path;
this.input = input;
this.parameters = parameters;
this.options = options;
}
@Override
public String call() throws FormatterException {
if (parameters.fixImportsOnly()) {
return fixImports(input);
}
public Result call() {
try {
if (parameters.fixImportsOnly()) {
return Result.create(path, input, fixImports(input), /* exception= */ null);
}
Formatter formatter = new Formatter(options);
String formatted = formatter.formatSource(input, characterRanges(input).asRanges());
formatted = fixImports(formatted);
if (parameters.reflowLongStrings()) {
formatted = StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, formatted, formatter);
Formatter formatter = new Formatter(options);
String formatted = formatter.formatSource(input, characterRanges(input).asRanges());
formatted = fixImports(formatted);
if (parameters.reflowLongStrings()) {
formatted = StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, formatted, formatter);
}
return Result.create(path, input, formatted, /* exception= */ null);
} catch (FormatterException e) {
return Result.create(path, input, /* output= */ null, e);
}
return formatted;
}
private String fixImports(String input) throws FormatterException {
@@ -14,8 +14,6 @@
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;
@@ -33,28 +31,25 @@ import com.google.googlejavaformat.FormattingError;
import com.google.googlejavaformat.Newlines;
import com.google.googlejavaformat.Op;
import com.google.googlejavaformat.OpsBuilder;
import openjdk.tools.javac.file.JavacFileManager;
import openjdk.tools.javac.parser.JavacParser;
import openjdk.tools.javac.parser.ParserFactory;
import openjdk.tools.javac.tree.JCTree.JCCompilationUnit;
import openjdk.tools.javac.util.Context;
import openjdk.tools.javac.util.Log;
import openjdk.tools.javac.util.Options;
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 jdkx.tools.Diagnostic;
import jdkx.tools.DiagnosticCollector;
import jdkx.tools.DiagnosticListener;
import jdkx.tools.JavaFileObject;
import jdkx.tools.SimpleJavaFileObject;
import jdkx.tools.StandardLocation;
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
@@ -106,111 +101,6 @@ public final class Formatter {
this.options = options;
}
/**
* 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;
}
private static int getJavaMajorVersion() {
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());
}
/**
* 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())));
}
/**
* 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);
}
/**
* Construct a {@code Formatter} given a Java compilation unit. Parses the code; builds a {@link
* JavaInput} and the corresponding {@link JavaOutput}.
@@ -246,9 +136,9 @@ public final class Formatter {
JavacParser parser =
parserFactory.newParser(
javaInput.getText(),
/*keepDocComments=*/ true,
/*keepEndPos=*/ true,
/*keepLineMap=*/ true);
/* keepDocComments= */ true,
/* keepEndPos= */ true,
/* keepLineMap= */ true);
unit = parser.parseCompilationUnit();
unit.sourcefile = source;
@@ -260,7 +150,20 @@ public final class Formatter {
}
OpsBuilder builder = new OpsBuilder(javaInput, javaOutput);
// Output the compilation unit.
JavaInputAstVisitor visitor = new JavaInputAstVisitor(builder, options.indentationMultiplier());
JavaInputAstVisitor visitor;
if (Runtime.version().feature() >= 17) {
try {
visitor =
Class.forName("com.google.googlejavaformat.java.java17.Java17InputAstVisitor")
.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();
@@ -285,6 +188,31 @@ public final class Formatter {
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.
*
@@ -304,4 +232,71 @@ public final class Formatter {
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;
}
}
@@ -19,11 +19,9 @@ import static java.util.Locale.ENGLISH;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.googlejavaformat.FormatterDiagnostic;
import java.util.List;
import jdkx.tools.Diagnostic;
import jdkx.tools.JavaFileObject;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
/** Checked exception class for formatter errors. */
public final class FormatterException extends Exception {
@@ -43,6 +41,10 @@ public final class FormatterException extends Exception {
this.diagnostics = ImmutableList.copyOf(diagnostics);
}
public List<FormatterDiagnostic> diagnostics() {
return diagnostics;
}
public static FormatterException fromJavacDiagnostics(
Iterable<Diagnostic<? extends JavaFileObject>> diagnostics) {
return new FormatterException(
@@ -53,8 +55,4 @@ public final class FormatterException extends Exception {
return FormatterDiagnostic.create(
(int) input.getLineNumber(), (int) input.getColumnNumber(), input.getMessage(ENGLISH));
}
public List<FormatterDiagnostic> diagnostics() {
return diagnostics;
}
}
@@ -0,0 +1,38 @@
/*
* Copyright 2021 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 com.google.auto.service.AutoService;
import java.io.PrintWriter;
import java.util.spi.ToolProvider;
/** Provide a way to be invoked without necessarily starting a new VM. */
@AutoService(ToolProvider.class)
public class GoogleJavaFormatToolProvider implements ToolProvider {
@Override
public String name() {
return "google-java-format";
}
@Override
public int run(PrintWriter out, PrintWriter err, String... args) {
try {
return Main.main(out, err, args);
} catch (RuntimeException e) {
err.print(e.getMessage());
return -1; // pass non-zero value back indicating an error has happened
}
}
}
@@ -16,7 +16,7 @@ package com.google.googlejavaformat.java;
class GoogleJavaFormatVersion {
static String version() {
return "%VERSION%";
}
static String version() {
return "%VERSION%";
}
}
@@ -25,8 +25,7 @@ import com.google.common.collect.ImmutableSortedSet;
import com.google.googlejavaformat.Newlines;
import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
import com.google.googlejavaformat.java.JavaInput.Tok;
import openjdk.tools.javac.parser.Tokens.TokenKind;
import com.sun.tools.javac.parser.Tokens.TokenKind;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@@ -38,81 +37,6 @@ import java.util.stream.Stream;
public class ImportOrderer {
private static final Splitter DOT_SPLITTER = Splitter.on('.');
/**
* {@link TokenKind}s that indicate the start of a type definition. We use this to avoid scanning
* the whole file, since we know that imports must precede any type definition.
*/
private static final ImmutableSet<TokenKind> CLASS_START =
ImmutableSet.of(TokenKind.CLASS, TokenKind.INTERFACE, TokenKind.ENUM);
/**
* We use this set to find the first import, and again to check that there are no imports after
* the place we stopped gathering them. An annotation definition ({@code @interface}) is two
* tokens, the second which is {@code interface}, so we don't need a separate entry for that.
*/
private static final ImmutableSet<String> IMPORT_OR_CLASS_START =
ImmutableSet.of("import", "class", "interface", "enum");
/**
* A {@link Comparator} that orders {@link Import}s by Google Style, defined at
* https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing.
*/
private static final Comparator<Import> GOOGLE_IMPORT_COMPARATOR =
Comparator.comparing(Import::isStatic, trueFirst()).thenComparing(Import::imported);
/**
* A {@link Comparator} that orders {@link Import}s by AOSP Style, defined at
* https://source.android.com/setup/contribute/code-style#order-import-statements and implemented
* in IntelliJ at
* https://android.googlesource.com/platform/development/+/master/ide/intellij/codestyles/AndroidStyle.xml.
*/
private static final Comparator<Import> AOSP_IMPORT_COMPARATOR =
Comparator.comparing(Import::isStatic, trueFirst())
.thenComparing(Import::isAndroid, trueFirst())
.thenComparing(Import::isThirdParty, trueFirst())
.thenComparing(Import::isJava, trueFirst())
.thenComparing(Import::imported);
private final String text;
private final ImmutableList<Tok> toks;
private final String lineSeparator;
private final Comparator<Import> importComparator;
private final BiFunction<Import, Import, Boolean> shouldInsertBlankLineFn;
private ImportOrderer(String text, ImmutableList<Tok> toks, Style style) {
this.text = text;
this.toks = toks;
this.lineSeparator = Newlines.guessLineSeparator(text);
if (style.equals(Style.GOOGLE)) {
this.importComparator = GOOGLE_IMPORT_COMPARATOR;
this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineGoogle;
} else if (style.equals(Style.AOSP)) {
this.importComparator = AOSP_IMPORT_COMPARATOR;
this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineAosp;
} else {
throw new IllegalArgumentException("Unsupported code style: " + style);
}
}
/**
* Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link
* Import}s based on Google style.
*/
private static boolean shouldInsertBlankLineGoogle(Import prev, Import curr) {
return prev.isStatic() && !curr.isStatic();
}
/**
* Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link
* Import}s based on AOSP style.
*/
private static boolean shouldInsertBlankLineAosp(Import prev, Import curr) {
if (prev.isStatic() && !curr.isStatic()) {
return true;
}
// insert blank line between "com.android" from "com.anythingelse"
if (prev.isAndroid() && !curr.isAndroid()) {
return true;
}
return !prev.topLevel().equals(curr.topLevel());
}
/**
* Reorder the inputs in {@code text}, a complete Java program. On success, another complete Java
@@ -180,6 +104,163 @@ public class ImportOrderer {
return result.toString();
}
/**
* {@link TokenKind}s that indicate the start of a type definition. We use this to avoid scanning
* the whole file, since we know that imports must precede any type definition.
*/
private static final ImmutableSet<TokenKind> CLASS_START =
ImmutableSet.of(TokenKind.CLASS, TokenKind.INTERFACE, TokenKind.ENUM);
/**
* We use this set to find the first import, and again to check that there are no imports after
* the place we stopped gathering them. An annotation definition ({@code @interface}) is two
* tokens, the second which is {@code interface}, so we don't need a separate entry for that.
*/
private static final ImmutableSet<String> IMPORT_OR_CLASS_START =
ImmutableSet.of("import", "class", "interface", "enum");
/**
* A {@link Comparator} that orders {@link Import}s by Google Style, defined at
* https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing.
*/
private static final Comparator<Import> GOOGLE_IMPORT_COMPARATOR =
Comparator.comparing(Import::isStatic, trueFirst()).thenComparing(Import::imported);
/**
* A {@link Comparator} that orders {@link Import}s by AOSP Style, defined at
* https://source.android.com/setup/contribute/code-style#order-import-statements and implemented
* in IntelliJ at
* https://android.googlesource.com/platform/development/+/master/ide/intellij/codestyles/AndroidStyle.xml.
*/
private static final Comparator<Import> AOSP_IMPORT_COMPARATOR =
Comparator.comparing(Import::isStatic, trueFirst())
.thenComparing(Import::isAndroid, trueFirst())
.thenComparing(Import::isThirdParty, trueFirst())
.thenComparing(Import::isJava, trueFirst())
.thenComparing(Import::imported);
/**
* Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link
* Import}s based on Google style.
*/
private static boolean shouldInsertBlankLineGoogle(Import prev, Import curr) {
return prev.isStatic() && !curr.isStatic();
}
/**
* Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link
* Import}s based on AOSP style.
*/
private static boolean shouldInsertBlankLineAosp(Import prev, Import curr) {
if (prev.isStatic() && !curr.isStatic()) {
return true;
}
// insert blank line between "com.android" from "com.anythingelse"
if (prev.isAndroid() && !curr.isAndroid()) {
return true;
}
return !prev.topLevel().equals(curr.topLevel());
}
private final String text;
private final ImmutableList<Tok> toks;
private final String lineSeparator;
private final Comparator<Import> importComparator;
private final BiFunction<Import, Import, Boolean> shouldInsertBlankLineFn;
private ImportOrderer(String text, ImmutableList<Tok> toks, Style style) {
this.text = text;
this.toks = toks;
this.lineSeparator = Newlines.guessLineSeparator(text);
if (style.equals(Style.GOOGLE)) {
this.importComparator = GOOGLE_IMPORT_COMPARATOR;
this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineGoogle;
} else if (style.equals(Style.AOSP)) {
this.importComparator = AOSP_IMPORT_COMPARATOR;
this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineAosp;
} else {
throw new IllegalArgumentException("Unsupported code style: " + style);
}
}
/** An import statement. */
class Import {
private final String imported;
private final boolean isStatic;
private final String trailing;
Import(String imported, String trailing, boolean isStatic) {
this.imported = imported;
this.trailing = trailing;
this.isStatic = isStatic;
}
/** The name being imported, for example {@code java.util.List}. */
String imported() {
return imported;
}
/** True if this is {@code import static}. */
boolean isStatic() {
return isStatic;
}
/** The top-level package of the import. */
String topLevel() {
return DOT_SPLITTER.split(imported()).iterator().next();
}
/** True if this is an Android import per AOSP style. */
boolean isAndroid() {
return Stream.of("android.", "androidx.", "dalvik.", "libcore.", "com.android.")
.anyMatch(imported::startsWith);
}
/** True if this is a Java import per AOSP style. */
boolean isJava() {
switch (topLevel()) {
case "java":
case "javax":
return true;
default:
return false;
}
}
/**
* The {@code //} comment lines after the final {@code ;}, up to and including the line
* terminator of the last one. Note: In case two imports were separated by a space (which is
* disallowed by the style guide), the trailing whitespace of the first import does not include
* a line terminator.
*/
String trailing() {
return trailing;
}
/** True if this is a third-party import per AOSP style. */
public boolean isThirdParty() {
return !(isAndroid() || isJava());
}
// One or multiple lines, the import itself and following comments, including the line
// terminator.
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("import ");
if (isStatic()) {
sb.append("static ");
}
sb.append(imported()).append(';');
if (trailing().trim().isEmpty()) {
sb.append(lineSeparator);
} else {
sb.append(trailing());
}
return sb.toString();
}
}
private String tokString(int start, int end) {
StringBuilder sb = new StringBuilder();
for (int i = start; i < end; i++) {
@@ -188,6 +269,16 @@ public class ImportOrderer {
return sb.toString();
}
private static class ImportsAndIndex {
final ImmutableSortedSet<Import> imports;
final int index;
ImportsAndIndex(ImmutableSortedSet<Import> imports, int index) {
this.imports = imports;
this.index = index;
}
}
/**
* Scans a sequence of import lines. The parsing uses this approximate grammar:
*
@@ -245,8 +336,7 @@ public class ImportOrderer {
trailing.append(tokenAt(i));
i++;
}
// Gather (if any) all single line comments and accompanied line terminators following
// this
// Gather (if any) all single line comments and accompanied line terminators following this
// import
while (isSlashSlashCommentToken(i)) {
trailing.append(tokenAt(i));
@@ -276,8 +366,7 @@ public class ImportOrderer {
private String reorderedImportsString(ImmutableSortedSet<Import> imports) {
Preconditions.checkArgument(!imports.isEmpty(), "imports");
// Pretend that the first import was preceded by another import of the same kind, so we
// don't
// Pretend that the first import was preceded by another import of the same kind, so we don't
// insert a newline there.
Import prevImport = imports.iterator().next();
@@ -293,6 +382,16 @@ public class ImportOrderer {
return sb.toString();
}
private static class StringAndIndex {
private final String string;
private final int index;
StringAndIndex(String string, int index) {
this.string = string;
this.index = index;
}
}
/**
* Scans the imported thing, the dot-separated name that comes after import [static] and before
* the semicolon. We don't allow spaces inside the dot-separated name. Wildcard imports are
@@ -381,102 +480,4 @@ public class ImportOrderer {
private boolean isNewlineToken(int i) {
return toks.get(i).isNewline();
}
private static class ImportsAndIndex {
final ImmutableSortedSet<Import> imports;
final int index;
ImportsAndIndex(ImmutableSortedSet<Import> imports, int index) {
this.imports = imports;
this.index = index;
}
}
private static class StringAndIndex {
private final String string;
private final int index;
StringAndIndex(String string, int index) {
this.string = string;
this.index = index;
}
}
/** An import statement. */
class Import {
private final String imported;
private final boolean isStatic;
private final String trailing;
Import(String imported, String trailing, boolean isStatic) {
this.imported = imported;
this.trailing = trailing;
this.isStatic = isStatic;
}
/** True if this is a third-party import per AOSP style. */
public boolean isThirdParty() {
return !(isAndroid() || isJava());
}
/** True if this is an Android import per AOSP style. */
boolean isAndroid() {
return Stream.of("android.", "androidx.", "dalvik.", "libcore.", "com.android.")
.anyMatch(imported::startsWith);
}
/** True if this is a Java import per AOSP style. */
boolean isJava() {
switch (topLevel()) {
case "java":
case "javax":
return true;
default:
return false;
}
}
/** The top-level package of the import. */
String topLevel() {
return DOT_SPLITTER.split(imported()).iterator().next();
}
/** The name being imported, for example {@code java.util.List}. */
String imported() {
return imported;
}
// One or multiple lines, the import itself and following comments, including the line
// terminator.
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("import ");
if (isStatic()) {
sb.append("static ");
}
sb.append(imported()).append(';');
if (trailing().trim().isEmpty()) {
sb.append(lineSeparator);
} else {
sb.append(trailing());
}
return sb.toString();
}
/** True if this is {@code import static}. */
boolean isStatic() {
return isStatic;
}
/**
* The {@code //} comment lines after the final {@code ;}, up to and including the line
* terminator of the last one. Note: In case two imports were separated by a space (which is
* disallowed by the style guide), the trailing whitespace of the first import does not include
* a line terminator.
*/
String trailing() {
return trailing;
}
}
}
@@ -20,7 +20,6 @@ import com.google.googlejavaformat.CommentsHelper;
import com.google.googlejavaformat.Input.Tok;
import com.google.googlejavaformat.Newlines;
import com.google.googlejavaformat.java.javadoc.JavadocFormatter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@@ -30,10 +29,6 @@ import java.util.regex.Pattern;
/** {@code JavaCommentsHelper} extends {@link CommentsHelper} to rewrite Java comments. */
public final class JavaCommentsHelper implements CommentsHelper {
// Preserve special `//noinspection` and `//$NON-NLS-x$` comments used by IDEs, which cannot
// contain leading spaces.
private static final Pattern LINE_COMMENT_MISSING_SPACE_PREFIX =
Pattern.compile("^(//+)(?!noinspection|\\$NON-NLS-\\d+\\$)[^\\s/]");
private final String lineSeparator;
private final JavaFormatterOptions options;
@@ -58,11 +53,13 @@ public final class JavaCommentsHelper implements CommentsHelper {
}
if (tok.isSlashSlashComment()) {
return indentLineComments(lines, column0);
} else if (javadocShaped(lines)) {
return indentJavadoc(lines, column0);
} else {
return preserveIndentation(lines, column0);
}
return CommentsHelper.reformatParameterComment(tok)
.orElseGet(
() ->
javadocShaped(lines)
? indentJavadoc(lines, column0)
: preserveIndentation(lines, column0));
}
// For non-javadoc-shaped block comments, shift the entire block to the correct
@@ -107,6 +104,11 @@ public final class JavaCommentsHelper implements CommentsHelper {
return builder.toString();
}
// Preserve special `//noinspection` and `//$NON-NLS-x$` comments used by IDEs, which cannot
// contain leading spaces.
private static final Pattern LINE_COMMENT_MISSING_SPACE_PREFIX =
Pattern.compile("^(//+)(?!noinspection|\\$NON-NLS-\\d+\\$)[^\\s/]");
private List<String> wrapLineComments(List<String> lines, int column0) {
List<String> result = new ArrayList<>();
for (String line : lines) {
@@ -179,3 +181,4 @@ public final class JavaCommentsHelper implements CommentsHelper {
return true;
}
}
@@ -14,6 +14,7 @@
package com.google.googlejavaformat.java;
import com.google.auto.value.AutoValue;
import com.google.errorprone.annotations.Immutable;
/**
@@ -27,39 +28,8 @@ import com.google.errorprone.annotations.Immutable;
* preferences, and in fact it would work directly against our primary goals.
*/
@Immutable
public class JavaFormatterOptions {
private final Style style;
private final boolean formatJavadoc;
private JavaFormatterOptions(Style style, boolean formatJavadoc) {
this.style = style;
this.formatJavadoc = formatJavadoc;
}
/** Returns the default formatting options. */
public static JavaFormatterOptions defaultOptions() {
return builder().build();
}
/** Returns a builder for {@link JavaFormatterOptions}. */
public static Builder builder() {
return new Builder();
}
/** Returns the multiplier for the unit of indent. */
public int indentationMultiplier() {
return style.indentationMultiplier();
}
public boolean formatJavadoc() {
return formatJavadoc;
}
/** Returns the code style. */
public Style style() {
return style;
}
@AutoValue
public abstract class JavaFormatterOptions {
public enum Style {
/** The default Google Java Style configuration. */
@@ -79,25 +49,41 @@ public class JavaFormatterOptions {
}
}
/** Returns the multiplier for the unit of indent. */
public int indentationMultiplier() {
return style().indentationMultiplier();
}
public abstract boolean formatJavadoc();
public abstract boolean reorderModifiers();
/** Returns the code style. */
public abstract Style style();
/** Returns the default formatting options. */
public static JavaFormatterOptions defaultOptions() {
return builder().build();
}
/** Returns a builder for {@link JavaFormatterOptions}. */
public static Builder builder() {
return new AutoValue_JavaFormatterOptions.Builder()
.style(Style.GOOGLE)
.formatJavadoc(true)
.reorderModifiers(true);
}
/** A builder for {@link JavaFormatterOptions}. */
public static class Builder {
private Style style = Style.GOOGLE;
private boolean formatJavadoc = true;
@AutoValue.Builder
public abstract static class Builder {
private Builder() {}
public abstract Builder style(Style style);
public Builder style(Style style) {
this.style = style;
return this;
}
public abstract Builder formatJavadoc(boolean formatJavadoc);
public Builder formatJavadoc(boolean formatJavadoc) {
this.formatJavadoc = formatJavadoc;
return this;
}
public abstract Builder reorderModifiers(boolean reorderModifiers);
public JavaFormatterOptions build() {
return new JavaFormatterOptions(style, formatJavadoc);
}
public abstract JavaFormatterOptions build();
}
}
@@ -33,452 +33,29 @@ import com.google.common.collect.TreeRangeSet;
import com.google.googlejavaformat.Input;
import com.google.googlejavaformat.Newlines;
import com.google.googlejavaformat.java.JavacTokens.RawTok;
import openjdk.tools.javac.file.JavacFileManager;
import openjdk.tools.javac.parser.Tokens.TokenKind;
import openjdk.tools.javac.tree.JCTree.JCCompilationUnit;
import openjdk.tools.javac.util.Context;
import openjdk.tools.javac.util.Log;
import openjdk.tools.javac.util.Log.DeferredDiagnosticHandler;
import openjdk.tools.javac.util.Options;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.parser.Tokens.TokenKind;
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.Log.DeferredDiagnosticHandler;
import com.sun.tools.javac.util.Options;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import jdkx.tools.Diagnostic;
import jdkx.tools.DiagnosticCollector;
import jdkx.tools.DiagnosticListener;
import jdkx.tools.JavaFileObject;
import jdkx.tools.JavaFileObject.Kind;
import jdkx.tools.SimpleJavaFileObject;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.DiagnosticListener;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.SimpleJavaFileObject;
/** {@code JavaInput} extends {@link Input} to represent a Java input document. */
public final class JavaInput extends Input {
private final String text; // The input.
private final ImmutableMap<Integer, Integer> positionToColumnMap; // Map Tok position to column.
private final ImmutableList<Token> tokens; // The Tokens for this input.
private final ImmutableRangeMap<Integer, Token> positionTokenMap; // Map position to Token.
/*
* The following lists record the sequential indices of the {@code Tok}s on each input line. (Only
* tokens and comments have sequential indices.) Tokens and {@code //} comments lie on just one
* line; {@code /*} comments can lie on multiple lines. These data structures (along with
* equivalent ones for the formatted output) let us compute correspondences between the input and
* output.
*/
/** Map from Tok index to the associated Token. */
private final Token[] kToToken;
private int kN; // The number of numbered toks (tokens or comments), excluding the EOF.
private JCCompilationUnit unit;
/**
* Input constructor.
*
* @param text the input text
* @throws FormatterException if the input cannot be parsed
*/
public JavaInput(String text) throws FormatterException {
this.text = checkNotNull(text);
setLines(ImmutableList.copyOf(Newlines.lineIterator(text)));
ImmutableList<Tok> toks = buildToks(text);
positionToColumnMap = makePositionToColumnMap(toks);
tokens = buildTokens(toks);
ImmutableRangeMap.Builder<Integer, Token> tokenLocations = ImmutableRangeMap.builder();
for (Token token : tokens) {
Input.Tok end = JavaOutput.endTok(token);
int upper = end.getPosition();
if (!end.getText().isEmpty()) {
upper += end.length() - 1;
}
tokenLocations.put(Range.closed(JavaOutput.startTok(token).getPosition(), upper), token);
}
positionTokenMap = tokenLocations.build();
// adjust kN for EOF
kToToken = new Token[kN + 1];
for (Token token : tokens) {
for (Input.Tok tok : token.getToksBefore()) {
if (tok.getIndex() < 0) {
continue;
}
kToToken[tok.getIndex()] = token;
}
kToToken[token.getTok().getIndex()] = token;
for (Input.Tok tok : token.getToksAfter()) {
if (tok.getIndex() < 0) {
continue;
}
kToToken[tok.getIndex()] = token;
}
}
}
private static ImmutableMap<Integer, Integer> makePositionToColumnMap(List<Tok> toks) {
ImmutableMap.Builder<Integer, Integer> builder = ImmutableMap.builder();
for (Tok tok : toks) {
builder.put(tok.getPosition(), tok.getColumn());
}
return builder.buildOrThrow();
}
/** Lex the input and build the list of toks. */
private ImmutableList<Tok> buildToks(String text) throws FormatterException {
ImmutableList<Tok> toks = buildToks(text, ImmutableSet.of());
kN = getLast(toks).getIndex();
computeRanges(toks);
return toks;
}
/**
* Lex the input and build the list of toks.
*
* @param text the text to be lexed.
* @param stopTokens a set of tokens which should cause lexing to stop. If one of these is found,
* the returned list will include tokens up to but not including that token.
*/
static ImmutableList<Tok> buildToks(String text, ImmutableSet<TokenKind> stopTokens)
throws FormatterException {
stopTokens = ImmutableSet.<TokenKind>builder().addAll(stopTokens).add(TokenKind.EOF).build();
Context context = new Context();
Options.instance(context).put("--enable-preview", "true");
new JavacFileManager(context, true, UTF_8);
DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
context.put(DiagnosticListener.class, diagnosticCollector);
Log log = Log.instance(context);
log.useSource(
new SimpleJavaFileObject(URI.create("Source.java"), Kind.SOURCE) {
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return text;
}
});
DeferredDiagnosticHandler diagnostics = new DeferredDiagnosticHandler(log);
ImmutableList<RawTok> rawToks = JavacTokens.getTokens(text, context, stopTokens);
if (diagnostics.getDiagnostics().stream().anyMatch(d -> d.getKind() == Diagnostic.Kind.ERROR)) {
return ImmutableList.of(new Tok(0, "", "", 0, 0, true, null)); // EOF
}
int kN = 0;
List<Tok> toks = new ArrayList<>();
int charI = 0;
int columnI = 0;
for (RawTok t : rawToks) {
if (stopTokens.contains(t.kind())) {
break;
}
int charI0 = t.pos();
// Get string, possibly with Unicode escapes.
String originalTokText = text.substring(charI0, t.endPos());
String tokText =
t.kind() == TokenKind.STRINGLITERAL
? t.stringVal() // Unicode escapes removed.
: originalTokText;
char tokText0 = tokText.charAt(0); // The token's first character.
final boolean isToken; // Is this tok a token?
final boolean isNumbered; // Is this tok numbered? (tokens and comments)
String extraNewline = null; // Extra newline at end?
List<String> strings = new ArrayList<>();
if (Character.isWhitespace(tokText0)) {
isToken = false;
isNumbered = false;
Iterator<String> it = Newlines.lineIterator(originalTokText);
while (it.hasNext()) {
String line = it.next();
String newline = Newlines.getLineEnding(line);
if (newline != null) {
String spaces = line.substring(0, line.length() - newline.length());
if (!spaces.isEmpty()) {
strings.add(spaces);
}
strings.add(newline);
} else if (!line.isEmpty()) {
strings.add(line);
}
}
} else if (tokText.startsWith("'") || tokText.startsWith("\"")) {
isToken = true;
isNumbered = true;
strings.add(originalTokText);
} else if (tokText.startsWith("//") || tokText.startsWith("/*")) {
// For compatibility with an earlier lexer, the newline after a // comment is its
// own tok.
if (tokText.startsWith("//")
&& (originalTokText.endsWith("\n") || originalTokText.endsWith("\r"))) {
extraNewline = Newlines.getLineEnding(originalTokText);
tokText = tokText.substring(0, tokText.length() - extraNewline.length());
originalTokText =
originalTokText.substring(0, originalTokText.length() - extraNewline.length());
}
isToken = false;
isNumbered = true;
strings.add(originalTokText);
} else if (Character.isJavaIdentifierStart(tokText0)
|| Character.isDigit(tokText0)
|| (tokText0 == '.' && tokText.length() > 1 && Character.isDigit(tokText.charAt(1)))) {
// Identifier, keyword, or numeric literal (a dot may begin a number, as in .2D).
isToken = true;
isNumbered = true;
strings.add(tokText);
} else {
// Other tokens ("+" or "++" or ">>" are broken into one-character toks, because
// ">>"
// cannot be lexed without syntactic knowledge. This implementation fails if the
// token
// contains Unicode escapes.
isToken = true;
isNumbered = true;
for (char c : tokText.toCharArray()) {
strings.add(String.valueOf(c));
}
}
if (strings.size() == 1) {
toks.add(
new Tok(
isNumbered ? kN++ : -1,
originalTokText,
tokText,
charI,
columnI,
isToken,
t.kind()));
charI += originalTokText.length();
columnI = updateColumn(columnI, originalTokText);
} else {
if (strings.size() != 1 && !tokText.equals(originalTokText)) {
throw new FormatterException(
"Unicode escapes not allowed in whitespace or multi-character" + " operators");
}
for (String str : strings) {
toks.add(new Tok(isNumbered ? kN++ : -1, str, str, charI, columnI, isToken, null));
charI += str.length();
columnI = updateColumn(columnI, originalTokText);
}
}
if (extraNewline != null) {
toks.add(new Tok(-1, extraNewline, extraNewline, charI, columnI, false, null));
columnI = 0;
charI += extraNewline.length();
}
}
toks.add(new Tok(kN, "", "", charI, columnI, true, null)); // EOF tok.
return ImmutableList.copyOf(toks);
}
private static int updateColumn(int columnI, String originalTokText) {
Integer last = Iterators.getLast(Newlines.lineOffsetIterator(originalTokText));
if (last > 0) {
columnI = originalTokText.length() - last;
} else {
columnI += originalTokText.length();
}
return columnI;
}
private static ImmutableList<Token> buildTokens(List<Tok> toks) {
ImmutableList.Builder<Token> tokens = ImmutableList.builder();
int k = 0;
int kN = toks.size();
// Remaining non-tokens before the token go here.
ImmutableList.Builder<Tok> toksBefore = ImmutableList.builder();
OUTERMOST:
while (k < kN) {
while (!toks.get(k).isToken()) {
Tok tok = toks.get(k++);
toksBefore.add(tok);
if (isParamComment(tok)) {
while (toks.get(k).isNewline()) {
// drop newlines after parameter comments
k++;
}
}
}
Tok tok = toks.get(k++);
// Non-tokens starting on the same line go here too.
ImmutableList.Builder<Tok> toksAfter = ImmutableList.builder();
OUTER:
while (k < kN && !toks.get(k).isToken()) {
// Don't attach inline comments to certain leading tokens, e.g. for
// `f(/*flag1=*/true).
//
// Attaching inline comments to the right token is hard, and this barely
// scratches the surface. But it's enough to do a better job with parameter
// name comments.
//
// TODO(cushon): find a better strategy.
if (toks.get(k).isSlashStarComment()) {
switch (tok.getText()) {
case "(":
case "<":
case ".":
break OUTER;
default:
break;
}
}
if (toks.get(k).isJavadocComment()) {
switch (tok.getText()) {
case ";":
break OUTER;
default:
break;
}
}
if (isParamComment(toks.get(k))) {
tokens.add(new Token(toksBefore.build(), tok, toksAfter.build()));
toksBefore = ImmutableList.<Tok>builder().add(toks.get(k++));
// drop newlines after parameter comments
while (toks.get(k).isNewline()) {
k++;
}
continue OUTERMOST;
}
Tok nonTokenAfter = toks.get(k++);
toksAfter.add(nonTokenAfter);
if (Newlines.containsBreaks(nonTokenAfter.getText())) {
break;
}
}
tokens.add(new Token(toksBefore.build(), tok, toksAfter.build()));
toksBefore = ImmutableList.builder();
}
return tokens.build();
}
private static boolean isParamComment(Tok tok) {
return tok.isSlashStarComment()
&& tok.getText().matches("\\/\\*[A-Za-z0-9\\s_\\-]+=\\s*\\*\\/");
}
/**
* Get the input tokens.
*
* @return the input tokens
*/
@Override
public ImmutableList<? extends Input.Token> getTokens() {
return tokens;
}
/**
* Get the navigable map from position to {@link Token}. Used to look for tokens following a given
* one, and to implement the --offset and --length flags to reformat a character range in the
* input file.
*
* @return the navigable map from position to {@link Token}
*/
@Override
public ImmutableRangeMap<Integer, Token> getPositionTokenMap() {
return positionTokenMap;
}
@Override
public ImmutableMap<Integer, Integer> getPositionToColumnMap() {
return positionToColumnMap;
}
/**
* Get the input text.
*
* @return the input text
*/
@Override
public String getText() {
return text;
}
/**
* Get the number of toks.
*
* @return the number of toks, excluding the EOF tok
*/
@Override
public int getkN() {
return kN;
}
/**
* Get the Token by index.
*
* @param k the Tok index
*/
@Override
public Token getToken(int k) {
return kToToken[k];
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("tokens", tokens)
.add("super", super.toString())
.toString();
}
@Override
public int getLineNumber(int inputPosition) {
Verify.verifyNotNull(unit, "Expected compilation unit to be set.");
return unit.getLineMap().getLineNumber(inputPosition);
}
@Override
public int getColumnNumber(int inputPosition) {
Verify.verifyNotNull(unit, "Expected compilation unit to be set.");
return unit.getLineMap().getColumnNumber(inputPosition);
}
// TODO(cushon): refactor JavaInput so the CompilationUnit can be passed into
// the constructor.
public void setCompilationUnit(JCCompilationUnit unit) {
this.unit = unit;
}
public RangeSet<Integer> characterRangesToTokenRanges(Collection<Range<Integer>> characterRanges)
throws FormatterException {
RangeSet<Integer> tokenRangeSet = TreeRangeSet.create();
for (Range<Integer> characterRange : characterRanges) {
tokenRangeSet.add(
characterRangeToTokenRange(characterRange.canonical(DiscreteDomain.integers())));
}
return tokenRangeSet;
}
/**
* Convert from a character range to a token range.
*
* @param characterRange the {@code 0}-based {@link Range} of characters
* @return the {@code 0}-based {@link Range} of tokens
* @throws FormatterException if the upper endpoint of the range is outside the file
*/
Range<Integer> characterRangeToTokenRange(Range<Integer> characterRange)
throws FormatterException {
if (characterRange.upperEndpoint() > text.length()) {
throw new FormatterException(
String.format(
"error: invalid length %d, offset + length (%d) is outside the file",
characterRange.upperEndpoint() - characterRange.lowerEndpoint(),
characterRange.upperEndpoint()));
}
// empty range stands for "format the line under the cursor"
Range<Integer> nonEmptyRange =
characterRange.isEmpty()
? Range.closedOpen(characterRange.lowerEndpoint(), characterRange.lowerEndpoint() + 1)
: characterRange;
ImmutableCollection<Token> enclosed =
getPositionTokenMap().subRangeMap(nonEmptyRange).asMapOfRanges().values();
if (enclosed.isEmpty()) {
return EMPTY_RANGE;
}
return Range.closedOpen(
enclosed.iterator().next().getTok().getIndex(), getLast(enclosed).getTok().getIndex() + 1);
}
/**
* A {@code JavaInput} is a sequence of {@link Tok}s that cover the Java input. A {@link Tok} is
* either a token (if {@code isToken()}), or a non-token, which is a comment (if {@code
@@ -533,16 +110,6 @@ public final class JavaInput extends Input {
return index;
}
@Override
public int getPosition() {
return position;
}
@Override
public int getColumn() {
return columnI;
}
@Override
public String getText() {
return text;
@@ -558,6 +125,20 @@ public final class JavaInput extends Input {
return originalText.length();
}
@Override
public int getPosition() {
return position;
}
@Override
public int getColumn() {
return columnI;
}
boolean isToken() {
return isToken;
}
@Override
public boolean isNewline() {
return Newlines.isNewline(text);
@@ -575,8 +156,7 @@ public final class JavaInput extends Input {
@Override
public boolean isJavadocComment() {
// comments like `/***` are also javadoc, but their formatting probably won't be
// improved
// comments like `/***` are also javadoc, but their formatting probably won't be improved
// by the javadoc formatter
return text.startsWith("/**") && text.charAt("/**".length()) != '*' && text.length() > 4;
}
@@ -600,10 +180,6 @@ public final class JavaInput extends Input {
public TokenKind kind() {
return kind;
}
boolean isToken() {
return isToken;
}
}
/**
@@ -670,4 +246,425 @@ public final class JavaInput extends Input {
.toString();
}
}
private final String text; // The input.
private int kN; // The number of numbered toks (tokens or comments), excluding the EOF.
/*
* The following lists record the sequential indices of the {@code Tok}s on each input line. (Only
* tokens and comments have sequential indices.) Tokens and {@code //} comments lie on just one
* line; {@code /*} comments can lie on multiple lines. These data structures (along with
* equivalent ones for the formatted output) let us compute correspondences between the input and
* output.
*/
private final ImmutableMap<Integer, Integer> positionToColumnMap; // Map Tok position to column.
private final ImmutableList<Token> tokens; // The Tokens for this input.
private final ImmutableRangeMap<Integer, Token> positionTokenMap; // Map position to Token.
/** Map from Tok index to the associated Token. */
private final Token[] kToToken;
/**
* Input constructor.
*
* @param text the input text
* @throws FormatterException if the input cannot be parsed
*/
public JavaInput(String text) throws FormatterException {
this.text = checkNotNull(text);
setLines(ImmutableList.copyOf(Newlines.lineIterator(text)));
ImmutableList<Tok> toks = buildToks(text);
positionToColumnMap = makePositionToColumnMap(toks);
tokens = buildTokens(toks);
ImmutableRangeMap.Builder<Integer, Token> tokenLocations = ImmutableRangeMap.builder();
for (Token token : tokens) {
Input.Tok end = JavaOutput.endTok(token);
int upper = end.getPosition();
if (!end.getText().isEmpty()) {
upper += end.length() - 1;
}
tokenLocations.put(Range.closed(JavaOutput.startTok(token).getPosition(), upper), token);
}
positionTokenMap = tokenLocations.build();
// adjust kN for EOF
kToToken = new Token[kN + 1];
for (Token token : tokens) {
for (Input.Tok tok : token.getToksBefore()) {
if (tok.getIndex() < 0) {
continue;
}
kToToken[tok.getIndex()] = token;
}
kToToken[token.getTok().getIndex()] = token;
for (Input.Tok tok : token.getToksAfter()) {
if (tok.getIndex() < 0) {
continue;
}
kToToken[tok.getIndex()] = token;
}
}
}
private static ImmutableMap<Integer, Integer> makePositionToColumnMap(List<Tok> toks) {
ImmutableMap.Builder<Integer, Integer> builder = ImmutableMap.builder();
for (Tok tok : toks) {
builder.put(tok.getPosition(), tok.getColumn());
}
return builder.buildOrThrow();
}
/**
* Get the input text.
*
* @return the input text
*/
@Override
public String getText() {
return text;
}
@Override
public ImmutableMap<Integer, Integer> getPositionToColumnMap() {
return positionToColumnMap;
}
/** Lex the input and build the list of toks. */
private ImmutableList<Tok> buildToks(String text) throws FormatterException {
ImmutableList<Tok> toks = buildToks(text, ImmutableSet.of());
kN = getLast(toks).getIndex();
computeRanges(toks);
return toks;
}
/**
* Lex the input and build the list of toks.
*
* @param text the text to be lexed.
* @param stopTokens a set of tokens which should cause lexing to stop. If one of these is found,
* the returned list will include tokens up to but not including that token.
*/
static ImmutableList<Tok> buildToks(String text, ImmutableSet<TokenKind> stopTokens)
throws FormatterException {
stopTokens = ImmutableSet.<TokenKind>builder().addAll(stopTokens).add(TokenKind.EOF).build();
Context context = new Context();
Options.instance(context).put("--enable-preview", "true");
JavaFileManager fileManager = new JavacFileManager(context, false, UTF_8);
context.put(JavaFileManager.class, fileManager);
DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
context.put(DiagnosticListener.class, diagnosticCollector);
Log log = Log.instance(context);
log.useSource(
new SimpleJavaFileObject(URI.create("Source.java"), Kind.SOURCE) {
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return text;
}
});
DeferredDiagnosticHandler diagnostics = new DeferredDiagnosticHandler(log);
ImmutableList<RawTok> rawToks = JavacTokens.getTokens(text, context, stopTokens);
if (diagnostics.getDiagnostics().stream().anyMatch(d -> d.getKind() == Diagnostic.Kind.ERROR)) {
return ImmutableList.of(new Tok(0, "", "", 0, 0, true, null)); // EOF
}
int kN = 0;
List<Tok> toks = new ArrayList<>();
int charI = 0;
int columnI = 0;
for (RawTok t : rawToks) {
if (stopTokens.contains(t.kind())) {
break;
}
int charI0 = t.pos();
// Get string, possibly with Unicode escapes.
String originalTokText = text.substring(charI0, t.endPos());
String tokText =
t.kind() == TokenKind.STRINGLITERAL
? t.stringVal() // Unicode escapes removed.
: originalTokText;
char tokText0 = tokText.charAt(0); // The token's first character.
final boolean isToken; // Is this tok a token?
final boolean isNumbered; // Is this tok numbered? (tokens and comments)
String extraNewline = null; // Extra newline at end?
List<String> strings = new ArrayList<>();
if (Character.isWhitespace(tokText0)) {
isToken = false;
isNumbered = false;
Iterator<String> it = Newlines.lineIterator(originalTokText);
while (it.hasNext()) {
String line = it.next();
String newline = Newlines.getLineEnding(line);
if (newline != null) {
String spaces = line.substring(0, line.length() - newline.length());
if (!spaces.isEmpty()) {
strings.add(spaces);
}
strings.add(newline);
} else if (!line.isEmpty()) {
strings.add(line);
}
}
} else if (tokText.startsWith("'") || tokText.startsWith("\"")) {
isToken = true;
isNumbered = true;
strings.add(originalTokText);
} else if (tokText.startsWith("//") || tokText.startsWith("/*")) {
// For compatibility with an earlier lexer, the newline after a // comment is its own tok.
if (tokText.startsWith("//")
&& (originalTokText.endsWith("\n") || originalTokText.endsWith("\r"))) {
extraNewline = Newlines.getLineEnding(originalTokText);
tokText = tokText.substring(0, tokText.length() - extraNewline.length());
originalTokText =
originalTokText.substring(0, originalTokText.length() - extraNewline.length());
}
isToken = false;
isNumbered = true;
strings.add(originalTokText);
} else if (Character.isJavaIdentifierStart(tokText0)
|| Character.isDigit(tokText0)
|| (tokText0 == '.' && tokText.length() > 1 && Character.isDigit(tokText.charAt(1)))) {
// Identifier, keyword, or numeric literal (a dot may begin a number, as in .2D).
isToken = true;
isNumbered = true;
strings.add(tokText);
} else {
// Other tokens ("+" or "++" or ">>" are broken into one-character toks, because ">>"
// cannot be lexed without syntactic knowledge. This implementation fails if the token
// contains Unicode escapes.
isToken = true;
isNumbered = true;
for (char c : tokText.toCharArray()) {
strings.add(String.valueOf(c));
}
}
if (strings.size() == 1) {
toks.add(
new Tok(
isNumbered ? kN++ : -1,
originalTokText,
tokText,
charI,
columnI,
isToken,
t.kind()));
charI += originalTokText.length();
columnI = updateColumn(columnI, originalTokText);
} else {
if (strings.size() != 1 && !tokText.equals(originalTokText)) {
throw new FormatterException(
"Unicode escapes not allowed in whitespace or multi-character operators");
}
for (String str : strings) {
toks.add(new Tok(isNumbered ? kN++ : -1, str, str, charI, columnI, isToken, null));
charI += str.length();
columnI = updateColumn(columnI, originalTokText);
}
}
if (extraNewline != null) {
toks.add(new Tok(-1, extraNewline, extraNewline, charI, columnI, false, null));
columnI = 0;
charI += extraNewline.length();
}
}
toks.add(new Tok(kN, "", "", charI, columnI, true, null)); // EOF tok.
return ImmutableList.copyOf(toks);
}
private static int updateColumn(int columnI, String originalTokText) {
Integer last = Iterators.getLast(Newlines.lineOffsetIterator(originalTokText));
if (last > 0) {
columnI = originalTokText.length() - last;
} else {
columnI += originalTokText.length();
}
return columnI;
}
private static ImmutableList<Token> buildTokens(List<Tok> toks) {
ImmutableList.Builder<Token> tokens = ImmutableList.builder();
int k = 0;
int kN = toks.size();
// Remaining non-tokens before the token go here.
ImmutableList.Builder<Tok> toksBefore = ImmutableList.builder();
OUTERMOST:
while (k < kN) {
while (!toks.get(k).isToken()) {
Tok tok = toks.get(k++);
toksBefore.add(tok);
if (isParamComment(tok)) {
while (toks.get(k).isNewline()) {
// drop newlines after parameter comments
k++;
}
}
}
Tok tok = toks.get(k++);
// Non-tokens starting on the same line go here too.
ImmutableList.Builder<Tok> toksAfter = ImmutableList.builder();
OUTER:
while (k < kN && !toks.get(k).isToken()) {
// Don't attach inline comments to certain leading tokens, e.g. for `f(/*flag1=*/true).
//
// Attaching inline comments to the right token is hard, and this barely
// scratches the surface. But it's enough to do a better job with parameter
// name comments.
//
// TODO(cushon): find a better strategy.
if (toks.get(k).isSlashStarComment()) {
switch (tok.getText()) {
case "(":
case "<":
case ".":
break OUTER;
default:
break;
}
}
if (toks.get(k).isJavadocComment()) {
switch (tok.getText()) {
case ";":
break OUTER;
default:
break;
}
}
if (isParamComment(toks.get(k))) {
tokens.add(new Token(toksBefore.build(), tok, toksAfter.build()));
toksBefore = ImmutableList.<Tok>builder().add(toks.get(k++));
// drop newlines after parameter comments
while (toks.get(k).isNewline()) {
k++;
}
continue OUTERMOST;
}
Tok nonTokenAfter = toks.get(k++);
toksAfter.add(nonTokenAfter);
if (Newlines.containsBreaks(nonTokenAfter.getText())) {
break;
}
}
tokens.add(new Token(toksBefore.build(), tok, toksAfter.build()));
toksBefore = ImmutableList.builder();
}
return tokens.build();
}
private static boolean isParamComment(Tok tok) {
return tok.isSlashStarComment()
&& tok.getText().matches("\\/\\*[A-Za-z0-9\\s_\\-]+=\\s*\\*\\/");
}
/**
* Convert from a character range to a token range.
*
* @param characterRange the {@code 0}-based {@link Range} of characters
* @return the {@code 0}-based {@link Range} of tokens
* @throws FormatterException if the upper endpoint of the range is outside the file
*/
Range<Integer> characterRangeToTokenRange(Range<Integer> characterRange)
throws FormatterException {
if (characterRange.upperEndpoint() > text.length()) {
throw new FormatterException(
String.format(
"error: invalid length %d, offset + length (%d) is outside the file",
characterRange.upperEndpoint() - characterRange.lowerEndpoint(),
characterRange.upperEndpoint()));
}
// empty range stands for "format the line under the cursor"
Range<Integer> nonEmptyRange =
characterRange.isEmpty()
? Range.closedOpen(characterRange.lowerEndpoint(), characterRange.lowerEndpoint() + 1)
: characterRange;
ImmutableCollection<Token> enclosed =
getPositionTokenMap().subRangeMap(nonEmptyRange).asMapOfRanges().values();
if (enclosed.isEmpty()) {
return EMPTY_RANGE;
}
return Range.closedOpen(
enclosed.iterator().next().getTok().getIndex(), getLast(enclosed).getTok().getIndex() + 1);
}
/**
* Get the number of toks.
*
* @return the number of toks, excluding the EOF tok
*/
@Override
public int getkN() {
return kN;
}
/**
* Get the Token by index.
*
* @param k the Tok index
*/
@Override
public Token getToken(int k) {
return kToToken[k];
}
/**
* Get the input tokens.
*
* @return the input tokens
*/
@Override
public ImmutableList<? extends Input.Token> getTokens() {
return tokens;
}
/**
* Get the navigable map from position to {@link Token}. Used to look for tokens following a given
* one, and to implement the --offset and --length flags to reformat a character range in the
* input file.
*
* @return the navigable map from position to {@link Token}
*/
@Override
public ImmutableRangeMap<Integer, Token> getPositionTokenMap() {
return positionTokenMap;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("tokens", tokens)
.add("super", super.toString())
.toString();
}
private JCCompilationUnit unit;
@Override
public int getLineNumber(int inputPosition) {
Verify.verifyNotNull(unit, "Expected compilation unit to be set.");
return unit.getLineMap().getLineNumber(inputPosition);
}
@Override
public int getColumnNumber(int inputPosition) {
Verify.verifyNotNull(unit, "Expected compilation unit to be set.");
return unit.getLineMap().getColumnNumber(inputPosition);
}
// TODO(cushon): refactor JavaInput so the CompilationUnit can be passed into
// the constructor.
public void setCompilationUnit(JCCompilationUnit unit) {
this.unit = unit;
}
public RangeSet<Integer> characterRangesToTokenRanges(Collection<Range<Integer>> characterRanges)
throws FormatterException {
RangeSet<Integer> tokenRangeSet = TreeRangeSet.create();
for (Range<Integer> characterRange : characterRanges) {
tokenRangeSet.add(
characterRangeToTokenRange(characterRange.canonical(DiscreteDomain.integers())));
}
return tokenRangeSet;
}
}
@@ -31,7 +31,6 @@ import com.google.googlejavaformat.Input.Token;
import com.google.googlejavaformat.Newlines;
import com.google.googlejavaformat.OpsBuilder.BlankLineWanted;
import com.google.googlejavaformat.Output;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -75,31 +74,20 @@ public final class JavaOutput extends Output {
kN = javaInput.getkN();
}
public static String applyReplacements(String input, List<Replacement> replacements) {
replacements = new ArrayList<>(replacements);
replacements.sort(comparing((Replacement r) -> r.getReplaceRange().lowerEndpoint()).reversed());
StringBuilder writer = new StringBuilder(input);
for (Replacement replacement : replacements) {
writer.replace(
replacement.getReplaceRange().lowerEndpoint(),
replacement.getReplaceRange().upperEndpoint(),
replacement.getReplacementString());
@Override
public void blankLine(int k, BlankLineWanted wanted) {
if (blankLines.containsKey(k)) {
blankLines.put(k, blankLines.get(k).merge(wanted));
} else {
blankLines.put(k, wanted);
}
return writer.toString();
}
/** The earliest position of any Tok in the Token, including leading whitespace. */
public static int startPosition(Token token) {
int min = token.getTok().getPosition();
for (Input.Tok tok : token.getToksBefore()) {
min = min(min, tok.getPosition());
}
return min;
}
@Override
public void indent(int indent) {
spacesPending.append(Strings.repeat(" ", indent));
public void markForPartialFormat(Token start, Token end) {
int lo = JavaOutput.startTok(start).getIndex();
int hi = JavaOutput.endTok(end).getIndex();
partialFormatRanges.add(Range.closed(lo, hi));
}
// TODO(user): Add invariant.
@@ -123,7 +111,7 @@ public final class JavaOutput extends Output {
* there's a blank line here and it's a comment.
*/
BlankLineWanted wanted = blankLines.getOrDefault(lastK, BlankLineWanted.NO);
if (isComment(text) ? sawNewlines : wanted.wanted().orElse(sawNewlines)) {
if ((sawNewlines && isComment(text)) || wanted.wanted().orElse(sawNewlines)) {
++newlinesPending;
}
}
@@ -190,67 +178,8 @@ public final class JavaOutput extends Output {
}
@Override
public void blankLine(int k, BlankLineWanted wanted) {
if (blankLines.containsKey(k)) {
blankLines.put(k, blankLines.get(k).merge(wanted));
} else {
blankLines.put(k, wanted);
}
}
// The following methods can be used after the Output has been built.
@Override
public void markForPartialFormat(Token start, Token end) {
int lo = JavaOutput.startTok(start).getIndex();
int hi = JavaOutput.endTok(end).getIndex();
partialFormatRanges.add(Range.closed(lo, hi));
}
@Override
public CommentsHelper getCommentsHelper() {
return commentsHelper;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("iLine", iLine)
.add("lastK", lastK)
.add("spacesPending", spacesPending.toString().replace("\t", "\\t"))
.add("newlinesPending", newlinesPending)
.add("blankLines", blankLines)
.add("super", super.toString())
.toString();
}
/** The earliest non-whitespace Tok in the Token. */
public static Input.Tok startTok(Token token) {
for (Input.Tok tok : token.getToksBefore()) {
if (tok.getIndex() >= 0) {
return tok;
}
}
return token.getTok();
}
/** The last non-whitespace Tok in the Token. */
public static Input.Tok endTok(Token token) {
for (int i = token.getToksAfter().size() - 1; i >= 0; i--) {
Input.Tok tok = token.getToksAfter().get(i);
if (tok.getIndex() >= 0) {
return tok;
}
}
return token.getTok();
}
private boolean isComment(String text) {
return text.startsWith("//") || text.startsWith("/*");
}
private static Range<Integer> union(Range<Integer> x, Range<Integer> y) {
return x.isEmpty() ? y : y.isEmpty() ? x : x.span(y).canonical(DiscreteDomain.integers());
public void indent(int indent) {
spacesPending.append(Strings.repeat(" ", indent));
}
/** Flush any incomplete last line, then add the EOF token into our data structures. */
@@ -268,6 +197,13 @@ public final class JavaOutput extends Output {
setLines(ImmutableList.copyOf(mutableLines));
}
// The following methods can be used after the Output has been built.
@Override
public CommentsHelper getCommentsHelper() {
return commentsHelper;
}
/**
* Emit a list of {@link Replacement}s to convert from input to output.
*
@@ -333,10 +269,8 @@ public final class JavaOutput extends Output {
replaceTo = javaInput.getText().length();
}
// Replace trailing whitespace in the input with the whitespace from the formatted file.
// If the trailing whitespace in the input includes one or more line breaks, preserve
// the
// whitespace after the last newline to avoid re-indenting the line following the
// formatted
// If the trailing whitespace in the input includes one or more line breaks, preserve the
// whitespace after the last newline to avoid re-indenting the line following the formatted
// line.
int newline = -1;
while (replaceTo < javaInput.getText().length()) {
@@ -347,8 +281,7 @@ public final class JavaOutput extends Output {
int newlineLength = Newlines.hasNewlineAt(javaInput.getText(), replaceTo);
if (newlineLength != -1) {
newline = replaceTo;
// Skip over the entire newline; don't count the second character of \r\n as a
// newline.
// Skip over the entire newline; don't count the second character of \r\n as a newline.
replaceTo += newlineLength;
} else {
replaceTo++;
@@ -401,4 +334,67 @@ public final class JavaOutput extends Output {
hiTok = partialFormatRanges.rangeContaining(hiTok).upperEndpoint();
return Range.closedOpen(loTok, hiTok + 1);
}
public static String applyReplacements(String input, List<Replacement> replacements) {
replacements = new ArrayList<>(replacements);
replacements.sort(comparing((Replacement r) -> r.getReplaceRange().lowerEndpoint()).reversed());
StringBuilder writer = new StringBuilder(input);
for (Replacement replacement : replacements) {
writer.replace(
replacement.getReplaceRange().lowerEndpoint(),
replacement.getReplaceRange().upperEndpoint(),
replacement.getReplacementString());
}
return writer.toString();
}
/** The earliest position of any Tok in the Token, including leading whitespace. */
public static int startPosition(Token token) {
int min = token.getTok().getPosition();
for (Input.Tok tok : token.getToksBefore()) {
min = min(min, tok.getPosition());
}
return min;
}
/** The earliest non-whitespace Tok in the Token. */
public static Input.Tok startTok(Token token) {
for (Input.Tok tok : token.getToksBefore()) {
if (tok.getIndex() >= 0) {
return tok;
}
}
return token.getTok();
}
/** The last non-whitespace Tok in the Token. */
public static Input.Tok endTok(Token token) {
for (int i = token.getToksAfter().size() - 1; i >= 0; i--) {
Input.Tok tok = token.getToksAfter().get(i);
if (tok.getIndex() >= 0) {
return tok;
}
}
return token.getTok();
}
private boolean isComment(String text) {
return text.startsWith("//") || text.startsWith("/*");
}
private static Range<Integer> union(Range<Integer> x, Range<Integer> y) {
return x.isEmpty() ? y : y.isEmpty() ? x : x.span(y).canonical(DiscreteDomain.integers());
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("iLine", iLine)
.add("lastK", lastK)
.add("spacesPending", spacesPending.toString().replace("\t", "\\t"))
.add("newlinesPending", newlinesPending)
.add("blankLines", blankLines)
.add("super", super.toString())
.toString();
}
}
@@ -18,16 +18,15 @@ import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import openjdk.tools.javac.parser.JavaTokenizer;
import openjdk.tools.javac.parser.Scanner;
import openjdk.tools.javac.parser.ScannerFactory;
import openjdk.tools.javac.parser.Tokens.Comment;
import openjdk.tools.javac.parser.Tokens.Comment.CommentStyle;
import openjdk.tools.javac.parser.Tokens.Token;
import openjdk.tools.javac.parser.Tokens.TokenKind;
import openjdk.tools.javac.parser.UnicodeReader;
import openjdk.tools.javac.util.Context;
import com.sun.tools.javac.parser.JavaTokenizer;
import com.sun.tools.javac.parser.Scanner;
import com.sun.tools.javac.parser.ScannerFactory;
import com.sun.tools.javac.parser.Tokens.Comment;
import com.sun.tools.javac.parser.Tokens.Comment.CommentStyle;
import com.sun.tools.javac.parser.Tokens.Token;
import com.sun.tools.javac.parser.Tokens.TokenKind;
import com.sun.tools.javac.parser.UnicodeReader;
import com.sun.tools.javac.util.Context;
import java.util.Set;
/** A wrapper around javac's lexer. */
@@ -37,6 +36,41 @@ class JavacTokens {
// TODO(b/33103797): fix javac and remove the work-around
private static final CharSequence EOF_COMMENT = "\n//EOF";
/** An unprocessed input token, including whitespace and comments. */
static class RawTok {
private final String stringVal;
private final TokenKind kind;
private final int pos;
private final int endPos;
RawTok(String stringVal, TokenKind kind, int pos, int endPos) {
this.stringVal = stringVal;
this.kind = kind;
this.pos = pos;
this.endPos = endPos;
}
/** The token kind, or {@code null} for whitespace and comments. */
public TokenKind kind() {
return kind;
}
/** The start position. */
public int pos() {
return pos;
}
/** The end position. */
public int endPos() {
return endPos;
}
/** The escaped string value of a literal, or {@code null} for other tokens. */
public String stringVal() {
return stringVal;
}
}
/** Lex the input and return a list of {@link RawTok}s. */
public static ImmutableList<RawTok> getTokens(
String source, Context context, Set<TokenKind> stopTokens) {
@@ -86,41 +120,6 @@ class JavacTokens {
return tokens.build();
}
/** An unprocessed input token, including whitespace and comments. */
static class RawTok {
private final String stringVal;
private final TokenKind kind;
private final int pos;
private final int endPos;
RawTok(String stringVal, TokenKind kind, int pos, int endPos) {
this.stringVal = stringVal;
this.kind = kind;
this.pos = pos;
this.endPos = endPos;
}
/** The token kind, or {@code null} for whitespace and comments. */
public TokenKind kind() {
return kind;
}
/** The start position. */
public int pos() {
return pos;
}
/** The end position. */
public int endPos() {
return endPos;
}
/** The escaped string value of a literal, or {@code null} for other tokens. */
public String stringVal() {
return stringVal;
}
}
/** A {@link JavaTokenizer} that saves comments. */
static class CommentSavingTokenizer extends JavaTokenizer {
CommentSavingTokenizer(ScannerFactory fac, char[] buffer, int length) {
@@ -171,20 +170,6 @@ class JavacTokens {
this.style = style;
}
@Override
public String toString() {
return String.format("Comment: '%s'", getText());
}
@Override
public String getText() {
String text = this.text;
if (text == null) {
this.text = text = new String(reader.getRawCharacters());
}
return text;
}
/**
* Returns the source position of the character at index {@code index} in the comment text.
*
@@ -206,6 +191,15 @@ class JavacTokens {
return style;
}
@Override
public String getText() {
String text = this.text;
if (text == null) {
this.text = text = new String(reader.getRawCharacters());
}
return text;
}
/**
* We don't care about {@code @deprecated} javadoc tags (see the DepAnn check).
*
@@ -215,6 +209,11 @@ class JavacTokens {
public boolean isDeprecated() {
return false;
}
@Override
public String toString() {
return String.format("Comment: '%s'", getText());
}
}
// Scanner(ScannerFactory, JavaTokenizer) is package-private
@@ -16,11 +16,12 @@ package com.google.googlejavaformat.java;
import static java.lang.Math.min;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Comparator.comparing;
import com.google.common.io.ByteStreams;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.googlejavaformat.FormatterDiagnostic;
import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
@@ -29,18 +30,25 @@ import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/** The main class for the Java formatter CLI. */
public final class Main {
private static final int MAX_THREADS = 20;
private static final String STDIN_FILENAME = "<stdin>";
static String versionString() {
return "google-java-format: Version " + GoogleJavaFormatVersion.version();
}
private final PrintWriter outWriter;
private final PrintWriter errWriter;
private final InputStream inStream;
@@ -75,7 +83,8 @@ public final class Main {
return formatter.format(args);
} catch (UsageException e) {
err.print(e.getMessage());
return 0;
// We return exit code 2 to differentiate usage issues from code formatting issues.
return 2;
} finally {
err.flush();
out.flush();
@@ -112,58 +121,59 @@ public final class Main {
}
}
static String versionString() {
return "google-java-format: Version " + GoogleJavaFormatVersion.version();
}
private int formatFiles(CommandLineOptions parameters, JavaFormatterOptions options) {
int numThreads = min(MAX_THREADS, parameters.files().size());
ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
Map<Path, String> inputs = new LinkedHashMap<>();
Map<Path, Future<String>> results = new LinkedHashMap<>();
ExecutorCompletionService<FormatFileCallable.Result> cs =
new ExecutorCompletionService<>(executorService);
boolean allOk = true;
int files = 0;
for (String fileName : parameters.files()) {
if (!fileName.endsWith(".java")) {
errWriter.println("Skipping non-Java file: " + fileName);
continue;
}
Path path = Paths.get(fileName);
String input;
try {
input = new String(Files.readAllBytes(path), UTF_8);
inputs.put(path, input);
results.put(
path, executorService.submit(new FormatFileCallable(parameters, input, options)));
String input = new String(Files.readAllBytes(path), UTF_8);
cs.submit(new FormatFileCallable(parameters, path, input, options));
files++;
} catch (IOException e) {
errWriter.println(fileName + ": could not read file: " + e.getMessage());
allOk = false;
}
}
for (Map.Entry<Path, Future<String>> result : results.entrySet()) {
Path path = result.getKey();
String formatted;
List<FormatFileCallable.Result> results = new ArrayList<>();
while (files > 0) {
try {
formatted = result.getValue().get();
files--;
results.add(cs.take().get());
} catch (InterruptedException e) {
errWriter.println(e.getMessage());
allOk = false;
continue;
} catch (ExecutionException e) {
if (e.getCause() instanceof FormatterException) {
for (FormatterDiagnostic diagnostic : ((FormatterException) e.getCause()).diagnostics()) {
errWriter.println(path + ":" + diagnostic);
}
} else {
errWriter.println(path + ": error: " + e.getCause().getMessage());
e.getCause().printStackTrace(errWriter);
errWriter.println("error: " + e.getCause().getMessage());
e.getCause().printStackTrace(errWriter);
allOk = false;
continue;
}
}
Collections.sort(results, comparing(FormatFileCallable.Result::path));
for (FormatFileCallable.Result result : results) {
Path path = result.path();
if (result.exception() != null) {
for (FormatterDiagnostic diagnostic : result.exception().diagnostics()) {
errWriter.println(path + ":" + diagnostic);
}
allOk = false;
continue;
}
boolean changed = !formatted.equals(inputs.get(path));
String formatted = result.output();
boolean changed = result.changed();
if (changed && parameters.setExitIfChanged()) {
allOk = false;
}
@@ -186,6 +196,10 @@ public final class Main {
outWriter.write(formatted);
}
}
if (!MoreExecutors.shutdownAndAwaitTermination(executorService, Duration.ofSeconds(5))) {
errWriter.println("Failed to shut down ExecutorService");
allOk = false;
}
return allOk ? 0 : 1;
}
@@ -198,9 +212,16 @@ public final class Main {
}
String stdinFilename = parameters.assumeFilename().orElse(STDIN_FILENAME);
boolean ok = true;
try {
String output = new FormatFileCallable(parameters, input, options).call();
boolean changed = !input.equals(output);
FormatFileCallable.Result result =
new FormatFileCallable(parameters, null, input, options).call();
if (result.exception() != null) {
for (FormatterDiagnostic diagnostic : result.exception().diagnostics()) {
errWriter.println(stdinFilename + ":" + diagnostic);
}
ok = false;
} else {
String output = result.output();
boolean changed = result.changed();
if (changed && parameters.setExitIfChanged()) {
ok = false;
}
@@ -211,12 +232,6 @@ public final class Main {
} else {
outWriter.write(output);
}
} catch (FormatterException e) {
for (FormatterDiagnostic diagnostic : e.diagnostics()) {
errWriter.println(stdinFilename + ":" + diagnostic);
}
ok = false;
// TODO(cpovirk): Catch other types of exception (as we do in the formatFiles case).
}
return ok ? 0 : 1;
}
@@ -23,8 +23,7 @@ import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeMap;
import com.google.googlejavaformat.Input.Tok;
import com.google.googlejavaformat.Input.Token;
import openjdk.tools.javac.parser.Tokens.TokenKind;
import com.sun.tools.javac.parser.Tokens.TokenKind;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -32,8 +31,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import jdkx.lang.model.element.Modifier;
import javax.lang.model.element.Modifier;
/** Fixes sequences of modifiers to be in JLS order. */
final class ModifierOrderer {
@@ -112,7 +110,7 @@ final class ModifierOrderer {
}
/**
* Returns the given token as a {@link jdkx.lang.model.element.Modifier}, or {@code null} if it
* Returns the given token as a {@link javax.lang.model.element.Modifier}, or {@code null} if it
* is not a modifier.
*/
private static Modifier asModifier(Token token) {
@@ -159,8 +157,7 @@ final class ModifierOrderer {
/** Applies replacements to the given string. */
private static JavaInput applyReplacements(
JavaInput javaInput, TreeRangeMap<Integer, String> replacementMap) throws FormatterException {
// process in descending order so the replacement ranges aren't perturbed if any
// replacements
// process in descending order so the replacement ranges aren't perturbed if any replacements
// differ in size from the input
Map<Range<Integer>, String> ranges = replacementMap.asDescendingMapOfRanges();
if (ranges.isEmpty()) {
@@ -30,31 +30,29 @@ import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeMap;
import com.google.common.collect.TreeRangeSet;
import com.google.googlejavaformat.Newlines;
import openjdk.source.doctree.DocCommentTree;
import openjdk.source.doctree.ReferenceTree;
import openjdk.source.tree.CaseTree;
import openjdk.source.tree.IdentifierTree;
import openjdk.source.tree.ImportTree;
import openjdk.source.tree.Tree;
import openjdk.source.util.DocTreePath;
import openjdk.source.util.DocTreePathScanner;
import openjdk.source.util.TreePathScanner;
import openjdk.source.util.TreeScanner;
import openjdk.tools.javac.api.JavacTrees;
import openjdk.tools.javac.file.JavacFileManager;
import openjdk.tools.javac.parser.JavacParser;
import openjdk.tools.javac.parser.ParserFactory;
import openjdk.tools.javac.tree.DCTree;
import openjdk.tools.javac.tree.DCTree.DCReference;
import openjdk.tools.javac.tree.JCTree;
import openjdk.tools.javac.tree.JCTree.JCCompilationUnit;
import openjdk.tools.javac.tree.JCTree.JCFieldAccess;
import openjdk.tools.javac.tree.JCTree.JCIdent;
import openjdk.tools.javac.tree.JCTree.JCImport;
import openjdk.tools.javac.util.Context;
import openjdk.tools.javac.util.Log;
import openjdk.tools.javac.util.Options;
import com.sun.source.doctree.DocCommentTree;
import com.sun.source.doctree.ReferenceTree;
import com.sun.source.tree.CaseTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.DocTreePath;
import com.sun.source.util.DocTreePathScanner;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.api.JavacTrees;
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.DCTree;
import com.sun.tools.javac.tree.DCTree.DCReference;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCImport;
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;
@@ -63,13 +61,12 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import jdkx.tools.Diagnostic;
import jdkx.tools.DiagnosticCollector;
import jdkx.tools.DiagnosticListener;
import jdkx.tools.JavaFileObject;
import jdkx.tools.SimpleJavaFileObject;
import jdkx.tools.StandardLocation;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.DiagnosticListener;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardLocation;
/**
* Removes unused imports from a source file. Imports that are only used in javadoc are also
@@ -77,6 +74,145 @@ import jdkx.tools.StandardLocation;
*/
public class RemoveUnusedImports {
// Visits an AST, recording all simple names that could refer to imported
// types and also any javadoc references that could refer to imported
// types (`@link`, `@see`, `@throws`, etc.)
//
// No attempt is made to determine whether simple names occur in contexts
// where they are type names, so there will be false positives. For example,
// `List` is not identified as unused import below:
//
// ```
// import java.util.List;
// class List {}
// ```
//
// This is still reasonably effective in practice because type names differ
// from other kinds of names in casing convention, and simple name
// clashes between imported and declared types are rare.
private static class UnusedImportScanner extends TreePathScanner<Void, Void> {
private final Set<String> usedNames = new LinkedHashSet<>();
private final Multimap<String, Range<Integer>> usedInJavadoc = HashMultimap.create();
final JavacTrees trees;
final DocTreeScanner docTreeSymbolScanner;
private UnusedImportScanner(JavacTrees trees) {
this.trees = trees;
docTreeSymbolScanner = new DocTreeScanner();
}
/** Skip the imports themselves when checking for usage. */
@Override
public Void visitImport(ImportTree importTree, Void usedSymbols) {
return null;
}
@Override
public Void visitIdentifier(IdentifierTree tree, Void unused) {
if (tree == null) {
return null;
}
usedNames.add(tree.getName().toString());
return null;
}
// TODO(cushon): remove this override when pattern matching in switch is no longer a preview
// feature, and TreePathScanner visits CaseTree#getLabels instead of CaseTree#getExpressions
@SuppressWarnings("unchecked") // reflection
@Override
public Void visitCase(CaseTree tree, Void unused) {
if (CASE_TREE_GET_LABELS != null) {
try {
scan((List<? extends Tree>) CASE_TREE_GET_LABELS.invoke(tree), null);
} catch (ReflectiveOperationException e) {
throw new LinkageError(e.getMessage(), e);
}
}
return super.visitCase(tree, null);
}
private static final Method CASE_TREE_GET_LABELS = caseTreeGetLabels();
private static Method caseTreeGetLabels() {
try {
return CaseTree.class.getMethod("getLabels");
} catch (NoSuchMethodException e) {
return null;
}
}
@Override
public Void scan(Tree tree, Void unused) {
if (tree == null) {
return null;
}
scanJavadoc();
return super.scan(tree, unused);
}
private void scanJavadoc() {
if (getCurrentPath() == null) {
return;
}
DocCommentTree commentTree = trees.getDocCommentTree(getCurrentPath());
if (commentTree == null) {
return;
}
docTreeSymbolScanner.scan(new DocTreePath(getCurrentPath(), commentTree), null);
}
// scan javadoc comments, checking for references to imported types
class DocTreeScanner extends DocTreePathScanner<Void, Void> {
@Override
public Void visitIdentifier(com.sun.source.doctree.IdentifierTree node, Void aVoid) {
return null;
}
@Override
public Void visitReference(ReferenceTree referenceTree, Void unused) {
DCReference reference = (DCReference) referenceTree;
long basePos =
reference
.pos((DCTree.DCDocComment) getCurrentPath().getDocComment())
.getStartPosition();
// the position of trees inside the reference node aren't stored, but the qualifier's
// start position is the beginning of the reference node
if (reference.qualifierExpression != null) {
new ReferenceScanner(basePos).scan(reference.qualifierExpression, null);
}
// Record uses inside method parameters. The javadoc tool doesn't use these, but
// IntelliJ does.
if (reference.paramTypes != null) {
for (JCTree param : reference.paramTypes) {
// TODO(cushon): get start positions for the parameters
new ReferenceScanner(-1).scan(param, null);
}
}
return null;
}
// scans the qualifier and parameters of a javadoc reference for possible type names
private class ReferenceScanner extends TreeScanner<Void, Void> {
private final long basePos;
public ReferenceScanner(long basePos) {
this.basePos = basePos;
}
@Override
public Void visitIdentifier(IdentifierTree node, Void aVoid) {
usedInJavadoc.put(
node.getName().toString(),
basePos != -1
? Range.closedOpen((int) basePos, (int) basePos + node.getName().length())
: null);
return super.visitIdentifier(node, aVoid);
}
}
}
}
public static String removeUnusedImports(final String contents) throws FormatterException {
Context context = new Context();
JCCompilationUnit unit = parse(context, contents);
@@ -115,7 +251,10 @@ public class RemoveUnusedImports {
ParserFactory parserFactory = ParserFactory.instance(context);
JavacParser parser =
parserFactory.newParser(
javaInput, /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true);
javaInput,
/* keepDocComments= */ true,
/* keepEndPos= */ true,
/* keepLineMap= */ true);
unit = parser.parseCompilationUnit();
unit.sourcefile = source;
Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
@@ -153,9 +292,7 @@ public class RemoveUnusedImports {
}
private static String getSimpleName(JCImport importTree) {
return importTree.getQualifiedIdentifier() instanceof JCIdent
? ((JCIdent) importTree.getQualifiedIdentifier()).getName().toString()
: ((JCFieldAccess) importTree.getQualifiedIdentifier()).getIdentifier().toString();
return getQualifiedIdentifier(importTree).getIdentifier().toString();
}
private static boolean isUnused(
@@ -164,18 +301,15 @@ public class RemoveUnusedImports {
Multimap<String, Range<Integer>> usedInJavadoc,
JCImport importTree,
String simpleName) {
String qualifier =
((JCFieldAccess) importTree.getQualifiedIdentifier()).getExpression().toString();
JCFieldAccess qualifiedIdentifier = getQualifiedIdentifier(importTree);
String qualifier = qualifiedIdentifier.getExpression().toString();
if (qualifier.equals("java.lang")) {
return true;
}
if (unit.getPackageName() != null && unit.getPackageName().toString().equals(qualifier)) {
return true;
}
if (importTree.getQualifiedIdentifier() instanceof JCFieldAccess
&& ((JCFieldAccess) importTree.getQualifiedIdentifier())
.getIdentifier()
.contentEquals("*")) {
if (qualifiedIdentifier.getIdentifier().contentEquals("*")) {
return false;
}
@@ -188,6 +322,15 @@ public class RemoveUnusedImports {
return true;
}
private static JCFieldAccess getQualifiedIdentifier(JCImport importTree) {
// Use reflection because the return type is JCTree in some versions and JCFieldAccess in others
try {
return (JCFieldAccess) JCImport.class.getMethod("getQualifiedIdentifier").invoke(importTree);
} catch (ReflectiveOperationException e) {
throw new LinkageError(e.getMessage(), e);
}
}
/** Applies the replacements to the given source, and re-format any edited javadoc. */
private static String applyReplacements(String source, RangeMap<Integer, String> replacements) {
// save non-empty fixed ranges for reformatting after fixes are applied
@@ -212,143 +355,4 @@ public class RemoveUnusedImports {
}
return sb.toString();
}
// Visits an AST, recording all simple names that could refer to imported
// types and also any javadoc references that could refer to imported
// types (`@link`, `@see`, `@throws`, etc.)
//
// No attempt is made to determine whether simple names occur in contexts
// where they are type names, so there will be false positives. For example,
// `List` is not identified as unused import below:
//
// ```
// import java.util.List;
// class List {}
// ```
//
// This is still reasonably effective in practice because type names differ
// from other kinds of names in casing convention, and simple name
// clashes between imported and declared types are rare.
private static class UnusedImportScanner extends TreePathScanner<Void, Void> {
private static final Method CASE_TREE_GET_LABELS = caseTreeGetLabels();
private final Set<String> usedNames = new LinkedHashSet<>();
private final Multimap<String, Range<Integer>> usedInJavadoc = HashMultimap.create();
final JavacTrees trees;
final DocTreeScanner docTreeSymbolScanner;
private UnusedImportScanner(JavacTrees trees) {
this.trees = trees;
docTreeSymbolScanner = new DocTreeScanner();
}
private static Method caseTreeGetLabels() {
try {
return CaseTree.class.getMethod("getLabels");
} catch (NoSuchMethodException e) {
return null;
}
}
/** Skip the imports themselves when checking for usage. */
@Override
public Void visitImport(ImportTree importTree, Void usedSymbols) {
return null;
}
// TODO(cushon): remove this override when pattern matching in switch is no longer a preview
// feature, and TreePathScanner visits CaseTree#getLabels instead of CaseTree#getExpressions
@SuppressWarnings("unchecked") // reflection
@Override
public Void visitCase(CaseTree tree, Void unused) {
if (CASE_TREE_GET_LABELS != null) {
try {
scan((List<? extends Tree>) CASE_TREE_GET_LABELS.invoke(tree), null);
} catch (ReflectiveOperationException e) {
throw new LinkageError(e.getMessage(), e);
}
}
return super.visitCase(tree, null);
}
@Override
public Void visitIdentifier(IdentifierTree tree, Void unused) {
if (tree == null) {
return null;
}
usedNames.add(tree.getName().toString());
return null;
}
@Override
public Void scan(Tree tree, Void unused) {
if (tree == null) {
return null;
}
scanJavadoc();
return super.scan(tree, unused);
}
private void scanJavadoc() {
if (getCurrentPath() == null) {
return;
}
DocCommentTree commentTree = trees.getDocCommentTree(getCurrentPath());
if (commentTree == null) {
return;
}
docTreeSymbolScanner.scan(new DocTreePath(getCurrentPath(), commentTree), null);
}
// scan javadoc comments, checking for references to imported types
class DocTreeScanner extends DocTreePathScanner<Void, Void> {
@Override
public Void visitIdentifier(openjdk.source.doctree.IdentifierTree node, Void aVoid) {
return null;
}
@Override
public Void visitReference(ReferenceTree referenceTree, Void unused) {
DCReference reference = (DCReference) referenceTree;
long basePos =
reference
.pos((DCTree.DCDocComment) getCurrentPath().getDocComment())
.getStartPosition();
// the position of trees inside the reference node aren't stored, but the
// qualifier's
// start position is the beginning of the reference node
if (reference.qualifierExpression != null) {
new ReferenceScanner(basePos).scan(reference.qualifierExpression, null);
}
// Record uses inside method parameters. The javadoc tool doesn't use these, but
// IntelliJ does.
if (reference.paramTypes != null) {
for (JCTree param : reference.paramTypes) {
// TODO(cushon): get start positions for the parameters
new ReferenceScanner(-1).scan(param, null);
}
}
return null;
}
// scans the qualifier and parameters of a javadoc reference for possible type names
private class ReferenceScanner extends TreeScanner<Void, Void> {
private final long basePos;
public ReferenceScanner(long basePos) {
this.basePos = basePos;
}
@Override
public Void visitIdentifier(IdentifierTree node, Void aVoid) {
usedInJavadoc.put(
node.getName().toString(),
basePos != -1
? Range.closedOpen((int) basePos, (int) basePos + node.getName().length())
: null);
return super.visitIdentifier(node, aVoid);
}
}
}
}
}
@@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.Range;
import java.util.Objects;
/**
@@ -29,6 +28,12 @@ import java.util.Objects;
*/
public final class Replacement {
public static Replacement create(int startPosition, int endPosition, String replaceWith) {
checkArgument(startPosition >= 0, "startPosition must be non-negative");
checkArgument(startPosition <= endPosition, "startPosition cannot be after endPosition");
return new Replacement(Range.closedOpen(startPosition, endPosition), replaceWith);
}
private final Range<Integer> replaceRange;
private final String replacementString;
@@ -37,15 +42,14 @@ public final class Replacement {
this.replacementString = checkNotNull(replacementString, "Null replacementString");
}
public static Replacement create(int startPosition, int endPosition, String replaceWith) {
checkArgument(startPosition >= 0, "startPosition must be non-negative");
checkArgument(startPosition <= endPosition, "startPosition cannot be after endPosition");
return new Replacement(Range.closedOpen(startPosition, endPosition), replaceWith);
/** The range of characters in the original source to replace. */
public Range<Integer> getReplaceRange() {
return replaceRange;
}
@Override
public int hashCode() {
return Objects.hash(replaceRange, replacementString);
/** The string to replace the range of characters with. */
public String getReplacementString() {
return replacementString;
}
@Override
@@ -61,13 +65,8 @@ public final class Replacement {
return false;
}
/** The range of characters in the original source to replace. */
public Range<Integer> getReplaceRange() {
return replaceRange;
}
/** The string to replace the range of characters with. */
public String getReplacementString() {
return replacementString;
@Override
public int hashCode() {
return Objects.hash(replaceRange, replacementString);
}
}
@@ -23,16 +23,71 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;
import java.util.ArrayList;
import java.util.List;
/** Formats a subset of a compilation unit. */
public class SnippetFormatter {
/** The kind of snippet to format. */
public enum SnippetKind {
COMPILATION_UNIT,
CLASS_BODY_DECLARATIONS,
STATEMENTS,
EXPRESSION
}
private class SnippetWrapper {
int offset;
final StringBuilder contents = new StringBuilder();
public SnippetWrapper append(String str) {
contents.append(str);
return this;
}
public SnippetWrapper appendSource(String source) {
this.offset = contents.length();
contents.append(source);
return this;
}
public void closeBraces(int initialIndent) {
for (int i = initialIndent; --i >= 0; ) {
contents.append("\n").append(createIndentationString(i)).append("}");
}
}
}
private static final int INDENTATION_SIZE = 2;
private static final CharMatcher NOT_WHITESPACE = CharMatcher.whitespace().negate();
private final Formatter formatter = new Formatter();
private static final CharMatcher NOT_WHITESPACE = CharMatcher.whitespace().negate();
public String createIndentationString(int indentationLevel) {
Preconditions.checkArgument(
indentationLevel >= 0,
"Indentation level cannot be less than zero. Given: %s",
indentationLevel);
int spaces = indentationLevel * INDENTATION_SIZE;
StringBuilder buf = new StringBuilder(spaces);
for (int i = 0; i < spaces; i++) {
buf.append(' ');
}
return buf.toString();
}
private static Range<Integer> offsetRange(Range<Integer> range, int offset) {
range = range.canonical(DiscreteDomain.integers());
return Range.closedOpen(range.lowerEndpoint() + offset, range.upperEndpoint() + offset);
}
private static List<Range<Integer>> offsetRanges(List<Range<Integer>> ranges, int offset) {
List<Range<Integer>> result = new ArrayList<>();
for (Range<Integer> range : ranges) {
result.add(offsetRange(range, offset));
}
return result;
}
/** Runs the Google Java formatter on the given source, with only the given ranges specified. */
public ImmutableList<Replacement> format(
@@ -67,19 +122,6 @@ public class SnippetFormatter {
.collect(toImmutableList());
}
private static List<Range<Integer>> offsetRanges(List<Range<Integer>> ranges, int offset) {
List<Range<Integer>> result = new ArrayList<>();
for (Range<Integer> range : ranges) {
result.add(offsetRange(range, offset));
}
return result;
}
private static Range<Integer> offsetRange(Range<Integer> range, int offset) {
range = range.canonical(DiscreteDomain.integers());
return Range.closedOpen(range.lowerEndpoint() + offset, range.upperEndpoint() + offset);
}
/**
* Generates {@code Replacement}s rewriting {@code source} to {@code replacement}, under the
* assumption that they differ in whitespace alone.
@@ -170,47 +212,4 @@ public class SnippetFormatter {
throw new IllegalArgumentException("Unknown snippet kind: " + kind);
}
}
public String createIndentationString(int indentationLevel) {
Preconditions.checkArgument(
indentationLevel >= 0,
"Indentation level cannot be less than zero. Given: %s",
indentationLevel);
int spaces = indentationLevel * INDENTATION_SIZE;
StringBuilder buf = new StringBuilder(spaces);
for (int i = 0; i < spaces; i++) {
buf.append(' ');
}
return buf.toString();
}
/** The kind of snippet to format. */
public enum SnippetKind {
COMPILATION_UNIT,
CLASS_BODY_DECLARATIONS,
STATEMENTS,
EXPRESSION
}
private class SnippetWrapper {
final StringBuilder contents = new StringBuilder();
int offset;
public SnippetWrapper append(String str) {
contents.append(str);
return this;
}
public SnippetWrapper appendSource(String source) {
this.offset = contents.length();
contents.append(source);
return this;
}
public void closeBraces(int initialIndent) {
for (int i = initialIndent; --i >= 0; ) {
contents.append("\n").append(createIndentationString(i)).append("}");
}
}
}
}
@@ -26,22 +26,21 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Range;
import com.google.common.collect.TreeRangeMap;
import com.google.googlejavaformat.Newlines;
import openjdk.source.tree.BinaryTree;
import openjdk.source.tree.LiteralTree;
import openjdk.source.tree.MemberSelectTree;
import openjdk.source.tree.Tree;
import openjdk.source.tree.Tree.Kind;
import openjdk.source.util.TreePath;
import openjdk.source.util.TreePathScanner;
import openjdk.tools.javac.file.JavacFileManager;
import openjdk.tools.javac.parser.JavacParser;
import openjdk.tools.javac.parser.ParserFactory;
import openjdk.tools.javac.tree.JCTree;
import openjdk.tools.javac.util.Context;
import openjdk.tools.javac.util.Log;
import openjdk.tools.javac.util.Options;
import openjdk.tools.javac.util.Position;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
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;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Log;
import com.sun.tools.javac.util.Options;
import com.sun.tools.javac.util.Position;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
@@ -53,21 +52,15 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
import jdkx.tools.Diagnostic;
import jdkx.tools.DiagnosticCollector;
import jdkx.tools.DiagnosticListener;
import jdkx.tools.JavaFileObject;
import jdkx.tools.SimpleJavaFileObject;
import jdkx.tools.StandardLocation;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.DiagnosticListener;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardLocation;
/** Wraps string literals that exceed the column limit. */
public final class StringWrapper {
public static final CharMatcher STRING_CONCAT_DELIMITER =
CharMatcher.whitespace().or(CharMatcher.anyOf("\"+"));
private StringWrapper() {}
/** Reflows long string literals in the given Java source code. */
public static String wrap(String input, Formatter formatter) throws FormatterException {
return StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, input, formatter);
@@ -87,8 +80,7 @@ public final class StringWrapper {
String firstPass = formatter.formatSource(input, replacements.asMapOfRanges().keySet());
if (!firstPass.equals(input)) {
// If formatting the replacement ranges resulted in a change, recalculate the
// replacements on
// If formatting the replacement ranges resulted in a change, recalculate the replacements on
// the updated input.
input = firstPass;
replacements = getReflowReplacements(columnLimit, input);
@@ -98,14 +90,15 @@ public final class StringWrapper {
{
// We really don't want bugs in this pass to change the behaviour of programs we're
// formatting, so check that the pretty-printed AST is the same before and after
// reformatting.
// formatting, so check that the pretty-printed AST is the same before and after reformatting.
String expected = parse(input, /* allowStringFolding= */ true).toString();
String actual = parse(result, /* allowStringFolding= */ true).toString();
if (!expected.equals(actual)) {
throw new FormatterException(
String.format(
"Something has gone terribly wrong. Please file a bug: "
"Something has gone terribly wrong. We planned to make the below formatting change,"
+ " but have aborted because it would unexpectedly change the AST.\n"
+ "Please file a bug: "
+ "https://github.com/google/google-java-format/issues/new"
+ "\n\n=== Actual: ===\n%s\n=== Expected: ===\n%s\n",
actual, expected));
@@ -158,8 +151,7 @@ public final class StringWrapper {
// i.e. `ONE + TWO + THREE`
// We need this information to handle continuation indents.
AtomicBoolean first = new AtomicBoolean(false);
// Finds the set of string literals in the concat expression that includes the one that
// needs
// Finds the set of string literals in the concat expression that includes the one that needs
// to be wrapped.
List<Tree> flat = flatten(input, unit, path, enclosing, first);
// Zero-indexed start column
@@ -250,7 +242,7 @@ public final class StringWrapper {
* @param columnLimit the number of columns to wrap at
* @param startColumn the column position of the beginning of the original text
* @param trailing extra space to leave after the last line, to accommodate a ; or )
* @param components the text to reflow. This is a list of "words" of a single literal. Its first
* @param components the text to reflow. This is a list of “words” of a single literal. Its first
* and last quotes have been stripped
* @param first0 true if the text includes the beginning of its enclosing concat chain
*/
@@ -270,8 +262,7 @@ public final class StringWrapper {
while (!input.isEmpty()) {
int length = 0;
List<String> line = new ArrayList<>();
// If we know this is going to be the last line, then remove a bit of width to account
// for the
// If we know this is going to be the last line, then remove a bit of width to account for the
// trailing characters.
if (input.stream().mapToInt(String::length).sum() <= width) {
// This isn’t quite optimal, but arguably good enough. See b/179561701
@@ -357,12 +348,28 @@ public final class StringWrapper {
input.subSequence(getEndPosition(unit, one), getStartPosition(two)));
}
public static final CharMatcher STRING_CONCAT_DELIMITER =
CharMatcher.whitespace().or(CharMatcher.anyOf("\"+"));
private static int getEndPosition(JCTree.JCCompilationUnit unit, Tree tree) {
return ((JCTree) tree).getEndPosition(unit.endPositions);
}
private static int getStartPosition(Tree tree) {
return ((JCTree) tree).getStartPosition();
}
private static int getEndPosition(JCTree.JCCompilationUnit unit, Tree tree) {
return ((JCTree) tree).getEndPosition(unit.endPositions);
/** Returns true if any lines in the given Java source exceed the column limit. */
private static boolean longLines(int columnLimit, String input) {
// TODO(cushon): consider adding Newlines.lineIterable?
Iterator<String> it = Newlines.lineIterator(input);
while (it.hasNext()) {
String line = it.next();
if (line.length() > columnLimit) {
return true;
}
}
return false;
}
/** Parses the given Java source. */
@@ -391,7 +398,7 @@ public final class StringWrapper {
ParserFactory parserFactory = ParserFactory.instance(context);
JavacParser parser =
parserFactory.newParser(
source, /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true);
source, /* keepDocComments= */ true, /* keepEndPos= */ true, /* keepLineMap= */ true);
unit = parser.parseCompilationUnit();
unit.sourcefile = sjfo;
Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
@@ -403,24 +410,10 @@ public final class StringWrapper {
return unit;
}
/** Returns true if any lines in the given Java source exceed the column limit. */
private static boolean longLines(int columnLimit, String input) {
// TODO(cushon): consider adding Newlines.lineIterable?
Iterator<String> it = Newlines.lineIterator(input);
while (it.hasNext()) {
String line = it.next();
if (line.length() > columnLimit) {
return true;
}
}
return false;
}
/** Applies replacements to the given string. */
private static String applyReplacements(
String javaInput, TreeRangeMap<Integer, String> replacementMap) throws FormatterException {
// process in descending order so the replacement ranges aren't perturbed if any
// replacements
// process in descending order so the replacement ranges aren't perturbed if any replacements
// differ in size from the input
Map<Range<Integer>, String> ranges = replacementMap.asDescendingMapOfRanges();
if (ranges.isEmpty()) {
@@ -433,4 +426,6 @@ public final class StringWrapper {
}
return sb.toString();
}
private StringWrapper() {}
}
@@ -14,23 +14,21 @@
package com.google.googlejavaformat.java;
import openjdk.source.tree.ClassTree;
import openjdk.source.tree.CompoundAssignmentTree;
import openjdk.source.tree.ExpressionTree;
import openjdk.source.tree.IdentifierTree;
import openjdk.source.tree.MemberSelectTree;
import openjdk.source.tree.MethodInvocationTree;
import openjdk.source.tree.ParenthesizedTree;
import openjdk.source.tree.Tree;
import openjdk.source.util.TreePath;
import openjdk.tools.javac.tree.JCTree;
import openjdk.tools.javac.tree.Pretty;
import openjdk.tools.javac.tree.TreeInfo;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompoundAssignmentTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.Pretty;
import com.sun.tools.javac.tree.TreeInfo;
import java.io.IOError;
import java.io.IOException;
import jdkx.lang.model.element.Name;
import javax.lang.model.element.Name;
/** Utilities for working with {@link Tree}s. */
class Trees {
@@ -15,7 +15,6 @@
package com.google.googlejavaformat.java;
import com.google.common.base.Verify;
import java.util.List;
import java.util.Optional;
@@ -24,33 +23,6 @@ public final class TypeNameClassifier {
private TypeNameClassifier() {}
/**
* Returns the end index (inclusive) of the longest prefix that matches the naming conventions of
* a type or static field access, or -1 if no such prefix was found.
*
* <p>Examples:
*
* <ul>
* <li>ClassName
* <li>ClassName.staticMemberName
* <li>com.google.ClassName.InnerClass.staticMemberName
* </ul>
*/
static Optional<Integer> typePrefixLength(List<String> nameParts) {
TyParseState state = TyParseState.START;
Optional<Integer> typeLength = Optional.empty();
for (int i = 0; i < nameParts.size(); i++) {
state = state.next(JavaCaseFormat.from(nameParts.get(i)));
if (state == TyParseState.REJECT) {
break;
}
if (state.isSingleUnit()) {
typeLength = Optional.of(i);
}
}
return typeLength;
}
/** A state machine for classifying qualified names. */
private enum TyParseState {
@@ -138,6 +110,33 @@ public final class TypeNameClassifier {
public abstract TyParseState next(JavaCaseFormat n);
}
/**
* Returns the end index (inclusive) of the longest prefix that matches the naming conventions of
* a type or static field access, or -1 if no such prefix was found.
*
* <p>Examples:
*
* <ul>
* <li>ClassName
* <li>ClassName.staticMemberName
* <li>com.google.ClassName.InnerClass.staticMemberName
* </ul>
*/
static Optional<Integer> typePrefixLength(List<String> nameParts) {
TyParseState state = TyParseState.START;
Optional<Integer> typeLength = Optional.empty();
for (int i = 0; i < nameParts.size(); i++) {
state = state.next(JavaCaseFormat.from(nameParts.get(i)));
if (state == TyParseState.REJECT) {
break;
}
if (state.isSingleUnit()) {
typeLength = Optional.of(i);
}
}
return typeLength;
}
/** Case formats used in Java identifiers. */
public enum JavaCaseFormat {
UPPERCASE,
@@ -81,6 +81,10 @@ final class UsageException extends Exception {
super(buildMessage(null));
}
UsageException(String message) {
super(buildMessage(checkNotNull(message)));
}
private static String buildMessage(String message) {
StringBuilder builder = new StringBuilder();
if (message != null) {
@@ -101,8 +105,4 @@ final class UsageException extends Exception {
private static void appendLines(StringBuilder builder, String[] lines) {
NEWLINE_JOINER.appendTo(builder, lines).append(System.lineSeparator());
}
UsageException(String message) {
super(buildMessage(checkNotNull(message)));
}
}
@@ -17,17 +17,14 @@ package com.google.googlejavaformat.java.filer;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.googlejavaformat.java.Formatter;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.io.IOException;
import jdkx.annotation.processing.Filer;
import jdkx.annotation.processing.Messager;
import jdkx.lang.model.element.Element;
import jdkx.tools.FileObject;
import jdkx.tools.JavaFileManager;
import jdkx.tools.JavaFileObject;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.lang.model.element.Element;
import javax.tools.FileObject;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* A decorating {@link Filer} implementation which formats Java source files with a {@link
@@ -40,9 +37,7 @@ public final class FormattingFiler implements Filer {
private final Formatter formatter = new Formatter();
private final Messager messager;
/**
* @param delegate filer to decorate
*/
/** @param delegate filer to decorate */
public FormattingFiler(Filer delegate) {
this(delegate, null);
}
@@ -20,16 +20,13 @@ import com.google.common.io.CharSink;
import com.google.common.io.CharSource;
import com.google.googlejavaformat.java.Formatter;
import com.google.googlejavaformat.java.FormatterException;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.io.IOException;
import java.io.Writer;
import jdkx.annotation.processing.Messager;
import jdkx.tools.Diagnostic;
import jdkx.tools.ForwardingJavaFileObject;
import jdkx.tools.JavaFileObject;
import javax.annotation.processing.Messager;
import javax.tools.Diagnostic;
import javax.tools.ForwardingJavaFileObject;
import javax.tools.JavaFileObject;
import org.checkerframework.checker.nullness.qual.Nullable;
/** A {@link JavaFileObject} decorator which {@linkplain Formatter formats} source code. */
final class FormattingJavaFileObject extends ForwardingJavaFileObject<JavaFileObject> {
@@ -81,10 +78,8 @@ final class FormattingJavaFileObject extends ForwardingJavaFileObject<JavaFileOb
}
});
} catch (FormatterException e) {
// An exception will happen when the code being formatted has an error. It's
// better to
// log the exception and emit unformatted code so the developer can view the
// code which
// An exception will happen when the code being formatted has an error. It's better to
// log the exception and emit unformatted code so the developer can view the code which
// caused a problem.
try (Writer writer = fileObject.openWriter()) {
writer.append(stringBuilder.toString());
@@ -0,0 +1,270 @@
/*
* Copyright 2020 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.java17;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Iterables.getOnlyElement;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
import com.google.googlejavaformat.OpsBuilder;
import com.google.googlejavaformat.OpsBuilder.BlankLineWanted;
import com.google.googlejavaformat.java.JavaInputAstVisitor;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.BindingPatternTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.CaseLabelTree;
import com.sun.source.tree.CaseTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.InstanceOfTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.ModuleTree;
import com.sun.source.tree.SwitchExpressionTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.tree.YieldTree;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.tree.TreeInfo;
import java.util.List;
import java.util.Optional;
import javax.lang.model.element.Name;
/**
* Extends {@link JavaInputAstVisitor} with support for AST nodes that were added or modified in
* Java 17.
*/
public class Java17InputAstVisitor extends JavaInputAstVisitor {
public Java17InputAstVisitor(OpsBuilder builder, int indentMultiplier) {
super(builder, indentMultiplier);
}
@Override
protected void handleModule(boolean first, CompilationUnitTree node) {
ModuleTree module = node.getModule();
if (module != null) {
if (!first) {
builder.blankLineWanted(BlankLineWanted.YES);
}
markForPartialFormat();
visitModule(module, null);
builder.forcedBreak();
}
}
@Override
protected List<? extends Tree> getPermitsClause(ClassTree node) {
return node.getPermitsClause();
}
@Override
public Void visitBindingPattern(BindingPatternTree node, Void unused) {
sync(node);
VariableTree variableTree = node.getVariable();
visitBindingPattern(
variableTree.getModifiers(), variableTree.getType(), variableTree.getName());
return null;
}
private void visitBindingPattern(ModifiersTree modifiers, Tree type, Name name) {
if (modifiers != null) {
List<AnnotationTree> annotations =
visitModifiers(modifiers, Direction.HORIZONTAL, Optional.empty());
visitAnnotations(annotations, BreakOrNot.NO, BreakOrNot.YES);
}
scan(type, null);
builder.breakOp(" ");
visit(name);
}
@Override
public Void visitYield(YieldTree node, Void aVoid) {
sync(node);
token("yield");
builder.space();
scan(node.getValue(), null);
token(";");
return null;
}
@Override
public Void visitSwitchExpression(SwitchExpressionTree node, Void aVoid) {
sync(node);
visitSwitch(node.getExpression(), node.getCases());
return null;
}
@Override
public Void visitClass(ClassTree tree, Void unused) {
switch (tree.getKind()) {
case ANNOTATION_TYPE:
visitAnnotationType(tree);
break;
case CLASS:
case INTERFACE:
visitClassDeclaration(tree);
break;
case ENUM:
visitEnumDeclaration(tree);
break;
case RECORD:
visitRecordDeclaration(tree);
break;
default:
throw new AssertionError(tree.getKind());
}
return null;
}
public void visitRecordDeclaration(ClassTree node) {
sync(node);
typeDeclarationModifiers(node.getModifiers());
Verify.verify(node.getExtendsClause() == null);
boolean hasSuperInterfaceTypes = !node.getImplementsClause().isEmpty();
token("record");
builder.space();
visit(node.getSimpleName());
if (!node.getTypeParameters().isEmpty()) {
token("<");
}
builder.open(plusFour);
{
if (!node.getTypeParameters().isEmpty()) {
typeParametersRest(node.getTypeParameters(), hasSuperInterfaceTypes ? plusFour : ZERO);
}
ImmutableList<JCVariableDecl> parameters = recordVariables(node);
token("(");
if (!parameters.isEmpty()) {
// Break before args.
builder.breakToFill("");
}
// record headers can't declare receiver parameters
visitFormals(/* receiver= */ Optional.empty(), parameters);
token(")");
if (hasSuperInterfaceTypes) {
builder.breakToFill(" ");
builder.open(node.getImplementsClause().size() > 1 ? plusFour : ZERO);
token("implements");
builder.space();
boolean first = true;
for (Tree superInterfaceType : node.getImplementsClause()) {
if (!first) {
token(",");
builder.breakOp(" ");
}
scan(superInterfaceType, null);
first = false;
}
builder.close();
}
}
builder.close();
if (node.getMembers() == null) {
token(";");
} else {
List<Tree> members =
node.getMembers().stream()
.filter(t -> (TreeInfo.flags((JCTree) t) & Flags.GENERATED_MEMBER) == 0)
.collect(toImmutableList());
addBodyDeclarations(members, BracesOrNot.YES, FirstDeclarationsOrNot.YES);
}
dropEmptyDeclarations();
}
private static ImmutableList<JCVariableDecl> recordVariables(ClassTree node) {
return node.getMembers().stream()
.filter(JCVariableDecl.class::isInstance)
.map(JCVariableDecl.class::cast)
.filter(m -> (m.mods.flags & RECORD) == RECORD)
.collect(toImmutableList());
}
@Override
public Void visitInstanceOf(InstanceOfTree node, Void unused) {
sync(node);
builder.open(plusFour);
scan(node.getExpression(), null);
builder.breakOp(" ");
builder.open(ZERO);
token("instanceof");
builder.breakOp(" ");
if (node.getPattern() != null) {
scan(node.getPattern(), null);
} else {
scan(node.getType(), null);
}
builder.close();
builder.close();
return null;
}
@Override
public Void visitCase(CaseTree node, Void unused) {
sync(node);
markForPartialFormat();
builder.forcedBreak();
List<? extends CaseLabelTree> labels = node.getLabels();
boolean isDefault =
labels.size() == 1 && getOnlyElement(labels).getKind().name().equals("DEFAULT_CASE_LABEL");
if (isDefault) {
token("default", plusTwo);
} else {
token("case", plusTwo);
builder.open(labels.size() > 1 ? plusFour : ZERO);
builder.space();
boolean first = true;
for (Tree expression : labels) {
if (!first) {
token(",");
builder.breakOp(" ");
}
scan(expression, null);
first = false;
}
builder.close();
}
switch (node.getCaseKind()) {
case STATEMENT:
token(":");
builder.open(plusTwo);
visitStatements(node.getStatements());
builder.close();
break;
case RULE:
builder.space();
token("-");
token(">");
builder.space();
if (node.getBody().getKind() == Tree.Kind.BLOCK) {
// Explicit call with {@link CollapseEmptyOrNot.YES} to handle empty case blocks.
visitBlock(
(BlockTree) node.getBody(),
CollapseEmptyOrNot.YES,
AllowLeadingBlankLine.NO,
AllowTrailingBlankLine.NO);
} else {
scan(node.getBody(), null);
}
builder.guessToken(";");
break;
default:
throw new AssertionError(node.getCaseKind());
}
return null;
}
}
@@ -22,7 +22,6 @@ import static java.util.regex.Pattern.compile;
import com.google.common.collect.ImmutableList;
import com.google.googlejavaformat.java.javadoc.JavadocLexer.LexException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -37,19 +36,8 @@ import java.util.regex.Pattern;
*/
public final class JavadocFormatter {
private static final Token STANDARD_BR_TOKEN = new Token(BR_TAG, "<br>");
private static final Token STANDARD_P_TOKEN = new Token(PARAGRAPH_OPEN_TAG, "<p>");
private static final Pattern SIMPLE_TAG_PATTERN = compile("^<\\w+\\s*/?\\s*>", CASE_INSENSITIVE);
/*
* TODO(cpovirk): Is this really the right location for the standardize* methods? Maybe the lexer
* should include them as part of its own postprocessing? Or even the writer could make sense.
*/
private static final Pattern ONE_CONTENT_LINE_PATTERN = compile(" */[*][*]\n *[*] (.*)\n *[*]/");
static final int MAX_LINE_LENGTH = 100;
private JavadocFormatter() {}
/**
* Formats the given Javadoc comment, which must start with ∕✱✱ and end with ✱∕. The output will
* start and end with the same characters.
@@ -150,6 +138,11 @@ public final class JavadocFormatter {
throw new AssertionError();
}
/*
* TODO(cpovirk): Is this really the right location for the standardize* methods? Maybe the lexer
* should include them as part of its own postprocessing? Or even the writer could make sense.
*/
private static Token standardizeBrToken(Token token) {
return standardize(token, STANDARD_BR_TOKEN);
}
@@ -162,6 +155,12 @@ public final class JavadocFormatter {
return SIMPLE_TAG_PATTERN.matcher(token.getValue()).matches() ? standardToken : token;
}
private static final Token STANDARD_BR_TOKEN = new Token(BR_TAG, "<br>");
private static final Token STANDARD_P_TOKEN = new Token(PARAGRAPH_OPEN_TAG, "<p>");
private static final Pattern SIMPLE_TAG_PATTERN = compile("^<\\w+\\s*/?\\s*>", CASE_INSENSITIVE);
private static final Pattern ONE_CONTENT_LINE_PATTERN = compile(" */[*][*]\n *[*] (.*)\n *[*]/");
/**
* Returns the given string or a one-line version of it (e.g., "∕✱✱ Tests for foos. ✱∕") if it
* fits on one line.
@@ -191,4 +190,6 @@ public final class JavadocFormatter {
}
return true;
}
private JavadocFormatter() {}
}
@@ -54,7 +54,6 @@ import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.PeekingIterator;
import com.google.googlejavaformat.java.javadoc.Token.Type;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
@@ -63,78 +62,6 @@ import java.util.regex.Pattern;
/** Lexer for the Javadoc formatter. */
final class JavadocLexer {
private static final Pattern NON_UNIX_LINE_ENDING = Pattern.compile("\r\n?");
private static final CharMatcher NEWLINE = CharMatcher.is('\n');
/*
* This also eats any trailing whitespace. We would be smart enough to ignore that, anyway --
* except in the case of <pre>/<table>, inside which we otherwise leave whitespace intact.
*
* We'd remove the trailing whitespace later on (in JavaCommentsHelper.rewrite), but I feel safer
* stripping it now: It otherwise might confuse our line-length count, which we use for wrapping.
*/
private static final Pattern NEWLINE_PATTERN = compile("^[ \t]*\n[ \t]*[*]?[ \t]?");
// We ensure elsewhere that we match this only at the beginning of a line.
// Only match tags that start with a lowercase letter, to avoid false matches on unescaped
// annotations inside code blocks.
// Match "@param <T>" specially in case the <T> is a <P> or other HTML tag we treat specially.
private static final Pattern FOOTER_TAG_PATTERN = compile("^@(param\\s+<\\w+>|[a-z]\\w*)");
private static final Pattern MOE_BEGIN_STRIP_COMMENT_PATTERN =
compile("^<!--\\s*M" + "OE:begin_intracomment_strip\\s*-->");
private static final Pattern MOE_END_STRIP_COMMENT_PATTERN =
compile("^<!--\\s*M" + "OE:end_intracomment_strip\\s*-->");
private static final Pattern HTML_COMMENT_PATTERN = fullCommentPattern();
private static final Pattern PRE_OPEN_PATTERN = openTagPattern("pre");
private static final Pattern PRE_CLOSE_PATTERN = closeTagPattern("pre");
private static final Pattern CODE_OPEN_PATTERN = openTagPattern("code");
private static final Pattern CODE_CLOSE_PATTERN = closeTagPattern("code");
private static final Pattern TABLE_OPEN_PATTERN = openTagPattern("table");
private static final Pattern TABLE_CLOSE_PATTERN = closeTagPattern("table");
private static final Pattern LIST_OPEN_PATTERN = openTagPattern("ul|ol|dl");
private static final Pattern LIST_CLOSE_PATTERN = closeTagPattern("ul|ol|dl");
private static final Pattern LIST_ITEM_OPEN_PATTERN = openTagPattern("li|dt|dd");
private static final Pattern LIST_ITEM_CLOSE_PATTERN = closeTagPattern("li|dt|dd");
private static final Pattern HEADER_OPEN_PATTERN = openTagPattern("h[1-6]");
private static final Pattern HEADER_CLOSE_PATTERN = closeTagPattern("h[1-6]");
private static final Pattern PARAGRAPH_OPEN_PATTERN = openTagPattern("p");
private static final Pattern PARAGRAPH_CLOSE_PATTERN = closeTagPattern("p");
private static final Pattern BLOCKQUOTE_OPEN_PATTERN = openTagPattern("blockquote");
private static final Pattern BLOCKQUOTE_CLOSE_PATTERN = closeTagPattern("blockquote");
private static final Pattern BR_PATTERN = openTagPattern("br");
private static final Pattern INLINE_TAG_OPEN_PATTERN = compile("^[{]@\\w*");
/*
* We exclude < so that we don't swallow following HTML tags. This lets us fix up "foo<p>" (~400
* hits in Google-internal code). We will join unnecessarily split "words" (like "foo<b>bar</b>")
* in a later step. There's a similar story for braces. I'm not sure I actually need to exclude @
* or *. TODO(cpovirk): Try removing them.
*
* Thanks to the "rejoin" step in joinAdjacentLiteralsAndAdjacentWhitespace(), we could get away
* with matching only one character here. That would eliminate the need for the regex entirely.
* That might be faster or slower than what we do now.
*/
private static final Pattern LITERAL_PATTERN = compile("^.[^ \t\n@<{}*]*", DOTALL);
private final CharStream input;
private final NestingCounter braceDepth = new NestingCounter();
private final NestingCounter preDepth = new NestingCounter();
private final NestingCounter codeDepth = new NestingCounter();
private final NestingCounter tableDepth = new NestingCounter();
private boolean somethingSinceNewline;
private JavadocLexer(CharStream input) {
this.input = checkNotNull(input);
}
private static Pattern fullCommentPattern() {
return compile("^<!--.*?-->", DOTALL);
}
private static Pattern openTagPattern(String namePattern) {
return compile(format("^<(?:%s)\\b[^>]*>", namePattern), CASE_INSENSITIVE);
}
private static Pattern closeTagPattern(String namePattern) {
return compile(format("^</(?:%s)\\b[^>]*>", namePattern), CASE_INSENSITIVE);
}
/** Takes a Javadoc comment, including ∕✱✱ and ✱∕, and returns tokens, including ∕✱✱ and ✱∕. */
static ImmutableList<Token> lex(String input) throws LexException {
/*
@@ -153,6 +80,8 @@ final class JavadocLexer {
return NON_UNIX_LINE_ENDING.matcher(input).replaceAll("\n");
}
private static final Pattern NON_UNIX_LINE_ENDING = Pattern.compile("\r\n?");
private static String stripJavadocBeginAndEnd(String input) {
/*
* We do this ahead of time so that the main part of the lexer need not say things like
@@ -163,6 +92,17 @@ final class JavadocLexer {
return input.substring("/**".length(), input.length() - "*/".length());
}
private final CharStream input;
private final NestingCounter braceDepth = new NestingCounter();
private final NestingCounter preDepth = new NestingCounter();
private final NestingCounter codeDepth = new NestingCounter();
private final NestingCounter tableDepth = new NestingCounter();
private boolean somethingSinceNewline;
private JavadocLexer(CharStream input) {
this.input = checkNotNull(input);
}
private ImmutableList<Token> generateTokens() throws LexException {
ImmutableList.Builder<Token> tokens = ImmutableList.builder();
@@ -200,10 +140,8 @@ final class JavadocLexer {
somethingSinceNewline = false;
return preserveExistingFormatting ? FORCED_NEWLINE : WHITESPACE;
} else if (input.tryConsume(" ") || input.tryConsume("\t")) {
// TODO(cpovirk): How about weird whitespace chars? Ideally we'd distinguish breaking
// vs. not.
// Returning LITERAL here prevent us from breaking a <pre> line. For more info, see
// LITERAL.
// TODO(cpovirk): How about weird whitespace chars? Ideally we'd distinguish breaking vs. not.
// Returning LITERAL here prevent us from breaking a <pre> line. For more info, see LITERAL.
return preserveExistingFormatting ? LITERAL : WHITESPACE;
}
@@ -377,8 +315,7 @@ final class JavadocLexer {
output.add(new Token(WHITESPACE, seenWhitespace.toString()));
}
// We have another token coming, possibly of type OTHER. Leave it for the next
// iteration.
// We have another token coming, possibly of type OTHER. Leave it for the next iteration.
}
/*
@@ -411,8 +348,7 @@ final class JavadocLexer {
}
}
} else {
// TODO(cpovirk): Or just `continue` from the <p> case and move this out of the
// `else`?
// TODO(cpovirk): Or just `continue` from the <p> case and move this out of the `else`?
output.add(tokens.next());
}
}
@@ -426,10 +362,6 @@ final class JavadocLexer {
*/
}
private static boolean hasMultipleNewlines(String s) {
return NEWLINE.countIn(s) > 1;
}
/**
* Replaces whitespace after a {@code href=...>} token with an "optional link break." This allows
* us to output either {@code <a href=foo>foo</a>} or {@code <a href=foo>\nfoo</a>}, depending on
@@ -554,5 +486,72 @@ final class JavadocLexer {
}
}
private static final CharMatcher NEWLINE = CharMatcher.is('\n');
private static boolean hasMultipleNewlines(String s) {
return NEWLINE.countIn(s) > 1;
}
/*
* This also eats any trailing whitespace. We would be smart enough to ignore that, anyway --
* except in the case of <pre>/<table>, inside which we otherwise leave whitespace intact.
*
* We'd remove the trailing whitespace later on (in JavaCommentsHelper.rewrite), but I feel safer
* stripping it now: It otherwise might confuse our line-length count, which we use for wrapping.
*/
private static final Pattern NEWLINE_PATTERN = compile("^[ \t]*\n[ \t]*[*]?[ \t]?");
// We ensure elsewhere that we match this only at the beginning of a line.
// Only match tags that start with a lowercase letter, to avoid false matches on unescaped
// annotations inside code blocks.
// Match "@param <T>" specially in case the <T> is a <P> or other HTML tag we treat specially.
private static final Pattern FOOTER_TAG_PATTERN = compile("^@(param\\s+<\\w+>|[a-z]\\w*)");
private static final Pattern MOE_BEGIN_STRIP_COMMENT_PATTERN =
compile("^<!--\\s*M" + "OE:begin_intracomment_strip\\s*-->");
private static final Pattern MOE_END_STRIP_COMMENT_PATTERN =
compile("^<!--\\s*M" + "OE:end_intracomment_strip\\s*-->");
private static final Pattern HTML_COMMENT_PATTERN = fullCommentPattern();
private static final Pattern PRE_OPEN_PATTERN = openTagPattern("pre");
private static final Pattern PRE_CLOSE_PATTERN = closeTagPattern("pre");
private static final Pattern CODE_OPEN_PATTERN = openTagPattern("code");
private static final Pattern CODE_CLOSE_PATTERN = closeTagPattern("code");
private static final Pattern TABLE_OPEN_PATTERN = openTagPattern("table");
private static final Pattern TABLE_CLOSE_PATTERN = closeTagPattern("table");
private static final Pattern LIST_OPEN_PATTERN = openTagPattern("ul|ol|dl");
private static final Pattern LIST_CLOSE_PATTERN = closeTagPattern("ul|ol|dl");
private static final Pattern LIST_ITEM_OPEN_PATTERN = openTagPattern("li|dt|dd");
private static final Pattern LIST_ITEM_CLOSE_PATTERN = closeTagPattern("li|dt|dd");
private static final Pattern HEADER_OPEN_PATTERN = openTagPattern("h[1-6]");
private static final Pattern HEADER_CLOSE_PATTERN = closeTagPattern("h[1-6]");
private static final Pattern PARAGRAPH_OPEN_PATTERN = openTagPattern("p");
private static final Pattern PARAGRAPH_CLOSE_PATTERN = closeTagPattern("p");
private static final Pattern BLOCKQUOTE_OPEN_PATTERN = openTagPattern("blockquote");
private static final Pattern BLOCKQUOTE_CLOSE_PATTERN = closeTagPattern("blockquote");
private static final Pattern BR_PATTERN = openTagPattern("br");
private static final Pattern INLINE_TAG_OPEN_PATTERN = compile("^[{]@\\w*");
/*
* We exclude < so that we don't swallow following HTML tags. This lets us fix up "foo<p>" (~400
* hits in Google-internal code). We will join unnecessarily split "words" (like "foo<b>bar</b>")
* in a later step. There's a similar story for braces. I'm not sure I actually need to exclude @
* or *. TODO(cpovirk): Try removing them.
*
* Thanks to the "rejoin" step in joinAdjacentLiteralsAndAdjacentWhitespace(), we could get away
* with matching only one character here. That would eliminate the need for the regex entirely.
* That might be faster or slower than what we do now.
*/
private static final Pattern LITERAL_PATTERN = compile("^.[^ \t\n@<{}*]*", DOTALL);
private static Pattern fullCommentPattern() {
return compile("^<!--.*?-->", DOTALL);
}
private static Pattern openTagPattern(String namePattern) {
return compile(format("^<(?:%s)\\b[^>]*>", namePattern), CASE_INSENSITIVE);
}
private static Pattern closeTagPattern(String namePattern) {
return compile(format("^</(?:%s)\\b[^>]*>", namePattern), CASE_INSENSITIVE);
}
static class LexException extends Exception {}
}
@@ -27,7 +27,6 @@ import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_OPEN_TA
import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG;
import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.googlejavaformat.java.javadoc.Token.Type;
@@ -39,22 +38,8 @@ import com.google.googlejavaformat.java.javadoc.Token.Type;
* are we inside?"
*/
final class JavadocWriter {
/**
* Tokens that are always pinned to the following token. For example, {@code <p>} in {@code <p>Foo
* bar} (never {@code <p> Foo bar} or {@code <p>\nFoo bar}).
*
* <p>This is not the only kind of "pinning" that we do: See also the joining of LITERAL tokens
* done by the lexer. The special pinning here is necessary because these tokens are not of type
* LITERAL (because they require other special handling).
*/
private static final ImmutableSet<Type> START_OF_LINE_TOKENS =
immutableEnumSet(LIST_ITEM_OPEN_TAG, PARAGRAPH_OPEN_TAG, HEADER_OPEN_TAG);
private final int blockIndent;
private final StringBuilder output = new StringBuilder();
private final NestingCounter continuingListItemCount = new NestingCounter();
private final NestingCounter continuingListCount = new NestingCounter();
private final NestingCounter postWriteModifiedContinuingListCount = new NestingCounter();
/**
* Whether we are inside an {@code <li>} element, excluding the case in which the {@code <li>}
* contains a {@code <ul>} or {@code <ol>} that we are also inside -- unless of course we're
@@ -63,6 +48,9 @@ final class JavadocWriter {
private boolean continuingListItemOfInnermostList;
private boolean continuingFooterTag;
private final NestingCounter continuingListItemCount = new NestingCounter();
private final NestingCounter continuingListCount = new NestingCounter();
private final NestingCounter postWriteModifiedContinuingListCount = new NestingCounter();
private int remainingOnLine;
private boolean atStartOfLine;
private RequestedWhitespace requestedWhitespace = NONE;
@@ -74,95 +62,6 @@ final class JavadocWriter {
this.blockIndent = blockIndent;
}
@Override
public String toString() {
return output.toString();
}
private void requestBlankLine() {
requestWhitespace(BLANK_LINE);
}
private void requestNewline() {
requestWhitespace(NEWLINE);
}
private void writeToken(Token token) {
if (requestedMoeBeginStripComment != null) {
requestNewline();
}
if (requestedWhitespace == BLANK_LINE
&& (postWriteModifiedContinuingListCount.isPositive() || continuingFooterTag)) {
/*
* We don't write blank lines inside lists or footer tags, even in cases where we otherwise
* would (e.g., before a <p> tag). Justification: We don't write blank lines _between_ list
* items or footer tags, so it would be strange to write blank lines _within_ one. Of course,
* an alternative approach would be to go ahead and write blank lines between items/tags,
* either always or only in the case that an item contains a blank line.
*/
requestedWhitespace = NEWLINE;
}
if (requestedWhitespace == BLANK_LINE) {
writeBlankLine();
requestedWhitespace = NONE;
} else if (requestedWhitespace == NEWLINE) {
writeNewline();
requestedWhitespace = NONE;
}
boolean needWhitespace = (requestedWhitespace == WHITESPACE);
/*
* Write a newline if necessary to respect the line limit. (But if we're at the beginning of the
* line, a newline won't help. Or it might help but only by separating "<p>veryverylongword,"
* which goes against our style.)
*/
if (!atStartOfLine && token.length() + (needWhitespace ? 1 : 0) > remainingOnLine) {
writeNewline();
}
if (!atStartOfLine && needWhitespace) {
output.append(" ");
remainingOnLine--;
}
if (requestedMoeBeginStripComment != null) {
output.append(requestedMoeBeginStripComment.getValue());
requestedMoeBeginStripComment = null;
indentForMoeEndStripComment = innerIndent();
requestNewline();
writeToken(token);
return;
}
output.append(token.getValue());
if (!START_OF_LINE_TOKENS.contains(token.getType())) {
atStartOfLine = false;
}
/*
* TODO(cpovirk): We really want the number of "characters," not chars. Figure out what the
* right way of measuring that is (grapheme count (with BreakIterator?)? sum of widths of all
* graphemes? I don't think that our style guide is specific about this.). Moreover, I am
* probably brushing other problems with surrogates, etc. under the table. Hopefully I mostly
* get away with it by joining all non-space, non-tab characters together.
*
* Possibly the "width" question has no right answer:
* http://denisbider.blogspot.com/2015/09/when-monospace-fonts-arent-unicode.html
*/
remainingOnLine -= token.length();
requestedWhitespace = NONE;
wroteAnythingSignificant = true;
}
private void writeBlankLine() {
output.append("\n");
appendSpaces(blockIndent + 1);
output.append("*");
writeNewline();
}
/**
* Requests whitespace between the previously written token and the next written token. The
* request may be honored, or it may be overridden by a request for "more significant" whitespace,
@@ -172,10 +71,6 @@ final class JavadocWriter {
requestWhitespace(WHITESPACE);
}
private void requestWhitespace(RequestedWhitespace requestedWhitespace) {
this.requestedWhitespace = max(requestedWhitespace, this.requestedWhitespace);
}
void requestMoeBeginStripComment(Token token) {
// We queue this up so that we can put it after any requested whitespace.
requestedMoeBeginStripComment = checkNotNull(token);
@@ -190,37 +85,6 @@ final class JavadocWriter {
writeNewline();
}
private void writeNewline() {
writeNewline(AUTO_INDENT);
}
private void writeNewline(AutoIndent autoIndent) {
output.append("\n");
appendSpaces(blockIndent + 1);
output.append("*");
appendSpaces(1);
remainingOnLine = JavadocFormatter.MAX_LINE_LENGTH - blockIndent - 3;
if (autoIndent == AUTO_INDENT) {
appendSpaces(innerIndent());
remainingOnLine -= innerIndent();
}
atStartOfLine = true;
}
private int innerIndent() {
int innerIndent = continuingListItemCount.value() * 4 + continuingListCount.value() * 2;
if (continuingFooterTag) {
innerIndent += 4;
}
return innerIndent;
}
// If this is a hotspot, keep a String of many spaces around, and call append(string, start,
// end).
private void appendSpaces(int count) {
output.append(Strings.repeat(" ", count));
}
void writeEndJavadoc() {
output.append("\n");
appendSpaces(blockIndent + 1);
@@ -246,8 +110,7 @@ final class JavadocWriter {
postWriteModifiedContinuingListCount.reset();
if (!wroteAnythingSignificant) {
// Javadoc consists solely of tags. This is frowned upon in general but OK for
// @Overrides.
// Javadoc consists solely of tags. This is frowned upon in general but OK for @Overrides.
} else if (!continuingFooterTag) {
// First footer tag after a body tag.
requestBlankLine();
@@ -392,6 +255,23 @@ final class JavadocWriter {
writeToken(token);
}
@Override
public String toString() {
return output.toString();
}
private void requestBlankLine() {
requestWhitespace(BLANK_LINE);
}
private void requestNewline() {
requestWhitespace(NEWLINE);
}
private void requestWhitespace(RequestedWhitespace requestedWhitespace) {
this.requestedWhitespace = max(requestedWhitespace, this.requestedWhitespace);
}
/**
* The kind of whitespace that has been requested between the previous and next tokens. The order
* of the values is significant: It goes from lowest priority to highest. For example, if the
@@ -406,8 +286,125 @@ final class JavadocWriter {
;
}
private void writeToken(Token token) {
if (requestedMoeBeginStripComment != null) {
requestNewline();
}
if (requestedWhitespace == BLANK_LINE
&& (postWriteModifiedContinuingListCount.isPositive() || continuingFooterTag)) {
/*
* We don't write blank lines inside lists or footer tags, even in cases where we otherwise
* would (e.g., before a <p> tag). Justification: We don't write blank lines _between_ list
* items or footer tags, so it would be strange to write blank lines _within_ one. Of course,
* an alternative approach would be to go ahead and write blank lines between items/tags,
* either always or only in the case that an item contains a blank line.
*/
requestedWhitespace = NEWLINE;
}
if (requestedWhitespace == BLANK_LINE) {
writeBlankLine();
requestedWhitespace = NONE;
} else if (requestedWhitespace == NEWLINE) {
writeNewline();
requestedWhitespace = NONE;
}
boolean needWhitespace = (requestedWhitespace == WHITESPACE);
/*
* Write a newline if necessary to respect the line limit. (But if we're at the beginning of the
* line, a newline won't help. Or it might help but only by separating "<p>veryverylongword,"
* which goes against our style.)
*/
if (!atStartOfLine && token.length() + (needWhitespace ? 1 : 0) > remainingOnLine) {
writeNewline();
}
if (!atStartOfLine && needWhitespace) {
output.append(" ");
remainingOnLine--;
}
if (requestedMoeBeginStripComment != null) {
output.append(requestedMoeBeginStripComment.getValue());
requestedMoeBeginStripComment = null;
indentForMoeEndStripComment = innerIndent();
requestNewline();
writeToken(token);
return;
}
output.append(token.getValue());
if (!START_OF_LINE_TOKENS.contains(token.getType())) {
atStartOfLine = false;
}
/*
* TODO(cpovirk): We really want the number of "characters," not chars. Figure out what the
* right way of measuring that is (grapheme count (with BreakIterator?)? sum of widths of all
* graphemes? I don't think that our style guide is specific about this.). Moreover, I am
* probably brushing other problems with surrogates, etc. under the table. Hopefully I mostly
* get away with it by joining all non-space, non-tab characters together.
*
* Possibly the "width" question has no right answer:
* http://denisbider.blogspot.com/2015/09/when-monospace-fonts-arent-unicode.html
*/
remainingOnLine -= token.length();
requestedWhitespace = NONE;
wroteAnythingSignificant = true;
}
private void writeBlankLine() {
output.append("\n");
appendSpaces(blockIndent + 1);
output.append("*");
writeNewline();
}
private void writeNewline() {
writeNewline(AUTO_INDENT);
}
private void writeNewline(AutoIndent autoIndent) {
output.append("\n");
appendSpaces(blockIndent + 1);
output.append("*");
appendSpaces(1);
remainingOnLine = JavadocFormatter.MAX_LINE_LENGTH - blockIndent - 3;
if (autoIndent == AUTO_INDENT) {
appendSpaces(innerIndent());
remainingOnLine -= innerIndent();
}
atStartOfLine = true;
}
enum AutoIndent {
AUTO_INDENT,
NO_AUTO_INDENT
}
private int innerIndent() {
int innerIndent = continuingListItemCount.value() * 4 + continuingListCount.value() * 2;
if (continuingFooterTag) {
innerIndent += 4;
}
return innerIndent;
}
// If this is a hotspot, keep a String of many spaces around, and call append(string, start, end).
private void appendSpaces(int count) {
output.append(" ".repeat(count));
}
/**
* Tokens that are always pinned to the following token. For example, {@code <p>} in {@code <p>Foo
* bar} (never {@code <p> Foo bar} or {@code <p>\nFoo bar}).
*
* <p>This is not the only kind of "pinning" that we do: See also the joining of LITERAL tokens
* done by the lexer. The special pinning here is necessary because these tokens are not of type
* LITERAL (because they require other special handling).
*/
private static final ImmutableSet<Type> START_OF_LINE_TOKENS =
immutableEnumSet(LIST_ITEM_OPEN_TAG, PARAGRAPH_OPEN_TAG, HEADER_OPEN_TAG);
}
@@ -20,31 +20,6 @@ package com.google.googlejavaformat.java.javadoc;
* are in {@link JavadocLexer}.
*/
final class Token {
private final Type type;
private final String value;
Token(Type type, String value) {
this.type = type;
this.value = value;
}
@Override
public String toString() {
return "\n" + getType() + ": " + getValue();
}
Type getType() {
return type;
}
String getValue() {
return value;
}
int length() {
return value.length();
}
/**
* Javadoc token type.
*
@@ -128,4 +103,29 @@ final class Token {
LITERAL,
;
}
private final Type type;
private final String value;
Token(Type type, String value) {
this.type = type;
this.value = value;
}
Type getType() {
return type;
}
String getValue() {
return value;
}
int length() {
return value.length();
}
@Override
public String toString() {
return "\n" + getType() + ": " + getValue();
}
}