/*
* 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.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.getType;
import static java.util.stream.Collectors.joining;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.google.errorprone.util.Signatures;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
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.Type;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
/** @author cushon@google.com (Liam Miller-Cushon) */
@BugPattern(
name = "FunctionalInterfaceClash",
summary = "Overloads will be ambiguous when passing lambda arguments",
category = JDK,
severity = WARNING
)
public class FunctionalInterfaceClash extends BugChecker implements ClassTreeMatcher {
@Override
public Description matchClass(ClassTree tree, VisitorState state) {
ClassSymbol origin = getSymbol(tree);
Types types = state.getTypes();
// collect declared and inherited methods whose signature contains a functional interface
Multimap<String, MethodSymbol> methods = HashMultimap.create();
for (Symbol sym : types.membersClosure(getType(tree), /*skipInterface=*/ false).getSymbols()) {
if (!(sym instanceof MethodSymbol)) {
continue;
}
if (isBugCheckerSuppressed((MethodSymbol) sym)) {
continue;
}
MethodSymbol msym = (MethodSymbol) sym;
if (msym.getParameters().stream().noneMatch(p -> maybeFunctionalInterface(p.type, types))) {
continue;
}
if (msym.isConstructor() && !msym.owner.equals(origin)) {
continue;
}
methods.put(functionalInterfaceSignature(state, msym), msym);
}
// check if any declared members clash with another declared or inherited member
// (don't report clashes between inherited members)
for (Tree member : tree.getMembers()) {
if (!(member instanceof MethodTree)) {
continue;
}
MethodSymbol msym = getSymbol((MethodTree) member);
if (msym.getParameters().stream().noneMatch(p -> maybeFunctionalInterface(p.type, types))) {
continue;
}
Collection<MethodSymbol> clash =
new ArrayList<>(methods.removeAll(functionalInterfaceSignature(state, msym)));
clash.remove(msym);
// ignore inherited methods that are overridden in the original class
clash.removeIf(m -> msym.overrides(m, origin, types, false));
if (!clash.isEmpty()) {
String message =
"When passing lambda arguments to this function, callers will need a cast to"
+ " disambiguate with: "
+ clash
.stream()
.map(m -> Signatures.prettyMethodSignature(origin, m))
.collect(joining("\n "));
state.reportMatch(buildDescription(member).setMessage(message).build());
}
}
return NO_MATCH;
}
/**
* A string representation of a method descriptor, where all parameters whose type is a functional
* interface are "erased" to the interface's function type. For example, `foo(Supplier<String>)`
* is represented as `foo(()->Ljava/lang/String;)`.
*/
private static String functionalInterfaceSignature(VisitorState state, MethodSymbol msym) {
return String.format(
"%s(%s)",
msym.getSimpleName(),
msym.getParameters()
.stream()
.map(p -> functionalInterfaceSignature(state, p.type))
.collect(joining(",")));
}
private static String functionalInterfaceSignature(VisitorState state, Type type) {
Types types = state.getTypes();
if (!maybeFunctionalInterface(type, types)) {
return Signatures.descriptor(type, types);
}
Type descriptorType = types.findDescriptorType(type);
List<Type> fiparams = descriptorType.getParameterTypes();
// Implicitly typed block-statement-bodied lambdas are potentially compatible with
// void-returning and value-returning functional interface types, so we don't consider return
// types in general. The except is nullary functional interfaces, since the lambda parameters
// will never be implicitly typed.
String result =
fiparams.isEmpty() ? Signatures.descriptor(descriptorType.getReturnType(), types) : "_";
return String.format(
"(%s)->%s",
fiparams.stream().map(t -> Signatures.descriptor(t, types)).collect(joining(",")), result);
}
private static boolean maybeFunctionalInterface(Type type, Types types) {
try {
return types.isFunctionalInterface(type);
} catch (CompletionFailure e) {
return false;
}
}
private boolean isBugCheckerSuppressed(MethodSymbol method) {
SuppressWarnings suppression = ASTHelpers.getAnnotation(method, SuppressWarnings.class);
return suppression != null
&& !Collections.disjoint(Arrays.asList(suppression.value()), allNames());
}
}