[java-analysis] LambdaUtil.isSafeLambdaReplacement: check all calls in-between; check applicability

Fixes IDEA-350194 Inspection QuickFix results in compilation error

GitOrigin-RevId: fb83bfcb4e4aef9dc87a5c84de6d08202dd52ec6
This commit is contained in:
Tagir Valeev
2024-04-15 16:36:43 +02:00
committed by intellij-monorepo-bot
parent 00b2e6cb16
commit 5ae32b3ec9
8 changed files with 120 additions and 41 deletions

View File

@@ -999,42 +999,43 @@ public final class LambdaUtil {
if (body == null) return false;
final PsiCall call = treeWalkUp(body);
if (call != null) {
JavaResolveResult result = call.resolveMethodGenerics();
PsiElement oldTarget = result.getElement();
if (oldTarget != null) {
String origErrorMessage = result instanceof MethodCandidateInfo ? ((MethodCandidateInfo)result).getInferenceErrorMessage() : null;
Object marker = new Object();
PsiTreeUtil.mark(lambda, marker);
PsiType origType = call instanceof PsiExpression ? ((PsiExpression)call).getType() : null;
PsiCall copyCall = copyTopLevelCall(call);
if (copyCall == null) return false;
PsiLambdaExpression lambdaCopy = ObjectUtils.tryCast(PsiTreeUtil.releaseMark(copyCall, marker), PsiLambdaExpression.class);
if (lambdaCopy == null) return false;
PsiExpression function = replacer.apply(lambdaCopy);
if (function == null) return false;
JavaResolveResult resultCopy = copyCall.resolveMethodGenerics();
if (!oldTarget.getManager().areElementsEquivalent(resultCopy.getElement(), oldTarget)) return false;
String copyMessage = resultCopy instanceof MethodCandidateInfo ? ((MethodCandidateInfo)resultCopy).getInferenceErrorMessage() : null;
if (!Objects.equals(origErrorMessage, copyMessage)) return false;
if (function instanceof PsiFunctionalExpression) {
PsiType functionalType = ((PsiFunctionalExpression)function).getFunctionalInterfaceType();
if (functionalType == null) return false;
PsiType lambdaFunctionalType = lambda.getFunctionalInterfaceType();
if (lambdaFunctionalType != null && !functionalType.getCanonicalText().equals(lambdaFunctionalType.getCanonicalText())) {
return false;
}
Object marker = new Object();
PsiTreeUtil.mark(lambda, marker);
PsiType origType = call instanceof PsiExpression ? ((PsiExpression)call).getType() : null;
PsiCall copyCall = copyTopLevelCall(call);
if (copyCall == null) return false;
PsiLambdaExpression lambdaCopy = ObjectUtils.tryCast(PsiTreeUtil.releaseMark(copyCall, marker), PsiLambdaExpression.class);
if (lambdaCopy == null) return false;
PsiExpression function = replacer.apply(lambdaCopy);
if (function == null) return false;
PsiElement copyCur = function, cur = lambda;
while(cur != call) {
cur = cur.getParent();
copyCur = copyCur.getParent();
if (cur instanceof PsiCall && copyCur instanceof PsiCall) {
JavaResolveResult result = ((PsiCall)cur).resolveMethodGenerics();
JavaResolveResult resultCopy = ((PsiCall)copyCur).resolveMethodGenerics();
if (!equalResolveResult(result, resultCopy)) return false;
}
if (origType instanceof PsiClassType && !((PsiClassType)origType).isRaw() &&
//when lambda has no formal parameter types, it's ignored during applicability check
//so unchecked warnings inside lambda's body won't lead to erasure of the type of the containing call
//but after replacement of lambda with the equivalent method call, unchecked warning won't be ignored anymore
//and the type of the call would be erased => red code may appear
!lambda.hasFormalParameterTypes()) {
PsiExpression expressionFromBody = extractSingleExpressionFromBody(body);
if (expressionFromBody instanceof PsiMethodCallExpression &&
PsiTypesUtil.isUncheckedCall(((PsiMethodCallExpression)expressionFromBody).resolveMethodGenerics())) {
return false;
}
}
if (function instanceof PsiFunctionalExpression) {
PsiType functionalType = ((PsiFunctionalExpression)function).getFunctionalInterfaceType();
PsiType lambdaFunctionalType = lambda.getFunctionalInterfaceType();
boolean sameType = functionalType == null ? lambdaFunctionalType == null :
lambdaFunctionalType != null &&
functionalType.getCanonicalText().equals(lambdaFunctionalType.getCanonicalText());
if (!sameType) return false;
}
if (origType instanceof PsiClassType && !((PsiClassType)origType).isRaw() &&
//when lambda has no formal parameter types, it's ignored during applicability check
//so unchecked warnings inside lambda's body won't lead to erasure of the type of the containing call
//but after replacement of lambda with the equivalent method call, unchecked warning won't be ignored anymore
//and the type of the call would be erased => red code may appear
!lambda.hasFormalParameterTypes()) {
PsiExpression expressionFromBody = extractSingleExpressionFromBody(body);
if (expressionFromBody instanceof PsiMethodCallExpression &&
PsiTypesUtil.isUncheckedCall(((PsiMethodCallExpression)expressionFromBody).resolveMethodGenerics())) {
return false;
}
}
}
@@ -1050,6 +1051,19 @@ public final class LambdaUtil {
return true;
}
private static boolean equalResolveResult(JavaResolveResult r1, JavaResolveResult r2) {
PsiElement target1 = r1.getElement();
PsiElement target2 = r2.getElement();
boolean targetMatch = target1 == null ? target2 == null : target1.getManager().areElementsEquivalent(target2, target1);
if (!targetMatch) return false;
boolean applicable1 = !(r1 instanceof MethodCandidateInfo) || ((MethodCandidateInfo)r1).isApplicable();
boolean applicable2 = !(r2 instanceof MethodCandidateInfo) || ((MethodCandidateInfo)r2).isApplicable();
if (applicable1 != applicable2) return false;
String message1 = r1 instanceof MethodCandidateInfo ? ((MethodCandidateInfo)r1).getInferenceErrorMessage() : null;
String message2 = r2 instanceof MethodCandidateInfo ? ((MethodCandidateInfo)r2).getInferenceErrorMessage() : null;
return Objects.equals(message1, message2);
}
/**
* Returns false if after suggested replacement of lambda body, containing method call would resolve to something else
* or its return type will change.

View File

@@ -0,0 +1,10 @@
// "Replace lambda with method reference" "false"
import java.util.*;
import java.util.stream.*;
class Test {
void test() {
Set<String> lorem = Collections.unmodifiableSet(Stream.of("Lorem")
.collect(Collectors.toCollection(() -> new TreeSet<<caret>>())));
}
}

View File

@@ -1,6 +1,5 @@
// "Collapse loop with stream 'findFirst()'" "true-preview"
import java.awt.*;
import java.util.List;
import java.util.Objects;
@@ -10,4 +9,14 @@ public class Main {
public Point find(List<Point> points) {
return points.stream().filter(Objects::nonNull).findFirst().orElse(field);
}
static class Point {
private int x;
private int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
}

View File

@@ -1,6 +1,5 @@
// "Collapse loop with stream 'findFirst()'" "true-preview"
import java.awt.*;
import java.util.List;
import java.util.Objects;
@@ -10,4 +9,14 @@ public class Main {
public static Point find(List<Point> points) {
return points.stream().filter(Objects::nonNull).findFirst().orElse(ZERO);
}
static class Point {
private int x;
private int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
}

View File

@@ -1,6 +1,5 @@
// "Collapse loop with stream 'findFirst()'" "false"
import java.awt.*;
import java.util.List;
public class Main {
@@ -10,4 +9,15 @@ public class Main {
}
return new Point(0, 0);
}
static class Point {
private int x;
private int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
}

View File

@@ -1,6 +1,5 @@
// "Collapse loop with stream 'findFirst()'" "true-preview"
import java.awt.*;
import java.util.List;
public class Main {
@@ -12,4 +11,14 @@ public class Main {
}
return field;
}
static class Point {
private int x;
private int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
}

View File

@@ -1,6 +1,5 @@
// "Collapse loop with stream 'findFirst()'" "false"
import java.awt.*;
import java.util.List;
public class Main {
@@ -12,4 +11,14 @@ public class Main {
}
return other.field;
}
static class Point {
private int x;
private int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
}

View File

@@ -1,6 +1,5 @@
// "Collapse loop with stream 'findFirst()'" "true-preview"
import java.awt.*;
import java.util.List;
public class Main {
@@ -12,4 +11,14 @@ public class Main {
}
return ZERO;
}
static class Point {
private int x;
private int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
}