don't visit jdk Stream API usages when searching for unrelated functional interface implementations

This commit is contained in:
peter
2016-08-02 17:22:02 +02:00
parent 4db293153c
commit 522e0b9c2d
10 changed files with 286 additions and 31 deletions

View File

@@ -15,6 +15,7 @@
*/
package com.intellij.psi.impl.search;
import com.google.common.annotations.VisibleForTesting;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.application.ApplicationManager;
@@ -51,8 +52,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.*;
import static com.intellij.util.ObjectUtils.assertNotNull;
@@ -64,6 +64,9 @@ public class JavaFunctionalExpressionSearcher extends QueryExecutorBase<PsiFunct
* and then for their usages,
*/
public static final int SMART_SEARCH_THRESHOLD = 5;
private static final java.util.HashSet<String> KNOWN_STREAM_CLASSES =
ContainerUtil.newHashSet(Stream.class.getName(),
IntStream.class.getName(), DoubleStream.class.getName(), LongStream.class.getName());
@Override
public void processQuery(@NotNull FunctionalExpressionSearch.SearchParameters queryParameters,
@@ -186,6 +189,10 @@ public class JavaFunctionalExpressionSearcher extends QueryExecutorBase<PsiFunct
private static Set<Location> getPossibleCallLocations(PsiClass samClass, PsiMethod calledMethod) {
Set<Location> keys = new HashSet<>();
String samName = samClass.getQualifiedName();
boolean includeStreamApi = samName != null && samName.startsWith("java.util.function.") ||
hasStreamLikeApi(samClass.getProject());
String methodName = calledMethod.getName();
PsiParameter[] parameters = calledMethod.getParameterList().getParameters();
for (int paramIndex = 0; paramIndex < parameters.length; paramIndex++) {
@@ -193,7 +200,10 @@ public class JavaFunctionalExpressionSearcher extends QueryExecutorBase<PsiFunct
if (canPassFunctionalExpression(samClass, parameter)) {
for (int argCount : getPossibleArgCounts(parameters, paramIndex)) {
for (int argIndex : getPossibleArgIndices(parameter, paramIndex, argCount)) {
keys.add(new CallLocation(methodName, argCount, argIndex));
keys.add(new CallLocation(methodName, argCount, argIndex, false));
if (includeStreamApi) {
keys.add(new CallLocation(methodName, argCount, argIndex, true));
}
}
}
}
@@ -375,4 +385,35 @@ public class JavaFunctionalExpressionSearcher extends QueryExecutorBase<PsiFunct
return result;
}
}
@VisibleForTesting
public static boolean hasStreamLikeApi(final Project project) {
return CachedValuesManager.getManager(project).getCachedValue(project, () ->
CachedValueProvider.Result.create(hasStreamLikeApi(project, "Arrays", "stream") || hasStreamLikeApi(project, "Stream", "of"),
PsiModificationTracker.JAVA_STRUCTURE_MODIFICATION_COUNT));
}
private static boolean hasStreamLikeApi(Project project, String qualifier, String methodName) {
PsiShortNamesCache cache = PsiShortNamesCache.getInstance(project);
for (PsiClass candidate : cache.getClassesByName(qualifier, GlobalSearchScope.allScope(project))) {
if (hasMethodWithNonStreamType(methodName, candidate)) return true;
}
for (PsiField field : cache.getFieldsByName(qualifier, GlobalSearchScope.allScope(project))) {
PsiClass fieldType = PsiUtil.resolveClassInClassTypeOnly(field.getType());
if (fieldType == null || fieldType instanceof PsiTypeParameter || hasMethodWithNonStreamType(methodName, fieldType)) return true;
}
return false;
}
private static boolean hasMethodWithNonStreamType(@NotNull String methodName, @NotNull PsiClass candidate) {
for (PsiMethod method : candidate.findMethodsByName(methodName, true)) {
PsiClass returnType = PsiUtil.resolveClassInClassTypeOnly(method.getReturnType());
if (returnType == null || !KNOWN_STREAM_CLASSES.contains(returnType.getQualifiedName())) {
return true;
}
}
return false;
}
}

