diff --git a/java/java-indexing-impl/src/com/intellij/psi/impl/java/FunExprOccurrence.java b/java/java-indexing-impl/src/com/intellij/psi/impl/java/FunExprOccurrence.java new file mode 100644 index 000000000000..d10bc3df8fc6 --- /dev/null +++ b/java/java-indexing-impl/src/com/intellij/psi/impl/java/FunExprOccurrence.java @@ -0,0 +1,171 @@ +/* + * 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; + +import com.google.common.base.MoreObjects; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.psi.impl.search.ApproximateResolver; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.psi.util.PsiUtil; +import com.intellij.psi.util.TypeConversionUtil; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.io.DataInputOutputUtil; +import com.intellij.util.io.IOUtil; +import org.jetbrains.annotations.NotNull; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * @author peter + */ +public class FunExprOccurrence { + public final int funExprOffset; + private final int argIndex; + private final List referenceContext; + + public FunExprOccurrence(int funExprOffset, + int argIndex, + List referenceContext) { + this.funExprOffset = funExprOffset; + this.argIndex = argIndex; + this.referenceContext = referenceContext; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FunExprOccurrence)) return false; + + FunExprOccurrence that = (FunExprOccurrence)o; + + if (funExprOffset != that.funExprOffset) return false; + if (argIndex != that.argIndex) return false; + if (!referenceContext.equals(that.referenceContext)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = funExprOffset; + result = 31 * result + argIndex; + result = 31 * result + referenceContext.hashCode(); + return result; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("offset", funExprOffset) + .add("argIndex", argIndex) + .add("chain", referenceContext) + .toString(); + } + + void serialize(DataOutput out) throws IOException { + DataInputOutputUtil.writeINT(out, funExprOffset); + DataInputOutputUtil.writeINT(out, argIndex); + + DataInputOutputUtil.writeINT(out, referenceContext.size()); + for (ReferenceChainLink link : referenceContext) { + serializeLink(out, link); + } + } + + static FunExprOccurrence deserialize(DataInput in) throws IOException { + int offset = DataInputOutputUtil.readINT(in); + int argIndex = DataInputOutputUtil.readINT(in); + + int contextSize = DataInputOutputUtil.readINT(in); + List context = new ArrayList<>(contextSize); + for (int i = 0; i < contextSize; i++) { + context.add(deserializeLink(in)); + } + return new FunExprOccurrence(offset, argIndex, context); + } + + private static void serializeLink(DataOutput out, ReferenceChainLink link) throws IOException { + IOUtil.writeUTF(out, link.referenceName); + out.writeBoolean(link.isCall); + if (link.isCall) { + DataInputOutputUtil.writeINT(out, link.argCount); + } + } + + @NotNull + private static ReferenceChainLink deserializeLink(DataInput in) throws IOException { + String referenceName = IOUtil.readUTF(in); + boolean isCall = in.readBoolean(); + return new ReferenceChainLink(referenceName, isCall, isCall ? DataInputOutputUtil.readINT(in) : -1); + } + + public boolean canHaveType(@NotNull List samClasses, @NotNull VirtualFile placeFile) { + if (referenceContext.isEmpty()) return true; + + Set qualifiers = null; + for (int i = 0; i < referenceContext.size(); i++) { + ReferenceChainLink link = referenceContext.get(i); + List candidates = i == 0 ? link.getGlobalMembers(placeFile, samClasses.get(0).getProject()) + : link.getSymbolMembers(qualifiers); + if (candidates == null) return true; + + if (i == referenceContext.size() - 1) { + return ContainerUtil.exists(candidates, m -> isCompatible(link, m, samClasses)); + } + qualifiers = ApproximateResolver.getDefiniteSymbolTypes(candidates); + if (qualifiers == null) return true; + } + + return true; + } + + private boolean isCompatible(ReferenceChainLink link, PsiMember member, List samClasses) { + if (link.isCall) { + return member instanceof PsiMethod && hasCompatibleParameter((PsiMethod)member, argIndex, samClasses); + } + if (member instanceof PsiClass) { + return ContainerUtil.exists(samClasses, c -> InheritanceUtil.isInheritorOrSelf((PsiClass)member, c, true)); + } + return member instanceof PsiField && + ContainerUtil.exists(samClasses, c -> canPassFunctionalExpression(c, ((PsiField)member).getType())); + } + + public static boolean hasCompatibleParameter(PsiMethod method, int argIndex, List samClasses) { + PsiParameter[] parameters = method.getParameterList().getParameters(); + int paramIndex = method.isVarArgs() ? Math.min(argIndex, parameters.length - 1) : argIndex; + return paramIndex < parameters.length && + ContainerUtil.exists(samClasses, c -> canPassFunctionalExpression(c, parameters[paramIndex].getType())); + } + + private static boolean canPassFunctionalExpression(PsiClass sam, PsiType paramType) { + if (paramType instanceof PsiEllipsisType) { + paramType = ((PsiEllipsisType)paramType).getComponentType(); + } + PsiClass functionalCandidate = PsiUtil.resolveClassInClassTypeOnly(paramType); + if (functionalCandidate instanceof PsiTypeParameter) { + return InheritanceUtil.isInheritorOrSelf(sam, PsiUtil.resolveClassInClassTypeOnly(TypeConversionUtil.erasure(paramType)), true); + } + + return InheritanceUtil.isInheritorOrSelf(functionalCandidate, sam, true); + } +} diff --git a/java/java-indexing-impl/src/com/intellij/psi/impl/java/JavaFunctionalExpressionIndex.java b/java/java-indexing-impl/src/com/intellij/psi/impl/java/JavaFunctionalExpressionIndex.java index 263e959c26cd..8a6d07971889 100644 --- a/java/java-indexing-impl/src/com/intellij/psi/impl/java/JavaFunctionalExpressionIndex.java +++ b/java/java-indexing-impl/src/com/intellij/psi/impl/java/JavaFunctionalExpressionIndex.java @@ -20,11 +20,13 @@ import com.intellij.ide.highlighter.JavaFileType; import com.intellij.lang.LighterAST; import com.intellij.lang.LighterASTNode; import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.JavaTokenType; import com.intellij.psi.PsiBinaryExpression; import com.intellij.psi.impl.java.stubs.FunctionalExpressionKey; import com.intellij.psi.impl.source.Constants; +import com.intellij.psi.impl.source.FileLocalResolver; import com.intellij.psi.impl.source.JavaFileElementType; import com.intellij.psi.impl.source.JavaLightTreeUtil; import com.intellij.psi.impl.source.tree.ElementType; @@ -32,26 +34,26 @@ import com.intellij.psi.impl.source.tree.LightTreeUtil; import com.intellij.psi.impl.source.tree.RecursiveLighterASTNodeWalkingVisitor; import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.TokenSet; +import com.intellij.util.SmartList; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.JBIterable; import com.intellij.util.indexing.*; import com.intellij.util.io.DataExternalizer; import com.intellij.util.io.DataInputOutputUtil; import com.intellij.util.io.KeyDescriptor; -import gnu.trove.TIntArrayList; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import static com.intellij.psi.impl.source.tree.JavaElementType.*; -public class JavaFunctionalExpressionIndex extends FileBasedIndexExtension implements PsiDependentIndex { - public static final ID INDEX_ID = ID.create("java.fun.expression"); +public class JavaFunctionalExpressionIndex extends FileBasedIndexExtension> implements PsiDependentIndex { + public static final ID> INDEX_ID = ID.create("java.fun.expression"); private static final KeyDescriptor KEY_DESCRIPTOR = new KeyDescriptor() { @Override public int getHashCode(FunctionalExpressionKey value) { @@ -75,33 +77,100 @@ public class JavaFunctionalExpressionIndex extends FileBasedIndexExtension 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, StreamApiDetector.isStreamApiCall(tree, call)); + private static List createCallChain(FileLocalResolver resolver, @Nullable LighterASTNode expr) { + List chain = new ArrayList<>(); + while (true) { + if (expr == null) return reversedChain(chain); + if (expr.getTokenType() == PARENTH_EXPRESSION) { + expr = LightTreeUtil.firstChildOfType(resolver.getLightTree(), expr, ElementType.EXPRESSION_BIT_SET); + continue; + } + if (expr.getTokenType() == TYPE_CAST_EXPRESSION) { + String typeName = resolver.getShortClassTypeName(expr); + ContainerUtil.addIfNotNull(chain, typeName != null ? new ReferenceChainLink(typeName, false, -1) : null); + return reversedChain(chain); + } + + boolean isCall = expr.getTokenType() == METHOD_CALL_EXPRESSION || expr.getTokenType() == NEW_EXPRESSION; + String referenceName = getReferencedMemberName(resolver.getLightTree(), expr, isCall); + if (referenceName == null) return reversedChain(chain); + + LighterASTNode qualifier = getQualifier(resolver.getLightTree(), expr, isCall); + if (qualifier == null) { + ContainerUtil.addIfNotNull(chain, createChainStart(resolver, expr, isCall, referenceName)); + return reversedChain(chain); + } + + chain.add(new ReferenceChainLink(referenceName, isCall, getArgCount(resolver.getLightTree(), expr))); + expr = qualifier; + } } @NotNull - private static FunctionalExpressionKey.Location createTypedLocation(LighterAST tree, LighterASTNode funExpr) { - LighterASTNode scope = skipExpressionsUp(tree, funExpr, TokenSet.create(LOCAL_VARIABLE, FIELD, TYPE_CAST_EXPRESSION, RETURN_STATEMENT)); + private static List reversedChain(List chain) { + Collections.reverse(chain); + return chain; + } + + private static int getArgCount(LighterAST tree, LighterASTNode expr) { + List args = JavaLightTreeUtil.getArgList(tree, expr); + return args == null ? -1 : args.size(); + } + + @Nullable + private static LighterASTNode getQualifier(LighterAST tree, LighterASTNode expr, boolean isCall) { + LighterASTNode qualifier = tree.getChildren(expr).get(0); + if (isCall) { + List children = tree.getChildren(qualifier); + qualifier = children.isEmpty() ? null : children.get(0); + } + return qualifier != null && ElementType.EXPRESSION_BIT_SET.contains(qualifier.getTokenType()) ? qualifier : null; + } + + @Nullable + private static String getReferencedMemberName(LighterAST tree, LighterASTNode expr, boolean isCall) { + if (isCall) { + return getCalledMethodName(tree, expr); + } + if (expr.getTokenType() == REFERENCE_EXPRESSION) { + return JavaLightTreeUtil.getNameIdentifierText(tree, expr); + } + return null; + } + + @Nullable + private static ReferenceChainLink createChainStart(FileLocalResolver resolver, + LighterASTNode expr, + boolean isCall, + String referenceName) { + if (!isCall) { + FileLocalResolver.LightResolveResult result = resolver.resolveLocally(expr); + if (result == FileLocalResolver.LightResolveResult.UNKNOWN) return null; + + LighterASTNode target = result.getTarget(); + if (target != null) { + String typeName = resolver.getShortClassTypeName(target); + return typeName != null ? new ReferenceChainLink(typeName, false, -1) : null; + } + } + return new ReferenceChainLink(referenceName, isCall, getArgCount(resolver.getLightTree(), expr)); + } + + @NotNull + private static String calcExprType(LighterASTNode funExpr, FileLocalResolver resolver) { + LighterASTNode scope = skipExpressionsUp(resolver.getLightTree(), funExpr, TokenSet.create(LOCAL_VARIABLE, FIELD, TYPE_CAST_EXPRESSION, RETURN_STATEMENT, ASSIGNMENT_EXPRESSION)); if (scope != null) { - if (scope.getTokenType() == RETURN_STATEMENT) { - scope = LightTreeUtil.getParentOfType(tree, scope, + if (scope.getTokenType() == ASSIGNMENT_EXPRESSION) { + LighterASTNode lValue = findExpressionChild(scope, resolver.getLightTree()); + scope = lValue == null ? null : resolver.resolveLocally(lValue).getTarget(); + } + else if (scope.getTokenType() == RETURN_STATEMENT) { + scope = LightTreeUtil.getParentOfType(resolver.getLightTree(), scope, TokenSet.create(METHOD), TokenSet.orSet(ElementType.MEMBER_BIT_SET, TokenSet.create(LAMBDA_EXPRESSION))); } - - LighterASTNode typeElement = LightTreeUtil.firstChildOfType(tree, scope, TYPE); - String typeText = JavaLightTreeUtil.getNameIdentifierText(tree, LightTreeUtil.firstChildOfType(tree, typeElement, JAVA_CODE_REFERENCE)); - if (typeText != null) { - return new FunctionalExpressionKey.TypedLocation(typeText); - } } - return FunctionalExpressionKey.Location.UNKNOWN; + return StringUtil.notNullize(scope == null ? null : resolver.getShortClassTypeName(scope)); } private static int getArgIndex(List args, LighterASTNode expr) { @@ -113,7 +182,7 @@ public class JavaFunctionalExpressionIndex extends FileBasedIndexExtension n.getTokenType() == CLASS); } @NotNull @@ -268,35 +337,38 @@ public class JavaFunctionalExpressionIndex extends FileBasedIndexExtension getName() { + public ID> getName() { return INDEX_ID; } @NotNull @Override - public DataIndexer getIndexer() { + public DataIndexer, FileContent> getIndexer() { return inputData -> { - Map result = new HashMap<>(); + CharSequence text = inputData.getContentAsText(); + if (!StringUtil.contains(text, "->") && !StringUtil.contains(text, "::")) return Collections.emptyMap(); + Map> result = new HashMap<>(); LighterAST tree = ((FileContentImpl)inputData).getLighterASTForPsiDependentIndex(); + FileLocalResolver resolver = new FileLocalResolver(tree); new RecursiveLighterASTNodeWalkingVisitor(tree) { @Override public void visitNode(@NotNull LighterASTNode element) { if (element.getTokenType() == METHOD_REF_EXPRESSION || element.getTokenType() == LAMBDA_EXPRESSION) { FunctionalExpressionKey key = new FunctionalExpressionKey(getFunExprParameterCount(tree, element), - calcType(tree, element), - calcLocation(tree, element)); - TIntArrayList list = result.get(key); + calcReturnType(tree, element), + calcExprType(element, resolver)); + List list = result.get(key); if (list == null) { - result.put(key, list = new TIntArrayList()); + result.put(key, list = new SmartList<>()); } - list.add(element.getStartOffset()); + list.add(createOccurrence(element, resolver)); } super.visitNode(element); @@ -307,24 +379,43 @@ public class JavaFunctionalExpressionIndex extends FileBasedIndexExtension args = JavaLightTreeUtil.getArgList(tree, containingCall); + int argIndex = args == null ? -1 : getArgIndex(args, funExpr); + + LighterASTNode chainExpr = containingCall; + if (chainExpr == null) { + LighterASTNode assignment = skipExpressionsUp(tree, funExpr, TokenSet.create(ASSIGNMENT_EXPRESSION)); + if (assignment != null) { + chainExpr = findExpressionChild(assignment, tree); + } + } + + return new FunExprOccurrence(funExpr.getStartOffset(), argIndex, + createCallChain(resolver, chainExpr)); + } + @NotNull @Override - public DataExternalizer getValueExternalizer() { - return new DataExternalizer() { + public DataExternalizer> getValueExternalizer() { + return new DataExternalizer>() { @Override - public void save(@NotNull DataOutput out, TIntArrayList value) throws IOException { + public void save(@NotNull DataOutput out, List value) throws IOException { DataInputOutputUtil.writeINT(out, value.size()); - for (int i = 0; i < value.size(); i++) { - DataInputOutputUtil.writeINT(out, value.get(i)); + for (FunExprOccurrence info : value) { + info.serialize(out); } } @Override - public TIntArrayList read(@NotNull DataInput in) throws IOException { + public List read(@NotNull DataInput in) throws IOException { int length = DataInputOutputUtil.readINT(in); - TIntArrayList list = new TIntArrayList(length); + List list = new SmartList(); for (int i = 0; i < length; i++) { - list.add(DataInputOutputUtil.readINT(in)); + list.add(FunExprOccurrence.deserialize(in)); } return list; } diff --git a/java/java-indexing-impl/src/com/intellij/psi/impl/java/ReferenceChainLink.java b/java/java-indexing-impl/src/com/intellij/psi/impl/java/ReferenceChainLink.java new file mode 100644 index 000000000000..c3e0295e3e03 --- /dev/null +++ b/java/java-indexing-impl/src/com/intellij/psi/impl/java/ReferenceChainLink.java @@ -0,0 +1,135 @@ +/* + * 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; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.psi.impl.ResolveScopeManager; +import com.intellij.psi.impl.search.ApproximateResolver; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.search.PsiShortNamesCache; +import com.intellij.psi.util.PsiUtilCore; +import com.intellij.util.Processor; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author peter + */ +public class ReferenceChainLink { + final String referenceName; + final boolean isCall; + final int argCount; + + public ReferenceChainLink(@NotNull String referenceName, boolean isCall, int argCount) { + this.referenceName = referenceName; + this.isCall = isCall; + this.argCount = argCount; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ReferenceChainLink)) return false; + + ReferenceChainLink link = (ReferenceChainLink)o; + + if (isCall != link.isCall) return false; + if (argCount != link.argCount) return false; + if (!referenceName.equals(link.referenceName)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = referenceName.hashCode(); + result = 31 * result + (isCall ? 1 : 0); + result = 31 * result + argCount; + return result; + } + + @Override + public String toString() { + return referenceName + (isCall ? "(" + argCount + ")" : ""); + } + + private static final Key> EXPENSIVE_LINKS = Key.create("EXPENSIVE_CHAIN_LINKS"); + + + @Nullable + List getGlobalMembers(VirtualFile placeFile, Project project) { + if (isExpensive(project)) return null; + + List candidates = new ArrayList<>(); + AtomicInteger count = new AtomicInteger(); + Processor processor = member -> { + if (canBeAccessible(placeFile, member) && (!(member instanceof PsiMethod) || + ApproximateResolver.canHaveArgCount((PsiMethod)member, argCount))) { + candidates.add(member); + } + return count.incrementAndGet() < 42; + }; + PsiShortNamesCache cache = PsiShortNamesCache.getInstance(project); + GlobalSearchScope scope = ResolveScopeManager.getInstance(project).getDefaultResolveScope(placeFile); + if (isCall) { + if (!cache.processMethodsWithName(referenceName, processor, scope, null)) { + markExpensive(project); + return null; + } + } + else { + PsiPackage pkg = JavaPsiFacade.getInstance(project).findPackage(referenceName); + if (pkg != null && pkg.getDirectories(scope).length > 0) return null; + + if (!cache.processFieldsWithName(referenceName, processor, scope, null) || + !cache.processClassesWithName(referenceName, processor, scope, null)) { + markExpensive(project); + return null; + } + } + return candidates; + } + + private static boolean canBeAccessible(VirtualFile placeFile, PsiMember member) { + return !member.hasModifierProperty(PsiModifier.PRIVATE) || placeFile.equals(PsiUtilCore.getVirtualFile(member)); + } + + private boolean isExpensive(Project project) { + Set expensive = project.getUserData(EXPENSIVE_LINKS); + return expensive != null && expensive.contains(this); + } + + private void markExpensive(Project project) { + Set expensive = project.getUserData(EXPENSIVE_LINKS); + if (expensive == null) { + project.putUserData(EXPENSIVE_LINKS, expensive = ContainerUtil.newConcurrentSet()); + } + expensive.add(this); + } + + public List getSymbolMembers(Set qualifiers) { + return isCall ? ApproximateResolver.getPossibleMethods(qualifiers, referenceName, argCount) + : ApproximateResolver.getPossibleNonMethods(qualifiers, referenceName); + } + +} diff --git a/java/java-indexing-impl/src/com/intellij/psi/impl/search/ApproximateResolver.java b/java/java-indexing-impl/src/com/intellij/psi/impl/search/ApproximateResolver.java index a9e74b1f70ee..5a3a071356f4 100644 --- a/java/java-indexing-impl/src/com/intellij/psi/impl/search/ApproximateResolver.java +++ b/java/java-indexing-impl/src/com/intellij/psi/impl/search/ApproximateResolver.java @@ -18,16 +18,16 @@ package com.intellij.psi.impl.search; import com.intellij.psi.*; import com.intellij.psi.util.PsiUtil; import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.JBIterable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Collections; -import java.util.Set; +import java.util.*; /** * @author peter */ -class ApproximateResolver { +public class ApproximateResolver { /** * Tries to calculate the type of a given expression using lightweight resolve: no type inference for method calls, no exact candidate selection, works only with raw types. * If it returns a non-empty set, the "real" type class is guaranteed to be present in that set. This method can be used to quickly @@ -43,31 +43,37 @@ class ApproximateResolver { expression = PsiUtil.skipParenthesizedExprDown(expression); - if (expression instanceof PsiTypeCastExpression) return extractClass(expression.getType()); + return expression instanceof PsiTypeCastExpression ? extractClass(expression.getType()) : + expression instanceof PsiMethodCallExpression ? getCallType(expression, maxDepth) : + expression instanceof PsiReferenceExpression ? getNonCallType((PsiReferenceExpression)expression, maxDepth) : + null; + } - boolean method = false; - if (expression instanceof PsiMethodCallExpression) { - method = true; - expression = ((PsiMethodCallExpression)expression).getMethodExpression(); + @Nullable + private static Set getCallType(@NotNull PsiExpression expression, int maxDepth) { + PsiReferenceExpression ref = ((PsiMethodCallExpression)expression).getMethodExpression(); + PsiExpression qualifier = ref.getQualifierExpression(); + if (qualifier == null) return extractClass(expression.getType()); + + Set qualifierType = getPossibleTypes(qualifier, maxDepth - 1); + String refName = ref.getReferenceName(); + int argCount = ((PsiMethodCallExpression)expression).getArgumentList().getExpressions().length; + List methods = refName == null || qualifierType == null ? null : getPossibleMethods(qualifierType, refName, argCount); + return methods == null ? null : getDefiniteSymbolTypes(methods); + } + + @Nullable + private static Set getNonCallType(@NotNull PsiReferenceExpression expression, int maxDepth) { + PsiExpression qualifier = expression.getQualifierExpression(); + if (qualifier == null) { + PsiElement target = expression.resolve(); + return target instanceof PsiClass ? Collections.singleton((PsiClass)target) : extractClass(expression.getType()); } - if (!(expression instanceof PsiReferenceExpression)) return null; - - String name = ((PsiReferenceExpression)expression).getReferenceName(); - if (name == null) return null; - - PsiExpression qualifier = ((PsiReferenceExpression)expression).getQualifierExpression(); - if (qualifier != null) { - Set qualifierType = getPossibleTypes(qualifier, maxDepth - 1); - if (qualifierType == null) return null; - - return method ? getPossibleCallTypes(qualifierType, name) : getPossibleMemberTypes(qualifierType, name); - } - - if (method) return null; - - PsiElement target = ((PsiReferenceExpression)expression).resolve(); - return target instanceof PsiClass ? Collections.singleton((PsiClass)target) : extractClass(expression.getType()); + Set qualifierType = getPossibleTypes(qualifier, maxDepth - 1); + String refName = expression.getReferenceName(); + List members = refName == null || qualifierType == null ? null : getPossibleNonMethods(qualifierType, refName); + return members == null ? null : getDefiniteSymbolTypes(members); } @Nullable @@ -76,32 +82,54 @@ class ApproximateResolver { return psiClass == null || psiClass instanceof PsiTypeParameter ? null : Collections.singleton(psiClass); } - @Nullable - private static Set getPossibleCallTypes(Set inClasses, @NotNull String name) { - Set allTypes = ContainerUtil.newHashSet(); - for (PsiClass aClass : inClasses) { - for (PsiMethod method : aClass.findMethodsByName(name, true)) { - PsiClass type = PsiUtil.resolveClassInClassTypeOnly(method.getReturnType()); - if (type == null || type instanceof PsiTypeParameter) return null; - allTypes.add(type); - } + @NotNull + public static List getPossibleMethods(@NotNull Set symbols, @NotNull String name, int callArgCount) { + return JBIterable.from(symbols). + flatMap(sym -> Arrays.asList(sym.findMethodsByName(name, true))). + filter(m -> canHaveArgCount(m, callArgCount)). + toList(); + } + + @NotNull + public static List getPossibleNonMethods(@NotNull Set symbols, @NotNull String name) { + List result = new ArrayList<>(); + for (PsiClass sym : symbols) { + ContainerUtil.addIfNotNull(result, sym.findFieldByName(name, true)); + ContainerUtil.addIfNotNull(result, sym.findInnerClassByName(name, true)); } - return allTypes; + return result; } @Nullable - private static Set getPossibleMemberTypes(Set inClasses, @NotNull String name) { - Set allTypes = ContainerUtil.newHashSet(); - for (PsiClass aClass : inClasses) { - PsiField field = aClass.findFieldByName(name, true); - if (field != null) { - PsiClass fieldType = PsiUtil.resolveClassInClassTypeOnly(field.getType()); - if (fieldType == null || fieldType instanceof PsiTypeParameter) return null; - allTypes.add(fieldType); + public static Set getDefiniteSymbolTypes(@NotNull List candidates) { + Set possibleTypes = new HashSet<>(); + for (PsiMember candidate : candidates) { + if (candidate instanceof PsiClass) { + possibleTypes.add((PsiClass)candidate); } + else if (candidate instanceof PsiMethod && ((PsiMethod)candidate).isConstructor()) { + ContainerUtil.addIfNotNull(possibleTypes, candidate.getContainingClass()); + } + else { + //noinspection ConstantConditions + PsiType type = candidate instanceof PsiField ? ((PsiField)candidate).getType() : ((PsiMethod)candidate).getReturnType(); + if (type instanceof PsiPrimitiveType) continue; + if (type instanceof PsiArrayType) { + type = PsiType.getJavaLangObject(candidate.getManager(), candidate.getResolveScope()); + } - ContainerUtil.addIfNotNull(allTypes, aClass.findInnerClassByName(name, true)); + PsiClass typeClass = PsiUtil.resolveClassInClassTypeOnly(type); + if (typeClass == null || typeClass instanceof PsiTypeParameter) { + return null; + } + possibleTypes.add(typeClass); + } } - return allTypes; + return possibleTypes; + } + + public static boolean canHaveArgCount(PsiMethod method, int argCount) { + return method.isVarArgs() ? argCount >= method.getParameterList().getParametersCount() - 1 + : argCount == method.getParameterList().getParametersCount(); } } diff --git a/java/java-indexing-impl/src/com/intellij/psi/impl/search/JavaFunctionalExpressionSearcher.java b/java/java-indexing-impl/src/com/intellij/psi/impl/search/JavaFunctionalExpressionSearcher.java index 971534bd18ea..024201b8591e 100644 --- a/java/java-indexing-impl/src/com/intellij/psi/impl/search/JavaFunctionalExpressionSearcher.java +++ b/java/java-indexing-impl/src/com/intellij/psi/impl/search/JavaFunctionalExpressionSearcher.java @@ -15,9 +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.QueryExecutorBase; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.diagnostic.Logger; @@ -30,88 +28,148 @@ import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.*; +import com.intellij.psi.impl.java.FunExprOccurrence; import com.intellij.psi.impl.java.JavaFunctionalExpressionIndex; import com.intellij.psi.impl.java.stubs.FunctionalExpressionKey; -import com.intellij.psi.impl.java.stubs.FunctionalExpressionKey.CallLocation; -import com.intellij.psi.impl.java.stubs.FunctionalExpressionKey.Location; -import com.intellij.psi.impl.java.stubs.JavaMethodElementType; import com.intellij.psi.impl.java.stubs.index.JavaMethodParameterTypesIndex; import com.intellij.psi.search.*; import com.intellij.psi.search.searches.DirectClassInheritorsSearch; -import com.intellij.psi.search.searches.FunctionalExpressionSearch; +import com.intellij.psi.search.searches.FunctionalExpressionSearch.SearchParameters; import com.intellij.psi.stubs.StubIndex; -import com.intellij.psi.stubs.StubIndexKey; -import com.intellij.psi.util.*; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtil; +import com.intellij.psi.util.PsiUtilCore; +import com.intellij.util.ObjectUtils; +import com.intellij.util.PairProcessor; import com.intellij.util.Processor; import com.intellij.util.Processors; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.HashSet; +import com.intellij.util.containers.JBIterable; import com.intellij.util.containers.MultiMap; import com.intellij.util.indexing.FileBasedIndex; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; import java.util.*; -import java.util.stream.*; +import java.util.concurrent.atomic.AtomicInteger; import static com.intellij.util.ObjectUtils.assertNotNull; -public class JavaFunctionalExpressionSearcher extends QueryExecutorBase { +public class JavaFunctionalExpressionSearcher extends QueryExecutorBase { private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.search.JavaFunctionalExpressionSearcher"); - /** - * The least number of candidate files with functional expressions that directly scanning them becomes expensive - * and more advanced ways of searching become necessary: e.g. first searching for methods where the functional interface class is used - * and then for their usages, - */ public static final int SMART_SEARCH_THRESHOLD = 5; - private static final java.util.HashSet 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, - @NotNull Processor consumer) { - final Set highLevelModules; - final List funInterfaces; - - try (AccessToken ignored = ReadAction.start()) { - PsiClass aClass = queryParameters.getElementToSearch(); - if (!aClass.isValid() || - !aClass.isInterface() || - InjectedLanguageManager.getInstance(aClass.getProject()).isInjectedFragment(aClass.getContainingFile())) { - return; - } - - highLevelModules = getJava8Modules(aClass.getProject()); - if (highLevelModules.isEmpty()) return; - - funInterfaces = ContainerUtil.filter(processSubInterfaces(aClass), LambdaUtil::isFunctionalClass); - } - - for (PsiClass funInterface : funInterfaces) { - SamDescriptor descriptor = ReadAction.compute(() -> { - if (!funInterface.isValid()) return null; - - final PsiMethod functionalInterfaceMethod = LambdaUtil.getFunctionalInterfaceMethod(funInterface); - LOG.assertTrue(functionalInterfaceMethod != null); - final PsiType samType = functionalInterfaceMethod.getReturnType(); - if (samType == null) return null; - - return new SamDescriptor(funInterface, functionalInterfaceMethod, samType, - convertToGlobalScope(funInterface.getProject(), - funInterface.getUseScope().intersectWith(queryParameters.getEffectiveSearchScope()))); - }); - if (descriptor == null || !descriptor.search(consumer, highLevelModules)) return; + public void processQuery(@NotNull SearchParameters p, @NotNull Processor consumer) { + List descriptors = calcDescriptors(p); + AtomicInteger exprCount = new AtomicInteger(); + AtomicInteger fileCount = new AtomicInteger(); + processOffsets(descriptors, (file, offsets) -> { + fileCount.incrementAndGet(); + exprCount.addAndGet(offsets.size()); + return processFile(consumer, descriptors, file, offsets); + }); + if (exprCount.get() > 0) { + LOG.debug("Loaded " + exprCount.get() + " fun-expressions in " + fileCount.get() + " files"); } } + @TestOnly + public static Set getFilesToSearchInPsi(PsiClass samClass) { + Set result = new HashSet<>(); + processOffsets(calcDescriptors(new SearchParameters(samClass, samClass.getUseScope())), (file, offsets) -> result.add(file)); + return result; + } + + @NotNull + private static List calcDescriptors(@NotNull SearchParameters queryParameters) { + List descriptors = new ArrayList<>(); + + ReadAction.run(() -> { + PsiClass aClass = queryParameters.getElementToSearch(); + if (!aClass.isValid() || !aClass.isInterface()) { + return; + } + Project project = aClass.getProject(); + if (InjectedLanguageManager.getInstance(project).isInjectedFragment(aClass.getContainingFile()) || !hasJava8Modules(project)) { + return; + } + + for (PsiClass samClass : processSubInterfaces(aClass)) { + if (LambdaUtil.isFunctionalClass(samClass)) { + PsiMethod saMethod = assertNotNull(LambdaUtil.getFunctionalInterfaceMethod(samClass)); + PsiType samType = saMethod.getReturnType(); + if (samType == null) continue; + + SearchScope scope = samClass.getUseScope().intersectWith(queryParameters.getEffectiveSearchScope()); + descriptors.add(new SamDescriptor(samClass, saMethod, samType, convertToGlobalScope(project, scope))); + } + } + }); + return descriptors; + } + + @NotNull + private static Set getLikelyFiles(List descriptors) { + return JBIterable.from(descriptors).flatMap(SamDescriptor::getMostLikelyFiles).toSet(); + } + + @NotNull + private static MultiMap getAllOccurrences(List descriptors) { + MultiMap result = MultiMap.createLinkedSet(); + for (SamDescriptor descriptor : descriptors) { + ReadAction.run(() -> { + for (FunctionalExpressionKey key : descriptor.generateKeys()) { + FileBasedIndex.getInstance().processValues(JavaFunctionalExpressionIndex.INDEX_ID, key, null, (file, infos) -> { + ProgressManager.checkCanceled(); + result.putValues(file, infos); + return true; + }, new JavaSourceFilterScope(descriptor.useScope)); + } + }); + } + LOG.debug("Found " + result.values().size() + " fun-expressions in " + result.keySet().size() + " files"); + return result; + } + + private static void processOffsets(List descriptors, PairProcessor> processor) { + if (descriptors.isEmpty()) return; + + List samClasses = ContainerUtil.map(descriptors, d -> d.samClass); + MultiMap allCandidates = getAllOccurrences(descriptors); + for (VirtualFile vFile : putLikelyFilesFirst(descriptors, allCandidates.keySet())) { + List toLoad = filterInapplicable(samClasses, vFile, allCandidates.get(vFile)); + if (!toLoad.isEmpty()) { + LOG.trace("To load " + vFile.getPath() + " with values: " + toLoad); + if (!processor.process(vFile, ContainerUtil.map(toLoad, it -> it.funExprOffset))) { + return; + } + } + } + } + + @NotNull + private static Set putLikelyFilesFirst(List descriptors, Set allFiles) { + Set orderedFiles = new LinkedHashSet<>(allFiles); + orderedFiles.retainAll(getLikelyFiles(descriptors)); + orderedFiles.addAll(allFiles); + return orderedFiles; + } + + @NotNull + private static List filterInapplicable(List samClasses, + VirtualFile vFile, + Collection occurrences) { + return ReadAction.compute(() -> ContainerUtil.filter(occurrences, it -> it.canHaveType(samClasses, vFile))); + } + private static boolean processFile(@NotNull Processor consumer, - PsiClass samClass, + List descriptors, VirtualFile vFile, Collection offsets) { return ReadAction.compute(() -> { - if (!samClass.isValid()) return true; - - PsiFile file = samClass.getManager().findFile(vFile); + PsiFile file = descriptors.get(0).samClass.getManager().findFile(vFile); if (!(file instanceof PsiJavaFile)) { LOG.error("Non-java file " + file + "; " + vFile); return true; @@ -124,7 +182,7 @@ public class JavaFunctionalExpressionSearcher extends QueryExecutorBase descriptors, PsiFunctionalExpression expression) { + if (!canHaveType(expression, ContainerUtil.map(descriptors, d -> d.samClass))) return false; - return InheritanceUtil.isInheritorOrSelf(PsiUtil.resolveClassInType(expression.getFunctionalInterfaceType()), samClass, true); + PsiClass actualClass = PsiUtil.resolveClassInType(expression.getFunctionalInterfaceType()); + return ContainerUtil.exists(descriptors, d -> InheritanceUtil.isInheritorOrSelf(actualClass, d.samClass, true)); } - private static boolean canHaveType(PsiFunctionalExpression expression, PsiClass samClass) { + private static boolean canHaveType(PsiFunctionalExpression expression, List samClasses) { PsiElement parent = expression.getParent(); if (parent instanceof PsiExpressionList && parent.getParent() instanceof PsiMethodCallExpression) { - int argIndex = Arrays.asList(((PsiExpressionList)parent).getExpressions()).indexOf(expression); + PsiExpression[] args = ((PsiExpressionList)parent).getExpressions(); + int argIndex = Arrays.asList(args).indexOf(expression); PsiReferenceExpression methodExpression = ((PsiMethodCallExpression)parent.getParent()).getMethodExpression(); PsiExpression qualifier = methodExpression.getQualifierExpression(); String methodName = methodExpression.getReferenceName(); if (qualifier != null && methodName != null && argIndex >= 0) { Set approximateTypes = ApproximateResolver.getPossibleTypes(qualifier, 10); - return approximateTypes == null || hasMethodWithSamCompatibleParameter(approximateTypes, methodName, argIndex, samClass); + List methods = approximateTypes == null ? null : + ApproximateResolver.getPossibleMethods(approximateTypes, methodName, args.length); + return methods == null || + ContainerUtil.exists(methods, m -> FunExprOccurrence.hasCompatibleParameter(m, argIndex, samClasses)); } } return true; } - private static boolean hasMethodWithSamCompatibleParameter(Set qualifierClasses, String methodName, int argIndex, PsiClass samClass) { - for (PsiClass qualifierClass : qualifierClasses) { - for (PsiMethod method : qualifierClass.findMethodsByName(methodName, true)) { - PsiParameter[] parameters = method.getParameterList().getParameters(); - int paramIndex = method.isVarArgs() ? Math.min(argIndex, parameters.length - 1) : argIndex; - if (paramIndex < parameters.length && canPassFunctionalExpression(samClass, parameters[paramIndex])) { - return true; - } - } - } - return false; - } - - @NotNull - private static MultiMap getCandidateOffsets(SamDescriptor descriptor, MultiMap queries, Set likelyFiles) { - MultiMap result = MultiMap.createLinked(); - MultiMap unlikely = MultiMap.createLinked(); - for (Location location : queries.keySet()) { - MultiMap offsets = getOffsetsForLocation(descriptor, location, queries.get(location)); - for (VirtualFile file : offsets.keySet()) { - (likelyFiles.contains(file) ? result : unlikely).putValues(file, offsets.get(file)); - } - if (LOG.isDebugEnabled()) { - logIfLarge(offsets, location); - } - } - - result.putAllValues(unlikely); - if (LOG.isDebugEnabled()) { - LOG.debug("checking " + result.values().size() + " fun-expressions in " + result.keySet().size() + " files"); - } - return result; - } - - private static void logIfLarge(MultiMap offsets, Location location) { - int delta = offsets.values().size(); - if (delta > 5) { - String sample = offsets.entrySet().stream().limit(10).map(e -> e.getKey().getName() + "->" + e.getValue()).collect(Collectors.joining(", ")); - LOG.debug(delta + " expressions for " + location + "; " + sample); - } - } - - @NotNull - private static MultiMap getOffsetsForLocation(SamDescriptor descriptor, Location location, Collection scopes) { - MultiMap map = MultiMap.create(); - ReadAction.run(() -> { - ProgressManager.checkCanceled(); - GlobalSearchScope combinedScope = descriptor.useScope.intersectWith( - GlobalSearchScope.union(scopes.toArray(new GlobalSearchScope[0]))); - for (FunctionalExpressionKey key : descriptor.generateKeys(location)) { - FileBasedIndex.getInstance().processValues(JavaFunctionalExpressionIndex.INDEX_ID, key, null, (file, offsets) -> { - for (int i : offsets.toNativeArray()) { - map.putValue(file, i); - } - return true; - }, new JavaSourceFilterScope(combinedScope)); - } - }); - return map; - } - - @NotNull - private static MultiMap collectQueryKeys(SamDescriptor descriptor, Iterable methods) { - MultiMap queries = MultiMap.createSet(); - queries.putValue(Location.UNKNOWN, descriptor.useScope); - queries.putValue(new FunctionalExpressionKey.TypedLocation(descriptor.getClassName()), descriptor.useScope); - - for (PsiMethod psiMethod : methods) { - ReadAction.run(() -> { - if (!psiMethod.isValid()) return; - - GlobalSearchScope methodUseScope = getGlobalUseScope(psiMethod); - for (Location location : getPossibleCallLocations(descriptor.samClass, psiMethod)) { - queries.putValue(location, methodUseScope); - } - }); - } - return queries; - } - - @NotNull - private static GlobalSearchScope getGlobalUseScope(PsiMethod method) { - if (PsiTreeUtil.getContextOfType(method, PsiAnonymousClass.class) != null || method.hasModifierProperty(PsiModifier.PRIVATE)) { - // don't call method.getUseScope as it'll be too specific and might cause stub-AST switch - VirtualFile vFile = PsiUtilCore.getVirtualFile(method); - if (vFile != null) { - return GlobalSearchScope.fileScope(method.getProject(), vFile); - } - } - - return convertToGlobalScope(method.getProject(), method.getUseScope()); - } - - @NotNull - private static Set getPossibleCallLocations(PsiClass samClass, PsiMethod calledMethod) { - Set 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++) { - PsiParameter parameter = parameters[paramIndex]; - if (canPassFunctionalExpression(samClass, parameter)) { - for (int argCount : getPossibleArgCounts(parameters, paramIndex)) { - for (int argIndex : getPossibleArgIndices(parameter, paramIndex, argCount)) { - keys.add(new CallLocation(methodName, argCount, argIndex, false)); - if (includeStreamApi) { - keys.add(new CallLocation(methodName, argCount, argIndex, true)); - } - } - } - } - } - - return keys; - } - - private static int[] getPossibleArgCounts(PsiParameter[] parameters, int paramIndex) { - if (parameters[parameters.length - 1].isVarArgs()) { - return IntStream - .rangeClosed(parameters.length - 1, CallLocation.MAX_ARG_COUNT) - .filter(i -> i > paramIndex) - .toArray(); - } - return new int[]{Math.min(parameters.length, CallLocation.MAX_ARG_COUNT)}; - } - - private static int[] getPossibleArgIndices(PsiParameter parameter, int paramIndex, int argCount) { - if (parameter.isVarArgs()) { - return IntStream - .rangeClosed(paramIndex + 1, CallLocation.MAX_ARG_COUNT) - .filter(i -> i < argCount) - .toArray(); - } - return new int[]{Math.min(paramIndex, CallLocation.MAX_ARG_COUNT)}; - } - - @NotNull - private static Set getJava8Modules(Project project) { + private static boolean hasJava8Modules(Project project) { final boolean projectLevelIsHigh = PsiUtil.getLanguageLevel(project).isAtLeast(LanguageLevel.JDK_1_8); - final Set highLevelModules = new HashSet<>(); for (Module module : ModuleManager.getInstance(project).getModules()) { final LanguageLevelModuleExtension extension = ModuleRootManager.getInstance(module).getModuleExtension(LanguageLevelModuleExtension.class); if (extension != null) { final LanguageLevel level = extension.getLanguageLevel(); if (level == null && projectLevelIsHigh || level != null && level.isAtLeast(LanguageLevel.JDK_1_8)) { - highLevelModules.add(module); + return true; } } } - return highLevelModules; - } - - @NotNull - private static Set collectMethodNamesCalledWithFunExpressions(SamDescriptor descriptor) { - Set usedMethodNames = new HashSet<>(); - ReadAction.run(() -> FileBasedIndex.getInstance().processAllKeys(JavaFunctionalExpressionIndex.INDEX_ID, key -> { - ProgressManager.checkCanceled(); - if (key.canRepresent(descriptor.samParamCount, descriptor.booleanCompatible, descriptor.isVoid) && - key.location instanceof CallLocation) { - usedMethodNames.add(((CallLocation)key.location).methodName); - } - return true; - }, descriptor.useScope, null)); - return usedMethodNames; - } - - @Nullable - private static GlobalSearchScope combineResolveScopes(Set candidateModules, PsiClass samClass) { - List scopes = candidateModules.stream() - .map(GlobalSearchScope::moduleWithDependenciesAndLibrariesScope) - .filter(s -> PsiSearchScopeUtil.isInScope(s, samClass)) - .collect(Collectors.toList()); - return scopes.isEmpty() ? null : GlobalSearchScope.union(scopes.toArray(new GlobalSearchScope[scopes.size()])); + return false; } @NotNull @@ -351,19 +249,6 @@ public class JavaFunctionalExpressionSearcher extends QueryExecutorBase processSubInterfaces(PsiClass base) { Set result = new HashSet<>(); new Object() { @@ -396,120 +281,52 @@ public class JavaFunctionalExpressionSearcher extends QueryExecutorBase getOffsets(Set highLevelModules) { - GlobalSearchScope visibleFromCandidates = ReadAction.compute( - () -> samClass.isValid() ? combineResolveScopes(highLevelModules, samClass) : null); - if (visibleFromCandidates == null) return MultiMap.empty(); - - Set usedMethodNames = collectMethodNamesCalledWithFunExpressions(this); - Set exactTypeMethods = getMethodsWithParameterType(usedMethodNames, useScope.intersectWith(visibleFromCandidates), getClassName()); - Set genericMethods = getMethodsWithParameterType(usedMethodNames, visibleFromCandidates, JavaMethodElementType.TYPE_PARAMETER_PSEUDO_NAME); - LOG.debug("#methods: " + (exactTypeMethods.size() + genericMethods.size())); - - MultiMap queries = collectQueryKeys(this, ContainerUtil.concat(exactTypeMethods, genericMethods)); - return getCandidateOffsets(this, queries, getMostLikelyFiles(exactTypeMethods)); - } - - private boolean search(@NotNull Processor consumer, Set highLevelModules) { - for (Map.Entry> entry : getOffsets(highLevelModules).entrySet()) { - if (!processFile(consumer, samClass, entry.getKey(), entry.getValue())) { - return false; + List generateKeys() { + List result = new ArrayList<>(); + for (String lambdaType : new String[]{assertNotNull(samClass.getName()), ""}) { + for (int lambdaParamCount : new int[]{FunctionalExpressionKey.UNKNOWN_PARAM_COUNT, samParamCount}) { + result.add(new FunctionalExpressionKey(lambdaParamCount, FunctionalExpressionKey.CoarseType.UNKNOWN, lambdaType)); + if (isVoid) { + result.add(new FunctionalExpressionKey(lambdaParamCount, FunctionalExpressionKey.CoarseType.VOID, lambdaType)); + } else { + if (booleanCompatible) { + result.add(new FunctionalExpressionKey(lambdaParamCount, FunctionalExpressionKey.CoarseType.BOOLEAN, lambdaType)); + } + result.add(new FunctionalExpressionKey(lambdaParamCount, FunctionalExpressionKey.CoarseType.NON_VOID, lambdaType)); + } } } - return true; + + return result; } @NotNull - private Set getMostLikelyFiles(Set exactTypeMethods) { + private Set getMostLikelyFiles() { Set files = ContainerUtil.newLinkedHashSet(); ReadAction.run(() -> { if (!samClass.isValid()) return; - Set likelyNames = ContainerUtil.newLinkedHashSet(getClassName()); - for (PsiMethod method : exactTypeMethods) { - likelyNames.add(method.getName()); - } + String className = samClass.getName(); + Project project = samClass.getProject(); + if (className == null) return; - PsiSearchHelperImpl helper = (PsiSearchHelperImpl)PsiSearchHelper.SERVICE.getInstance(samClass.getProject()); + Set likelyNames = ContainerUtil.newLinkedHashSet(className); + StubIndex.getInstance().processElements(JavaMethodParameterTypesIndex.getInstance().getKey(), className, + project, useScope, PsiMethod.class, method -> { + ProgressManager.checkCanceled(); + likelyNames.add(method.getName()); + return true; + }); + + PsiSearchHelperImpl helper = (PsiSearchHelperImpl)PsiSearchHelper.SERVICE.getInstance(project); Processor processor = Processors.cancelableCollectProcessor(files); - for (String name : likelyNames) { - helper.processFilesWithText(this.useScope, UsageSearchContext.IN_CODE, true, name, processor); + for (String word : likelyNames) { + helper.processFilesWithText(useScope, UsageSearchContext.IN_CODE, true, word, processor); } }); return files; } - private Set getMethodsWithParameterType(Set usedMethodNames, - GlobalSearchScope scope, - String type) { - Set methods = new HashSet<>(); - ReadAction.run(() -> { - if (!samClass.isValid()) return; - - StubIndexKey key = JavaMethodParameterTypesIndex.getInstance().getKey(); - StubIndex.getInstance().processElements(key, type, samClass.getProject(), scope, PsiMethod.class, method -> { - ProgressManager.checkCanceled(); - if (usedMethodNames.contains(method.getName())) { - methods.add(method); - } - - return true; - }); - }); - return methods; - } - - @NotNull - String getClassName() { - return assertNotNull(samClass.getName()); - } - - List generateKeys(Location location) { - List result = new ArrayList<>(); - for (int lambdaParamCount : new int[]{FunctionalExpressionKey.UNKNOWN_PARAM_COUNT, samParamCount}) { - result.add(new FunctionalExpressionKey(lambdaParamCount, FunctionalExpressionKey.CoarseType.UNKNOWN, location)); - if (isVoid) { - result.add(new FunctionalExpressionKey(lambdaParamCount, FunctionalExpressionKey.CoarseType.VOID, location)); - } else { - if (booleanCompatible) { - result.add(new FunctionalExpressionKey(lambdaParamCount, FunctionalExpressionKey.CoarseType.BOOLEAN, location)); - } - result.add(new FunctionalExpressionKey(lambdaParamCount, FunctionalExpressionKey.CoarseType.NON_VOID, location)); - } - } - 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; - } } \ No newline at end of file diff --git a/java/java-psi-impl/src/com/intellij/psi/impl/java/StreamApiDetector.java b/java/java-psi-impl/src/com/intellij/psi/impl/java/StreamApiDetector.java deleted file mode 100644 index 1d9f12302442..000000000000 --- a/java/java-psi-impl/src/com/intellij/psi/impl/java/StreamApiDetector.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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; - -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 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 argList = JavaLightTreeUtil.getArgList(tree, methodCall); - if (argList == null) return false; - return argList.isEmpty() || !argList.isEmpty() && hasQualifier(tree, methodExpr, "Arrays"); - } - if ("of".equals(name)) { - List 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(); - } -} diff --git a/java/java-psi-impl/src/com/intellij/psi/impl/java/stubs/FunctionalExpressionKey.java b/java/java-psi-impl/src/com/intellij/psi/impl/java/stubs/FunctionalExpressionKey.java index 3606046a24b2..6c8370d8fa3b 100644 --- a/java/java-psi-impl/src/com/intellij/psi/impl/java/stubs/FunctionalExpressionKey.java +++ b/java/java-psi-impl/src/com/intellij/psi/impl/java/stubs/FunctionalExpressionKey.java @@ -16,10 +16,12 @@ package com.intellij.psi.impl.java.stubs; import com.google.common.base.MoreObjects; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiType; import com.intellij.psi.util.TypeConversionUtil; import com.intellij.util.io.IOUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.DataInput; import java.io.DataOutput; @@ -32,47 +34,26 @@ public class FunctionalExpressionKey { public static final int UNKNOWN_PARAM_COUNT = -1; private final int lambdaParameterCount; private final CoarseType lambdaReturnType; - public final Location location; + @NotNull private final String knownType; - public FunctionalExpressionKey(int lambdaParameterCount, @NotNull CoarseType lambdaReturnType, @NotNull Location location) { - this.location = location; + public FunctionalExpressionKey(int lambdaParameterCount, @NotNull CoarseType lambdaReturnType, @Nullable String knownFunExprType) { this.lambdaParameterCount = lambdaParameterCount; this.lambdaReturnType = lambdaReturnType; + this.knownType = StringUtil.notNullize(knownFunExprType); } @NotNull public static FunctionalExpressionKey deserializeKey(@NotNull DataInput dataStream) throws IOException { int parameterCount = dataStream.readByte(); CoarseType type = CoarseType.values()[dataStream.readByte()]; - return new FunctionalExpressionKey(parameterCount, type, deserializeLocation(dataStream)); + String knownType = IOUtil.readUTF(dataStream); + return new FunctionalExpressionKey(parameterCount, type, knownType); } public void serializeKey(@NotNull DataOutput dataStream) throws IOException { dataStream.writeByte(lambdaParameterCount); dataStream.writeByte(lambdaReturnType.ordinal()); - serializeLocation(dataStream); - } - - private static Location deserializeLocation(DataInput dataStream) throws IOException { - byte locationType = dataStream.readByte(); - if (locationType == 0) return Location.UNKNOWN; - if (locationType == 1) return CallLocation.deserializeCall(dataStream); - if (locationType == 2) return TypedLocation.deserializeField(dataStream); - throw new AssertionError(locationType); - } - - private void serializeLocation(@NotNull DataOutput dataStream) throws IOException { - if (location == Location.UNKNOWN) { - dataStream.writeByte(0); - } - else if (location instanceof CallLocation) { - dataStream.writeByte(1); - ((CallLocation)location).serializeCall(dataStream); - } - else if (location instanceof TypedLocation) { - dataStream.writeByte(2); - ((TypedLocation)location).serializeVariable(dataStream); - } + IOUtil.writeUTF(dataStream, knownType); } public boolean canRepresent(int samParamCount, boolean booleanCompatible, boolean isVoid) { @@ -99,7 +80,7 @@ public class FunctionalExpressionKey { if (lambdaParameterCount != key.lambdaParameterCount) return false; if (lambdaReturnType != key.lambdaReturnType) return false; - if (!location.equals(key.location)) return false; + if (!knownType.equals(key.knownType)) return false; return true; } @@ -108,7 +89,7 @@ public class FunctionalExpressionKey { public int hashCode() { int result = lambdaParameterCount; result = 31 * result + lambdaReturnType.ordinal(); - result = 31 * result + location.hashCode(); + result = 31 * result + knownType.hashCode(); return result; } @@ -117,130 +98,10 @@ public class FunctionalExpressionKey { return MoreObjects.toStringHelper(this) .add("lambdaParameterCount", lambdaParameterCount) .add("type", lambdaReturnType) - .add("location", location) + .add("knownType", knownType) .toString(); } - public interface Location { - Location UNKNOWN = new Location() { - @Override - public String toString() { - return "UNKNOWN"; - } - - @Override - public int hashCode() { - return 0; - } - }; - } - - public static class CallLocation implements Location { - public static final int MAX_ARG_COUNT = 10; - @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, 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 - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof CallLocation)) return false; - - CallLocation location = (CallLocation)o; - - 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; - } - - @Override - public int hashCode() { - 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 MoreObjects.toStringHelper(this) - .add("methodName", methodName) - .add("methodArgsLength", methodArgsLength) - .add("callArgIndex", callArgIndex) - .add("streamApi", streamApi) - .toString(); - } - - @NotNull - private static Location deserializeCall(DataInput dataStream) throws IOException { - String methodName = IOUtil.readUTF(dataStream); - int methodArgsLength = dataStream.readByte(); - int argIndex = dataStream.readByte(); - 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); - } - - } - - public static class TypedLocation implements Location { - @NotNull public final String varType; - - public TypedLocation(@NotNull String varType) { - this.varType = varType; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof TypedLocation)) return false; - - TypedLocation location = (TypedLocation)o; - - if (!varType.equals(location.varType)) return false; - - return true; - } - - @Override - public int hashCode() { - return varType.hashCode(); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("fieldType", varType) - .toString(); - } - - public static TypedLocation deserializeField(DataInput dataStream) throws IOException { - return new TypedLocation(IOUtil.readUTF(dataStream)); - } - - public void serializeVariable(DataOutput dataStream) throws IOException { - IOUtil.writeUTF(dataStream, varType); - } - } - public enum CoarseType { VOID, UNKNOWN, BOOLEAN, NON_VOID } diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/lambda/findUsages/ArraysStreamLikeApi.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/lambda/findUsages/ArraysStreamLikeApi.java index 70e86fdb589c..0b5fbfaedf6a 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/lambda/findUsages/ArraysStreamLikeApi.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/lambda/findUsages/ArraysStreamLikeApi.java @@ -13,6 +13,6 @@ interface MyStream { class Test { { - Arrays.stream().map(p -> "a"); + Arrays.stream(1).map(p -> "a"); } } diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/lambda/findUsages/CallOnGenericParameter.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/lambda/findUsages/CallOnGenericParameter.java new file mode 100644 index 000000000000..2e5c56f96f2a --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/lambda/findUsages/CallOnGenericParameter.java @@ -0,0 +1,13 @@ +interface I { + int a(); +} + +class A { + void foo(I i); +} + +class C> { + void bar(SR t) { + t.foo(() -> 1); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/lambda/findUsages/CallWithQualifiedName.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/lambda/findUsages/CallWithQualifiedName.java new file mode 100644 index 000000000000..ea0ce8eba071 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/lambda/findUsages/CallWithQualifiedName.java @@ -0,0 +1,5 @@ +class C { + { + pkg.p1.p2.p3.Util.foo(() -> {}); + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/lambda/FindFunctionalInterfaceTest.java b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/lambda/FindFunctionalInterfaceTest.java index 3636b7b83727..584031b37d13 100644 --- a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/lambda/FindFunctionalInterfaceTest.java +++ b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/lambda/FindFunctionalInterfaceTest.java @@ -16,6 +16,7 @@ package com.intellij.codeInsight.daemon.lambda; import com.intellij.JavaTestUtil; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.impl.search.JavaFunctionalExpressionSearcher; import com.intellij.psi.search.GlobalSearchScope; @@ -122,13 +123,55 @@ public class FindFunctionalInterfaceTest extends LightCodeInsightFixtureTestCase } 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()); } + public void testCallWithQualifiedName() { + myFixture.addClass("package pkg.p1.p2.p3; public interface I { void run() {} }"); + myFixture.addClass("package pkg.p1.p2.p3; public class Util { public static void foo(I i) {} }"); + configure(); + assertSize(1, FunctionalExpressionSearch.search(findClass("pkg.p1.p2.p3.I")).findAll()); + } + + public void testCallOnGenericParameter() { + configure(); + assertSize(1, FunctionalExpressionSearch.search(findClass("I")).findAll()); + } + + public void testDontVisitInapplicableFiles() { + PsiClass sam = myFixture.addClass("interface I { void foo(); }"); + myFixture.addClass("class Some { " + + "{ I i = () -> {}; }" + + "void doTest(int a) {} " + + "void doTest(I i, I j) {} " + + "Some intermediate() {} " + + "Object intermediate(int a, int b) {} " + + "}"); + myFixture.addClass("class _WrongSignature {{ I i = a -> {}; I j = () -> true; }}"); + myFixture.addClass("class _CallArgumentCountMismatch extends Some {{ " + + " doTest(() -> {}); " + + " intermediate(4).doTest(() -> {}, () -> {}); " + + "}}"); + myFixture.addClass("class _KnownTypeVariableAssignment {" + + "static Runnable field;" + + "{ Runnable r = () -> {}; field = () -> {}; } " + + "}"); + myFixture.addClass("class _SuperFieldAssignment extends _KnownTypeVariableAssignment {" + + "{ field = () -> {}; } " + + "}"); + myFixture.addClass("import static _KnownTypeVariableAssignment.*; " + + "class _StaticallyImportedFieldAssignment {" + + "{ field = () -> {}; } " + + "}"); + + assertSize(1, FunctionalExpressionSearch.search(sam).findAll()); + for (VirtualFile file : JavaFunctionalExpressionSearcher.getFilesToSearchInPsi(sam)) { + assertFalse(file.getName(), file.getName().startsWith("_")); + } + } + private PsiClass findClass(String i) { return JavaPsiFacade.getInstance(getProject()).findClass(i, GlobalSearchScope.allScope(getProject())); } diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/overriding/FindOverridingMethodsAndClassesTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/overriding/FindOverridingMethodsAndClassesTest.groovy index 1a94778452ef..25eb7cbfcea1 100644 --- a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/overriding/FindOverridingMethodsAndClassesTest.groovy +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/overriding/FindOverridingMethodsAndClassesTest.groovy @@ -18,6 +18,7 @@ package org.jetbrains.plugins.groovy.lang.overriding import com.intellij.psi.PsiClass import com.intellij.psi.PsiMethod import com.intellij.psi.search.searches.ClassInheritorsSearch +import com.intellij.psi.search.searches.FunctionalExpressionSearch import com.intellij.psi.search.searches.OverridingMethodsSearch import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase import org.jetbrains.plugins.groovy.lang.psi.GroovyFile @@ -62,4 +63,10 @@ public class FindOverridingMethodsAndClassesTest extends LightCodeInsightFixture final Collection classes = ClassInheritorsSearch.search(psiClass).findAll() assertEquals("Class count is wrong", classCount, classes.size()) } + + public void "test find java functional expression passed into a groovy method"() { + myFixture.addFileToProject("a.groovy", "interface I { void foo(); }; class C { static void bar(I i) {}}") + myFixture.addFileToProject("a.java", "class D {{ C.bar(() -> {}); }") + assertSize(1, FunctionalExpressionSearch.search(myFixture.findClass("I")).findAll()); + } }