From 8f35ddf93a0c5c17b36db4f574fdf5afe6e6dab4 Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:08:15 +0700 Subject: [PATCH 1/2] Fix FormatString false positive for switch expression arguments When a switch expression is passed directly to printf, javac types the expression as Object. Infer the result type from case bodies so format validation uses the real primitive or reference type. Fixes google/error-prone#5831 --- .../formatstring/FormatStringValidation.java | 57 ++++++++++++++++++- .../formatstring/FormatStringTest.java | 30 ++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/FormatStringValidation.java b/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/FormatStringValidation.java index 660bcb32750..96f1711c633 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/FormatStringValidation.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/FormatStringValidation.java @@ -26,9 +26,14 @@ import com.google.errorprone.VisitorState; import com.google.errorprone.suppliers.Supplier; import com.google.errorprone.util.ASTHelpers; +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.CaseTree; import com.sun.source.tree.ConditionalExpressionTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.StatementTree; +import com.sun.source.tree.SwitchExpressionTree; import com.sun.source.tree.Tree; +import com.sun.source.tree.YieldTree; import com.sun.source.util.SimpleTreeVisitor; import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Type; @@ -150,14 +155,64 @@ protected Void defaultAction(Tree tree, Void unused) { * or {@link Integer}. */ private static @Nullable Object getInstance(Tree tree, VisitorState state) { + tree = ASTHelpers.stripParentheses(tree); Object value = ASTHelpers.constValue(tree); if (value != null) { return value; } - Type type = ASTHelpers.getType(tree); + Type type = getExpressionType(tree, state); return getInstance(type, state); } + private static @Nullable Type getExpressionType(Tree tree, VisitorState state) { + tree = ASTHelpers.stripParentheses(tree); + if (tree instanceof SwitchExpressionTree switchExpression) { + Type fromCases = switchExpressionResultType(switchExpression, state); + if (fromCases != null && fromCases.getKind() != TypeKind.ERROR) { + return fromCases; + } + } + return ASTHelpers.getType(tree); + } + + private static @Nullable Type switchExpressionResultType( + SwitchExpressionTree switchExpression, VisitorState state) { + Types types = state.getTypes(); + Type common = null; + for (CaseTree caseTree : switchExpression.getCases()) { + Type caseType = getSwitchCaseResultType(caseTree, state); + if (caseType == null || caseType.getKind() == TypeKind.ERROR) { + continue; + } + Type normalized = types.unboxedTypeOrType(types.erasure(caseType)); + if (common == null) { + common = normalized; + } else if (!types.isSameType(common, normalized)) { + return null; + } + } + return common; + } + + private static @Nullable Type getSwitchCaseResultType(CaseTree caseTree, VisitorState state) { + Tree body = caseTree.getBody(); + if (body == null) { + return null; + } + body = ASTHelpers.stripParentheses(body); + if (body instanceof ExpressionTree expressionTree) { + return ASTHelpers.getType(expressionTree); + } + if (body instanceof BlockTree blockTree) { + for (StatementTree statement : blockTree.getStatements()) { + if (statement instanceof YieldTree yieldTree) { + return getExpressionType(yieldTree.getValue(), state); + } + } + } + return ASTHelpers.getType(body); + } + private static @Nullable Object getInstance(Type type, VisitorState state) { Types types = state.getTypes(); if (type.getKind() == TypeKind.NULL) { diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/formatstring/FormatStringTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/formatstring/FormatStringTest.java index ca69f6e2c79..ecce7cb5607 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/formatstring/FormatStringTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/formatstring/FormatStringTest.java @@ -466,4 +466,34 @@ public static void main() { """) .doTest(); } + + @Test + public void switchExpressionArgument() { + compilationHelper + .addSourceLines( + "Test.java", + """ + class Test { + static final int FRIDAY = 5; + + void f() { + int day = FRIDAY; + var viaVar = switch (day) { + case 1, 5, 7 -> 6; + case 2 -> 7; + default -> 9; + }; + System.out.printf("via var: %d%n", viaVar); + System.out.printf( + "inline: %d%n", + switch (day) { + case 1, 5, 7 -> 6; + case 2 -> 7; + default -> 9; + }); + } + } + """) + .doTest(); + } } From 2c64580ab5b3db29c1a67dc31855359d5df5fd58 Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:18:03 +0700 Subject: [PATCH 2/2] fix(formatstring): recurse switch case typing for nested/yield forms Use getExpressionType for arrow-case bodies and the body fallback so nested switch expressions are typed correctly. Handle colon/yield cases via CaseTree.getStatements when getBody() is null. Add lean regression tests for nested switch, block/colon yield, string cases, and mixed types. --- .../formatstring/FormatStringValidation.java | 9 ++- .../formatstring/FormatStringTest.java | 81 +++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/FormatStringValidation.java b/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/FormatStringValidation.java index 96f1711c633..787ccd6ab6b 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/FormatStringValidation.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/FormatStringValidation.java @@ -197,11 +197,16 @@ protected Void defaultAction(Tree tree, Void unused) { private static @Nullable Type getSwitchCaseResultType(CaseTree caseTree, VisitorState state) { Tree body = caseTree.getBody(); if (body == null) { + for (StatementTree statement : caseTree.getStatements()) { + if (statement instanceof YieldTree yieldTree) { + return getExpressionType(yieldTree.getValue(), state); + } + } return null; } body = ASTHelpers.stripParentheses(body); if (body instanceof ExpressionTree expressionTree) { - return ASTHelpers.getType(expressionTree); + return getExpressionType(expressionTree, state); } if (body instanceof BlockTree blockTree) { for (StatementTree statement : blockTree.getStatements()) { @@ -210,7 +215,7 @@ protected Void defaultAction(Tree tree, Void unused) { } } } - return ASTHelpers.getType(body); + return getExpressionType(body, state); } private static @Nullable Object getInstance(Type type, VisitorState state) { diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/formatstring/FormatStringTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/formatstring/FormatStringTest.java index ecce7cb5607..f4564a82b7e 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/formatstring/FormatStringTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/formatstring/FormatStringTest.java @@ -496,4 +496,85 @@ void f() { """) .doTest(); } + + @Test + public void switchExpressionArgument_nestedSwitch() { + compilationHelper + .addSourceLines( + "Test.java", + """ + class Test { + void f(int day, int mode) { + System.out.printf( + "%d%n", + switch (day) { + case 1 -> switch (mode) { + case 0 -> 1; + default -> 2; + }; + default -> 3; + }); + } + } + """) + .doTest(); + } + + @Test + public void switchExpressionArgument_blockYield() { + compilationHelper + .addSourceLines( + "Test.java", + """ + class Test { + void f(int day) { + System.out.printf( + "%d%n", + switch (day) { + case 1 -> { + yield 6; + } + default -> 9; + }); + } + } + """) + .doTest(); + } + + @Test + public void switchExpressionArgument_colonYield() { + compilationHelper + .addSourceLines( + "Test.java", + """ + class Test { + void f(int day) { + System.out.printf( + "%d%n", + switch (day) { + case 1: + yield 6; + default: + yield 9; + }); + } + } + """) + .doTest(); + } + + @Test + public void switchExpressionArgument_stringCases() { + testFormat( + "illegal format conversion: 'java.lang.String' cannot be formatted using '%d'", + "System.out.printf(\"%d\", switch (1) { case 1 -> \"a\"; default -> \"b\"; });"); + } + + @Test + public void switchExpressionArgument_mixedNumericCases() { + testFormat( + "cannot be formatted using '%d'", + "System.out.printf(\"%d\", switch (1) { case 1 -> 1; default -> 2.0; });"); + } }