CallMatcher/CallMapper introduced experimentally; used in SimplifyStreamApiCallChainsInspection.java

This commit is contained in:
Tagir Valeev
2017-02-03 15:58:15 +03:00
parent 7e80778956
commit a529975033
6 changed files with 800 additions and 576 deletions
@@ -4,6 +4,6 @@ import java.util.Arrays;
class Test {
void print() {
Arrays.asList('d', 'e', 'f').st<caret>ream().forEach(c -> System.out.print(" " + c));
Arrays.asList('d', 'e', 'f').stream().fo<caret>rEach(c -> System.out.print(" " + c));
}
}
@@ -4,6 +4,6 @@ import java.util.Arrays;
class Test {
void print() {
Arrays.asList('d', 'e', 'f').str<caret>eam().forEachOrdered(c -> System.out.print(" " + c));
Arrays.asList('d', 'e', 'f').stream().forEac<caret>hOrdered(c -> System.out.print(" " + c));
}
}
@@ -0,0 +1,59 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* 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.siyeh.ig.callMatcher;
import com.intellij.psi.PsiMethodCallExpression;
import java.util.function.Function;
/**
* A pair of {@link CallMatcher} and a transformer function which maps a call to some new object.
*
* @author Tagir Valeev
*/
public class CallHandler<T> implements Function<PsiMethodCallExpression, T> {
private final CallMatcher myMatcher;
private final Function<PsiMethodCallExpression, T> myTransformer;
public CallHandler(CallMatcher matcher, Function<PsiMethodCallExpression, T> transformer) {
myMatcher = matcher;
myTransformer = transformer;
}
public final CallMatcher matcher() {
return myMatcher;
}
/**
* @param call method call to transform
* @return null if call does not pass matcher check or the result of original transformer otherwise
*/
@Override
public T apply(PsiMethodCallExpression call) {
return matcher().test(call) ? myTransformer.apply(call) : null;
}
/**
* Creates a new CallHandler with specific matcher and specific transformer function
* @param matcher a matcher to be applied to the elements
* @param transformer a transformer which accepts a method call which successfully passes matcher check
* @param <T> a type of transformer return value
* @return a new CallHandler
*/
public static <T> CallHandler<T> of(CallMatcher matcher, Function<PsiMethodCallExpression, T> transformer) {
return new CallHandler<>(matcher, transformer);
}
}
@@ -0,0 +1,81 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* 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.siyeh.ig.callMatcher;
import com.intellij.psi.PsiMethodCallExpression;
import one.util.streamex.StreamEx;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* A mutable bunch of CallHandlers which allows to dispatch a transformer call based on CallMatcher
*
* @author Tagir Valeev
*/
public class CallMapper<T> {
private Map<String, List<Function<PsiMethodCallExpression, T>>> myMap = new HashMap<>();
public CallMapper() {}
public CallMapper(CallHandler<T>... handlers) {
for (CallHandler<T> handler : handlers) {
register(handler);
}
}
public CallMapper<T> register(CallHandler<T> handler) {
handler.matcher().names().forEach(name -> myMap.computeIfAbsent(name, k -> new ArrayList<>()).add(handler));
return this;
}
public CallMapper<T> register(CallMatcher matcher, Function<PsiMethodCallExpression, T> handler) {
return register(CallHandler.of(matcher, handler));
}
public CallMapper<T> register(CallMatcher matcher, T value) {
return register(CallHandler.of(matcher, call -> value));
}
public CallMapper<T> registerAll(List<CallHandler<T>> handlers) {
handlers.forEach(this::register);
return this;
}
public T mapFirst(PsiMethodCallExpression call) {
if (call == null) return null;
List<Function<PsiMethodCallExpression, T>> functions = myMap.get(call.getMethodExpression().getReferenceName());
if (functions == null) return null;
for (Function<PsiMethodCallExpression, T> function : functions) {
T t = function.apply(call);
if (t != null) {
return t;
}
}
return null;
}
public Stream<T> mapAll(PsiMethodCallExpression call) {
if (call == null) return null;
List<Function<PsiMethodCallExpression, T>> functions = myMap.get(call.getMethodExpression().getReferenceName());
if (functions == null) return StreamEx.empty();
return StreamEx.of(functions).map(fn -> fn.apply(call)).nonNull();
}
}
@@ -0,0 +1,188 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* 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.siyeh.ig.callMatcher;
import com.intellij.psi.*;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.psiutils.MethodCallUtils;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* This interface represents a condition upon method call
*
* @author Tagir Valeev
*/
public interface CallMatcher extends Predicate<PsiMethodCallExpression> {
/**
* @return names of the methods for which this matcher may return true. For any other method it guaranteed to return false
*/
Stream<String> names();
@Contract("null -> false")
boolean test(@Nullable PsiMethodCallExpression call);
/**
* Returns a new matcher which will return true if any of supplied matchers return true
*
* @param matchers
* @return a new matcher
*/
static CallMatcher anyOf(CallMatcher... matchers) {
return new CallMatcher() {
@Override
public Stream<String> names() {
return Stream.of(matchers).flatMap(CallMatcher::names);
}
@Override
public boolean test(PsiMethodCallExpression call) {
for (CallMatcher m : matchers) {
if (m.test(call)) {
return true;
}
}
return false;
}
@Override
public String toString() {
return Stream.of(matchers).map(CallMatcher::toString).collect(Collectors.joining(" or ", "{", "}"));
}
};
}
/**
* Creates a matcher which matches an instance method having one of supplied names which class (or any of superclasses) is className
*
* @param className fully-qualified class name
* @param methodNames names of the methods
* @return a new matcher
*/
static Simple instanceCall(@NotNull String className, String... methodNames) {
return new Simple(className, ContainerUtil.newTroveSet(methodNames), null, false);
}
/**
* Creates a matcher which matches a static method having one of supplied names which class is className
*
* @param className fully-qualified class name
* @param methodNames names of the methods
* @return a new matcher
*/
static Simple staticCall(@NotNull String className, String... methodNames) {
return new Simple(className, ContainerUtil.newTroveSet(methodNames), null, true);
}
class Simple implements CallMatcher {
private final @NotNull String myClassName;
private final @NotNull Set<String> myNames;
private final @Nullable String[] myParameters;
private final boolean myStatic;
private Simple(@NotNull String className, @NotNull Set<String> names, @Nullable String[] parameters, boolean aStatic) {
myClassName = className;
myNames = names;
myParameters = parameters;
myStatic = aStatic;
}
@Override
public Stream<String> names() {
return myNames.stream();
}
/**
* Creates a new matcher which in addition to current matcher checks the number of parameters of the called method
*
* @param count expected number of parameters
* @return a new matcher
* @throws IllegalStateException if this matcher is already limited to parameters count or types
*/
public Simple parameterCount(int count) {
if (myParameters != null) {
throw new IllegalStateException("Parameter count is already set to " + count);
}
return new Simple(myClassName, myNames, count == 0 ? ArrayUtil.EMPTY_STRING_ARRAY : new String[count], myStatic);
}
/**
* Creates a new matcher which in addition to current matcher checks the number of parameters of the called method
* and their types
*
* @param types textual representation of parameter types (may contain null to ignore checking parameter type of specific argument)
* @return a new matcher
* @throws IllegalStateException if this matcher is already limited to parameters count or types
*/
public Simple parameterTypes(@NotNull String... types) {
if (myParameters != null) {
throw new IllegalStateException("Parameters are already registered");
}
return new Simple(myClassName, myNames, types.length == 0 ? ArrayUtil.EMPTY_STRING_ARRAY : types.clone(), myStatic);
}
private static boolean parameterTypeMatches(String type, PsiParameter parameter) {
if (type == null) return true;
PsiType psiType = parameter.getType();
return psiType.equalsToText(type) ||
psiType instanceof PsiClassType && ((PsiClassType)psiType).rawType().equalsToText(type);
}
@Override
public boolean test(PsiMethodCallExpression call) {
String name = call.getMethodExpression().getReferenceName();
if (!myNames.contains(name)) return false;
PsiExpression[] args = call.getArgumentList().getExpressions();
if (myParameters != null && myParameters.length > 0) {
if (args.length < myParameters.length) return false;
}
PsiMethod method = call.resolveMethod();
if (method == null) return false;
PsiClass aClass = method.getContainingClass();
if (aClass == null) return false;
if (myStatic != method.getModifierList().hasExplicitModifier(PsiModifier.STATIC) ||
(myStatic && !myClassName.equals(aClass.getQualifiedName())) ||
(!myStatic && !InheritanceUtil.isInheritor(aClass, myClassName))) {
return false;
}
PsiParameterList parameterList = method.getParameterList();
if (parameterList.getParametersCount() > args.length ||
(!MethodCallUtils.isVarArgCall(call) && parameterList.getParametersCount() < args.length)) {
return false;
}
if (myParameters != null) {
if (myParameters.length != parameterList.getParametersCount()) return false;
return StreamEx.zip(myParameters, parameterList.getParameters(),
Simple::parameterTypeMatches).allMatch(Boolean.TRUE::equals);
}
return true;
}
@Override
public String toString() {
return myClassName + "." + String.join("|", myNames);
}
}
}