Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
*/
package com.google.googlejavaformat.java;

import static com.google.common.collect.Iterables.getLast;
import static com.google.common.primitives.Booleans.trueFirst;

import com.google.common.base.CharMatcher;
Expand All @@ -22,13 +21,13 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Range;
import com.google.errorprone.annotations.InlineMe;
import com.google.googlejavaformat.Newlines;
import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
import com.google.googlejavaformat.java.JavaInput.Tok;
import com.sun.tools.javac.parser.Tokens.TokenKind;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.stream.Stream;
Expand All @@ -43,8 +42,15 @@ public class ImportOrderer {
///
/// @throws FormatterException if the input could not be parsed.
public static String reorderImports(String text, Style style) throws FormatterException {
ImmutableList<Tok> toks = JavaInput.buildToks(text, CLASS_START);
return new ImportOrderer(text, toks, style).reorderImports();
ImmutableList<Replacement> replacements = findReplacements(text, style);
if (replacements.isEmpty()) {
return text;
}
Replacement replacement = replacements.get(0);
Range<Integer> range = replacement.replaceRange();
return text.substring(0, range.lowerEndpoint())
+ replacement.replacementString()
+ text.substring(range.upperEndpoint());
}

/// Reorder the inputs in `text`, a complete Java program, in Google style. On success, another
Expand All @@ -54,15 +60,30 @@ public static String reorderImports(String text, Style style) throws FormatterEx
/// @deprecated Use [#reorderImports(String, Style)] instead
/// @throws FormatterException if the input could not be parsed.
@Deprecated
@InlineMe(
replacement = "ImportOrderer.reorderImports(text, Style.GOOGLE)",
imports = {
"com.google.googlejavaformat.java.ImportOrderer",
"com.google.googlejavaformat.java.JavaFormatterOptions.Style"
})
public static String reorderImports(String text) throws FormatterException {
return reorderImports(text, Style.GOOGLE);
}

private String reorderImports() throws FormatterException {
/// Returns a replacement that rewrites the import block, from the start of the first import's
/// line through the whitespace after the last import, or an empty list if the imports are already
/// in order.
static ImmutableList<Replacement> findReplacements(String text, Style style)
throws FormatterException {
ImmutableList<Tok> toks = JavaInput.buildToks(text, CLASS_START);
return new ImportOrderer(text, toks, style).findReplacements();
}

private ImmutableList<Replacement> findReplacements() throws FormatterException {
Optional<Integer> maybeFirstImport = findIdentifier(0, IMPORT_OR_CLASS_START);
if (!maybeFirstImport.isPresent() || !tokenAt(maybeFirstImport.get()).equals("import")) {
// No imports, so nothing to do.
return text;
return ImmutableList.of();
}
int firstImportStart = maybeFirstImport.get();
int unindentedFirstImportStart = unindent(firstImportStart);
Expand All @@ -76,27 +97,31 @@ private String reorderImports() throws FormatterException {
throw new FormatterException("Imports not contiguous (perhaps a comment separates them?)");
}

StringBuilder result = new StringBuilder();
String prefix = tokString(0, unindentedFirstImportStart);
result.append(prefix);
int start = toks.get(unindentedFirstImportStart).getPosition();

StringBuilder replacement = new StringBuilder();
String prefix = text.substring(0, start);
if (!prefix.isEmpty() && Newlines.getLineEnding(prefix) == null) {
result.append(lineSeparator).append(lineSeparator);
}
result.append(reorderedImportsString(imports.imports));

List<String> tail = new ArrayList<>();
tail.add(CharMatcher.whitespace().trimLeadingFrom(tokString(afterLastImport, toks.size())));
if (!toks.isEmpty()) {
Tok lastTok = getLast(toks);
int tailStart = lastTok.getPosition() + lastTok.length();
tail.add(text.substring(tailStart));
replacement.append(lineSeparator).append(lineSeparator);
}
if (tail.stream().anyMatch(s -> !s.isEmpty())) {
result.append(lineSeparator);
tail.forEach(result::append);
replacement.append(reorderedImportsString(imports.imports));

int afterLastImportPos =
afterLastImport < toks.size() ? toks.get(afterLastImport).getPosition() : text.length();
String afterText = text.substring(afterLastImportPos);
String trimmedAfterText = CharMatcher.whitespace().trimLeadingFrom(afterText);
int numTrimmed = afterText.length() - trimmedAfterText.length();
int end = afterLastImportPos + numTrimmed;

if (end < text.length()) {
replacement.append(lineSeparator);
}

return result.toString();
String replacementString = replacement.toString();
if (replacementString.equals(text.substring(start, end))) {
return ImmutableList.of();
}
return ImmutableList.of(Replacement.create(start, end, replacementString));
}

/// [TokenKind]s that indicate the start of a type definition. We use this to avoid scanning the
Expand Down Expand Up @@ -236,14 +261,6 @@ public String toString() {
}
}

private String tokString(int start, int end) {
StringBuilder sb = new StringBuilder();
for (int i = start; i < end; i++) {
sb.append(toks.get(i).getOriginalText());
}
return sb.toString();
}

private record ImportsAndIndex(ImmutableSortedSet<Import> imports, int index) {}

/// Scans a sequence of import lines. The parsing uses this approximate grammar:
Expand Down
49 changes: 33 additions & 16 deletions core/src/main/java/com/google/googlejavaformat/java/JavaInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
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.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
Expand Down Expand Up @@ -243,13 +242,10 @@
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.
*/
// The following lists record the sequential indices of the `Tok`s on each input line. (Only
// tokens and comments have sequential indices.) Tokens and `//` comments lie on just one line;
// `/*` 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.
Expand Down Expand Up @@ -345,14 +341,16 @@
log.useSource(
new SimpleJavaFileObject(URI.create("Source.java"), Kind.SOURCE) {
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
public String getCharContent(boolean ignoreEncodingErrors) {
return text;
}
});
DeferredDiagnosticHandler diagnostics = deferredDiagnosticHandler(log);
ImmutableList<RawTok> rawToks = JavacTokens.getTokens(text, context, stopTokens);
Collection<JCDiagnostic> ds;
try {
// GET_DIAGNOSTICS is DeferredDiagnosticHandler#getDiagnostics, which returns a
// Collection<JCDiagnostic> on all supported JDK versions
@SuppressWarnings("unchecked")
var extraLocalForSuppression = (Collection<JCDiagnostic>) GET_DIAGNOSTICS.invoke(diagnostics);
ds = extraLocalForSuppression;
Expand Down Expand Up @@ -428,7 +426,7 @@
// contains Unicode escapes.
isToken = true;
isNumbered = true;
for (char c : tokText.toCharArray()) {

Check warning on line 429 in core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

View workflow job for this annotation

GitHub Actions / JDK 25 on ubuntu-latest

[LoopOverCharArray] toCharArray allocates a new array, using charAt is more efficient

Check warning on line 429 in core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

View workflow job for this annotation

GitHub Actions / JDK 25 on ubuntu-latest

[LoopOverCharArray] toCharArray allocates a new array, using charAt is more efficient

Check warning on line 429 in core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

View workflow job for this annotation

GitHub Actions / JDK 25 on ubuntu-latest

[LoopOverCharArray] toCharArray allocates a new array, using charAt is more efficient

Check warning on line 429 in core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

View workflow job for this annotation

GitHub Actions / JDK 25 on ubuntu-latest

[LoopOverCharArray] toCharArray allocates a new array, using charAt is more efficient

Check warning on line 429 in core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

View workflow job for this annotation

GitHub Actions / JDK 21 on ubuntu-latest

[LoopOverCharArray] toCharArray allocates a new array, using charAt is more efficient

Check warning on line 429 in core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

View workflow job for this annotation

GitHub Actions / JDK 21 on ubuntu-latest

[LoopOverCharArray] toCharArray allocates a new array, using charAt is more efficient

Check warning on line 429 in core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

View workflow job for this annotation

GitHub Actions / JDK 25 on macos-latest

[LoopOverCharArray] toCharArray allocates a new array, using charAt is more efficient

Check warning on line 429 in core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

View workflow job for this annotation

GitHub Actions / JDK 25 on macos-latest

[LoopOverCharArray] toCharArray allocates a new array, using charAt is more efficient

Check warning on line 429 in core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

View workflow job for this annotation

GitHub Actions / JDK 25 on macos-latest

[LoopOverCharArray] toCharArray allocates a new array, using charAt is more efficient

Check warning on line 429 in core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

View workflow job for this annotation

GitHub Actions / JDK 25 on macos-latest

[LoopOverCharArray] toCharArray allocates a new array, using charAt is more efficient

Check warning on line 429 in core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

View workflow job for this annotation

GitHub Actions / JDK 21 on ubuntu-latest

[LoopOverCharArray] toCharArray allocates a new array, using charAt is more efficient

Check warning on line 429 in core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

View workflow job for this annotation

GitHub Actions / JDK 21 on ubuntu-latest

[LoopOverCharArray] toCharArray allocates a new array, using charAt is more efficient

Check warning on line 429 in core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

View workflow job for this annotation

GitHub Actions / Publish snapshot

[LoopOverCharArray] toCharArray allocates a new array, using charAt is more efficient
strings.add(String.valueOf(c));
}
}
Expand Down Expand Up @@ -584,22 +582,41 @@
&& tok.getText().matches("\\/\\*[A-Za-z0-9\\s_\\-]+=\\s*\\*\\/");
}

/// Convert from a character range to a token range.
/// Checks that `characterRange` ends within a text of length `textLength`.
///
/// @param characterRange the `0`-based [Range] of characters
/// @return the `0`-based [Range] of tokens
/// @throws FormatterException if the upper endpoint of the range is outside the file
private Range<Integer> characterRangeToTokenRange(Range<Integer> characterRange)
/// @throws FormatterException if the upper endpoint of the range is outside the text
static void validateRange(Range<Integer> characterRange, int textLength)
throws FormatterException {
if (characterRange.upperEndpoint() > text.length()) {
if (characterRange.upperEndpoint() > textLength) {
throw new FormatterException(
String.format(
"error: invalid offset (%d) or length (%d); offset + length (%d) > file length (%d)",
characterRange.lowerEndpoint(),
characterRange.upperEndpoint() - characterRange.lowerEndpoint(),
characterRange.upperEndpoint(),
text.length()));
textLength));
}
}

