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 @@ -26,6 +26,13 @@ ImmutableList<Replacement> findReplacements(Cleanups cleanups) throws FormatterE
cleanups.context(), cleanups.unit(), cleanups.contents());
}
},
/// AST cleanups: missing control-flow braces.
AST_CLEANUPS {
@Override
ImmutableList<Replacement> findReplacements(Cleanups cleanups) throws FormatterException {
return CleanupScanner.scan(cleanups.unit(), cleanups.contents(), cleanups.ranges());
}
},
/// Reorders modifiers into JLS order.
REORDER_MODIFIERS {
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/*
* Copyright 2026 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/

package com.google.googlejavaformat.java;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.googlejavaformat.java.Trees.getEndPosition;
import static com.google.googlejavaformat.java.Trees.getStartPosition;
import static java.util.Comparator.comparingInt;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;
import com.google.googlejavaformat.java.JavaInput.Tok;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.DoWhileLoopTree;
import com.sun.source.tree.EmptyStatementTree;
import com.sun.source.tree.EnhancedForLoopTree;
import com.sun.source.tree.ForLoopTree;
import com.sun.source.tree.IfTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.WhileLoopTree;
import com.sun.source.util.TreePathScanner;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import org.jspecify.annotations.Nullable;

/// Scans the compilation unit for non-whitespace cleanup opportunities, inserting missing
/// control-flow braces.
final class CleanupScanner extends TreePathScanner<@Nullable Void, @Nullable Void> {

static ImmutableList<Replacement> scan(
JCCompilationUnit unit, String contents, Collection<Range<Integer>> ranges)
throws FormatterException {
RangeSet<Integer> targetRanges = TreeRangeSet.create(ranges);
ImmutableList<Tok> toks = JavaInput.buildToks(contents, ImmutableSet.of());
RangeSet<Integer> activeDeletions = TreeRangeSet.create();
NavigableMap<Integer, StringBuilder> activeInsertions = new TreeMap<>();
new CleanupScanner(unit, toks, targetRanges, activeDeletions, activeInsertions)
.scan(unit, null);
if (activeDeletions.isEmpty() && activeInsertions.isEmpty()) {
return ImmutableList.of();
}
List<Replacement> replacements = new ArrayList<>();
for (Range<Integer> del : activeDeletions.asRanges()) {
int dStart = del.lowerEndpoint();
int dEnd = del.upperEndpoint();
var subMap =
activeInsertions.subMap(dStart, /* fromInclusive= */ true, dEnd, /* toInclusive= */ true);
String rep = String.join("", subMap.values());
subMap.clear();
replacements.add(Replacement.create(dStart, dEnd, rep));
}
for (Map.Entry<Integer, StringBuilder> entry : activeInsertions.entrySet()) {
int pos = entry.getKey();
replacements.add(Replacement.create(pos, pos, entry.getValue().toString()));
}
replacements.sort(comparingInt(r -> r.replaceRange().lowerEndpoint()));
verifyCommentsPreserved(toks, contents, replacements);
return ImmutableList.copyOf(replacements);
}

private static void verifyCommentsPreserved(
ImmutableList<Tok> oldToks, String contents, List<Replacement> replacements)
throws FormatterException {
StringBuilder sb = new StringBuilder(contents.length());
int cursor = 0;
for (Replacement replacement : replacements) {
Range<Integer> range = replacement.replaceRange();
sb.append(contents, cursor, range.lowerEndpoint());
sb.append(replacement.replacementString());
cursor = range.upperEndpoint();
}
sb.append(contents, cursor, contents.length());
String newContents = sb.toString();

ImmutableList<String> expectedComments =
oldToks.stream()
.filter(Tok::isComment)
.map(Tok::getOriginalText)
.collect(toImmutableList());
ImmutableList<String> actualComments =
JavaInput.buildToks(newContents, ImmutableSet.of()).stream()
.filter(Tok::isComment)
.map(Tok::getOriginalText)
.collect(toImmutableList());
if (!expectedComments.equals(actualComments)) {
throw new FormatterException("error: cleanups modified comments");
}
}

private final JCCompilationUnit unit;
private final ImmutableList<Tok> toks;
private final RangeSet<Integer> targetRanges;
private final RangeSet<Integer> activeDeletions;
private final NavigableMap<Integer, StringBuilder> activeInsertions;

CleanupScanner(
JCCompilationUnit unit,
ImmutableList<Tok> toks,
RangeSet<Integer> targetRanges,
RangeSet<Integer> activeDeletions,
NavigableMap<Integer, StringBuilder> activeInsertions) {
this.unit = unit;
this.toks = toks;
this.targetRanges = targetRanges;
this.activeDeletions = activeDeletions;
this.activeInsertions = activeInsertions;
}

