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 @@ -36,6 +36,7 @@
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.EmptyStatementTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
Expand All @@ -49,7 +50,7 @@
import org.jspecify.annotations.Nullable;

/// Scans the compilation unit for non-whitespace cleanup opportunities, such as removing redundant
/// semicolons and empty annotation parentheses.
/// semicolons, empty annotation parentheses, and single-parameter lambda parentheses.
final class CleanupScanner extends TreePathScanner<@Nullable Void, @Nullable Void> {

// Replace with Flags.IMPLICIT_CLASS once JDK 25 is the minimum supported version
Expand Down Expand Up @@ -316,6 +317,33 @@ private void deleteSemicolonsInGaps(int start, int end, RangeSet<Integer> validS
return null;
}

@Override
public @Nullable Void visitLambdaExpression(LambdaExpressionTree tree, @Nullable Void unused) {
super.visitLambdaExpression(tree, null);
if (tree.getParameters().size() != 1) {
return null;
}
int startPos = getStartPosition(tree);
int lparenIdx = nextTokenIndex(tokIndexAtOrAfter(startPos));
if (lparenIdx < 0 || !toks.get(lparenIdx).getText().equals("(")) {
return null;
}
int paramIdx = nextTokenIndex(lparenIdx + 1);
int rparenIdx = nextTokenIndex(paramIdx + 1);
if (rparenIdx < 0 || !toks.get(rparenIdx).getText().equals(")")) {
return null;
}
Tok lparen = toks.get(lparenIdx);
Tok rparen = toks.get(rparenIdx);
int lparenStart = lparen.getPosition();
int rparenEnd = rparen.getPosition() + rparen.length();
if (!hasCommentsInRange(lparenStart, rparenEnd) && inRange(lparenStart, rparenEnd)) {
activeDeletions.add(Range.closedOpen(lparenStart, lparenStart + lparen.length()));
activeDeletions.add(Range.closedOpen(rparen.getPosition(), rparenEnd));
}
return null;
}

// Hand-rolled lower-bound binary search to avoid allocating a dummy Tok or wrapper list for
// Collections.binarySearch.
private int tokIndexAtOrAfter(int pos) {
Expand All @@ -332,6 +360,16 @@ private int tokIndexAtOrAfter(int pos) {
return lo;
}

private int nextTokenIndex(int startIdx) {
for (int i = max(0, startIdx); i < toks.size(); i++) {
Tok tok = toks.get(i);
if (tok.isToken() && !tok.getText().isEmpty()) {
return i;
}
}
return -1;
}

private @Nullable Tok findTokenAtOrAfter(int startPos, int endPos, String text) {
for (int i = tokIndexAtOrAfter(startPos); i < toks.size(); i++) {
Tok tok = toks.get(i);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,20 +461,109 @@ void f(String @Anno ... xs) {}
assertThat(formatWithCleanups(input)).isEqualTo(expected);
}

@Test
public void singleParameterLambdaParens() throws Exception {
String input =
"""
import java.util.function.Function;
import java.util.function.BiFunction;
import java.util.function.Supplier;

class T {
Function<String, String> f1 = (x) -> x;
Function<String, String> f2 = (_) -> "";
Function<String, String> f3 = (String x) -> x;
Function<String, String> f4 = (var x) -> x;
Function<String, String> f5 = (@Anno var x) -> x;
Function<String, String> f6 = (/* keep */ x) -> x;
BiFunction<String, String, String> f7 = (x, y) -> x;
Supplier<String> f8 = () -> "";
Function<String, Function<String, String>> f9 = (x) -> (y) -> y;

Function<String, String> g() {
return(x) -> x;
}
}
""";
String expected =
"""
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

class T {
Function<String, String> f1 = x -> x;
Function<String, String> f2 = _ -> "";
Function<String, String> f3 = (String x) -> x;
Function<String, String> f4 = (var x) -> x;
Function<String, String> f5 = (@Anno var x) -> x;
Function<String, String> f6 = (/* keep */ x) -> x;
BiFunction<String, String, String> f7 = (x, y) -> x;
Supplier<String> f8 = () -> "";
Function<String, Function<String, String>> f9 = x -> y -> y;

Function<String, String> g() {
return x -> x;
}
}
""";
assertThat(formatWithCleanups(input)).isEqualTo(expected);
}

@Test
public void singleParameterLambdaParens_outsideRange() throws Exception {
String input =
"""
class T {
Function<String, String> f = (x) -> {
return x;
};
}
""";
// Partial formatting of a lambda body reformats the enclosing statement, but the cleanup only
// applies to the requested range, so the parentheses on the unrequested line are kept.
String expected =
"""
class T {
Function<String, String> f =
(x) -> {
return x;
};
}
""";
int start = input.indexOf("return");
int end = input.indexOf(';', start) + 1;
assertThat(new Formatter().formatSource(input, ImmutableList.of(Range.closedOpen(start, end))))
.isEqualTo(expected);
}

@Test
public void aospStyleCleanups() throws Exception {
String input =
"""
import java.util.function.Function;

@Anno()
class T {
int x;;
void f() {
Runnable r = () -> {};
Function<String, String> fn = (s) -> s;
}
}
""";
String expected =
"""
import java.util.function.Function;

@Anno
class T {
int x;

void f() {
Runnable r = () -> {};
Function<String, String> fn = s -> s;
}
}
""";
assertThat(formatWithCleanupsAosp(input)).isEqualTo(expected);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ public class FormatterIntegrationTest {
"i281")
// Empty annotation parens (b/381242320)
.add("B381242320")
// Single-parameter lambda parentheses (b/154644558)
.add("B21305044")
.build();

@Parameters(name = "{index}: {0}")
Expand Down
Loading