View File

@@ -21,10 +21,10 @@ import com.intellij.openapi.util.Ref;
import com.intellij.psi.JavaTokenType;
import com.intellij.psi.PsiBinaryExpression;
import com.intellij.psi.PsiFunctionalExpression;
import com.intellij.psi.impl.cache.RecordUtil;
import com.intellij.psi.impl.java.stubs.FunctionalExpressionKey.CoarseType;
import com.intellij.psi.impl.java.stubs.index.JavaStubIndexKeys;
import com.intellij.psi.impl.source.Constants;
import com.intellij.psi.impl.source.JavaLightTreeUtil;
import com.intellij.psi.impl.source.tree.ElementType;
import com.intellij.psi.impl.source.tree.LightTreeUtil;
import com.intellij.psi.impl.source.tree.RecursiveLighterASTNodeWalkingVisitor;
@@ -75,12 +75,12 @@ public abstract class FunctionalExpressionElementType<T extends PsiFunctionalExp
@NotNull
private static FunctionalExpressionKey.Location calcLocation(LighterAST tree, LighterASTNode funExpr) {
LighterASTNode call = getContainingCall(tree, funExpr);
List<LighterASTNode> args = getArgList(tree, call);
List<LighterASTNode> args = JavaLightTreeUtil.getArgList(tree, call);
int argIndex = args == null ? -1 : getArgIndex(args, funExpr);
String methodName = call == null ? null : getCalledMethodName(tree, call);
return methodName == null || argIndex < 0
? createTypedLocation(tree, funExpr)
: new FunctionalExpressionKey.CallLocation(methodName, args.size(), argIndex);
: new FunctionalExpressionKey.CallLocation(methodName, args.size(), argIndex, StreamApiDetector.isStreamApiCall(tree, call));
}
@NotNull
@@ -94,7 +94,7 @@ public abstract class FunctionalExpressionElementType<T extends PsiFunctionalExp
}
LighterASTNode typeElement = LightTreeUtil.firstChildOfType(tree, scope, TYPE);
String typeText = getNameIdentifierText(tree, LightTreeUtil.firstChildOfType(tree, typeElement, JAVA_CODE_REFERENCE));
String typeText = JavaLightTreeUtil.getNameIdentifierText(tree, LightTreeUtil.firstChildOfType(tree, typeElement, JAVA_CODE_REFERENCE));
if (typeText != null) {
return new FunctionalExpressionKey.TypedLocation(typeText);
}
@@ -111,13 +111,6 @@ public abstract class FunctionalExpressionElementType<T extends PsiFunctionalExp
return -1;
}
@Nullable
private static List<LighterASTNode> getArgList(LighterAST tree, LighterASTNode call) {
LighterASTNode anonClass = LightTreeUtil.firstChildOfType(tree, call, ANONYMOUS_CLASS);
LighterASTNode exprList = LightTreeUtil.firstChildOfType(tree, anonClass != null ? anonClass : call, EXPRESSION_LIST);
return exprList == null ? null : LightTreeUtil.getChildrenOfType(tree, exprList, ElementType.EXPRESSION_BIT_SET);
}
private static CoarseType calcType(final LighterAST tree, LighterASTNode funExpr) {
if (funExpr.getTokenType() == METHOD_REF_EXPRESSION) return CoarseType.UNKNOWN;
@@ -209,7 +202,7 @@ public abstract class FunctionalExpressionElementType<T extends PsiFunctionalExp
if (call.getTokenType() == NEW_EXPRESSION) {
LighterASTNode anonClass = LightTreeUtil.firstChildOfType(tree, call, ANONYMOUS_CLASS);
LighterASTNode ref = LightTreeUtil.firstChildOfType(tree, anonClass != null ? anonClass : call, JAVA_CODE_REFERENCE);
return ref == null ? null : getNameIdentifierText(tree, ref);
return ref == null ? null : JavaLightTreeUtil.getNameIdentifierText(tree, ref);
}
LighterASTNode methodExpr = tree.getChildren(call).get(0);
@@ -217,23 +210,17 @@ public abstract class FunctionalExpressionElementType<T extends PsiFunctionalExp
return getSuperClassName(tree, call);
}
if (LightTreeUtil.firstChildOfType(tree, methodExpr, JavaTokenType.THIS_KEYWORD) != null) {
return getNameIdentifierText(tree, findClass(tree, call));
return JavaLightTreeUtil.getNameIdentifierText(tree, findClass(tree, call));
}
return getNameIdentifierText(tree, methodExpr);
return JavaLightTreeUtil.getNameIdentifierText(tree, methodExpr);
}
@Nullable
private static String getSuperClassName(LighterAST tree, LighterASTNode call) {
LighterASTNode aClass = findClass(tree, call);
LighterASTNode extendsList = LightTreeUtil.firstChildOfType(tree, aClass, EXTENDS_LIST);
return getNameIdentifierText(tree, LightTreeUtil.firstChildOfType(tree, extendsList, JAVA_CODE_REFERENCE));
}
@Nullable
private static String getNameIdentifierText(LighterAST tree, LighterASTNode idOwner) {
LighterASTNode id = LightTreeUtil.firstChildOfType(tree, idOwner, JavaTokenType.IDENTIFIER);
return id != null ? RecordUtil.intern(tree.getCharTable(), id) : null;
return JavaLightTreeUtil.getNameIdentifierText(tree, LightTreeUtil.firstChildOfType(tree, extendsList, JAVA_CODE_REFERENCE));
}
@Nullable

