From 10c6de0ea713d001912061520cf994965ca81fee Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Thu, 29 Oct 2026 03:49:02 -0700 Subject: [PATCH] Remove single-parameter lambda parentheses in the cleanup pass. Extend `Cleanups` to remove parentheses around single-parameter untyped lambda parameters (`(x) -> x` -> `x -> x`), preserving comments inside the parentheses and inserting a separating space when the opening parenthesis is immediately preceded by an identifier (e.g. `return(x) -> x` -> `return x -> x`). PiperOrigin-RevId: 1009572816 --- .../googlejavaformat/java/CleanupScanner.java | 40 ++++++++- .../googlejavaformat/java/CleanupsTest.java | 89 +++++++++++++++++++ .../java/FormatterIntegrationTest.java | 2 + 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/google/googlejavaformat/java/CleanupScanner.java b/core/src/main/java/com/google/googlejavaformat/java/CleanupScanner.java index 9a0eb292f..b36167630 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/CleanupScanner.java +++ b/core/src/main/java/com/google/googlejavaformat/java/CleanupScanner.java @@ -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; @@ -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 @@ -316,6 +317,33 @@ private void deleteSemicolonsInGaps(int start, int end, RangeSet 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) { @@ -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); diff --git a/core/src/test/java/com/google/googlejavaformat/java/CleanupsTest.java b/core/src/test/java/com/google/googlejavaformat/java/CleanupsTest.java index a78fe9d6b..3e685ed8a 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/CleanupsTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/CleanupsTest.java @@ -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 f1 = (x) -> x; + Function f2 = (_) -> ""; + Function f3 = (String x) -> x; + Function f4 = (var x) -> x; + Function f5 = (@Anno var x) -> x; + Function f6 = (/* keep */ x) -> x; + BiFunction f7 = (x, y) -> x; + Supplier f8 = () -> ""; + Function> f9 = (x) -> (y) -> y; + + Function g() { + return(x) -> x; + } + } + """; + String expected = + """ + import java.util.function.BiFunction; + import java.util.function.Function; + import java.util.function.Supplier; + + class T { + Function f1 = x -> x; + Function f2 = _ -> ""; + Function f3 = (String x) -> x; + Function f4 = (var x) -> x; + Function f5 = (@Anno var x) -> x; + Function f6 = (/* keep */ x) -> x; + BiFunction f7 = (x, y) -> x; + Supplier f8 = () -> ""; + Function> f9 = x -> y -> y; + + Function g() { + return x -> x; + } + } + """; + assertThat(formatWithCleanups(input)).isEqualTo(expected); + } + + @Test + public void singleParameterLambdaParens_outsideRange() throws Exception { + String input = + """ + class T { + Function 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 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 fn = (s) -> s; + } } """; String expected = """ + import java.util.function.Function; + @Anno class T { int x; + + void f() { + Runnable r = () -> {}; + Function fn = s -> s; + } } """; assertThat(formatWithCleanupsAosp(input)).isEqualTo(expected); diff --git a/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java b/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java index 7f1f5042e..f9c3ebbdb 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java @@ -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}")