/// Checks that every range in `characterRanges` ends within a text of length `textLength`.
///
/// @throws FormatterException if the upper endpoint of any range is outside the text
static void validateRanges(Collection<Range<Integer>> characterRanges, int textLength)
throws FormatterException {
for (Range<Integer> characterRange : characterRanges) {
validateRange(characterRange, textLength);
}
}

/// Convert from a character range to a token range.
///
/// @param characterRange the `0`-based [Range] of characters
/// @return the `0`-based [Range] of tokens
/// @throws FormatterException if the upper endpoint of the range is outside the file
private Range<Integer> characterRangeToTokenRange(Range<Integer> characterRange)
throws FormatterException {
validateRange(characterRange, text.length());

// empty range stands for "format the line under the cursor"
Range<Integer> nonEmptyRange =
characterRange.isEmpty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,19 @@
import com.google.common.collect.Ordering;
import com.google.common.collect.Range;
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 com.sun.tools.javac.parser.Tokens.TokenKind;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.lang.model.element.Modifier;
import org.jspecify.annotations.Nullable;

/// Fixes sequences of modifiers to be in JLS order.
final class ModifierOrderer {

/// Reorders all modifiers in the given text to be in JLS order.
static JavaInput reorderModifiers(String text) throws FormatterException {
return reorderModifiers(
new JavaInput(text), ImmutableList.of(Range.closedOpen(0, text.length())));
}

/// A class that contains the tokens corresponding to a modifier. This is usually a single token
/// (e.g. for `public`), but may be multiple tokens for modifiers containing `-` (e.g.
/// `non-sealed`).
Expand Down Expand Up @@ -95,6 +86,12 @@ public int compareTo(ModifierTokens o) {
}
}

/// Reorders all modifiers in the given text to be in JLS order.
static JavaInput reorderModifiers(String text) throws FormatterException {
return reorderModifiers(
new JavaInput(text), ImmutableList.of(Range.closedOpen(0, text.length())));
}

/// Reorders all modifiers in the given text and within the given character ranges to be in JLS
/// order.
static JavaInput reorderModifiers(JavaInput javaInput, Collection<Range<Integer>> characterRanges)
Expand All @@ -104,9 +101,17 @@ static JavaInput reorderModifiers(JavaInput javaInput, Collection<Range<Integer>
// Errors about invalid input will be reported later after parsing.
return javaInput;
}
return applyReplacements(javaInput, findModifierReplacements(javaInput, characterRanges));
}

