mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[java] IDEA-251649 Sort 'Cast argument to' quickfixes
#IDEA-251649 Fixed Merge-request: IJ-MR-173597 Merged-by: Marcin Mikosik <marcin.mikosik@jetbrains.com> GitOrigin-RevId: 197b9c4e3fb9be1d753b895b36a44c08cb7afdef
This commit is contained in:
committed by
intellij-monorepo-bot
parent
539f7567fb
commit
3605bcd824
-1
@@ -754,7 +754,6 @@ public final class HighlightFixUtil {
|
||||
@NotNull JavaResolveResult @NotNull [] resolveResults) {
|
||||
PsiExpressionList list = methodCall.getArgumentList();
|
||||
MethodCandidateInfo[] candidates = toMethodCandidates(resolveResults);
|
||||
CastMethodArgumentFix.REGISTRAR.registerCastActions(candidates, methodCall, sink);
|
||||
WrapWithAdapterMethodCallFix.registerCastActions(candidates, methodCall, sink);
|
||||
WrapObjectWithOptionalOfNullableFix.REGISTAR.registerCastActions(candidates, methodCall, sink);
|
||||
WrapExpressionFix.registerWrapAction(candidates, list.getExpressions(), sink);
|
||||
|
||||
+53
-19
@@ -11,6 +11,7 @@ import com.intellij.psi.util.PsiTypesUtil;
|
||||
import com.intellij.psi.util.PsiUtil;
|
||||
import com.intellij.psi.util.TypeConversionUtil;
|
||||
import com.intellij.util.IncorrectOperationException;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -60,12 +61,13 @@ public abstract class ArgumentFixerActionFactory {
|
||||
try {
|
||||
PsiType expectedTypeByParent = PsiTypesUtil.getExpectedTypeByParent(call);
|
||||
Map<Integer, Set<String>> suggestedCasts = new HashMap<>();
|
||||
// find to which type we can cast this param to get valid method call
|
||||
// find to which type we can cast this param to get a valid method call
|
||||
List<ArgumentCast> actualCasts = new ArrayList<>();
|
||||
for (CandidateInfo candidate : methodCandidates) {
|
||||
PsiMethod method = (PsiMethod)candidate.getElement();
|
||||
PsiSubstitutor substitutor = candidate.getSubstitutor();
|
||||
PsiParameter[] parameters = method.getParameterList().getParameters();
|
||||
Map<Integer, PsiType> potentialCasts = new HashMap<>();
|
||||
List<ArgumentCast> potentialCasts = new ArrayList<>();
|
||||
for (int i = 0; i < expressions.length; i++) {
|
||||
PsiExpression expression = expressions[i];
|
||||
PsiType exprType = PsiUtil.skipParenthesizedExprDown(expression) instanceof PsiFunctionalExpression fn ?
|
||||
@@ -82,33 +84,34 @@ public abstract class ArgumentFixerActionFactory {
|
||||
}
|
||||
// strict compare since even widening cast may help
|
||||
if (Comparing.equal(exprType, parameterType)) continue;
|
||||
potentialCasts.put(i, parameterType);
|
||||
potentialCasts.add(new ArgumentCast(i, parameterType));
|
||||
}
|
||||
|
||||
if (!potentialCasts.isEmpty()) {
|
||||
PsiCall newCall = call;
|
||||
for (Map.Entry<Integer, PsiType> entry : potentialCasts.entrySet()) {
|
||||
newCall = replaceWithCast(expressions, newCall, entry, newCall == call);
|
||||
for (var argumentCast : potentialCasts) {
|
||||
newCall = replaceWithCast(expressions, newCall, argumentCast, newCall == call);
|
||||
if (newCall == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (newCall != null) {
|
||||
doCheckNewCall(expectedTypeByParent, newCall, () -> {
|
||||
for (Map.Entry<Integer, PsiType> entry : potentialCasts.entrySet()) {
|
||||
registerCastIntention(info, list, suggestedCasts, entry);
|
||||
for (var argumentCast : potentialCasts) {
|
||||
collectCast(actualCasts, suggestedCasts, argumentCast);
|
||||
}
|
||||
potentialCasts.clear();
|
||||
});
|
||||
}
|
||||
|
||||
for (Map.Entry<Integer, PsiType> entry : potentialCasts.entrySet()) {
|
||||
PsiCall callWithSingleCast = replaceWithCast(expressions, call, entry, true);
|
||||
|
||||
for (var argumentCast : potentialCasts) {
|
||||
PsiCall callWithSingleCast = replaceWithCast(expressions, call, argumentCast, true);
|
||||
if (callWithSingleCast == null) continue;
|
||||
doCheckNewCall(expectedTypeByParent, callWithSingleCast, () -> registerCastIntention(info, list, suggestedCasts, entry));
|
||||
doCheckNewCall(expectedTypeByParent, callWithSingleCast, () -> collectCast(actualCasts, suggestedCasts, argumentCast));
|
||||
}
|
||||
}
|
||||
}
|
||||
putCompilableCastsFirst(actualCasts, expressions).forEach(cast -> registerCastIntention(info, list, cast));
|
||||
}
|
||||
catch (IncorrectOperationException e) {
|
||||
LOG.error(e);
|
||||
@@ -128,20 +131,17 @@ public abstract class ArgumentFixerActionFactory {
|
||||
|
||||
private void registerCastIntention(@NotNull Consumer<? super CommonIntentionAction> info,
|
||||
PsiExpressionList list,
|
||||
Map<Integer, Set<String>> suggestedCasts,
|
||||
Map.Entry<Integer, PsiType> entry) {
|
||||
suggestedCasts.get(entry.getKey()).add(entry.getValue().getCanonicalText());
|
||||
IntentionAction action = createFix(list, entry.getKey(), entry.getValue());
|
||||
ArgumentCast argumentCast) {
|
||||
IntentionAction action = createFix(list, argumentCast.argumentIndex(), argumentCast.castType());
|
||||
if (action != null) {
|
||||
info.accept(action);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable PsiCall replaceWithCast(PsiExpression[] expressions, @NotNull PsiCall origCall, Map.Entry<Integer, PsiType> entry,
|
||||
private @Nullable PsiCall replaceWithCast(PsiExpression[] expressions, @NotNull PsiCall origCall, ArgumentCast argumentCast,
|
||||
boolean shouldCopy) {
|
||||
Integer i = entry.getKey();
|
||||
PsiType parameterType = entry.getValue();
|
||||
PsiExpression modifiedExpression = getModifiedArgument(expressions[i], parameterType);
|
||||
int i = argumentCast.argumentIndex();
|
||||
PsiExpression modifiedExpression = getModifiedArgument(expressions[i], argumentCast.castType());
|
||||
if (modifiedExpression == null) return null;
|
||||
PsiExpressionList argumentList = origCall.getArgumentList();
|
||||
if (argumentList == null) return null;
|
||||
@@ -151,7 +151,41 @@ public abstract class ArgumentFixerActionFactory {
|
||||
return newCall;
|
||||
}
|
||||
|
||||
private static void collectCast(@NotNull List<@NotNull ArgumentCast> actualCasts,
|
||||
@NotNull Map<@NotNull Integer, @NotNull Set<@NotNull String>> suggestedCasts,
|
||||
@NotNull ArgumentCast argumentCast) {
|
||||
actualCasts.add(argumentCast);
|
||||
suggestedCasts.get(argumentCast.argumentIndex()).add(argumentCast.castType().getCanonicalText());
|
||||
}
|
||||
|
||||
private List<ArgumentCast> putCompilableCastsFirst(@NotNull List<@NotNull ArgumentCast> casts,
|
||||
@NotNull PsiExpression @NotNull [] expressions) {
|
||||
List<ArgumentCast> compilableCasts = new ArrayList<>();
|
||||
List<ArgumentCast> nonCompilableCasts = new ArrayList<>();
|
||||
for (var argumentCast : casts) {
|
||||
if (doesFixCauseOtherCompilationErrors(expressions[argumentCast.argumentIndex()], argumentCast.castType())) {
|
||||
nonCompilableCasts.add(argumentCast);
|
||||
}
|
||||
else {
|
||||
compilableCasts.add(argumentCast);
|
||||
}
|
||||
}
|
||||
return ContainerUtil.concat(compilableCasts, nonCompilableCasts);
|
||||
}
|
||||
|
||||
protected boolean doesFixCauseOtherCompilationErrors(@NotNull PsiExpression expression, @NotNull PsiType parameterType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract boolean areTypesConvertible(@NotNull PsiType exprType, @NotNull PsiType parameterType, @NotNull PsiElement context);
|
||||
|
||||
public abstract IntentionAction createFix(PsiExpressionList list, int i, PsiType parameterType);
|
||||
|
||||
/**
|
||||
* Cast of an argument in a method call to a type.
|
||||
* @param argumentIndex index of the argument that is cast
|
||||
* @param castType type in a cast expression
|
||||
*/
|
||||
private record ArgumentCast(int argumentIndex, @NotNull PsiType castType) {
|
||||
}
|
||||
}
|
||||
+48
@@ -9,9 +9,19 @@ import com.intellij.modcommand.Presentation;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.util.PsiUtil;
|
||||
import com.intellij.util.IncorrectOperationException;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static com.intellij.codeInsight.ExceptionUtil.collectUnhandledExceptions;
|
||||
import static com.intellij.codeInsight.ExceptionUtil.isHandledBy;
|
||||
import static com.intellij.psi.LambdaUtil.getFunctionalInterfaceMethod;
|
||||
import static com.intellij.psi.util.PsiUtil.skipParenthesizedExprDown;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
public final class CastMethodArgumentFix extends MethodArgumentFix {
|
||||
private CastMethodArgumentFix(PsiExpressionList list, int i, PsiType toType, final ArgumentFixerActionFactory factory) {
|
||||
super(list, i, toType, factory);
|
||||
@@ -69,6 +79,44 @@ public final class CastMethodArgumentFix extends MethodArgumentFix {
|
||||
return parameterType instanceof PsiEllipsisType ellipsisType &&
|
||||
areTypesConvertible(exprType, ellipsisType.getComponentType(), context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true when the parameterType is a functional interface
|
||||
* AND argument is a functional interface instance
|
||||
* AND that instance throws an unchecked exception that is not declared by functional interface SAM.
|
||||
*/
|
||||
@Override
|
||||
protected boolean doesFixCauseOtherCompilationErrors(@NotNull PsiExpression expression, @NotNull PsiType parameterType) {
|
||||
var declaredExceptions = declaredExceptionsOfFunctionalInterface(parameterType);
|
||||
if (declaredExceptions == null) return false;
|
||||
var unhandledExceptions = exceptionsThrownByFunctionalExpression(skipParenthesizedExprDown(expression));
|
||||
return ContainerUtil.exists(unhandledExceptions, unhandled -> !isHandledBy(unhandled, declaredExceptions));
|
||||
}
|
||||
|
||||
private static @NotNull Collection<@NotNull PsiClassType> exceptionsThrownByFunctionalExpression(PsiExpression expression) {
|
||||
if (expression instanceof PsiLambdaExpression lambdaExpression) {
|
||||
PsiElement body = lambdaExpression.getBody();
|
||||
if (body != null) {
|
||||
return collectUnhandledExceptions(body, body);
|
||||
}
|
||||
}
|
||||
else if (expression instanceof PsiMethodReferenceExpression methodReferenceExpression) {
|
||||
PsiElement target = methodReferenceExpression.resolve();
|
||||
if (target instanceof PsiMethod psiMethod) {
|
||||
return asList(psiMethod.getThrowsList().getReferencedTypes());
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private static @NotNull PsiClassType @Nullable [] declaredExceptionsOfFunctionalInterface(@NotNull PsiType parameterType) {
|
||||
if (!(parameterType instanceof PsiClassType classType)) return null;
|
||||
var psiSubstitutor = classType.resolveGenerics().getSubstitutor();
|
||||
var psiMethod = getFunctionalInterfaceMethod(parameterType);
|
||||
if (psiMethod == null) return null;
|
||||
var throwListTypes = psiMethod.getThrowsList().getReferencedTypes();
|
||||
return ContainerUtil.map(throwListTypes, type -> (PsiClassType)psiSubstitutor.substitute(type)).toArray(PsiClassType[]::new);
|
||||
}
|
||||
}
|
||||
|
||||
public static final ArgumentFixerActionFactory REGISTRAR = new MyFixerActionFactory();
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.codeInsight.daemon.impl.quickfix;
|
||||
|
||||
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixTestCase;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.intellij.testFramework.assertions.Assertions.assertThat;
|
||||
|
||||
public class CastMethodParametersOrderTest extends LightQuickFixTestCase {
|
||||
|
||||
/**
|
||||
* Verifies that 'cast argument to' fixes, which generate code that contains further compilation errors,
|
||||
* are placed at the bottom of the quick-fix list.
|
||||
*/
|
||||
public void testOrderOfCastMethodArgumentQuickFixesForLambdaArgument() {
|
||||
String code = """
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Supplier;
|
||||
public class AClass {
|
||||
void foo1() {
|
||||
m1(<caret>() -> { throw new IOException(); });
|
||||
}
|
||||
void m1(Supplier<String> a) {}
|
||||
void m1(Callable<String> a) {}
|
||||
}
|
||||
""";
|
||||
var expectedQuickFixes = List.of(
|
||||
"Cast argument to 'Callable<String>'",
|
||||
"Cast argument to 'Supplier<String>'");
|
||||
assertThatFollowingQuickFixesAreAvailableInOrder(code, expectedQuickFixes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that 'cast argument to' fixes, which generate code that contains further compilation errors,
|
||||
* are placed at the bottom of the quick-fix list.
|
||||
*/
|
||||
public void testOrderOfCastMethodArgumentQuickFixesForMethodReferenceArgument() {
|
||||
String code = """
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Supplier;
|
||||
public class AClass {
|
||||
void foo2() {
|
||||
m1(<caret>this::returnString);
|
||||
}
|
||||
public String returnString() throws IOException {
|
||||
return null;
|
||||
}
|
||||
void m1(Supplier<String> a) {}
|
||||
void m1(Callable<String> a) {}
|
||||
}
|
||||
""";
|
||||
List<String> expectedQuickFixes = List.of(
|
||||
"Cast argument to 'Callable<String>'",
|
||||
"Cast argument to 'Supplier<String>'");
|
||||
assertThatFollowingQuickFixesAreAvailableInOrder(code, expectedQuickFixes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that 'cast argument to' fixes, which generate code that contains further compilation errors,
|
||||
* are placed at the bottom of the quick-fix list.
|
||||
*/
|
||||
public void testOrderOfCastMethodArgumentQuickFixesForParameterWithGenericThrow() {
|
||||
String code = """
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Supplier;
|
||||
public class AClass {
|
||||
void foo1() {
|
||||
m1(<caret>() -> { throw new IOException(); });
|
||||
}
|
||||
void m1(Supplier<String> a) {}
|
||||
void m1(Callable<String> a) {}
|
||||
<E extends Throwable> void m1(ThrowingRunnable<E> a) {}
|
||||
}
|
||||
interface ThrowingRunnable<T extends Throwable> {
|
||||
void run() throws T;
|
||||
}
|
||||
""";
|
||||
List<String> expectedQuickFixes = List.of(
|
||||
"Cast argument to 'Callable<String>'",
|
||||
"Cast argument to 'ThrowingRunnable<IOException>'",
|
||||
"Cast argument to 'Supplier<String>'");
|
||||
assertThatFollowingQuickFixesAreAvailableInOrder(code, expectedQuickFixes);
|
||||
}
|
||||
|
||||
public void testOrderOfCastMethodArgumentQuickFixesForParameterWithRawGenericThrow() {
|
||||
String code = """
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Supplier;
|
||||
public class AClass {
|
||||
void foo1() {
|
||||
m1(<caret>() -> { throw new IOException(); });
|
||||
}
|
||||
void m1(Supplier<String> a) {}
|
||||
void m1(Callable<String> a) {}
|
||||
<E extends Throwable> void m1(ThrowingRunnable a) {}
|
||||
}
|
||||
interface ThrowingRunnable<T extends Throwable> {
|
||||
void run() throws T;
|
||||
}
|
||||
""";
|
||||
List<String> expectedQuickFixes = List.of(
|
||||
"Cast argument to 'Callable<String>'",
|
||||
"Cast argument to 'ThrowingRunnable'",
|
||||
"Cast argument to 'Supplier<String>'");
|
||||
assertThatFollowingQuickFixesAreAvailableInOrder(code, expectedQuickFixes);
|
||||
}
|
||||
|
||||
private void assertThatFollowingQuickFixesAreAvailableInOrder(String code, List<@NotNull String> expectedQuickFixes) {
|
||||
configureFromFileText("AClass.java", code);
|
||||
var actionNames = ContainerUtil.map(getAvailableActions(), a -> a.getText());
|
||||
assertThat(actionNames).containsSubsequence(expectedQuickFixes);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user