View File

@@ -15,7 +15,7 @@
*/
package com.intellij.psi.impl.java.stubs;
import com.google.common.base.Objects;
import com.google.common.base.MoreObjects;
import com.intellij.psi.PsiType;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.io.IOUtil;
@@ -114,7 +114,7 @@ public class FunctionalExpressionKey {
@Override
public String toString() {
return Objects.toStringHelper(this)
return MoreObjects.toStringHelper(this)
.add("lambdaParameterCount", lambdaParameterCount)
.add("type", lambdaType)
.add("location", location)
@@ -140,11 +140,13 @@ public class FunctionalExpressionKey {
@NotNull public final String methodName;
public final int methodArgsLength;
public final int callArgIndex;
public final boolean streamApi;
public CallLocation(@NotNull String methodName, int methodArgsLength, int callArgIndex) {
public CallLocation(@NotNull String methodName, int methodArgsLength, int callArgIndex, boolean streamApi) {
this.methodName = methodName;
this.methodArgsLength = Math.min(methodArgsLength, MAX_ARG_COUNT);
this.callArgIndex = Math.min(callArgIndex, MAX_ARG_COUNT - 1);
this.streamApi = streamApi;
}
@Override
@@ -157,6 +159,7 @@ public class FunctionalExpressionKey {
if (methodArgsLength != location.methodArgsLength) return false;
if (callArgIndex != location.callArgIndex) return false;
if (!methodName.equals(location.methodName)) return false;
if (streamApi != location.streamApi) return false;
return true;
}
@@ -166,15 +169,17 @@ public class FunctionalExpressionKey {
int result = methodName.hashCode();
result = 31 * result + methodArgsLength;
result = 31 * result + callArgIndex;
result = 31 * result + (streamApi ? 0 : 1);
return result;
}
@Override
public String toString() {
return Objects.toStringHelper(this)
return MoreObjects.toStringHelper(this)
.add("methodName", methodName)
.add("methodArgsLength", methodArgsLength)
.add("callArgIndex", callArgIndex)
.add("streamApi", streamApi)
.toString();
}
@@ -183,13 +188,15 @@ public class FunctionalExpressionKey {
String methodName = IOUtil.readUTF(dataStream);
int methodArgsLength = dataStream.readByte();
int argIndex = dataStream.readByte();
return new CallLocation(methodName, methodArgsLength, argIndex);
boolean streamApi = dataStream.readBoolean();
return new CallLocation(methodName, methodArgsLength, argIndex, streamApi);
}
private void serializeCall(@NotNull DataOutput dataStream) throws IOException {
IOUtil.writeUTF(dataStream, methodName);
dataStream.writeByte(methodArgsLength);
dataStream.writeByte(callArgIndex);
dataStream.writeBoolean(streamApi);
}
}
@@ -220,7 +227,7 @@ public class FunctionalExpressionKey {
@Override
public String toString() {
return Objects.toStringHelper(this)
return MoreObjects.toStringHelper(this)
.add("fieldType", varType)
.toString();
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2000-2016 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.intellij.psi.impl.java.stubs;
import com.intellij.lang.LighterAST;
import com.intellij.lang.LighterASTNode;
import com.intellij.psi.impl.source.JavaLightTreeUtil;
import com.intellij.psi.impl.source.tree.RecursiveLighterASTNodeWalkingVisitor;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.intellij.psi.impl.source.tree.JavaElementType.*;
/**
* @author peter
*/
public class StreamApiDetector {
public static final Set<String> STREAM_INTERMEDIATE_METHODS = ContainerUtil.newHashSet("filter",
"map", "mapToInt", "mapToLong", "mapToDouble",
"flatMap", "flatMapToInt", "flatMapToLong", "flatMapToDouble",
"distinct", "sorted", "peek", "limit", "skip",
"forEach", "forEachOrdered",
"reduce", "collect", "max",
"anyMatch", "allMatch", "noneMatch",
"findFirst", "findAny");
static boolean isStreamApiCall(LighterAST tree, LighterASTNode methodCall) {
LighterASTNode methodExpr = tree.getChildren(methodCall).get(0);
String name = JavaLightTreeUtil.getNameIdentifierText(tree, methodExpr);
if (STREAM_INTERMEDIATE_METHODS.contains(name)) {
LighterASTNode qualifier = tree.getChildren(methodExpr).get(0);
return qualifier.getTokenType() == METHOD_CALL_EXPRESSION && isStreamApiCall(tree, qualifier);
}
return isRootCall(tree, methodCall, methodExpr, name);
}
private static boolean isRootCall(LighterAST tree, LighterASTNode methodCall, LighterASTNode methodExpr, String name) {
if ("stream".equals(name) || "parallelStream".equals(name)) {
List<LighterASTNode> argList = JavaLightTreeUtil.getArgList(tree, methodCall);
if (argList == null) return false;
return argList.isEmpty() || !argList.isEmpty() && hasQualifier(tree, methodExpr, "Arrays");
}
if ("of".equals(name)) {
List<LighterASTNode> argList = JavaLightTreeUtil.getArgList(tree, methodCall);
return argList != null && !argList.isEmpty() && hasQualifier(tree, methodExpr, "Stream");
}
return false;
}
private static boolean hasQualifier(LighterAST tree, LighterASTNode methodExpr, String refName) {
LighterASTNode node = tree.getChildren(methodExpr).get(0);
return node.getTokenType() == REFERENCE_EXPRESSION &&
refName.equals(JavaLightTreeUtil.getNameIdentifierText(tree, node)) &&
!hasContextVariable(tree, node, refName);
}
private static boolean hasContextVariable(final LighterAST tree, LighterASTNode node, final String varName) {
final AtomicBoolean result = new AtomicBoolean(false);
new RecursiveLighterASTNodeWalkingVisitor(tree) {
@Override
public void visitNode(@NotNull LighterASTNode element) {
if (element.getTokenType() == LOCAL_VARIABLE && varName.equals(JavaLightTreeUtil.getNameIdentifierText(tree, element))) {
result.set(true);
}
super.visitNode(element);
}
}.visitNode(tree.getRoot());
return result.get();
}
}

View File

@@ -38,7 +38,7 @@ import java.io.IOException;
* @author max
*/
public class JavaFileElementType extends ILightStubFileElementType<PsiJavaFileStub> {
public static final int STUB_VERSION = 33;
public static final int STUB_VERSION = 34;
public JavaFileElementType() {
super("java.FILE", JavaLanguage.INSTANCE);

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2000-2016 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.intellij.psi.impl.source;
import com.intellij.lang.LighterAST;
import com.intellij.lang.LighterASTNode;
import com.intellij.psi.JavaTokenType;
import com.intellij.psi.impl.cache.RecordUtil;
import com.intellij.psi.impl.source.tree.ElementType;
import com.intellij.psi.impl.source.tree.LightTreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import static com.intellij.psi.impl.source.tree.JavaElementType.ANONYMOUS_CLASS;
import static com.intellij.psi.impl.source.tree.JavaElementType.EXPRESSION_LIST;
/**
* @author peter
*/
public class JavaLightTreeUtil {
@Nullable
public static List<LighterASTNode> getArgList(@NotNull LighterAST tree, @Nullable LighterASTNode call) {
LighterASTNode anonClass = LightTreeUtil.firstChildOfType(tree, call, ANONYMOUS_CLASS);
LighterASTNode exprList = LightTreeUtil.firstChildOfType(tree, anonClass != null ? anonClass : call, EXPRESSION_LIST);
return exprList == null ? null : LightTreeUtil.getChildrenOfType(tree, exprList, ElementType.EXPRESSION_BIT_SET);
}
@Nullable
public static String getNameIdentifierText(@NotNull LighterAST tree, @Nullable LighterASTNode idOwner) {
LighterASTNode id = LightTreeUtil.firstChildOfType(tree, idOwner, JavaTokenType.IDENTIFIER);
return id != null ? RecordUtil.intern(tree.getCharTable(), id) : null;
}
}

View File

@@ -0,0 +1,18 @@
interface I {
Object fun(Object o);
}
class Arrays {
static MyStream stream(int a) { }
}
interface MyStream {
MyStream map(I i);
}
class Test {
{
Arrays.stream().map(p -> "a");
}
}

View File

@@ -0,0 +1,24 @@
interface I {
Object fun(Object o);
}
interface MyStream {
MyStream map(I i);
}
class StrType {
static MyStream of(int b) {}
}
class Test extends Base {
{
new Runnable() {
void run() {
Stream.of(1).map(p -> "b"));
}
}
}
}

View File

@@ -0,0 +1,24 @@
interface I {
Object fun(Object o);
}
interface MyStream {
MyStream map(I i);
}
class Test {
class StrType {
static MyStream of(int b) {}
}
{
StrType Stream = null;
new Runnable() {
void run() {
Stream.of(1).map(p -> "b"));
}
}
}
}

View File

@@ -17,6 +17,7 @@ package com.intellij.codeInsight.daemon.lambda;
import com.intellij.JavaTestUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.search.JavaFunctionalExpressionSearcher;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.searches.FunctionalExpressionSearch;
import com.intellij.psi.search.searches.ReferencesSearch;
@@ -105,6 +106,24 @@ public class FindFunctionalInterfaceTest extends LightCodeInsightFixtureTestCase
assertSize(6, FunctionalExpressionSearch.search(findClass("DumbAware")).findAll());
}
public void testArraysStreamLikeApi() {
configure();
assertSize(1, FunctionalExpressionSearch.search(findClass("I")).findAll());
}
public void testStreamOfLikeApiWithLocalVar() {
configure();
assertSize(1, FunctionalExpressionSearch.search(findClass("I")).findAll());
}
public void testStreamOfLikeApiWithField() {
assertFalse(JavaFunctionalExpressionSearcher.hasStreamLikeApi(getProject()));
myFixture.addClass("class Base { StrType Stream = null; }");
configure();
assertTrue(JavaFunctionalExpressionSearcher.hasStreamLikeApi(getProject()));
assertSize(1, FunctionalExpressionSearch.search(findClass("I")).findAll());
}
private PsiClass findClass(String i) {
return JavaPsiFacade.getInstance(getProject()).findClass(i, GlobalSearchScope.allScope(getProject()));
}