/*
* Copyright 2017 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.argumentselectiondefects;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.errorprone.VisitorState;
import com.google.errorprone.names.NamingConventions;
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.NewClassTree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.CompletionFailure;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.code.Type;
import java.util.List;
import java.util.Optional;
/**
* Represents either a formal or actual parameter and its position in the argument list.
*
* @author andrewrice@google.com (Andrew Rice)
*/
@AutoValue
abstract class Parameter {
private static final ImmutableSet<String> METHODNAME_PREFIXES_TO_REMOVE =
ImmutableSet.of("get", "set", "is");
/** We use this placeholder to indicate a name which is a null literal. */
@VisibleForTesting static final String NAME_NULL = "*NULL*";
/** We use this placeholder to indicate a name which we couldn't get a canonical string for. */
@VisibleForTesting static final String NAME_UNKNOWN = "*UNKNOWN*";
/**
* We use this placeholder to indicate a name which is a compile-time constant (other than null).
*/
@VisibleForTesting static final String NAME_CONSTANT = "*CONSTANT*";
abstract String name();
abstract Type type();
abstract int index();
abstract String text();
static ImmutableList<Parameter> createListFromVarSymbols(List<VarSymbol> varSymbols) {
return Streams.mapWithIndex(
varSymbols.stream(),
(s, i) ->
new AutoValue_Parameter(
s.getSimpleName().toString(),
s.asType(),
(int) i,
s.getSimpleName().toString()))
.collect(toImmutableList());
}
static ImmutableList<Parameter> createListFromExpressionTrees(
List<? extends ExpressionTree> trees) {
return Streams.mapWithIndex(
trees.stream(),
(t, i) ->
new AutoValue_Parameter(
getArgumentName(t),
Optional.ofNullable(ASTHelpers.getResultType(t)).orElse(Type.noType),
(int) i,
t.toString()))
.collect(toImmutableList());
}
static ImmutableList<Parameter> createListFromVariableTrees(List<? extends VariableTree> trees) {
return createListFromVarSymbols(
trees.stream().map(ASTHelpers::getSymbol).collect(toImmutableList()));
}
/**
* Return true if this parameter is assignable to the target parameter. This will consider
* subclassing, autoboxing and null.
*/
boolean isAssignableTo(Parameter target, VisitorState state) {
if (state.getTypes().isSameType(type(), Type.noType)
|| state.getTypes().isSameType(target.type(), Type.noType)) {
return false;
}
try {
return state.getTypes().isAssignable(type(), target.type());
} catch (CompletionFailure e) {
// bail out if necessary symbols to do the subtype check are not on the classpath
return false;
}
}
boolean isNullLiteral() {
return name().equals(NAME_NULL);
}
boolean isUnknownName() {
return name().equals(NAME_UNKNOWN);
}
boolean isConstant() {
return name().equals(NAME_CONSTANT);
}
boolean isNamed() {
return !isNullLiteral() && !isUnknownName() && !isConstant();
}
private static String getClassName(ClassSymbol s) {
if (s.isAnonymous()) {
return s.getSuperclass().tsym.getSimpleName().toString();
} else {
return s.getSimpleName().toString();
}
}
/**
* Extract the name from an argument.
*
* <p>
*
* <ul>
* <li>IdentifierTree - if the identifier is 'this' then use the name of the enclosing class,
* otherwise use the name of the identifier
* <li>MemberSelectTree - the name of its identifier
* <li>NewClassTree - the name of the class being constructed
* <li>Null literal - a wildcard name
* <li>MethodInvocationTree - use the method name stripping off 'get', 'set', 'is' prefix. If
* this results in an empty name then recursively search the receiver
* </ul>
*
* All other trees (including literals other than Null literal) do not have a name and this method
* will return the marker for an unknown name.
*/
@VisibleForTesting
static String getArgumentName(ExpressionTree expressionTree) {
switch (expressionTree.getKind()) {
case MEMBER_SELECT:
return ((MemberSelectTree) expressionTree).getIdentifier().toString();
case NULL_LITERAL:
// null could match anything pretty well
return NAME_NULL;
case IDENTIFIER:
IdentifierTree idTree = (IdentifierTree) expressionTree;
if (idTree.getName().contentEquals("this")) {
// for the 'this' keyword the argument name is the name of the object's class
Symbol sym = ASTHelpers.getSymbol(idTree);
return sym != null ? getClassName(ASTHelpers.enclosingClass(sym)) : NAME_UNKNOWN;
} else {
// if we have a variable, just extract its name
return idTree.getName().toString();
}
case METHOD_INVOCATION:
MethodInvocationTree methodInvocationTree = (MethodInvocationTree) expressionTree;
MethodSymbol methodSym = ASTHelpers.getSymbol(methodInvocationTree);
if (methodSym != null) {
String name = methodSym.getSimpleName().toString();
List<String> terms = NamingConventions.splitToLowercaseTerms(name);
String firstTerm = Iterables.getFirst(terms, null);
if (METHODNAME_PREFIXES_TO_REMOVE.contains(firstTerm)) {
if (terms.size() == 1) {
ExpressionTree receiver = ASTHelpers.getReceiver(methodInvocationTree);
if (receiver == null) {
return getClassName(ASTHelpers.enclosingClass(methodSym));
}
// recursively try to get a name from the receiver
return getArgumentName(receiver);
} else {
return name.substring(firstTerm.length());
}
} else {
return name;
}
} else {
return NAME_UNKNOWN;
}
case NEW_CLASS:
MethodSymbol constructorSym = ASTHelpers.getSymbol((NewClassTree) expressionTree);
return constructorSym != null && constructorSym.owner != null
? getClassName((ClassSymbol) constructorSym.owner)
: NAME_UNKNOWN;
default:
return ASTHelpers.constValue(expressionTree) != null ? NAME_CONSTANT : NAME_UNKNOWN;
}
}
}