From 581ee15e9eb5aed34cc4e5eb76a6c76a19ee9978 Mon Sep 17 00:00:00 2001 From: cpovirk Date: Tue, 19 May 2026 12:04:47 -0700 Subject: [PATCH] Make `AbstractToString` cover method references. PiperOrigin-RevId: 917955274 --- .../bugpatterns/AbstractToString.java | 86 ++++++++++++++++++- .../errorprone/bugpatterns/ArrayToString.java | 43 ++++++---- .../bugpatterns/ArrayToStringTest.java | 49 +++++++++++ 3 files changed, 161 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractToString.java b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractToString.java index 6a6d03303e8..ce9d7a2bb7b 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractToString.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractToString.java @@ -16,6 +16,7 @@ package com.google.errorprone.bugpatterns; +import static com.google.errorprone.VisitorState.memoize; import static com.google.errorprone.matchers.Description.NO_MATCH; import static com.google.errorprone.matchers.Matchers.anyOf; import static com.google.errorprone.matchers.Matchers.instanceMethod; @@ -30,21 +31,27 @@ import com.google.errorprone.annotations.FormatMethod; import com.google.errorprone.bugpatterns.BugChecker.BinaryTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.CompoundAssignmentTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.MemberReferenceTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; import com.google.errorprone.fixes.Fix; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; import com.google.errorprone.predicates.TypePredicate; +import com.google.errorprone.suppliers.Supplier; import com.sun.source.tree.BinaryTree; import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MemberReferenceTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; import com.sun.source.tree.Tree.Kind; import com.sun.source.util.TreePath; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.ArrayType; import com.sun.tools.javac.code.Type.MethodType; +import com.sun.tools.javac.util.Name; import java.util.List; import java.util.Optional; import javax.lang.model.type.TypeKind; @@ -56,7 +63,10 @@ *

