diff --git a/jvm/jvm-analysis-api/src/com/intellij/codeInspection/JvmAnalysisUastUtil.java b/jvm/jvm-analysis-api/src/com/intellij/codeInspection/JvmAnalysisUastUtil.java index bdd8c91ef96e..e3e2ad5d946f 100644 --- a/jvm/jvm-analysis-api/src/com/intellij/codeInspection/JvmAnalysisUastUtil.java +++ b/jvm/jvm-analysis-api/src/com/intellij/codeInspection/JvmAnalysisUastUtil.java @@ -6,10 +6,13 @@ import com.intellij.psi.PsiClass; import com.intellij.psi.PsiClassType; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiType; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.uast.*; +//TODO move to UastUtils.kt? Split all UAST utils to several less common util classes/files? +@ApiStatus.Experimental public final class JvmAnalysisUastUtil { @Nullable public static UCallExpression getUCallExpression(@NotNull PsiElement element) { @@ -34,19 +37,28 @@ public final class JvmAnalysisUastUtil { } @Nullable - public static String getExpressionReturnTypePsiClassFqnName(@NotNull UCallExpression expression) { - PsiClass psiClass = getTypePsiClass(expression.getReturnType()); - return psiClass == null ? null : psiClass.getQualifiedName(); + public static String getExpressionReturnTypePsiClassFqn(@NotNull UCallExpression expression) { + return getTypeClassFqn(expression.getReturnType()); } @Nullable public static PsiClass getTypePsiClass(@Nullable PsiType type) { - if (!(type instanceof PsiClassType)) { - return null; - } + if (!(type instanceof PsiClassType)) return null; return ((PsiClassType)type).rawType().resolve(); } + @Nullable + public static String getExpressionReceiverTypeClassFqn(@NotNull UCallExpression expression) { + return getTypeClassFqn(expression.getReceiverType()); + } + + @Nullable + public static String getTypeClassFqn(@Nullable PsiType type) { + PsiClass psiClass = getTypePsiClass(type); + if (psiClass == null) return null; + return psiClass.getQualifiedName(); + } + //TODO use UastContext#isExpressionValueUsed ? public static boolean isExpressionResultValueUsed(@NotNull UCallExpression expression) { UElement currentParent = expression; diff --git a/jvm/jvm-analysis-api/src/com/intellij/codeInspection/UastCallMatcher.java b/jvm/jvm-analysis-api/src/com/intellij/codeInspection/UastCallMatcher.java new file mode 100644 index 000000000000..ac587dac10dd --- /dev/null +++ b/jvm/jvm-analysis-api/src/com/intellij/codeInspection/UastCallMatcher.java @@ -0,0 +1,218 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.codeInspection; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiType; +import com.intellij.psi.util.InheritanceUtil; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UCallableReferenceExpression; +import org.jetbrains.uast.UExpression; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; + +/** + * Please, DO NOT use this interface in plugins until @Experimental is removed. + * Probably this interface will be extended in future, which will break its implementations. + * + * @see com.siyeh.ig.callMatcher.CallMatcher + */ +@ApiStatus.Experimental +public interface UastCallMatcher { + + @Contract("null -> false") + boolean testCallExpression(@Nullable UCallExpression expression); + + @Contract("null -> false") + boolean testCallableReferenceExpression(@Nullable UCallableReferenceExpression expression); + + + @NotNull + static Builder builder() { + return new Builder(); + } + + @NotNull + static UastCallMatcher anyOf(@NotNull UastCallMatcher... matchers) { + return new UastCallMatcher() { + @Override + public boolean testCallExpression(@Nullable UCallExpression expression) { + return Arrays.stream(matchers).anyMatch(matcher -> matcher.testCallExpression(expression)); + } + + @Override + public boolean testCallableReferenceExpression(@Nullable UCallableReferenceExpression expression) { + return Arrays.stream(matchers).anyMatch(matcher -> matcher.testCallableReferenceExpression(expression)); + } + }; + } + + + + + + //TODO support primitive types for receiver/return types and arguments + //TODO support static methods + class SimpleUastCallMatcher implements UastCallMatcher { + // for all fields 'null' = doesn't matter + + private final String myMethodName; + private final String[] myArguments; // array length is arguments count; each element is argument type FQN + private final boolean myMatchArgumentTypeInheritors; + private final String myReceiverTypeClassFqn; // e.g. for "abc".equals it is String + private final String myReturnTypeClassFqn; + + public SimpleUastCallMatcher(@Nullable String methodName, + @Nullable String[] arguments, + boolean matchArgumentTypeInheritors, + @Nullable String receiverTypeClassFqn, + @Nullable String returnTypeClassFqn) { + if (methodName == null && + arguments == null && + receiverTypeClassFqn == null && + returnTypeClassFqn == null) { + throw new IllegalArgumentException("At least one qualifier must be specified"); + } + myMethodName = methodName; + myArguments = arguments; + myMatchArgumentTypeInheritors = matchArgumentTypeInheritors; + myReceiverTypeClassFqn = receiverTypeClassFqn; + myReturnTypeClassFqn = returnTypeClassFqn; + } + + @Override + public boolean testCallExpression(@Nullable UCallExpression expression) { + if (expression == null || expression.getMethodName() == null) return false; // null method name for constructor calls + return methodNameMatches(expression) && + receiverTypeMatches(expression) && + returnTypeMatches(expression) && + argumentsMatch(expression); + } + + @Override + public boolean testCallableReferenceExpression(@Nullable UCallableReferenceExpression expression) { + return false; //TODO implement + } + + + private boolean methodNameMatches(@NotNull UCallExpression expression) { + return myMethodName == null || + myMethodName.equals(expression.getMethodName()); + } + + private boolean receiverTypeMatches(@NotNull UCallExpression expression) { + return myReceiverTypeClassFqn == null || + myReceiverTypeClassFqn.equals(JvmAnalysisUastUtil.getExpressionReceiverTypeClassFqn(expression)); + } + + private boolean returnTypeMatches(@NotNull UCallExpression expression) { + return myReturnTypeClassFqn == null || + myReturnTypeClassFqn.equals(JvmAnalysisUastUtil.getExpressionReturnTypePsiClassFqn(expression)); + } + + private boolean argumentsMatch(@NotNull UCallExpression expression) { + if (myArguments == null) return true; + if (myArguments.length != expression.getValueArgumentCount()) { + return false; + } + + List argumentExpressions = null; + for (int i = 0; i < myArguments.length; i++) { + String requiredArgumentTypeClassFqn = myArguments[i]; + if (requiredArgumentTypeClassFqn == null) continue; + if (argumentExpressions == null) { + argumentExpressions = expression.getValueArguments(); + } + + UExpression argumentExpression = argumentExpressions.get(i); + PsiType argumentExpressionType = argumentExpression.getExpressionType(); + if (!myMatchArgumentTypeInheritors) { + if (!requiredArgumentTypeClassFqn.equals(JvmAnalysisUastUtil.getTypeClassFqn(argumentExpressionType))) { + return false; + } + } + else { + PsiClass argumentExpressionTypeClass = JvmAnalysisUastUtil.getTypePsiClass(argumentExpressionType); + if (argumentExpressionTypeClass == null) return false; + + //TODO probably this can be optimized using BFS + LinkedHashSet expressionTypeSupers = InheritanceUtil.getSuperClasses(argumentExpressionTypeClass); + boolean argumentMatches = false; + for (PsiClass expressionTypeSuper : expressionTypeSupers) { + if (requiredArgumentTypeClassFqn.equals(expressionTypeSuper.getQualifiedName())) { + argumentMatches = true; + break; + } + } + if (!argumentMatches) return false; + } + } + return true; + } + } + + + /** + * Builder for {@link SimpleUastCallMatcher}. At least one qualifier must be specified. + * + * Please note that {@link #withArgumentsCount(int)} and {@link #withArgumentTypes(String...)} cannot be used + * at the same time (only the last call will have an effect). + */ + class Builder { + private String myMethodName; + private String[] myArguments; + private boolean myMatchArgumentTypeInheritors; + private String myReceiverTypeClassFqn; + private String myReturnTypeClassFqn; + + @NotNull + public Builder withMethodName(@NotNull String methodName) { + myMethodName = methodName; + return this; + } + + @NotNull + public Builder withReceiverType(@NotNull String receiverTypeClassFqn) { + myReceiverTypeClassFqn = receiverTypeClassFqn; + return this; + } + + @NotNull + public Builder withReturnType(@NotNull String returnTypeClassFqn) { + myReturnTypeClassFqn = returnTypeClassFqn; + return this; + } + + @NotNull + public Builder withArgumentsCount(int argumentsCount) { + myArguments = new String[argumentsCount]; + return this; + } + + @NotNull + public Builder withArgumentTypes(@NotNull String... arguments) { + myArguments = arguments; + return this; + } + + @NotNull + public Builder withMatchArgumentTypeInheritors(boolean matchArgumentTypeInheritors) { + myMatchArgumentTypeInheritors = matchArgumentTypeInheritors; + return this; + } + + @NotNull + public UastCallMatcher build() { + return new SimpleUastCallMatcher(myMethodName, + myArguments, + myMatchArgumentTypeInheritors, + myReceiverTypeClassFqn, + myReturnTypeClassFqn); + } + } +} diff --git a/jvm/jvm-analysis-java-tests/testData/codeInspection/uastCallMatcher/MyClass.java b/jvm/jvm-analysis-java-tests/testData/codeInspection/uastCallMatcher/MyClass.java new file mode 100644 index 000000000000..465454096369 --- /dev/null +++ b/jvm/jvm-analysis-java-tests/testData/codeInspection/uastCallMatcher/MyClass.java @@ -0,0 +1,12 @@ +public class MyClass { + public void foo() { + String s = "123"; + s.toUpperCase(); + s.toUpperCase(java.util.Locale.ENGLISH); + s.concat("1"); + s.chars(); + + java.util.List list = new java.util.ArrayList(); + list.addAll(new java.util.ArrayList()); + } +} \ No newline at end of file diff --git a/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/UastCallMatcherTest.java b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/UastCallMatcherTest.java new file mode 100644 index 000000000000..0b291e94f463 --- /dev/null +++ b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/UastCallMatcherTest.java @@ -0,0 +1,119 @@ +package com.intellij.codeInspection; + +import com.intellij.jvm.analysis.JvmAnalysisTestsUtil; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.search.PsiElementProcessor; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.testFramework.builders.JavaModuleFixtureBuilder; +import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase; +import com.intellij.util.PathUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UastContextKt; + +import java.util.*; + +import static com.intellij.codeInspection.UastCallMatcher.builder; + +public class UastCallMatcherTest extends JavaCodeInsightFixtureTestCase { + @Override + protected String getBasePath() { + return JvmAnalysisTestsUtil.TEST_DATA_PROJECT_RELATIVE_BASE_PATH + "/codeInspection/uastCallMatcher"; + } + + @Override + protected void tuneFixture(JavaModuleFixtureBuilder moduleBuilder) { + moduleBuilder.addLibrary("javaUtil", PathUtil.getJarPathForClass(Locale.class)); + } + + public void testSimpleMatcher() { + PsiFile file = myFixture.configureByFile("MyClass.java"); + + Set expressions = new HashSet<>(); + PsiTreeUtil.processElements(file, new PsiElementProcessor() { + @Override + public boolean execute(@NotNull PsiElement element) { + UCallExpression callExpression = UastContextKt.toUElement(element, UCallExpression.class); + if (callExpression != null && callExpression.getMethodName() != null) { // skip constructor calls + expressions.add(callExpression); + } + return true; + } + }); + assertSize(5, expressions); + + assertEquals(0, match( + builder().withReceiverType("java.util.ArrayList").build(), + expressions) + ); + assertEquals(0, match( + builder().withReceiverType("java.util.ArrayList").withMethodName("size").build(), + expressions) + ); + assertEquals(0, match( + builder().withMethodName("size").build(), + expressions) + ); + assertEquals(0, match( + builder().withReceiverType("java.util.ArrayList").withMethodName("addAll").withArgumentsCount(1).build(), + expressions) + ); + assertEquals(0, match( + builder().withReceiverType("java.util.ArrayList").withMethodName("addAll").withArgumentTypes("java.util.Collection").build(), + expressions) + ); + + assertEquals(4, match( + builder().withReceiverType("java.lang.String").build(), + expressions + )); + assertEquals(2, match( + builder().withMethodName("toUpperCase").build(), + expressions + )); + assertEquals(2, match( + builder().withReceiverType("java.lang.String").withMethodName("toUpperCase").build(), + expressions + )); + + assertEquals(3, match( + builder().withReturnType("java.lang.String").build(), + expressions + )); + assertEquals(2, match( + builder().withReturnType("java.lang.String").withMethodName("toUpperCase").build(), + expressions + )); + assertEquals(1, match( + builder().withReturnType("java.lang.String").withMethodName("toUpperCase").withArgumentsCount(1).build(), + expressions + )); + assertEquals(1, match( + builder().withReturnType("java.lang.String").withMethodName("toUpperCase").withArgumentTypes("java.util.Locale").build(), + expressions + )); + + assertEquals(2, match( + builder().withArgumentsCount(0).build(), + expressions + )); + assertEquals(3, match( + builder().withArgumentsCount(1).build(), + expressions + )); + + assertEquals(1, match( + builder().withArgumentTypes("java.util.Locale").build(), + expressions + )); + assertEquals(1, match( + builder().withArgumentTypes("java.util.Collection").withMatchArgumentTypeInheritors(true).build(), + expressions + )); + } + + private static int match(UastCallMatcher matcher, Set expressions) { + return (int)expressions.stream().filter(e -> matcher.testCallExpression(e)).count(); + } +} diff --git a/jvm/jvm-analysis-kotlin-tests/testData/codeInspection/uastCallMatcher/MyClass.kt b/jvm/jvm-analysis-kotlin-tests/testData/codeInspection/uastCallMatcher/MyClass.kt new file mode 100644 index 000000000000..61ef55867e27 --- /dev/null +++ b/jvm/jvm-analysis-kotlin-tests/testData/codeInspection/uastCallMatcher/MyClass.kt @@ -0,0 +1,12 @@ +class MyClass { + fun foo() { + val s = "123" + s.toUpperCase() + s.toUpperCase(java.util.Locale.ENGLISH) + s.concat("1") + s.chars() + + val list = java.util.ArrayList() + list.addAll(java.util.ArrayList()) + } +} \ No newline at end of file diff --git a/jvm/jvm-analysis-kotlin-tests/testSrc/com/intellij/codeInspection/KtUastCallMatcherTest.java b/jvm/jvm-analysis-kotlin-tests/testSrc/com/intellij/codeInspection/KtUastCallMatcherTest.java new file mode 100644 index 000000000000..320d6726fef4 --- /dev/null +++ b/jvm/jvm-analysis-kotlin-tests/testSrc/com/intellij/codeInspection/KtUastCallMatcherTest.java @@ -0,0 +1,122 @@ +package com.intellij.codeInspection; + +import com.intellij.jvm.analysis.JvmAnalysisKtTestsUtil; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.search.PsiElementProcessor; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.testFramework.builders.JavaModuleFixtureBuilder; +import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase; +import com.intellij.util.PathUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UastContextKt; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import static com.intellij.codeInspection.UastCallMatcher.builder; + +@SuppressWarnings("Duplicates") // TODO refactor once the tests pass +public class KtUastCallMatcherTest extends JavaCodeInsightFixtureTestCase { + @Override + protected String getBasePath() { + return JvmAnalysisKtTestsUtil.TEST_DATA_PROJECT_RELATIVE_BASE_PATH + "/codeInspection/uastCallMatcher"; + } + + @Override + protected void tuneFixture(JavaModuleFixtureBuilder moduleBuilder) { + moduleBuilder.addLibrary("javaUtil", PathUtil.getJarPathForClass(Locale.class)); + } + + public void _testSimpleMatcher() { // TODO https://youtrack.jetbrains.com/issue/KT-24679 + PsiFile file = myFixture.configureByFile("MyClass.kt"); + + Set expressions = new HashSet<>(); + PsiTreeUtil.processElements(file, new PsiElementProcessor() { + @Override + public boolean execute(@NotNull PsiElement element) { + UCallExpression callExpression = UastContextKt.toUElement(element, UCallExpression.class); + if (callExpression != null && callExpression.getMethodName() != null) { // skip constructor calls + expressions.add(callExpression); + } + return true; + } + }); + assertSize(5, expressions); + + assertEquals(0, match( + builder().withReceiverType("java.util.ArrayList").build(), + expressions) + ); + assertEquals(0, match( + builder().withReceiverType("java.util.ArrayList").withMethodName("size").build(), + expressions) + ); + assertEquals(0, match( + builder().withMethodName("size").build(), + expressions) + ); + assertEquals(0, match( + builder().withReceiverType("java.util.ArrayList").withMethodName("addAll").withArgumentsCount(1).build(), + expressions) + ); + assertEquals(0, match( + builder().withReceiverType("java.util.ArrayList").withMethodName("addAll").withArgumentTypes("java.util.Collection").build(), + expressions) + ); + + assertEquals(4, match( + builder().withReceiverType("java.lang.String").build(), + expressions + )); + assertEquals(2, match( + builder().withMethodName("toUpperCase").build(), + expressions + )); + assertEquals(2, match( + builder().withReceiverType("java.lang.String").withMethodName("toUpperCase").build(), + expressions + )); + + assertEquals(3, match( + builder().withReturnType("java.lang.String").build(), + expressions + )); + assertEquals(2, match( + builder().withReturnType("java.lang.String").withMethodName("toUpperCase").build(), + expressions + )); + assertEquals(1, match( + builder().withReturnType("java.lang.String").withMethodName("toUpperCase").withArgumentsCount(1).build(), + expressions + )); + assertEquals(1, match( + builder().withReturnType("java.lang.String").withMethodName("toUpperCase").withArgumentTypes("java.util.Locale").build(), + expressions + )); + + assertEquals(2, match( + builder().withArgumentsCount(0).build(), + expressions + )); + assertEquals(3, match( + builder().withArgumentsCount(1).build(), + expressions + )); + + assertEquals(1, match( + builder().withArgumentTypes("java.util.Locale").build(), + expressions + )); + assertEquals(1, match( + builder().withArgumentTypes("java.util.Collection").withMatchArgumentTypeInheritors(true).build(), + expressions + )); + } + + private static int match(UastCallMatcher matcher, Set expressions) { + return (int)expressions.stream().filter(e -> matcher.testCallExpression(e)).count(); + } +}