mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Introduce UastCallMatcher + related changes and fixes
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 <code>@Experimental</code> 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<UExpression> 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<PsiClass> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
+119
@@ -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<UCallExpression> 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<UCallExpression> expressions) {
|
||||
return (int)expressions.stream().filter(e -> matcher.testCallExpression(e)).count();
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
+122
@@ -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<UCallExpression> 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<UCallExpression> expressions) {
|
||||
return (int)expressions.stream().filter(e -> matcher.testCallExpression(e)).count();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user