private boolean inRange(int startPos, int endPos) {
return targetRanges.intersects(Range.closedOpen(startPos, endPos));
}

private void insert(int pos, String text) {
activeInsertions.computeIfAbsent(pos, k -> new StringBuilder()).append(text);
}

@Override
public @Nullable Void visitIf(IfTree tree, @Nullable Void unused) {
super.visitIf(tree, null);
checkBraces(tree, tree.getThenStatement());
if (!(tree.getElseStatement() instanceof IfTree)) {
checkBraces(tree, tree.getElseStatement());
}
return null;
}

@Override
public @Nullable Void visitForLoop(ForLoopTree tree, @Nullable Void unused) {
super.visitForLoop(tree, null);
checkBraces(tree, tree.getStatement());
return null;
}

@Override
public @Nullable Void visitEnhancedForLoop(EnhancedForLoopTree tree, @Nullable Void unused) {
super.visitEnhancedForLoop(tree, null);
checkBraces(tree, tree.getStatement());
return null;
}

@Override
public @Nullable Void visitWhileLoop(WhileLoopTree tree, @Nullable Void unused) {
super.visitWhileLoop(tree, null);
checkBraces(tree, tree.getStatement());
return null;
}

@Override
public @Nullable Void visitDoWhileLoop(DoWhileLoopTree tree, @Nullable Void unused) {
super.visitDoWhileLoop(tree, null);
checkBraces(tree, tree.getStatement());
return null;
}

private void checkBraces(Tree owner, @Nullable StatementTree body) {
if (body == null || body instanceof BlockTree) {
return;
}
int rangeEnd =
owner instanceof DoWhileLoopTree ? getEndPosition(owner, unit) : getEndPosition(body, unit);
if (!inRange(getStartPosition(owner), rangeEnd)) {
return;
}
Tok prev = prevToken(getStartPosition(body));
if (prev == null) {
return;
}
insert(prev.getPosition() + prev.length(), "{");
insert(closingBraceInsertionPos(body), "}");
if (body instanceof EmptyStatementTree) {
activeDeletions.add(Range.closedOpen(getStartPosition(body), getEndPosition(body, unit)));
}
}

private int closingBraceInsertionPos(StatementTree stmt) {
int stmtEnd = getEndPosition(stmt, unit);
if (stmt instanceof EmptyStatementTree) {
return stmtEnd;
}
boolean sawComment = false;
for (int i = tokIndexAtOrAfter(stmtEnd); i < toks.size(); i++) {
Tok tok = toks.get(i);
if (tok.isToken()) {
return stmtEnd;
}
if (tok.isNewline()) {
return sawComment ? tok.getPosition() + tok.length() : stmtEnd;
}
if (tok.isComment()) {
sawComment = true;
}
}
return stmtEnd;
}

// Hand-rolled lower-bound binary search to avoid allocating a dummy Tok or wrapper list for
// Collections.binarySearch.
private int tokIndexAtOrAfter(int pos) {
int lo = 0;
int hi = toks.size();
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (toks.get(mid).getPosition() < pos) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo;
}

