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();
+ }
+}