From 2d6d319d1e5586278d14da6e9073a88bef4f7cce Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Wed, 6 Apr 2022 18:03:45 +0700 Subject: [PATCH] [java-intentions] Adapt type using no-arg instance methods Part of IDEA-242022 WrapWithAdapterMethodCallFix.Wrapper improvement GitOrigin-RevId: 4e4bac1a4f3942589e45970a61478e71bf08482c --- .../WrapWithAdapterMethodCallFix.java | 131 ++++++++++++++---- .../afterInstanceConverterEnumeration.java | 9 ++ .../afterInstanceConverterFile.java | 9 ++ .../afterInstanceConverterPath.java | 9 ++ .../afterInstanceConverterStream.java | 9 ++ .../afterInstanceConverterToCharArray.java | 9 ++ .../beforeInstanceConverterEnumeration.java | 9 ++ .../beforeInstanceConverterFile.java | 9 ++ .../beforeInstanceConverterPath.java | 9 ++ .../beforeInstanceConverterStream.java | 9 ++ .../beforeInstanceConverterToCharArray.java | 9 ++ 11 files changed, 193 insertions(+), 28 deletions(-) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterEnumeration.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterFile.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterPath.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterStream.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterToCharArray.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterEnumeration.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterFile.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterPath.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterStream.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterToCharArray.java diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/WrapWithAdapterMethodCallFix.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/WrapWithAdapterMethodCallFix.java index a4c70333448a..9464e71f2163 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/WrapWithAdapterMethodCallFix.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/WrapWithAdapterMethodCallFix.java @@ -15,26 +15,94 @@ import com.intellij.psi.*; import com.intellij.psi.codeStyle.JavaCodeStyleManager; import com.intellij.psi.impl.source.PsiClassReferenceType; import com.intellij.psi.infos.CandidateInfo; -import com.intellij.psi.util.InheritanceUtil; -import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.psi.util.PsiTypesUtil; -import com.intellij.psi.util.PsiUtil; +import com.intellij.psi.util.*; import com.intellij.util.ArrayUtil; import com.intellij.util.IncorrectOperationException; import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; +import one.util.streamex.MoreCollectors; +import one.util.streamex.StreamEx; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Objects; import java.util.function.Predicate; import static com.intellij.pom.java.LanguageLevel.JDK_11; import static com.intellij.pom.java.LanguageLevel.JDK_1_9; public final class WrapWithAdapterMethodCallFix extends LocalQuickFixAndIntentionActionOnPsiElement implements HighPriorityAction { - static class Wrapper extends ArgumentFixerActionFactory { + static abstract class AbstractWrapper extends ArgumentFixerActionFactory { + abstract boolean isApplicable(PsiElement context, PsiType inType, PsiType outType); + + @Override + public boolean areTypesConvertible(@NotNull final PsiType exprType, + @NotNull final PsiType parameterType, + @NotNull final PsiElement context) { + return parameterType.isConvertibleFrom(exprType) || isApplicable(context, exprType, parameterType); + } + + @Override + public MethodArgumentFix createFix(final PsiExpressionList list, final int i, final PsiType toType) { + return new MyMethodArgumentFix(list, i, toType, this); + } + + abstract String getText(PsiExpression element, PsiType type); + } + + static class InstanceMethodFixer extends AbstractWrapper { + @Override + protected @Nullable PsiExpression getModifiedArgument(PsiExpression expression, PsiType toType) + throws IncorrectOperationException { + PsiMethod targetMethod = findOnlyMethod(expression.getType(), toType); + if (targetMethod == null) return null; + var replacement = (PsiMethodCallExpression)JavaPsiFacade.getElementFactory(expression.getProject()) + .createExpressionFromText("x." + targetMethod.getName() + "()", expression); + Objects.requireNonNull(replacement.getMethodExpression().getQualifierExpression()).replace(expression); + return replacement; + } + + @Nullable + private static PsiMethod findOnlyMethod(@Nullable PsiType inType, @NotNull PsiType outType) { + if (!(inType instanceof PsiClassType)) return null; + PsiClassType.ClassResolveResult result = ((PsiClassType)inType).resolveGenerics(); + PsiClass psiClass = result.getElement(); + if (psiClass == null || psiClass instanceof PsiTypeParameter) return null; + return StreamEx.of(psiClass.getAllMethods()) + .collect(MoreCollectors.onlyOne(method -> { + if (method.hasModifierProperty(PsiModifier.STATIC) || !method.hasModifierProperty(PsiModifier.PUBLIC)) return false; + if (method.isConstructor()) return false; + if (method.getName().equals("hashCode")) return false; + if (!method.getParameterList().isEmpty()) return false; + PsiType type = method.getReturnType(); + if (type == null || PsiType.VOID.equals(type)) return false; + if (type instanceof PsiClassType) { + PsiClass containingClass = method.getContainingClass(); + if (containingClass == null) return false; + type = TypeConversionUtil.getSuperClassSubstitutor(containingClass, (PsiClassType)inType).substitute(type); + } + return outType.isAssignableFrom(type); + })).orElse(null); + } + + @Override + boolean isApplicable(PsiElement context, PsiType inType, PsiType outType) { + return findOnlyMethod(inType, outType) != null; + } + + @Override + @Nullable String getText(PsiExpression expression, PsiType type) { + PsiMethod method = findOnlyMethod(expression.getType(), type); + if (method != null) { + return method.getName() + "()"; + } + return null; + } + } + + static class Wrapper extends AbstractWrapper { final Predicate myInTypeFilter; final Predicate myOutTypeFilter; final String myTemplate; @@ -52,6 +120,7 @@ public final class WrapWithAdapterMethodCallFix extends LocalQuickFixAndIntentio myTemplate = template; } + @Override boolean isApplicable(PsiElement context, PsiType inType, PsiType outType) { if (inType == null || outType == null || @@ -102,6 +171,11 @@ public final class WrapWithAdapterMethodCallFix extends LocalQuickFixAndIntentio return resultType != null && outType.isAssignableFrom(resultType); } + @Override + String getText(PsiExpression element, PsiType type) { + return toString(); + } + @NotNull private PsiExpression createReplacement(PsiElement context, @NonNls String replacement) { return JavaPsiFacade.getElementFactory(context.getProject()).createExpressionFromText( @@ -118,24 +192,13 @@ public final class WrapWithAdapterMethodCallFix extends LocalQuickFixAndIntentio return null; } - @Override - public boolean areTypesConvertible(@NotNull final PsiType exprType, - @NotNull final PsiType parameterType, - @NotNull final PsiElement context) { - return parameterType.isConvertibleFrom(exprType) || isApplicable(context, exprType, parameterType); - } - - @Override - public MethodArgumentFix createFix(final PsiExpressionList list, final int i, final PsiType toType) { - return new MyMethodArgumentFix(list, i, toType, this); - } - public String toString() { return myTemplate.replace("{0}", "").replaceAll("\\b[a-z.]+\\.", ""); } } - private static final Wrapper[] WRAPPERS = { + private static final AbstractWrapper[] WRAPPERS = { + new InstanceMethodFixer(), new Wrapper("new java.io.File({0})", inType -> inType.equalsToText(CommonClassNames.JAVA_LANG_STRING), outType -> outType.equalsToText(CommonClassNames.JAVA_IO_FILE)), @@ -178,13 +241,13 @@ public final class WrapWithAdapterMethodCallFix extends LocalQuickFixAndIntentio @SafeFieldForPreview @Nullable private final PsiType myType; @SafeFieldForPreview - @Nullable private final Wrapper myWrapper; + @Nullable private final AbstractWrapper myWrapper; public WrapWithAdapterMethodCallFix(@Nullable PsiType type, @NotNull PsiExpression expression) { this(type, expression, ContainerUtil.find(WRAPPERS, w -> w.isApplicable(expression, expression.getType(), type))); } - private WrapWithAdapterMethodCallFix(@Nullable PsiType type, @NotNull PsiExpression expression, @Nullable Wrapper wrapper) { + private WrapWithAdapterMethodCallFix(@Nullable PsiType type, @NotNull PsiExpression expression, @Nullable AbstractWrapper wrapper) { super(expression); myType = type; myWrapper = wrapper; @@ -194,7 +257,11 @@ public final class WrapWithAdapterMethodCallFix extends LocalQuickFixAndIntentio @NotNull @Override public String getText() { - return QuickFixBundle.message("wrap.with.adapter.text", myWrapper); + String wrapperText = myWrapper == null ? null : myWrapper.getText((PsiExpression)getStartElement(), myType); + if (wrapperText == null) { + return getFamilyName(); + } + return QuickFixBundle.message("wrap.with.adapter.text", wrapperText); } @Nls @@ -224,12 +291,15 @@ public final class WrapWithAdapterMethodCallFix extends LocalQuickFixAndIntentio private PsiExpression getModifiedExpression(@NotNull PsiElement expression) { assert myWrapper != null; - return myWrapper.createReplacement(expression, expression.getText()); + return myWrapper.getModifiedArgument((PsiExpression)expression, myType); } private static class MyMethodArgumentFix extends MethodArgumentFix implements HighPriorityAction { - protected MyMethodArgumentFix(@NotNull PsiExpressionList list, int i, @NotNull PsiType toType, @NotNull Wrapper fixerActionFactory) { + protected MyMethodArgumentFix(@NotNull PsiExpressionList list, + int i, + @NotNull PsiType toType, + @NotNull AbstractWrapper fixerActionFactory) { super(list, i, toType, fixerActionFactory); } @@ -238,9 +308,14 @@ public final class WrapWithAdapterMethodCallFix extends LocalQuickFixAndIntentio @Override public String getText() { PsiExpressionList list = myArgList.getElement(); - return list != null && list.getExpressionCount() == 1 - ? QuickFixBundle.message("wrap.with.adapter.parameter.single.text", myArgumentFixerActionFactory) - : QuickFixBundle.message("wrap.with.adapter.parameter.multiple.text", myIndex + 1, myArgumentFixerActionFactory); + if (list == null) return getFamilyName(); + AbstractWrapper wrapper = (AbstractWrapper)myArgumentFixerActionFactory; + String wrapperText = wrapper.getText(list.getExpressions()[myIndex], myToType); + if (wrapperText == null) return getFamilyName(); + if (list.getExpressionCount() == 1) { + return QuickFixBundle.message("wrap.with.adapter.parameter.single.text", wrapperText); + } + return QuickFixBundle.message("wrap.with.adapter.parameter.multiple.text", myIndex + 1, wrapperText); } @Override @@ -248,7 +323,7 @@ public final class WrapWithAdapterMethodCallFix extends LocalQuickFixAndIntentio PsiExpressionList list = myArgList.getElement(); if (list == null) return null; return new MyMethodArgumentFix(PsiTreeUtil.findSameElementInCopy(list, target), myIndex, myToType, - (Wrapper)myArgumentFixerActionFactory); + (AbstractWrapper)myArgumentFixerActionFactory); } } @@ -256,7 +331,7 @@ public final class WrapWithAdapterMethodCallFix extends LocalQuickFixAndIntentio @NotNull PsiCall call, HighlightInfo highlightInfo, final TextRange fixRange) { - for (Wrapper wrapper : WRAPPERS) { + for (AbstractWrapper wrapper : WRAPPERS) { wrapper.registerCastActions(candidates, call, highlightInfo, fixRange); } } diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterEnumeration.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterEnumeration.java new file mode 100644 index 000000000000..530018a3ba81 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterEnumeration.java @@ -0,0 +1,9 @@ +// "Wrap using 'asIterator()'" "true" +import java.util.*; +import java.util.stream.*; + +class Test { + void testIterator(Enumeration en) { + Iterator iterator = en.asIterator(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterFile.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterFile.java new file mode 100644 index 000000000000..36e5ea4f762c --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterFile.java @@ -0,0 +1,9 @@ +// "Wrap argument using 'toPath()'" "true" +import java.io.*; +import java.nio.file.*; + +class Test { + void test(File file) throws IOException { + Files.createFile(file.toPath()); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterPath.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterPath.java new file mode 100644 index 000000000000..1e257cd956ac --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterPath.java @@ -0,0 +1,9 @@ +// "Wrap using 'toFile()'" "true" +import java.io.*; +import java.nio.file.*; + +class Test { + void test(Path path) { + File f = path.toFile(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterStream.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterStream.java new file mode 100644 index 000000000000..19d99f23625e --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterStream.java @@ -0,0 +1,9 @@ +// "Wrap using 'iterator()'" "true" +import java.util.*; +import java.util.stream.*; + +class Test { + void test() { + Iterator iterator = Stream.of("1", "2", "3").iterator(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterToCharArray.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterToCharArray.java new file mode 100644 index 000000000000..996932793dbf --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/afterInstanceConverterToCharArray.java @@ -0,0 +1,9 @@ +// "Wrap using 'toCharArray()'" "true" +import java.util.*; +import java.util.stream.*; + +class Test { + char[] returnHelloChars() { + return "hello".toCharArray(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterEnumeration.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterEnumeration.java new file mode 100644 index 000000000000..f928122dbb95 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterEnumeration.java @@ -0,0 +1,9 @@ +// "Wrap using 'asIterator()'" "true" +import java.util.*; +import java.util.stream.*; + +class Test { + void testIterator(Enumeration en) { + Iterator iterator = en; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterFile.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterFile.java new file mode 100644 index 000000000000..8398ebf15fa2 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterFile.java @@ -0,0 +1,9 @@ +// "Wrap argument using 'toPath()'" "true" +import java.io.*; +import java.nio.file.*; + +class Test { + void test(File file) throws IOException { + Files.createFile(file); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterPath.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterPath.java new file mode 100644 index 000000000000..c70d8bc718d2 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterPath.java @@ -0,0 +1,9 @@ +// "Wrap using 'toFile()'" "true" +import java.io.*; +import java.nio.file.*; + +class Test { + void test(Path path) { + File f = path; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterStream.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterStream.java new file mode 100644 index 000000000000..f7011b04b520 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterStream.java @@ -0,0 +1,9 @@ +// "Wrap using 'iterator()'" "true" +import java.util.*; +import java.util.stream.*; + +class Test { + void test() { + Iterator iterator = Stream.of("1", "2", "3"); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterToCharArray.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterToCharArray.java new file mode 100644 index 000000000000..e3ecaf77efef --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/wrapAdapterCall/beforeInstanceConverterToCharArray.java @@ -0,0 +1,9 @@ +// "Wrap using 'toCharArray()'" "true" +import java.util.*; +import java.util.stream.*; + +class Test { + char[] returnHelloChars() { + return "hello"; + } +} \ No newline at end of file