/* * Copyright 2016 Google Inc. All Rights Reserved. * * 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.errorprone.bugpatterns; 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; import static com.google.errorprone.matchers.Matchers.staticMethod; import com.google.common.base.Optional; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.IdentifierTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MemberSelectTreeMatcher; 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.util.ASTHelpers; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; import com.sun.source.tree.Tree.Kind; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Symbol.VarSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.MethodType; /** * An abstract matcher for implicit and explicit calls to {@code Object.toString()}, for use * on types that do not have a human-readable {@code toString()} implementation. * * <p>See examples in {@link StreamToString} and {@link ArrayToString}. */ public abstract class AbstractToString extends BugChecker implements MethodInvocationTreeMatcher, IdentifierTreeMatcher, MemberSelectTreeMatcher { /** The type to match on. */ protected abstract TypePredicate typePredicate(); /** * Constructs a fix for an implicit toString call, e.g. from string concatenation or from * passing an argument to {@code println} or {@code StringBuilder.append}. * * @param tree the tree node for the expression being converted to a String */ protected abstract Optional<Fix> implicitToStringFix(ExpressionTree tree, VisitorState state); /** * Constructs a fix for an explicit toString call, e.g. from {@code Object.toString()} or * {@code String.valueOf()}. * * @param parent the expression's parent (e.g. {@code String.valueOf(expression)}) */ protected abstract Optional<Fix> toStringFix( Tree parent, ExpressionTree expression, VisitorState state); private static final Matcher<ExpressionTree> TO_STRING = instanceMethod().onDescendantOf("java.lang.Object").withSignature("toString()"); private static final Matcher<ExpressionTree> PRINT_STRING = anyOf( instanceMethod() .onDescendantOf("java.io.PrintStream") .withSignature("print(java.lang.Object)"), instanceMethod() .onDescendantOf("java.io.PrintStream") .withSignature("println(java.lang.Object)"), instanceMethod() .onDescendantOf("java.lang.StringBuilder") .withSignature("append(java.lang.Object)")); private static final Matcher<ExpressionTree> VALUE_OF = staticMethod().onClass("java.lang.String").withSignature("valueOf(java.lang.Object)"); @Override public Description matchIdentifier(IdentifierTree tree, VisitorState state) { return checkToString(tree, state); } @Override public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) { return checkToString(tree, state); } @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { ExpressionTree receiver = ASTHelpers.getReceiver(tree); if (TO_STRING.matches(tree, state) && typePredicate().apply(ASTHelpers.getType(receiver), state)) { return maybeFix(tree, toStringFix(tree, receiver, state)); } return checkToString(tree, state); } /** * Tests if the given expression is converted to a String by its parent (i.e. its parent * is a string concat expression, {@code String.format}, or {@code println(Object)}). */ private Description checkToString(ExpressionTree tree, VisitorState state) { Symbol sym = ASTHelpers.getSymbol(tree); if (!(sym instanceof VarSymbol || sym instanceof MethodSymbol)) { return NO_MATCH; } Type type = ASTHelpers.getType(tree); if (type instanceof MethodType) { type = type.getReturnType(); } Tree parent = state.getPath().getParentPath().getLeaf(); ToStringKind toStringKind = isToString(parent, tree, state); if (toStringKind == ToStringKind.NONE) { return NO_MATCH; } Optional<Fix> fix; switch (toStringKind) { case IMPLICIT: fix = implicitToStringFix(tree, state); break; case EXPLICIT: fix = toStringFix(parent, tree, state); break; default: throw new AssertionError(toStringKind); } if (!typePredicate().apply(type, state)) { return NO_MATCH; } return maybeFix(tree, fix); } enum ToStringKind { /** String concatenation, or an enclosing print method. */ IMPLICIT, /** {@code String.valueOf()} or {@code #toString()}. */ EXPLICIT, NONE } /** Classifies expressions that are converted to strings by their enclosing expression. */ ToStringKind isToString(Tree parent, ExpressionTree tree, VisitorState state) { // is the enclosing expression string concat? if (isStringConcat(parent, state)) { return ToStringKind.IMPLICIT; } if (parent instanceof ExpressionTree) { ExpressionTree parentExpression = (ExpressionTree) parent; // the enclosing method is print() or println() if (PRINT_STRING.matches(parentExpression, state)) { return ToStringKind.IMPLICIT; } // the enclosing method is String.valueOf() if (VALUE_OF.matches(parentExpression, state)) { return ToStringKind.EXPLICIT; } } return ToStringKind.NONE; } private boolean isStringConcat(Tree tree, VisitorState state) { return (tree.getKind() == Kind.PLUS || tree.getKind() == Kind.PLUS_ASSIGNMENT) && state.getTypes().isSameType(ASTHelpers.getType(tree), state.getSymtab().stringType); } private Description maybeFix(Tree tree, Optional<Fix> fix) { return fix.isPresent() ? describeMatch(tree, fix.get()) : describeMatch(tree); } }