private @Nullable Tok prevToken(int pos) {
for (int i = tokIndexAtOrAfter(pos) - 1; i >= 0; i--) {
Tok tok = toks.get(i);
if (tok.isToken() && !tok.getText().isEmpty()) {
return tok;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,14 @@ private record Parsed(Context context, JCCompilationUnit unit) {}

/// Returns the enabled passes for formatting or fixing imports.
static ImmutableSet<CleanupPass> enabledPasses(
boolean removeUnusedImports, boolean reorderModifiers, boolean sortImports) {
boolean cleanup, boolean removeUnusedImports, boolean reorderModifiers, boolean sortImports) {
EnumSet<CleanupPass> passes = EnumSet.noneOf(CleanupPass.class);
if (removeUnusedImports) {
passes.add(CleanupPass.REMOVE_UNUSED_IMPORTS);
}
if (cleanup) {
passes.add(CleanupPass.AST_CLEANUPS);
}
if (reorderModifiers) {
passes.add(CleanupPass.REORDER_MODIFIERS);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
/// @param setExitIfChanged Return exit code 1 if there are any formatting changes.
/// @param assumeFilename Return the name to use for diagnostics when formatting standard input.
/// @param reorderModifiers Reorder modifiers into the JLS-recommended order.
/// @param cleanup Enables non-whitespace cleanups.
record CommandLineOptions(
ImmutableList<String> files,
boolean inPlace,
Expand All @@ -57,7 +58,8 @@ record CommandLineOptions(
Optional<String> assumeFilename,
boolean reflowLongStrings,
boolean formatJavadoc,
boolean reorderModifiers) {
boolean reorderModifiers,
boolean cleanup) {

boolean aosp() {
return style().isAosp();
Expand All @@ -80,6 +82,7 @@ static Builder builder() {
.reflowLongStrings(true)
.formatJavadoc(true)
.reorderModifiers(true)
.cleanup(true)
.version(false)
.help(false)
.stdin(false)
Expand Down Expand Up @@ -140,6 +143,8 @@ default Builder addLength(Integer length) {

Builder reorderModifiers(boolean reorderModifiers);

Builder cleanup(boolean cleanup);

CommandLineOptions build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@ static CommandLineOptions parse(Iterable<String> options) {
case "--skip-reflowing-long-strings" -> optionsBuilder.reflowLongStrings(false);
case "--skip-javadoc-formatting" -> optionsBuilder.formatJavadoc(false);
case "--skip-reordering-modifiers" -> optionsBuilder.reorderModifiers(false);
case "--cleanup" -> {
if (value == null) {
optionsBuilder.cleanup(true);
} else {
switch (value) {
case "true" -> optionsBuilder.cleanup(true);
case "false" -> optionsBuilder.cleanup(false);
default -> throw new IllegalArgumentException("unexpected --cleanup value: " + value);
}
}
}
case "--max-line-length" -> {
int length = parseInteger(it, flag, value);
checkArgument(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ String formatSource(
boolean reflowLongStrings)
throws FormatterException {
ImmutableSet<CleanupPass> passes =
Cleanups.enabledPasses(removeUnusedImports, options.reorderModifiers(), sortImports);
Cleanups.enabledPasses(
options.cleanup(), removeUnusedImports, options.reorderModifiers(), sortImports);
Cleanups.Result cleaned = Cleanups.apply(input, characterRanges, options.style(), passes);
String formatted =
JavaOutput.applyReplacements(
Expand Down Expand Up @@ -231,7 +232,8 @@ public static String fixImports(
boolean sortImports)
throws FormatterException {
ImmutableSet<CleanupPass> passes =
Cleanups.enabledPasses(removeUnusedImports, /* reorderModifiers= */ false, sortImports);
Cleanups.enabledPasses(
/* cleanup= */ false, removeUnusedImports, /* reorderModifiers= */ false, sortImports);
Cleanups.Result result =
Cleanups.apply(input, ImmutableList.of(Range.closedOpen(0, input.length())), style, passes);
return result.contents();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@

/// Options for a google-java-format invocation.
///
/// Like gofmt, the google-java-format CLI exposes *no* configuration options (aside from `--aosp`).
/// The google-java-format CLI exposes very few configuration options.
///
/// The goal of google-java-format is to provide consistent formatting, and to free developers from
/// arguments over style choices. It is an explicit non-goal to support developers' individual
/// preferences, and in fact it would work directly against our primary goals.
///
/// @param cleanup Whether to enable non-whitespace cleanups (applies to formatSource* and the CLI
/// only).
/// @param style Returns the code style.
@Immutable
public record JavaFormatterOptions(boolean formatJavadoc, boolean reorderModifiers, Style style) {
public record JavaFormatterOptions(
boolean formatJavadoc, boolean reorderModifiers, boolean cleanup, Style style) {
public JavaFormatterOptions {
requireNonNull(style, "style");
}
Expand Down Expand Up @@ -229,7 +232,8 @@ public static Builder builder() {
return new AutoBuilder_JavaFormatterOptions_Builder()
.style(Style.GOOGLE)
.formatJavadoc(true)
.reorderModifiers(true);
.reorderModifiers(true)
.cleanup(true);
}

/// A builder for [JavaFormatterOptions].
Expand All @@ -242,6 +246,9 @@ public abstract static class Builder {

public abstract Builder reorderModifiers(boolean reorderModifiers);

/// Sets whether to enable non-whitespace cleanups (applies to formatSource* and the CLI only).
public abstract Builder cleanup(boolean cleanup);

public abstract JavaFormatterOptions build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public int format(String... args) throws UsageException {
.style(parameters.style())
.formatJavadoc(parameters.formatJavadoc())
.reorderModifiers(parameters.reorderModifiers())
.cleanup(parameters.cleanup())
.build();

if (parameters.stdin()) {
Expand Down
Loading
Loading