Files
openide/java/java-analysis-impl/src/com/intellij/codeInspection/java18StreamApi/StaticPseudoFunctionalStyleMethodInspection.java
T

359 lines
16 KiB
Java

/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInspection.java18StreamApi;
import com.intellij.codeInspection.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jdom.Element;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.List;
/**
* @author Dmitry Batkovich
*/
public class StaticPseudoFunctionalStyleMethodInspection extends BaseJavaBatchLocalInspectionTool {
private final static Logger LOG = Logger.getInstance(StaticPseudoFunctionalStyleMethodInspection.class);
private StaticPseudoFunctionalStyleMethodOptions myOptions = new StaticPseudoFunctionalStyleMethodOptions();
@Override
public void readSettings(@NotNull Element node) throws InvalidDataException {
super.readSettings(node);
}
@Override
public void writeSettings(@NotNull Element node) throws WriteExternalException {
super.writeSettings(node);
}
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
if (!PsiUtil.isLanguageLevel8OrHigher(holder.getFile())) {
return PsiElementVisitor.EMPTY_VISITOR;
}
return new PsiElementVisitor() {
@Override
public void visitElement(PsiElement element) {
if (element instanceof PsiMethodCallExpression) {
final PsiMethodCallExpression methodCallExpression = (PsiMethodCallExpression)element;
String qName = methodCallExpression.getMethodExpression().getQualifiedName();
if (qName == null) {
return;
}
final int dotIndex = qName.lastIndexOf('.');
if (dotIndex >= 0) {
qName = qName.substring(dotIndex + 1);
}
final Collection<StaticPseudoFunctionalStyleMethodOptions.PipelineElement> handlerInfos = myOptions.findElementsByMethodName(qName);
if (handlerInfos.isEmpty()) {
return;
}
final PsiMethod method = methodCallExpression.resolveMethod();
if (method == null) {
return;
}
final PsiClass aClass = method.getContainingClass();
if (aClass == null) {
return;
}
final String classQualifiedName = aClass.getQualifiedName();
if (classQualifiedName == null) {
return;
}
StaticPseudoFunctionalStyleMethodOptions.PipelineElement suitableHandler = null;
for (StaticPseudoFunctionalStyleMethodOptions.PipelineElement h : handlerInfos) {
if (h.getHandlerClass().equals(classQualifiedName)) {
suitableHandler = h;
break;
}
}
if (suitableHandler == null) {
return;
}
final int lambdaIndex = validateMethodParameters(methodCallExpression, method);
if (lambdaIndex != -1) {
holder.registerProblem(methodCallExpression.getMethodExpression(), "",
new ReplacePseudoLambdaWithLambda(lambdaIndex, methodCallExpression, method, suitableHandler));
}
}
}
};
}
public static class ReplacePseudoLambdaWithLambda implements LocalQuickFix {
private final int myLambdaIndex;
private final SmartPsiElementPointer<PsiMethod> myMethodPointer;
private final StaticPseudoFunctionalStyleMethodOptions.PipelineElement mySuitableHandler;
private ReplacePseudoLambdaWithLambda(int lambdaIndex,
@NotNull PsiMethodCallExpression expression,
@NotNull PsiMethod method,
@NotNull StaticPseudoFunctionalStyleMethodOptions.PipelineElement suitableHandler) {
myLambdaIndex = lambdaIndex;
myMethodPointer = SmartPointerManager.getInstance(expression.getProject()).createSmartPsiElementPointer(method);
mySuitableHandler = suitableHandler;
}
@Nls
@NotNull
@Override
public String getName() {
return getFamilyName();
}
@NotNull
@Override
public String getFamilyName() {
return "Replace with Java Stream API pipeline";
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiMethodCallExpression expression = PsiTreeUtil.getParentOfType(descriptor.getPsiElement(), PsiMethodCallExpression.class);
LOG.assertTrue(expression != null);
final PsiExpression[] expressions = expression.getArgumentList().getExpressions();
PsiExpression lambdaExpression = expressions[myLambdaIndex];
lambdaExpression = convertToJavaLambda(lambdaExpression, mySuitableHandler.getStreamApiMethodName());
LOG.assertTrue(lambdaExpression != null);
final PsiExpression collectionExpression = expressions[(1 + myLambdaIndex) % 2];
final String pipelineHead = createPipelineHeadText(collectionExpression);
final String patternForFake =
StreamApiConstants.FAKE_STREAM_API_METHODS_TO_PATTERN.getValue().get(mySuitableHandler.getStreamApiMethodName());
final String lambdaExpressionText;
final String elementText;
if (patternForFake == null) {
elementText = mySuitableHandler.getStreamApiMethodName();
lambdaExpressionText = lambdaExpression.getText();
}
else {
elementText = String.format(patternForFake, lambdaExpression.getText());
lambdaExpressionText = null;
}
final String pipelineTail =
StreamApiConstants.STREAM_STREAM_API_METHODS.getValue().contains(mySuitableHandler.getStreamApiMethodName())
? findSuitableTailMethodForCollection(myMethodPointer.getElement())
: null;
final PsiElement replaced =
expression.replace(createPipelineExpression(pipelineHead, elementText, lambdaExpressionText, pipelineTail, project));
JavaCodeStyleManager.getInstance(project).shortenClassReferences(replaced.getParent());
}
private static String createPipelineHeadText(PsiExpression collectionExpression) {
final PsiType type = collectionExpression.getType();
if (type instanceof PsiClassType) {
final PsiClass resolved = ((PsiClassType)type).resolve();
LOG.assertTrue(resolved != null && resolved.getQualifiedName() != null);
return collectionExpression.getText() + ".stream()";
}
else if (type instanceof PsiArrayType) {
return CommonClassNames.JAVA_UTIL_ARRAYS + ".stream(" + collectionExpression.getText() + ")";
}
throw new AssertionError("type: " + type + " is unexpected");
}
private static PsiExpression createPipelineExpression(String pipelineHead,
String elementText,
String lambdaExpression,
String pipelineTail,
Project project) {
final StringBuilder sb = new StringBuilder();
sb.append(pipelineHead).append(".").append(elementText);
if (lambdaExpression != null) {
sb.append("(").append(lambdaExpression).append(")");
}
if (pipelineTail != null) {
sb.append(".").append(pipelineTail);
}
return JavaPsiFacade.getElementFactory(project).createExpressionFromText(sb.toString(), null);
}
}
private static int validateMethodParameters(final PsiMethodCallExpression methodCallExpression, final PsiMethod method) {
final PsiType[] argumentTypes = methodCallExpression.getArgumentList().getExpressionTypes();
final PsiParameter[] expectedParameters = method.getParameterList().getParameters();
if (argumentTypes.length != expectedParameters.length || expectedParameters.length != 2) {
return -1;
}
final int collectionOrArrayIndex = findCollectionOrArrayPlacement(expectedParameters);
if (collectionOrArrayIndex == -1) {
return -1;
}
return (1 + collectionOrArrayIndex) % 2;
}
private static int findCollectionOrArrayPlacement(final PsiParameter[] parameters) {
for (int i = 0, length = parameters.length; i < length; i++) {
PsiParameter parameter = parameters[i];
final PsiType type = parameter.getType();
if (type instanceof PsiClassType || type instanceof PsiArrayType) {
return i;
}
}
return -1;
}
private static PsiExpression convertToJavaLambda(final PsiExpression expression, String streamApiMethodName) {
if (expression instanceof PsiLambdaExpression) {
return expression;
}
if (expression instanceof PsiMethodCallExpression) {
final PsiMethod method = ((PsiMethodCallExpression)expression).resolveMethod();
if (method == null) {
return null;
}
final PsiType type = method.getReturnType();
if (!(type instanceof PsiClassType)) {
return null;
}
final PsiClass lambdaClass = ((PsiClassType)type).resolve();
if (lambdaClass == null) {
return null;
}
final String methodName = lambdaClass.getMethods()[0].getName();
if (tryConvertLambdaToStreamApi(method, resolveStreamApiLambdaClass(expression.getProject(), streamApiMethodName))) {
return expression;
}
else {
return JavaPsiFacade.getElementFactory(expression.getProject())
.createExpressionFromText(expression.getText() + "::" + methodName, null);
}
}
return AnonymousCanBeLambdaInspection.replacePsiElementWithLambda(expression, true);
}
@NotNull
private static PsiClass resolveStreamApiLambdaClass(Project project, String streamApiMethodName) {
final PsiClass javaUtilStream = JavaPsiFacade.getInstance(project)
.findClass(StreamApiConstants.JAVA_UTIL_STREAM_STREAM, GlobalSearchScope.notScope(GlobalSearchScope.projectScope(project)));
LOG.assertTrue(javaUtilStream != null);
final PsiMethod[] methods = javaUtilStream.findMethodsByName(streamApiMethodName, false);
LOG.assertTrue(methods.length == 1);
final PsiMethod method = methods[0];
final PsiParameter[] parameters = method.getParameterList().getParameters();
LOG.assertTrue(parameters.length == 1);
final PsiType type = parameters[0].getType();
LOG.assertTrue(type instanceof PsiClassType);
final PsiClass resolved = ((PsiClassType)type).resolve();
LOG.assertTrue(resolved != null);
return resolved;
}
private static boolean tryConvertLambdaToStreamApi(final PsiMethod method, final PsiClass expectedReturnClass) {
final PsiCodeBlock body = method.getBody();
Collection<PsiReturnStatement> returnStatements = PsiTreeUtil.findChildrenOfType(body, PsiReturnStatement.class);
returnStatements = ContainerUtil.filter(returnStatements, new Condition<PsiReturnStatement>() {
@Override
public boolean value(PsiReturnStatement statement) {
return PsiTreeUtil.getParentOfType(statement, PsiMethod.class) == method;
}
});
if (returnStatements.size() != 1) {
return false;
}
final PsiReturnStatement returnStatement = ContainerUtil.getFirstItem(returnStatements);
assert returnStatement != null;
final PsiExpression returnValue = returnStatement.getReturnValue();
if (returnValue instanceof PsiNewExpression) {
convertNewExpression(method, (PsiNewExpression)returnValue, expectedReturnClass);
return true;
}
else {
return false;
}
}
private static void convertNewExpression(PsiMethod containingMethod, PsiNewExpression newExpression, PsiClass expectedReturnClass) {
final String expectedReturnQName = expectedReturnClass.getQualifiedName();
LOG.assertTrue(expectedReturnQName != null);
final PsiElementFactory factory = JavaPsiFacade.getElementFactory(newExpression.getProject());
PsiAnonymousClass anonymousClass = PsiTreeUtil.findChildOfType(newExpression, PsiAnonymousClass.class);
LOG.assertTrue(anonymousClass != null);
PsiJavaCodeReferenceElement referenceElement = PsiTreeUtil.findChildOfType(anonymousClass, PsiJavaCodeReferenceElement.class);
LOG.assertTrue(referenceElement != null);
final PsiReferenceParameterList parameterList = PsiTreeUtil.findChildOfType(referenceElement, PsiReferenceParameterList.class);
final PsiJavaCodeReferenceElement newCodeReferenceElement = factory.createReferenceFromText(expectedReturnClass.getQualifiedName()
+
(parameterList == null
? ""
: parameterList.getText()), null);
referenceElement.replace(newCodeReferenceElement);
final List<PsiMethod> methods = ContainerUtil.filter(anonymousClass.getMethods(), new Condition<PsiMethod>() {
@Override
public boolean value(PsiMethod method) {
return !"equals".equals(method.getName());
}
});
LOG.assertTrue(methods.size() == 1, methods);
final PsiMethod method = methods.get(0);
method.setName(expectedReturnClass.getMethods()[0].getName());
final PsiTypeElement element = containingMethod.getReturnTypeElement();
if (element != null) {
final PsiReferenceParameterList genericParameter = PsiTreeUtil.findChildOfType(element, PsiReferenceParameterList.class);
element.replace(factory.createTypeElementFromText(expectedReturnQName + (genericParameter == null ? "" : genericParameter.getText()), null));
}
}
@Nullable
private static String findSuitableTailMethodForCollection(PsiMethod lambdaHandler) {
final PsiType type = lambdaHandler.getReturnType();
if (type instanceof PsiArrayType) {
return "toArray(String[]::new)";
}
else if (type instanceof PsiClassType) {
final PsiClass resolvedClass = ((PsiClassType)type).resolve();
if (resolvedClass == null) {
return null;
}
final String qName = resolvedClass.getQualifiedName();
if (qName == null) {
return null;
}
if (qName.equals(CommonClassNames.JAVA_UTIL_LIST)
|| qName.equals(CommonClassNames.JAVA_UTIL_COLLECTION)
|| qName.equals(CommonClassNames.JAVA_LANG_ITERABLE)) {
return "collect(" + StreamApiConstants.JAVA_UTIL_STREAM_COLLECTORS + ".toList())";
}
else if (qName.equals(CommonClassNames.JAVA_UTIL_SET)) {
return "collect(" + StreamApiConstants.JAVA_UTIL_STREAM_COLLECTORS + ".toSet())";
}
else if (qName.equals(CommonClassNames.JAVA_UTIL_ITERATOR)) {
return "iterator()";
}
}
return null;
}
}