[java-highlighting] Refactor FunctionalInterfaceTest to avoid internals dependency; move completion stuff out of LambdaHighlightingUtil

Part of IDEA-365344 Create a new Java error highlighter with minimal dependencies (PSI only)

GitOrigin-RevId: 4bd5447dd5fc2342f5e9b23e693f7683594470f5
This commit is contained in:
Tagir Valeev
2025-02-04 12:07:36 +01:00
committed by intellij-monorepo-bot
parent 0eeed91d22
commit fd581aa354
19 changed files with 58 additions and 164 deletions

View File

@@ -6,43 +6,19 @@ import com.intellij.codeInsight.daemon.JavaErrorBundle;
import com.intellij.codeInsight.daemon.impl.HighlightInfo;
import com.intellij.codeInsight.daemon.impl.HighlightInfoType;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.lang.jvm.JvmModifier;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.*;
import com.intellij.psi.impl.IncompleteModelUtil;
import com.intellij.psi.impl.PsiClassImplUtil;
import com.intellij.psi.util.MethodSignature;
import com.intellij.psi.util.PsiTypesUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.TypeConversionUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import static com.intellij.psi.LambdaUtil.getFunction;
import static com.intellij.psi.LambdaUtil.getTargetMethod;
import java.util.List;
public final class LambdaHighlightingUtil {
private static final Logger LOG = Logger.getInstance(LambdaHighlightingUtil.class);
static @NlsContexts.DetailedDescription String checkInterfaceFunctional(@NotNull PsiClass psiClass) {
return checkInterfaceFunctional(psiClass, JavaErrorBundle.message("target.type.of.a.lambda.conversion.must.be.an.interface"));
}
static @NlsContexts.DetailedDescription String checkInterfaceFunctional(@NotNull PsiClass psiClass, @NotNull @Nls String interfaceNonFunctionalMessage) {
if (psiClass instanceof PsiTypeParameter) return null; //should be logged as cyclic inference
return switch (LambdaUtil.checkInterfaceFunctional(psiClass)) {
case VALID -> null;
case NOT_INTERFACE -> interfaceNonFunctionalMessage;
case NO_ABSTRACT_METHOD -> JavaErrorBundle.message("no.target.method.found");
case MULTIPLE_ABSTRACT_METHODS ->
JavaErrorBundle.message("multiple.non.overriding.abstract.methods.found.in.interface.0", HighlightUtil.formatClass(psiClass));
};
}
static HighlightInfo.Builder checkParametersCompatible(@NotNull PsiLambdaExpression expression,
PsiParameter @NotNull [] methodParameters,
@@ -71,84 +47,6 @@ public final class LambdaHighlightingUtil {
return null;
}
public static boolean insertSemicolonAfter(@NotNull PsiLambdaExpression lambdaExpression) {
return lambdaExpression.getBody() instanceof PsiCodeBlock || insertSemicolon(lambdaExpression.getParent());
}
public static boolean insertSemicolon(PsiElement parent) {
return !(parent instanceof PsiExpressionList) && !(parent instanceof PsiExpression);
}
public static @NlsContexts.DetailedDescription String checkInterfaceFunctional(@NotNull PsiElement context, @NotNull PsiType functionalInterfaceType) {
if (functionalInterfaceType instanceof PsiIntersectionType intersection) {
Set<MethodSignature> signatures = new HashSet<>();
Map<PsiType, MethodSignature> typeAndSignature = new HashMap<>();
for (PsiType type : intersection.getConjuncts()) {
if (checkInterfaceFunctional(context, type) == null) {
MethodSignature signature = getFunction(PsiUtil.resolveClassInType(type));
LOG.assertTrue(signature != null, type.getCanonicalText());
signatures.add(signature);
typeAndSignature.put(type, signature);
}
}
PsiType baseType = typeAndSignature.entrySet().iterator().next().getKey();
MethodSignature baseSignature = typeAndSignature.get(baseType);
LambdaUtil.TargetMethodContainer baseContainer = getTargetMethod(baseType, baseSignature, baseType);
if (baseContainer == null) {
return JavaErrorBundle.message("no.target.method.found");
}
PsiMethod baseMethod = baseContainer.targetMethod;
if (signatures.size() > 1) {
for (Map.Entry<PsiType, MethodSignature> entry : typeAndSignature.entrySet()) {
if (baseType == entry.getKey()) {
continue;
}
LambdaUtil.TargetMethodContainer container = getTargetMethod(entry.getKey(), baseSignature, baseType);
if (container == null) {
return JavaErrorBundle.message("multiple.non.overriding.abstract.methods.found.in.0",
functionalInterfaceType.getPresentableText());
}
if (!LambdaUtil.isLambdaSubsignature(baseMethod, baseType, container.targetMethod, entry.getKey()) ||
!container.inheritor.hasModifier(JvmModifier.ABSTRACT)) {
return JavaErrorBundle.message("multiple.non.overriding.abstract.methods.found.in.0",
functionalInterfaceType.getPresentableText());
}
}
}
for (PsiType type : intersection.getConjuncts()) {
if (typeAndSignature.containsKey(type)) {
continue;
}
LambdaUtil.TargetMethodContainer container = getTargetMethod(type, baseSignature, baseType);
if (container == null) {
continue;
}
PsiMethod inheritor = container.inheritor;
PsiMethod target = container.targetMethod;
if (!inheritor.hasModifier(JvmModifier.ABSTRACT) && LambdaUtil.isLambdaSubsignature(baseMethod, baseType, target, type)) {
return JavaErrorBundle.message("no.target.method.found");
}
}
return null;
}
PsiClassType.ClassResolveResult resolveResult = PsiUtil.resolveGenericsClassInType(functionalInterfaceType);
PsiClass aClass = resolveResult.getElement();
if (aClass != null) {
if (aClass instanceof PsiTypeParameter) return null; //should be logged as cyclic inference
MethodSignature functionalMethod = getFunction(aClass);
if (functionalMethod != null && functionalMethod.getTypeParameters().length > 0) {
return JavaErrorBundle
.message("target.method.is.generic");
}
return checkInterfaceFunctional(aClass);
}
if (IncompleteModelUtil.isIncompleteModel(context) &&
IncompleteModelUtil.isUnresolvedClassType(functionalInterfaceType)) {
return null;
}
return JavaErrorBundle.message("not.a.functional.interface", functionalInterfaceType.getPresentableText());
}
// 15.13 | 15.27
// It is a compile-time error if any class or interface mentioned by either U or the function type of U
// is not accessible from the class or interface in which the method reference expression appears.

View File

@@ -1,11 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight;
import com.intellij.codeInsight.completion.CompletionMemory;
import com.intellij.codeInsight.completion.CompletionUtil;
import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.codeInsight.completion.JavaMethodCallElement;
import com.intellij.codeInsight.daemon.impl.analysis.LambdaHighlightingUtil;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.hints.ParameterHintsPass;
import com.intellij.codeInsight.lookup.CommaTailType;
import com.intellij.openapi.components.Service;
@@ -427,8 +423,8 @@ public final class ExpectedTypesProvider {
final PsiType functionalInterfaceType = lambdaExpression.getFunctionalInterfaceType();
final PsiMethod scopeMethod = LambdaUtil.getFunctionalInterfaceMethod(functionalInterfaceType);
if (scopeMethod != null) {
visitMethodReturnType(scopeMethod, LambdaUtil.getFunctionalInterfaceReturnType(functionalInterfaceType), LambdaHighlightingUtil
.insertSemicolonAfter(lambdaExpression));
visitMethodReturnType(scopeMethod, LambdaUtil.getFunctionalInterfaceReturnType(functionalInterfaceType),
JavaCompletionUtil.insertSemicolonAfter(lambdaExpression));
}
}
@@ -442,7 +438,7 @@ public final class ExpectedTypesProvider {
final PsiType functionalInterfaceType = ((PsiLambdaExpression)psiElement).getFunctionalInterfaceType();
method = LambdaUtil.getFunctionalInterfaceMethod(functionalInterfaceType);
type = LambdaUtil.getFunctionalInterfaceReturnType(functionalInterfaceType);
tailTypeSemicolon = LambdaHighlightingUtil.insertSemicolonAfter((PsiLambdaExpression)psiElement);
tailTypeSemicolon = JavaCompletionUtil.insertSemicolonAfter((PsiLambdaExpression)psiElement);
}
else if (psiElement instanceof PsiMethod) {
method = (PsiMethod)psiElement;

View File

@@ -6,7 +6,6 @@ import com.intellij.codeInsight.*;
import com.intellij.codeInsight.completion.scope.CompletionElement;
import com.intellij.codeInsight.completion.scope.JavaCompletionProcessor;
import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil;
import com.intellij.codeInsight.daemon.impl.analysis.LambdaHighlightingUtil;
import com.intellij.codeInsight.daemon.impl.quickfix.BringVariableIntoScopeFix;
import com.intellij.codeInsight.lookup.*;
import com.intellij.featureStatistics.FeatureUsageTracker;
@@ -1002,7 +1001,7 @@ public final class JavaCompletionContributor extends CompletionContributor imple
static boolean shouldInsertSemicolon(PsiElement position) {
return position.getParent() instanceof PsiMethodReferenceExpression &&
LambdaHighlightingUtil.insertSemicolon(position.getParent().getParent());
JavaCompletionUtil.insertSemicolon(position.getParent().getParent());
}
private static @Unmodifiable List<LookupElement> processLabelReference(PsiLabelReference reference) {

View File

@@ -8,7 +8,6 @@ import com.intellij.codeInsight.completion.scope.JavaCompletionProcessor;
import com.intellij.codeInsight.completion.util.CompletionStyleUtil;
import com.intellij.codeInsight.completion.util.ParenthesesInsertHandler;
import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil;
import com.intellij.codeInsight.daemon.impl.analysis.LambdaHighlightingUtil;
import com.intellij.codeInsight.editorActions.TabOutScopesTracker;
import com.intellij.codeInsight.guess.GuessManager;
import com.intellij.codeInsight.lookup.*;
@@ -467,6 +466,14 @@ public final class JavaCompletionUtil {
return type instanceof PsiClassType ? ((PsiClassType)type).rawType() : type;
}
public static boolean insertSemicolonAfter(@NotNull PsiLambdaExpression lambdaExpression) {
return lambdaExpression.getBody() instanceof PsiCodeBlock || insertSemicolon(lambdaExpression.getParent());
}
static boolean insertSemicolon(PsiElement parent) {
return !(parent instanceof PsiExpressionList) && !(parent instanceof PsiExpression);
}
static class JavaLookupElementHighlighter {
private final @NotNull PsiElement myPlace;
private final @Nullable VirtualFile myOriginalFile;
@@ -882,7 +889,7 @@ public final class JavaCompletionUtil {
if (parent instanceof PsiMethodCallExpression) {
parent = parent.getParent();
}
if (parent instanceof PsiLambdaExpression && !LambdaHighlightingUtil.insertSemicolonAfter((PsiLambdaExpression)parent)) {
if (parent instanceof PsiLambdaExpression lambda && !insertSemicolonAfter(lambda)) {
insertAdditionalSemicolon = false;
}
if (parent instanceof PsiExpressionStatement && parent.getParent() instanceof PsiForStatement forStatement &&

View File

@@ -1 +1,2 @@
<error descr="Foo is not a functional interface">@FunctionalInterface</error>
abstract class Foo { public abstract void run(); }

View File

@@ -1,3 +1,4 @@
<error descr="Multiple non-overriding abstract methods found in Foo">@FunctionalInterface</error>
interface Foo {
int m();
Object clone();

View File

@@ -8,6 +8,6 @@ interface Y<T> {
class Test {
{
((X & <caret>Y<Integer>) (x) -> {}).m(1);
((X & Y<Integer>) <error descr="Multiple non-overriding abstract methods found in X & Y<Integer>">(x) -> {}</error>).m<error descr="Ambiguous method call: both 'X.m(Integer)' and 'Y.m(Integer)' match">(1)</error>;
}
}

View File

@@ -1,3 +1,8 @@
@FunctionalInterface
interface Foo { <T> T execute(Action<T> a); }
interface Action<A>{}
// Functional
class Use {
// Functional but not lambda-compatible
Foo foo = <error descr="Target method is generic">a -> null</error>;
}

View File

@@ -1,3 +1,4 @@
interface Bar { boolean equals(Object obj); }
@FunctionalInterface
interface Foo extends Bar { int compare(String o1, String o2); }

View File

@@ -1,3 +1,4 @@
@FunctionalInterface
interface Foo<T> {
boolean equals(Object obj);
int compare(T o1, T o2);

View File

@@ -1 +1,2 @@
<error descr="No target method found">@FunctionalInterface</error>
interface Foo { boolean equals(Object obj); }

View File

@@ -1 +1,2 @@
@FunctionalInterface
interface Foo { void run(); }

View File

@@ -1,4 +1,5 @@
interface X { int m(Iterable<String> arg); }
interface Y { int m(Iterable<Integer> arg); }
<error descr="Multiple non-overriding abstract methods found in Foo">@FunctionalInterface</error>
interface Foo extends X, Y {}
// Not functional: No method has a subsignature of all abstract methods

View File

@@ -1,4 +1,5 @@
interface X { int m(Iterable<String> arg, Class c); }
interface Y { int m(Iterable arg, Class<?> c); }
<error descr="Multiple non-overriding abstract methods found in Foo">@FunctionalInterface</error>
interface Foo extends X, Y {}
// Not functional: No method has a subsignature of all abstract methods

View File

@@ -1,4 +1,5 @@
import java.util.*;
interface X { int m(Iterable<String> arg); }
interface Y { int m(Iterable<String> arg); }
@FunctionalInterface
interface Foo extends X, Y {}

View File

@@ -1,5 +1,11 @@
interface Action<A>{}
interface X { <T> T execute(Action<T> a); }
interface Y { <S> S execute(Action<S> a); }
@FunctionalInterface
interface Foo extends X, Y {}
// Functional: signatures are "the same"
class Use {
// Functional: signatures are "the same"
// but not lambda-compatible
Foo foo = <error descr="Target method is generic">a -> null</error>;
}

View File

@@ -2,4 +2,5 @@ interface Foo1<T, N extends Number> {
void m(T arg);
void m(N arg);
}
@FunctionalInterface
interface Foo extends Foo1<Integer, Integer> {}

View File

@@ -1,4 +1,5 @@
import java.util.*;
interface X { Iterable m(Iterable<String> arg); }
interface Y { Iterable<String> m(Iterable arg); }
@FunctionalInterface
interface Foo extends X, Y {}

View File

@@ -16,104 +16,76 @@
package com.intellij.java.codeInsight.daemon.lambda;
import com.intellij.codeInsight.daemon.LightDaemonAnalyzerTestCase;
import com.intellij.codeInsight.daemon.impl.analysis.LambdaHighlightingUtil;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeCastExpression;
import com.intellij.psi.PsiTypeElement;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.Nullable;
public class FunctionalInterfaceTest extends LightDaemonAnalyzerTestCase {
@NonNls static final String BASE_PATH = "/codeInsight/daemonCodeAnalyzer/lambda/functionalInterface";
private void doTestFunctionalInterface(@Nullable String expectedErrorMessage) {
String filePath = BASE_PATH + "/" + getTestName(false) + ".java";
configureByFile(filePath);
final PsiClass psiClass = getJavaFacade().findClass("Foo", GlobalSearchScope.projectScope(getProject()));
assertNotNull("Class Foo not found", psiClass);
final String errorMessage = LambdaHighlightingUtil.checkInterfaceFunctional(psiClass,
getJavaFacade().getElementFactory().createType(psiClass));
assertEquals(expectedErrorMessage, errorMessage);
private void doTest() {
doTest(BASE_PATH + "/" + getTestName(false) + ".java", false, false);
}
public void testSimple() {
doTestFunctionalInterface(null);
doTest();
}
public void testNoMethods() {
doTestFunctionalInterface("No target method found");
doTest();
}
public void testMultipleMethods() {
doTestFunctionalInterface(null);
doTest();
}
public void testMultipleMethodsInOne() {
doTestFunctionalInterface(null);
doTest();
}
public void testIntersectionOf2FunctionalTypesWithEqualSignatures() {
doTestIntersection(null);
doTest();
}
public void testIntersectionOf2FunctionalTypesWithEqualAfterSubstitutionSignatures() {
doTestIntersection("Multiple non-overriding abstract methods found in X & Y<Integer>");
doTest();
}
public void testClone() {
doTestFunctionalInterface("Multiple non-overriding abstract methods found in interface Foo");
doTest();
}
public void testTwoMethodsSameSignature() {
doTestFunctionalInterface(null);
doTest();
}
public void testTwoMethodsSubSignature() {
doTestFunctionalInterface(null);
doTest();
}
public void testTwoMethodsNoSubSignature() {
doTestFunctionalInterface("Multiple non-overriding abstract methods found in interface Foo");
doTest();
}
public void testTwoMethodsNoSubSignature1() {
doTestFunctionalInterface("Multiple non-overriding abstract methods found in interface Foo");
doTest();
}
public void testTwoMethodsSameSubstSignature() {
doTestFunctionalInterface(null);
doTest();
}
public void testMethodWithTypeParam() {
doTestFunctionalInterface("Target method is generic");
doTest();
}
public void testTwoMethodsSameSignatureTypeParams() {
doTestFunctionalInterface("Target method is generic");
doTest();
}
public void testAbstractClass() {
doTestFunctionalInterface("Target type of a lambda conversion must be an interface");
doTest();
}
public void testIntersectionTypeWithSameBaseInterfaceInConjuncts() {
doTestIntersection(null);
}
private void doTestIntersection(final String expectedMessage) {
String filePath = BASE_PATH + "/" + getTestName(false) + ".java";
configureByFile(filePath);
final PsiTypeCastExpression castExpression =
PsiTreeUtil.getParentOfType(getFile().findElementAt(getEditor().getCaretModel().getOffset()), PsiTypeCastExpression.class);
assertNotNull(castExpression);
final PsiTypeElement castTypeElement = castExpression.getCastType();
assertNotNull(castTypeElement);
final PsiType type = castTypeElement.getType();
final String errorMessage = LambdaHighlightingUtil.checkInterfaceFunctional(castExpression, type);
assertEquals(expectedMessage, errorMessage);
doTest();
}
}