See examples in {@link StreamToString} and {@link ArrayToString}. */ public abstract class AbstractToString extends BugChecker - implements BinaryTreeMatcher, MethodInvocationTreeMatcher, CompoundAssignmentTreeMatcher { + implements BinaryTreeMatcher, + MethodInvocationTreeMatcher, + MemberReferenceTreeMatcher, + CompoundAssignmentTreeMatcher { /** The type to match on. */ protected abstract TypePredicate typePredicate(); @@ -93,6 +103,37 @@ protected boolean allowableToStringKind(ToStringKind toStringKind) { protected abstract Optional toStringFix( Tree toStringCall, ExpressionTree stringifiedExpr, VisitorState state); + /** + * Constructs a fix for a method reference to {@code toString()}, e.g. {@code Object::toString}. + * + * @param receiverType the type of the object {@code toString()} will be called on + */ + protected Optional memberReferenceFix( + MemberReferenceTree tree, Type receiverType, VisitorState state) { + return Optional.empty(); + } + + private static final Supplier TO_STRING_NAME = memoize(state -> state.getName("toString")); + + private static final Supplier STRING_VALUE_OF_OBJECT = + memoize( + state -> { + var stringSymbol = state.getSymtab().stringType.tsym; + for (Symbol sym : stringSymbol.members().getSymbolsByName(state.getName("valueOf"))) { + if (sym instanceof MethodSymbol methodSym + && methodSym.isStatic() + && methodSym.getParameters().size() == 1 + && state + .getTypes() + .isSameType( + methodSym.getParameters().get(0).asType(), + state.getSymtab().objectType)) { + return methodSym; + } + } + return null; + }); + private static final Matcher TO_STRING = instanceMethod().anyClass().named("toString").withNoParameters(); @@ -135,10 +176,12 @@ protected abstract Optional toStringFix( private final boolean handleJoinerVarargs; private final boolean handleJoinerIterable; + private final boolean handleMemberReference; protected AbstractToString(ErrorProneFlags flags) { this.handleJoinerVarargs = flags.getBoolean("AbstractToString:Joiner").orElse(true); this.handleJoinerIterable = flags.getBoolean("AbstractToString:JoinerIterable").orElse(true); + this.handleMemberReference = flags.getBoolean("AbstractToString:MemberReference").orElse(true); } private static boolean isInVarargsPosition( @@ -231,6 +274,7 @@ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState elementType.getTypeArguments().getFirst(), argTree, argTree, + // TODO(cpovirk): Suggest a fix. ToStringKind.NONE, state); } @@ -239,6 +283,41 @@ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState return NO_MATCH; } + @Override + public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) { + if (!handleMemberReference) { + return NO_MATCH; + } + + MethodSymbol symbol = getSymbol(tree); + if (symbol == null || !isToStringOrIsStringValueOf(symbol, state)) { + return NO_MATCH; + } + + Type targetType = getType(tree); + if (targetType == null) { + return NO_MATCH; + } + + Type descriptorType = state.getTypes().findDescriptorType(targetType); + if (descriptorType == null || descriptorType.getParameterTypes().isEmpty()) { + return NO_MATCH; + } + + Type receiverType = descriptorType.getParameterTypes().get(0); + if (typePredicate().apply(receiverType, state)) { + handleStringifiedTree(receiverType, tree, tree, ToStringKind.MEMBER_REFERENCE, state); + } + + return NO_MATCH; + } + + private boolean isToStringOrIsStringValueOf(MethodSymbol symbol, VisitorState state) { + return (symbol.getSimpleName().equals(TO_STRING_NAME.get(state)) + && symbol.getParameters().isEmpty()) + || symbol.equals(STRING_VALUE_OF_OBJECT.get(state)); + } + @Override public Description matchBinary(BinaryTree tree, VisitorState state) { if (!state.getTypes().isSameType(getType(tree), state.getSymtab().stringType)) { @@ -293,7 +372,7 @@ private void handleStringifiedTree( stringifiedExpr, state, type, - getFix(stringifiedExpr, state, toStringCall, toStringKind))); + getFix(type, stringifiedExpr, state, toStringCall, toStringKind))); } private static Type type(ExpressionTree tree) { @@ -305,6 +384,7 @@ private static Type type(ExpressionTree tree) { } private Optional getFix( + Type type, ExpressionTree stringifiedExpr, VisitorState state, Tree toStringCall, @@ -312,6 +392,7 @@ private Optional getFix( return switch (toStringKind) { case IMPLICIT, FLOGGER, FORMAT_METHOD -> implicitToStringFix(stringifiedExpr, state); case EXPLICIT -> toStringFix(toStringCall, stringifiedExpr, state); + case MEMBER_REFERENCE -> memberReferenceFix((MemberReferenceTree) toStringCall, type, state); case NONE -> Optional.empty(); }; } @@ -331,6 +412,7 @@ enum ToStringKind { EXPLICIT, FORMAT_METHOD, FLOGGER, + MEMBER_REFERENCE, NONE, } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ArrayToString.java b/core/src/main/java/com/google/errorprone/bugpatterns/ArrayToString.java index 155e656fdc7..3888627f99f 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ArrayToString.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ArrayToString.java @@ -17,7 +17,10 @@ package com.google.errorprone.bugpatterns; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; +import static com.google.errorprone.fixes.SuggestedFixes.qualifyType; import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod; +import static com.google.errorprone.util.ASTHelpers.getReceiver; +import static com.google.errorprone.util.ASTHelpers.getType; import com.google.errorprone.BugPattern; import com.google.errorprone.ErrorProneFlags; @@ -27,8 +30,8 @@ import com.google.errorprone.matchers.Matcher; import com.google.errorprone.predicates.TypePredicate; import com.google.errorprone.predicates.TypePredicates; -import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MemberReferenceTree; import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Types; @@ -77,14 +80,14 @@ protected Optional toStringFix( // If the array is the result of calling e.getStackTrace(), replace // e.getStackTrace().toString() with Guava's Throwables.getStackTraceAsString(e). if (GET_STACK_TRACE.matches(stringifiedExpr, state)) { + var fix = SuggestedFix.builder(); return Optional.of( - SuggestedFix.builder() - .addImport("com.google.common.base.Throwables") - .replace( + fix.replace( toStringCall, String.format( - "Throwables.getStackTraceAsString(%s)", - state.getSourceForNode(ASTHelpers.getReceiver(stringifiedExpr)))) + "%s.getStackTraceAsString(%s)", + qualifyType(state, fix, "com.google.common.base.Throwables"), + state.getSourceForNode(getReceiver(stringifiedExpr)))) .build()); } // e.g. String.valueOf(theArray) -> Arrays.toString(theArray) @@ -93,21 +96,31 @@ protected Optional toStringFix( return fix(toStringCall, stringifiedExpr, state); } - private static Optional fix(Tree toStringCall, Tree stringifiedExpr, VisitorState state) { - String method = isNestedArray(stringifiedExpr, state) ? "deepToString" : "toString"; + @Override + protected Optional memberReferenceFix( + MemberReferenceTree tree, Type receiverType, VisitorState state) { + String method = isNestedArray(receiverType, state) ? "deepToString" : "toString"; + var fix = SuggestedFix.builder(); + return Optional.of( + fix.replace(tree, qualifyType(state, fix, "java.util.Arrays") + "::" + method).build()); + } + private static Optional fix(Tree toStringCall, Tree stringifiedExpr, VisitorState state) { + String method = isNestedArray(getType(stringifiedExpr), state) ? "deepToString" : "toString"; + var fix = SuggestedFix.builder(); return Optional.of( - SuggestedFix.builder() - .addImport("java.util.Arrays") - .replace( + fix.replace( toStringCall, - String.format("Arrays.%s(%s)", method, state.getSourceForNode(stringifiedExpr))) + String.format( + "%s.%s(%s)", + qualifyType(state, fix, "java.util.Arrays"), + method, + state.getSourceForNode(stringifiedExpr))) .build()); } - private static boolean isNestedArray(Tree stringifiedExpr, VisitorState state) { + private static boolean isNestedArray(Type type, VisitorState state) { Types types = state.getTypes(); - Type withType = ASTHelpers.getType(stringifiedExpr); - return withType != null && types.isArray(withType) && types.isArray(types.elemtype(withType)); + return type != null && types.isArray(type) && types.isArray(types.elemtype(type)); } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/ArrayToStringTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/ArrayToStringTest.java index 74d8ce11d43..a4a3cc770f5 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/ArrayToStringTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/ArrayToStringTest.java @@ -487,4 +487,53 @@ String test(Joiner j, List a) { """) .doTest(); } + + @Test + public void methodReference() { + refactoringHelper + .addInputLines( + "Test.java", + """ + import static java.util.stream.Collectors.joining; + + import java.util.stream.Stream; + + class Test { + void f(Stream s) { + s.map(Object::toString).collect(joining(", ")); + } + + void g(Stream s) { + s.map(Object::toString).collect(joining(", ")); + } + + void h(Stream s) { + s.map(String::valueOf).collect(joining(", ")); + } + } + """) + .addOutputLines( + "Test.java", + """ + import static java.util.stream.Collectors.joining; + + import java.util.Arrays; + import java.util.stream.Stream; + + class Test { + void f(Stream s) { + s.map(Arrays::toString).collect(joining(", ")); + } + + void g(Stream s) { + s.map(Arrays::deepToString).collect(joining(", ")); + } + + void h(Stream s) { + s.map(Arrays::toString).collect(joining(", ")); + } + } + """) + .doTest(); + } }