static ImmutableList<Replacement> findModifierReplacements(
JavaInput javaInput, Collection<Range<Integer>> characterRanges) throws FormatterException {
ImmutableList.Builder<Replacement> replacements = ImmutableList.builder();
if (javaInput.getTokens().isEmpty()) {
return replacements.build();
}
RangeSet<Integer> tokenRanges = javaInput.characterRangesToTokenRanges(characterRanges);
Iterator<? extends Token> it = javaInput.getTokens().iterator();
TreeRangeMap<Integer, String> replacements = TreeRangeMap.create();
while (it.hasNext()) {
ModifierTokens tokens = getModifierTokens(it);
if (tokens.isEmpty()
Expand Down Expand Up @@ -142,10 +147,10 @@ static JavaInput reorderModifiers(JavaInput javaInput, Collection<Range<Integer>
addTrivia(replacement, modifierTokens.get(i).getToksAfter());
}
}
replacements.put(Range.closedOpen(begin, end), replacement.toString());
replacements.add(Replacement.create(begin, end, replacement.toString()));
}
}
return applyReplacements(javaInput, replacements);
return replacements.build();
}

private static void addTrivia(StringBuilder replacement, ImmutableList<? extends Tok> toks) {
Expand Down Expand Up @@ -214,18 +219,15 @@ private static void addTrivia(StringBuilder replacement, ImmutableList<? extends
}

/// 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
// differ in size from the input
Map<Range<Integer>, String> ranges = replacementMap.asDescendingMapOfRanges();
if (ranges.isEmpty()) {
private static JavaInput applyReplacements(JavaInput javaInput, List<Replacement> replacements)
throws FormatterException {
if (replacements.isEmpty()) {
return javaInput;
}
StringBuilder sb = new StringBuilder(javaInput.getText());
for (Entry<Range<Integer>, String> entry : ranges.entrySet()) {
Range<Integer> range = entry.getKey();
sb.replace(range.lowerEndpoint(), range.upperEndpoint(), entry.getValue());
for (Replacement replacement : replacements.reversed()) {
Range<Integer> range = replacement.replaceRange();
sb.replace(range.lowerEndpoint(), range.upperEndpoint(), replacement.replacementString());
}
return new JavaInput(sb.toString());
}
Expand Down
Loading
Loading