From 67ceee54df373ffac08abdf503fcbbea25968c90 Mon Sep 17 00:00:00 2001 From: Dmitry Batkovich Date: Wed, 15 Jul 2015 18:11:26 +0300 Subject: [PATCH] Insoection to convert guava's fluent iterables to java 8 streams --- .../FluentIterableMethodTransformer.java | 138 +++++ .../GuavaFluentIterableInspection.java | 476 ++++++++++++++++++ .../GuavaFluentIterableMethodConverters.java | 254 ++++++++++ .../GuavaFunctionAndPredicateConverter.java | 70 +++ .../GuavaOptionalConverter.java | 84 ++++ .../GuavaFluentIterable.html | 5 + resources/src/META-INF/IdeaPlugin.xml | 6 +- 7 files changed, 1032 insertions(+), 1 deletion(-) create mode 100644 java/java-impl/src/com/intellij/codeInspection/java18StreamApi/FluentIterableMethodTransformer.java create mode 100644 java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaFluentIterableInspection.java create mode 100644 java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaFluentIterableMethodConverters.java create mode 100644 java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaFunctionAndPredicateConverter.java create mode 100644 java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaOptionalConverter.java create mode 100644 resources-en/src/inspectionDescriptions/GuavaFluentIterable.html diff --git a/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/FluentIterableMethodTransformer.java b/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/FluentIterableMethodTransformer.java new file mode 100644 index 000000000000..165814bead8c --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/FluentIterableMethodTransformer.java @@ -0,0 +1,138 @@ +/* + * 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.openapi.diagnostic.Logger; +import com.intellij.psi.*; +import org.jetbrains.annotations.Nullable; + +/** + * @author Dmitry Batkovich + */ +abstract class FluentIterableMethodTransformer { + private final static Logger LOG = Logger.getInstance(FluentIterableMethodTransformer.class); + + protected abstract String formatMethod(PsiExpression[] initialMethodParameters); + + protected boolean negate() { + return false; + } + + @Nullable + public final PsiMethodCallExpression transform(PsiMethodCallExpression expression, PsiElementFactory elementFactory) { + final String formatted = formatMethod(expression.getArgumentList().getExpressions()); + final String negation = negate() ? "!" : ""; + final PsiExpression qualifierExpression = expression.getMethodExpression().getQualifierExpression(); + LOG.assertTrue(qualifierExpression != null); + final String oldQualifierText = qualifierExpression.getText(); + final String expressionText = negation + oldQualifierText + "." + formatted; + final PsiElement replaced = expression.replace(elementFactory.createExpressionFromText(expressionText, null)); + if (replaced instanceof PsiMethodCallExpression) { + PsiMethodCallExpression methodCallExpression = (PsiMethodCallExpression)replaced; + while (true) { + final PsiExpression qualifier = methodCallExpression.getMethodExpression().getQualifierExpression(); + LOG.assertTrue(qualifier != null); + if (oldQualifierText.equals(qualifier.getText())) { + return methodCallExpression; + } + methodCallExpression = (PsiMethodCallExpression)qualifier; + } + } + else { + return null; + } + } + + static class OneParameterMethodTransformer extends FluentIterableMethodTransformer { + private final String myTemplate; + private final boolean myParameterIsFunctionOrPredicate; + + OneParameterMethodTransformer(String template, boolean parameterIsFunctionOrPredicate) { + myTemplate = template; + myParameterIsFunctionOrPredicate = parameterIsFunctionOrPredicate; + } + + OneParameterMethodTransformer(String parameterIsFunctionOrPredicate) { + this(parameterIsFunctionOrPredicate, false); + } + + protected String formatMethod(PsiExpression[] initialMethodParameters) { + final String matchedElementText; + if (myParameterIsFunctionOrPredicate) { + if (initialMethodParameters.length == 1) { + PsiExpression parameter = initialMethodParameters[0]; + final PsiType type = parameter.getType(); + Boolean role; + if (!(type instanceof PsiMethodReferenceType) && !(type instanceof PsiLambdaExpressionType) + && (role = GuavaFunctionAndPredicateConverter.isClassConditionPredicate(parameter)) != null) { + matchedElementText = GuavaFunctionAndPredicateConverter.convertFunctionOrPredicateParameter(parameter, role); + } else { + matchedElementText = parameter.getText(); + } + } else { + matchedElementText = ""; + } + } else { + matchedElementText = initialMethodParameters.length > 0 ? initialMethodParameters[0].getText() : ""; + } + return String.format(myTemplate, matchedElementText); + } + } + + static class ToArrayMethodTransformer extends FluentIterableMethodTransformer { + @Override + protected String formatMethod(PsiExpression[] initialMethodParameters) { + final PsiExpression parameter = initialMethodParameters[0]; + if (parameter instanceof PsiClassObjectAccessExpression) { + final PsiType type = ((PsiClassObjectAccessExpression)parameter).getOperand().getType(); + if (type instanceof PsiClassType) { + final PsiClass resolvedClass = ((PsiClassType)type).resolve(); + if (resolvedClass != null) { + final String qName = resolvedClass.getQualifiedName(); + if (qName != null) { + return String.format("toArray(%s[]::new)", qName); + } + } + } + } + return "toArray()"; + } + } + + static class ParameterlessMethodTransformer extends FluentIterableMethodTransformer { + private final String myTemplate; + private final boolean myNegation; + + ParameterlessMethodTransformer(String template) { + this(template.endsWith(")") ? template : (template + "()"), false); + } + + ParameterlessMethodTransformer(String template, boolean negation) { + myTemplate = template; + myNegation = negation; + } + + @Override + protected boolean negate() { + return myNegation; + } + + @Override + protected String formatMethod(PsiExpression[] initialMethodParameters) { + return myTemplate; + } + } +} diff --git a/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaFluentIterableInspection.java b/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaFluentIterableInspection.java new file mode 100644 index 000000000000..21eeb7a9d6b2 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaFluentIterableInspection.java @@ -0,0 +1,476 @@ +/* + * 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.BaseJavaBatchLocalInspectionTool; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.module.impl.scopes.JdkScope; +import com.intellij.openapi.project.Project; +import com.intellij.psi.*; +import com.intellij.psi.codeStyle.JavaCodeStyleManager; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiTypesUtil; +import com.intellij.util.Function; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.MultiMap; +import gnu.trove.THashSet; +import gnu.trove.TObjectIdentityHashingStrategy; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Dmitry Batkovich + */ +@SuppressWarnings("DialogTitleCapitalization") +public class GuavaFluentIterableInspection extends BaseJavaBatchLocalInspectionTool { + private final static Logger LOG = Logger.getInstance(GuavaFluentIterableInspection.class); + private final static String PROBLEM_DESCRIPTION = "FluentIterable is used while Stream API is accessible"; + public final static String GUAVA_FLUENT_ITERABLE = "com.google.common.collect.FluentIterable"; + public final static String GUAVA_OPTIONAL = "com.google.common.base.Optional"; + public final static String GUAVA_IMMUTABLE_MAP = "com.google.common.collect.ImmutableMap"; + public final static String FLUENT_ITERABLE_FROM = "from"; + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) { + final PsiClass fluentIterable = JavaPsiFacade.getInstance(holder.getProject()) + .findClass(GUAVA_FLUENT_ITERABLE, GlobalSearchScope.allScope(holder.getProject())); + if (fluentIterable == null) { + return PsiElementVisitor.EMPTY_VISITOR; + } + return new JavaRecursiveElementVisitor() { + private final SmartPointerManager mySmartPointerManager = SmartPointerManager.getInstance(holder.getProject()); + private final Set myMethodCallsToIgnore = + new THashSet(new TObjectIdentityHashingStrategy()); + + private final MultiMap myLocalVariablesUsages = new MultiMap(); + private final Set myUnconvertibleVariables = new THashSet(); + + @Override + public void visitLocalVariable(final PsiLocalVariable localVariable) { + final PsiType type = localVariable.getType(); + if (!(type instanceof PsiClassType) || myUnconvertibleVariables.contains(localVariable)) { + return; + } + final PsiClass variableClass = ((PsiClassType)type).resolve(); + if (variableClass == null) { + return; + } + final String qualifiedName = variableClass.getQualifiedName(); + if (!GUAVA_FLUENT_ITERABLE.equals(qualifiedName)) { + return; + } + final PsiCodeBlock context = PsiTreeUtil.getParentOfType(localVariable, PsiCodeBlock.class); + if (context == null || !checkDeclaration(localVariable.getInitializer())) { + myUnconvertibleVariables.add(localVariable); + } + } + + private boolean checkDeclaration(PsiExpression declaration) { + if (declaration == null) { + return true; + } + if (!(declaration instanceof PsiMethodCallExpression)) { + return false; + } + + PsiMethodCallExpression currentCallExpression = (PsiMethodCallExpression)declaration; + while (true) { + final PsiExpression qualifier = currentCallExpression.getMethodExpression().getQualifierExpression(); + if (qualifier instanceof PsiMethodCallExpression) { + final PsiMethod method = currentCallExpression.resolveMethod(); + if (method == null || GuavaFluentIterableMethodConverters.isStopMethod(method.getName())) { + return false; + } + final PsiClass aClass = method.getContainingClass(); + if (aClass == null || !aClass.isEquivalentTo(fluentIterable)) { + return false; + } + currentCallExpression = (PsiMethodCallExpression)qualifier; + } else { + if (qualifier instanceof PsiReferenceExpression) { + final PsiMethod method = currentCallExpression.resolveMethod(); + if (method == null || !FLUENT_ITERABLE_FROM.equals(method.getName())) { + return false; + } + final PsiClass aClass = method.getContainingClass(); + if (aClass == null || !GUAVA_FLUENT_ITERABLE.equals(aClass.getQualifiedName())) { + return false; + } + break; + + } else { + return false; + } + } + } + return true; + } + + @Override + public void visitReferenceExpression(PsiReferenceExpression expression) { + super.visitReferenceExpression(expression); + final PsiElement resolvedElement = expression.resolve(); + if (resolvedElement instanceof PsiLocalVariable) { + PsiLocalVariable fluentIterableVariable = (PsiLocalVariable)resolvedElement; + if (!fluentIterable.isEquivalentTo(PsiTypesUtil.getPsiClass(fluentIterableVariable.getType())) || + myUnconvertibleVariables.contains(fluentIterableVariable)) { + return; + } + analyzeExpression(expression, fluentIterableVariable); + } + } + + private void addToUnconvertible(PsiLocalVariable variable) { + myUnconvertibleVariables.add(variable); + myLocalVariablesUsages.remove(variable); + } + + @Override + public void visitJavaFile(PsiJavaFile file) { + super.visitJavaFile(file); + for (Map.Entry> e : myLocalVariablesUsages.entrySet()) { + final PsiLocalVariable localVariable = e.getKey(); + final Collection foundUsages = e.getValue(); + final SmartPsiElementPointer variablePointer = mySmartPointerManager.createSmartPsiElementPointer(localVariable); + final ConvertGuavaFluentIterableQuickFix quickFix = new ConvertGuavaFluentIterableQuickFix(variablePointer, ContainerUtil.map( + new THashSet(foundUsages), new Function>() { + @Override + public SmartPsiElementPointer fun(PsiExpression expression) { + return mySmartPointerManager.createSmartPsiElementPointer(expression); + } + })); + holder.registerProblem(localVariable, PROBLEM_DESCRIPTION, quickFix); + for (PsiExpression usage : foundUsages) { + holder.registerProblem(usage, PROBLEM_DESCRIPTION, quickFix); + } + } + myLocalVariablesUsages.clear(); + myUnconvertibleVariables.clear(); + } + + @Override + public void visitMethodCallExpression(PsiMethodCallExpression expression) { + super.visitMethodCallExpression(expression); + if (!myMethodCallsToIgnore.add(expression)) { + return; + } + final PsiReferenceExpression methodExpression = expression.getMethodExpression(); + final String methodName = methodExpression.getReferenceName(); + if (FLUENT_ITERABLE_FROM.equals(methodName)) { + final PsiMethod method = expression.resolveMethod(); + if (method == null || !method.hasModifierProperty(PsiModifier.STATIC) || !fluentIterable.isEquivalentTo(method.getContainingClass())) { + return; + } + PsiMethodCallExpression currentExpression = expression; + while (true) { + myMethodCallsToIgnore.add(currentExpression); + PsiMethodCallExpression parentMethodCall = PsiTreeUtil.getParentOfType(currentExpression, PsiMethodCallExpression.class); + if (parentMethodCall != null) { + if (parentMethodCall.getMethodExpression().getQualifierExpression() == currentExpression) { + final PsiMethod parentCallMethod = parentMethodCall.resolveMethod(); + if (parentCallMethod != null && fluentIterable.isEquivalentTo(parentCallMethod.getContainingClass())) { + if (GuavaFluentIterableMethodConverters.isStopMethod(parentCallMethod.getName())) { + return; + } + currentExpression = parentMethodCall; + continue; + } + } + } + final PsiElement expressionParent = currentExpression.getParent(); + if (expressionParent instanceof PsiReturnStatement) { + final PsiType containingMethodReturnType = findContainingMethodReturnType(currentExpression); + if (containingMethodReturnType instanceof PsiClassType) { + final PsiClass resolvedClass = ((PsiClassType)containingMethodReturnType).resolve(); + if (resolvedClass == null || !(resolvedClass.getResolveScope() instanceof JdkScope)) { + return; + } + } + } else if (expressionParent instanceof PsiLocalVariable) { + final PsiType type = ((PsiLocalVariable)expressionParent).getType(); + if (type instanceof PsiClassType) { + final PsiClass resolvedClass = ((PsiClassType)type).resolve(); + if (resolvedClass == null || !(resolvedClass.getResolveScope() instanceof JdkScope)) { + return; + } + } + } else if (expressionParent instanceof PsiExpressionList) { + if (expressionParent.getParent() instanceof PsiMethodCallExpression + && !isMethodWithParamAcceptsConversion((PsiMethodCallExpression)expressionParent.getParent(), + currentExpression, + fluentIterable)) { + return; + } + } + + final List> exprAsList = + ContainerUtil.list(mySmartPointerManager.createSmartPsiElementPointer((PsiExpression)currentExpression)); + holder.registerProblem(currentExpression, PROBLEM_DESCRIPTION, new ConvertGuavaFluentIterableQuickFix(null, exprAsList)); + return; + } + } else { + final PsiExpression qualifierExpression = methodExpression.getQualifierExpression(); + if (GuavaFluentIterableMethodConverters.isFluentIterableMethod(methodName) && + qualifierExpression instanceof PsiReferenceExpression) { + final PsiElement resolvedElement = ((PsiReferenceExpression)qualifierExpression).resolve(); + if (resolvedElement instanceof PsiLocalVariable) { + PsiLocalVariable fluentIterableLocalVariable = (PsiLocalVariable)resolvedElement; + if (!fluentIterable.isEquivalentTo(PsiTypesUtil.getPsiClass(fluentIterableLocalVariable.getType())) || + myUnconvertibleVariables.contains(fluentIterableLocalVariable)) { + return; + } + analyzeExpression(expression, fluentIterableLocalVariable); + } + } + } + } + + private void analyzeExpression(PsiExpression expression, PsiLocalVariable fluentIterableLocalVariable) { + PsiExpression baseExpression = expression; + while (true) { + final PsiMethodCallExpression + methodCallExpression = PsiTreeUtil.getParentOfType(baseExpression, PsiMethodCallExpression.class); + if (methodCallExpression != null && methodCallExpression.getMethodExpression().getQualifierExpression() == baseExpression) { + final String currentMethodName = methodCallExpression.getMethodExpression().getReferenceName(); + if (GuavaFluentIterableMethodConverters.isFluentIterableMethod(currentMethodName)) { + if (GuavaFluentIterableMethodConverters.isStopMethod((currentMethodName))) { + addToUnconvertible(fluentIterableLocalVariable); + return; + } + else { + final PsiMethod method = methodCallExpression.resolveMethod(); + if (method != null && method.getContainingClass() != null && method.getContainingClass().isEquivalentTo(fluentIterable)) { + baseExpression = methodCallExpression; + myMethodCallsToIgnore.add(methodCallExpression); + continue; + } + } + } + } + break; + } + final PsiElement parent = baseExpression.getParent(); + if (parent instanceof PsiExpressionList) { + myLocalVariablesUsages.putValue(fluentIterableLocalVariable, baseExpression); + final boolean suitable = parent.getParent() instanceof PsiMethodCallExpression && + isMethodWithParamAcceptsConversion((PsiMethodCallExpression)parent.getParent(), baseExpression, + fluentIterable); + if (!suitable) { + addToUnconvertible(fluentIterableLocalVariable); + } + } else if (parent instanceof PsiReferenceExpression) { + final PsiMethodCallExpression parentMethodCall = PsiTreeUtil.getParentOfType(baseExpression, PsiMethodCallExpression.class); + if (parentMethodCall != null && parentMethodCall.getMethodExpression().getQualifier() == baseExpression) { + if (GuavaOptionalConverter.isConvertibleIfOption(parentMethodCall)) { + myLocalVariablesUsages.putValue(fluentIterableLocalVariable, baseExpression); + } else { + addToUnconvertible(fluentIterableLocalVariable); + } + } + } + else if (parent instanceof PsiLocalVariable) { + myLocalVariablesUsages.putValue(fluentIterableLocalVariable, baseExpression); + } else if (parent instanceof PsiAssignmentExpression) { + final PsiAssignmentExpression assignment = (PsiAssignmentExpression)parent; + final PsiExpression lExpression = assignment.getLExpression(); + if (lExpression instanceof PsiReferenceExpression) { + if (((PsiReferenceExpression)lExpression).isReferenceTo(fluentIterableLocalVariable)) { + if (isSelfAssignment(assignment, fluentIterableLocalVariable)) { + myLocalVariablesUsages.putValue(fluentIterableLocalVariable, baseExpression); + return; + } + if (checkDeclaration(assignment.getRExpression())) { + myLocalVariablesUsages.putValue(fluentIterableLocalVariable, assignment.getRExpression()); + return; + } + addToUnconvertible(fluentIterableLocalVariable); + } else { + myLocalVariablesUsages.putValue(fluentIterableLocalVariable, baseExpression); + } + } + else { + myLocalVariablesUsages.putValue(fluentIterableLocalVariable, baseExpression); + } + } else if (parent instanceof PsiReturnStatement) { + final PsiType containingMethodReturnType = findContainingMethodReturnType(baseExpression); + if (baseExpression == expression) { + if (!(containingMethodReturnType instanceof PsiClassType)) { + addToUnconvertible(fluentIterableLocalVariable); + return; + } + final PsiClass resolvedClass = ((PsiClassType)containingMethodReturnType).resolve(); + if (resolvedClass == null || (!CommonClassNames.JAVA_LANG_ITERABLE.equals(resolvedClass.getQualifiedName()) && + !CommonClassNames.JAVA_LANG_OBJECT.equals(resolvedClass.getQualifiedName()))) { + addToUnconvertible(fluentIterableLocalVariable); + } else { + myLocalVariablesUsages.putValue(fluentIterableLocalVariable, baseExpression); + } + } + else { + if (containingMethodReturnType instanceof PsiClassType) { + final PsiClass resolvedClass = ((PsiClassType)containingMethodReturnType).resolve(); + if (resolvedClass == null || !(resolvedClass.getResolveScope() instanceof JdkScope)) { + addToUnconvertible(fluentIterableLocalVariable); + } + } + } + } else if (parent instanceof PsiExpressionStatement) { + myLocalVariablesUsages.putValue(fluentIterableLocalVariable, baseExpression); + } + } + }; + + } + + public static boolean isMethodWithParamAcceptsConversion(PsiMethodCallExpression methodCallExpression, + PsiExpression baseExpression, + PsiClass fluentIterable) { + final PsiExpressionList argList = methodCallExpression.getArgumentList(); + final PsiMethod method = methodCallExpression.resolveMethod(); + if (method == null) { + return true; + } + final PsiParameterList paramList = method.getParameterList(); + if (paramList.getParametersCount() != argList.getExpressions().length && + !(paramList.getParameters()[paramList.getParametersCount() - 1].getType() instanceof PsiEllipsisType)) { + return false; + } + int index = -1; + PsiExpression[] expressions = argList.getExpressions(); + for (int i = 0, length = expressions.length; i < length; i++) { + if (expressions[i] == baseExpression) { + index = i; + break; + } + } + LOG.assertTrue(index >= 0); + PsiType parameterType; + if (index > paramList.getParametersCount() - 1) { + parameterType = paramList.getParameters()[paramList.getParametersCount() - 1].getType(); + } else { + parameterType = paramList.getParameters()[index].getType(); + } + if (parameterType instanceof PsiEllipsisType) { + parameterType = ((PsiEllipsisType)parameterType).getComponentType(); + } + if (parameterType instanceof PsiClassType) { + final PsiClass resolvedParameterClass = ((PsiClassType)parameterType).resolve(); + final JavaPsiFacade javaPsiFacade = JavaPsiFacade.getInstance(methodCallExpression.getProject()); + final GlobalSearchScope scope = GlobalSearchScope.allScope(methodCallExpression.getProject()); + final PsiClass optional = javaPsiFacade.findClass(GUAVA_OPTIONAL, scope); + final PsiClass immutableMap = javaPsiFacade.findClass(GUAVA_IMMUTABLE_MAP, scope); + if (resolvedParameterClass != null && + (InheritanceUtil.isInheritorOrSelf(resolvedParameterClass, fluentIterable, true) || + InheritanceUtil.isInheritorOrSelf(resolvedParameterClass, optional, true) || + InheritanceUtil.isInheritorOrSelf(resolvedParameterClass, immutableMap, true))){ + return false; + } + } + return true; + } + + @Nullable + static PsiType findContainingMethodReturnType(PsiElement methodElement) { + final PsiMethod containingMethod = PsiTreeUtil.getParentOfType(methodElement, PsiMethod.class); + if (containingMethod == null) { + return null; + } + final PsiType containingMethodReturnType = containingMethod.getReturnType(); + if (containingMethodReturnType == null) { + return null; + } + return containingMethodReturnType; + } + + private static boolean isSelfAssignment(PsiAssignmentExpression expression, PsiLocalVariable localVariable) { + final PsiExpression rExpression = expression.getRExpression(); + if (!(rExpression instanceof PsiMethodCallExpression)) { + return false; + } + + PsiMethodCallExpression methodCall = (PsiMethodCallExpression)rExpression; + while (true) { + final PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression(); + if (qualifier instanceof PsiMethodCallExpression) { + methodCall = (PsiMethodCallExpression)qualifier; + } else { + break; + } + } + + final PsiExpression qualifierExpression = methodCall.getMethodExpression().getQualifierExpression(); + if (qualifierExpression instanceof PsiReferenceExpression && + !((PsiReferenceExpression)qualifierExpression).isReferenceTo(localVariable)) { + return false; + } + return true; + } + + public static class ConvertGuavaFluentIterableQuickFix implements LocalQuickFix { + @Nullable private final SmartPsiElementPointer myVariable; + @NotNull private final List> myFoundUsages; + + protected ConvertGuavaFluentIterableQuickFix(@Nullable SmartPsiElementPointer variable, + @NotNull List> foundUsages) { + myVariable = variable; + myFoundUsages = foundUsages; + } + + @Nls + @NotNull + @Override + public String getName() { + return getFamilyName(); + } + + @NotNull + @Override + public String getFamilyName() { + return "Convert Guava's FluentIterable to java.util.stream.Stream"; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + final JavaPsiFacade javaPsiFacade = JavaPsiFacade.getInstance(project); + final PsiElementFactory elementFactory = javaPsiFacade.getElementFactory(); + final JavaCodeStyleManager codeStyleManager = JavaCodeStyleManager.getInstance(project); + for (SmartPsiElementPointer usage : myFoundUsages) { + final PsiExpression element = usage.getElement(); + if (element != null) { + GuavaFluentIterableMethodConverters.convert(element, elementFactory, codeStyleManager); + } + } + if (myVariable != null) { + final PsiLocalVariable element = myVariable.getElement(); + if (element != null) { + GuavaFluentIterableMethodConverters.convert(element, elementFactory, codeStyleManager); + } + } + } + } +} diff --git a/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaFluentIterableMethodConverters.java b/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaFluentIterableMethodConverters.java new file mode 100644 index 000000000000..1a44ef29a563 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaFluentIterableMethodConverters.java @@ -0,0 +1,254 @@ +/* + * 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.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.psi.*; +import com.intellij.psi.codeStyle.JavaCodeStyleManager; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.ArrayUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Dmitry Batkovich + */ +public class GuavaFluentIterableMethodConverters { + private static final Logger LOG = Logger.getInstance(GuavaFluentIterableMethodConverters.class); + + private static final Map METHOD_INDEX = new HashMap(); + private static final Map TO_OTHER_COLLECTION_METHODS = new HashMap(); + private static final Set STOP_METHODS = new HashSet(); + + static { + METHOD_INDEX.put("allMatch", new FluentIterableMethodTransformer.OneParameterMethodTransformer(StreamApiConstants.ALL_MATCH + "(%s)", true)); + METHOD_INDEX.put("anyMatch", new FluentIterableMethodTransformer.OneParameterMethodTransformer(StreamApiConstants.ANY_MATCH + "(%s)", true)); + + METHOD_INDEX.put("contains", new FluentIterableMethodTransformer.OneParameterMethodTransformer("anyMatch(e -> e != null && e.equals(%s))")); + METHOD_INDEX.put("copyInto", new FluentIterableMethodTransformer.OneParameterMethodTransformer("forEach(%s::add)")); + METHOD_INDEX.put("filter", new FluentIterableMethodTransformer.OneParameterMethodTransformer(StreamApiConstants.FILTER + "(%s)", true)); + METHOD_INDEX.put("first", new FluentIterableMethodTransformer.ParameterlessMethodTransformer(StreamApiConstants.FIND_FIRST)); + METHOD_INDEX.put("firstMatch", new FluentIterableMethodTransformer.OneParameterMethodTransformer("filter(%s).findFirst()", true)); + METHOD_INDEX.put("get", new FluentIterableMethodTransformer.OneParameterMethodTransformer("collect(java.util.stream.Collectors.toList()).get(%s)")); + METHOD_INDEX + .put("index", new FluentIterableMethodTransformer.OneParameterMethodTransformer("collect(java.util.stream.Collectors.toList()).indexOf(%s)")); + METHOD_INDEX.put("isEmpty", new FluentIterableMethodTransformer.ParameterlessMethodTransformer("findAny().isPresent()", true)); + METHOD_INDEX.put("last", new FluentIterableMethodTransformer.ParameterlessMethodTransformer("reduce((previous, current) -> current)")); + METHOD_INDEX.put("limit", new FluentIterableMethodTransformer.OneParameterMethodTransformer(StreamApiConstants.LIMIT + "(%s)")); + METHOD_INDEX.put("size", new FluentIterableMethodTransformer.ParameterlessMethodTransformer("collect(java.util.stream.Collectors.toList()).size()")); + METHOD_INDEX.put("skip", new FluentIterableMethodTransformer.OneParameterMethodTransformer(StreamApiConstants.SKIP + "(%s)")); + METHOD_INDEX.put("toArray", new FluentIterableMethodTransformer.ToArrayMethodTransformer()); + METHOD_INDEX.put("transform", new FluentIterableMethodTransformer.OneParameterMethodTransformer(StreamApiConstants.MAP + "(%s)", true)); + METHOD_INDEX.put("transformAndConcat", new FluentIterableMethodTransformer.OneParameterMethodTransformer(StreamApiConstants.FLAT_MAP + "(%s)", true)); + METHOD_INDEX.put("uniqueIndex", new FluentIterableMethodTransformer.OneParameterMethodTransformer( + "collect(java.util.stream.Collectors.toMap(%s, java.util.function.Function.identity()))")); + + TO_OTHER_COLLECTION_METHODS.put("toMap", "collect(java.util.stream.Collectors.toMap(java.util.function.Function.identity(), %s))"); + TO_OTHER_COLLECTION_METHODS.put("toList", "collect(java.util.stream.Collectors.toList())"); + TO_OTHER_COLLECTION_METHODS.put("toSet", "collect(java.util.stream.Collectors.toSet())"); + TO_OTHER_COLLECTION_METHODS.put("toSortedList", "sorted(%s).collect(java.util.stream.Collectors.toList())"); + TO_OTHER_COLLECTION_METHODS.put("toSortedSet", "sorted(%s).collect(java.util.stream.Collectors.toSet())"); + + STOP_METHODS.add("append"); + STOP_METHODS.add("cycle"); + } + + public static boolean isFluentIterableMethod(final String methodName) { + return STOP_METHODS.contains(methodName) || + TO_OTHER_COLLECTION_METHODS.containsKey(methodName) || + METHOD_INDEX.containsKey(methodName); + } + + public static boolean isStopMethod(final String methodName) { + return STOP_METHODS.contains(methodName); + } + + public static void convert(final PsiLocalVariable localVariable, + final PsiElementFactory elementFactory, + final JavaCodeStyleManager codeStyleManager) { + final PsiTypeElement typeElement = localVariable.getTypeElement(); + final PsiReferenceParameterList generics = PsiTreeUtil.findChildOfType(typeElement, PsiReferenceParameterList.class); + typeElement.replace(elementFactory.createTypeElementFromText( + StreamApiConstants.JAVA_UTIL_STREAM_STREAM + (generics == null ? "" : generics.getText()), null)); + + final PsiExpression initializer = localVariable.getInitializer(); + if (initializer != null) { + PsiMethodCallExpression initializerMethodCall = (PsiMethodCallExpression)initializer; + convertMethodCallDeep(elementFactory, initializerMethodCall); + } + codeStyleManager.shortenClassReferences(localVariable); + } + + public static void convert(PsiExpression expression, + final PsiElementFactory elementFactory, + final JavaCodeStyleManager codeStyleManager) { + if (expression instanceof PsiReferenceExpression) { + final PsiElement expressionParent = expression.getParent(); + if (expressionParent instanceof PsiReturnStatement || isIterableMethodParameter(expressionParent, expression)) { + expression = (PsiExpression)expression.replace( + elementFactory.createExpressionFromText(expression.getText() + ".collect(java.util.stream.Collectors.toList())", null)); + codeStyleManager.shortenClassReferences(expression); + } + return; + } + final PsiMethodCallExpression parentMethodCall = PsiTreeUtil.getParentOfType(expression, PsiMethodCallExpression.class); + if (parentMethodCall != null && parentMethodCall.getMethodExpression().getQualifierExpression() == expression) { + final PsiMethod seqTailMethod = parentMethodCall.resolveMethod(); + if (seqTailMethod == null) { + return; + } + final PsiClass seqTailMethodClass = seqTailMethod.getContainingClass(); + if (seqTailMethodClass != null && GuavaFluentIterableInspection.GUAVA_OPTIONAL.equals(seqTailMethodClass.getQualifiedName())) { + final PsiMethodCallExpression newParentMethodCall = + GuavaOptionalConverter.convertGuavaOptionalToJava(parentMethodCall, elementFactory); + expression = newParentMethodCall.getMethodExpression().getQualifierExpression(); + } + } + if (expression instanceof PsiMethodCallExpression) { + expression = convertMethodCallDeep(elementFactory, (PsiMethodCallExpression)expression); + } + if (expression == null) { + return; + } + final PsiElement parent = expression.getParent(); + if (parent instanceof PsiExpressionList) { + final PsiMethodCallExpression methodCall = (PsiMethodCallExpression)parent.getParent(); + PsiExpression[] expressions = methodCall.getArgumentList().getExpressions(); + int index = ArrayUtil.indexOf(expressions, expression); + LOG.assertTrue(index >= 0); + final PsiMethod method = methodCall.resolveMethod(); + LOG.assertTrue(method != null); + final PsiType parameterType = method.getParameterList().getParameters()[index].getType(); + expression = addCollectToListIfNeed(expression, parameterType, elementFactory); + } else if (parent instanceof PsiReturnStatement) { + final PsiMethod containingMethod = PsiTreeUtil.getParentOfType(parent, PsiMethod.class); + LOG.assertTrue(containingMethod != null); + final PsiType returnType = containingMethod.getReturnType(); + expression = addCollectToListIfNeed(expression, returnType, elementFactory); + } + codeStyleManager.shortenClassReferences(expression); + } + + private static PsiExpression addCollectToListIfNeed(PsiExpression expression, PsiType type, PsiElementFactory elementFactory) { + if (type instanceof PsiClassType) { + PsiClass resolvedParamClass = ((PsiClassType)type).resolve(); + if (resolvedParamClass != null && CommonClassNames.JAVA_LANG_ITERABLE.equals(resolvedParamClass.getQualifiedName())) { + final PsiExpression newExpression = + elementFactory.createExpressionFromText(expression.getText() + ".collect(java.util.stream.Collectors.toList())", null); + return (PsiExpression) expression.replace(newExpression); + } + } + return expression; + } + + private static boolean isIterableMethodParameter(PsiElement listExpression, PsiExpression parameterExpression) { + if (!(listExpression instanceof PsiExpressionList)) { + return false; + } + if (!(listExpression.getParent() instanceof PsiMethodCallExpression)) { + return false; + } + final Project project = parameterExpression.getProject(); + final PsiClass fluentIterable = JavaPsiFacade.getInstance(project) + .findClass(GuavaFluentIterableInspection.GUAVA_FLUENT_ITERABLE, GlobalSearchScope.allScope(project)); + return GuavaFluentIterableInspection.isMethodWithParamAcceptsConversion((PsiMethodCallExpression)listExpression.getParent(), parameterExpression, fluentIterable); + } + + @Nullable + private static PsiMethodCallExpression convertMethodCallDeep(PsiElementFactory elementFactory, + @NotNull PsiMethodCallExpression methodCall) { + PsiMethodCallExpression newMethodCall = methodCall; + PsiMethodCallExpression returnCall = null; + while (true) { + final Pair converted = convertMethodCall(elementFactory, newMethodCall); + if (converted.getSecond()) { + return returnCall; + } + if (returnCall == null) { + returnCall = converted.getFirst(); + } + if (converted.getFirst() == null) { + return returnCall; + } + newMethodCall = converted.getFirst(); + final PsiExpression expression = newMethodCall.getMethodExpression().getQualifierExpression(); + if (expression instanceof PsiMethodCallExpression) { + newMethodCall = (PsiMethodCallExpression)expression; + } + else { + return returnCall; + } + } + } + + public static Pair convertMethodCall(PsiElementFactory elementFactory, PsiMethodCallExpression methodCall) { + final PsiReferenceExpression methodExpression = methodCall.getMethodExpression(); + final String name = methodExpression.getReferenceName(); + if (TO_OTHER_COLLECTION_METHODS.containsKey(name)) { + return Pair.create(convertToCollection(methodCall, name, elementFactory), false); + } + else if (GuavaFluentIterableInspection.FLUENT_ITERABLE_FROM.equals(name)) { + final PsiExpression[] argumentList = methodCall.getArgumentList().getExpressions(); + LOG.assertTrue(argumentList.length == 1); + final PsiExpression expression = argumentList[0]; + + final PsiType type = expression.getType(); + LOG.assertTrue(type instanceof PsiClassType); + final PsiClass resolvedClass = ((PsiClassType)type).resolve(); + final String newExpressionText; + if (InheritanceUtil.isInheritor(resolvedClass, CommonClassNames.JAVA_UTIL_COLLECTION)) { + newExpressionText = expression.getText() + ".stream()"; + } else { + newExpressionText = "java.util.stream.StreamSupport.stream(" + expression.getText() + ".spliterator(), false)"; + } + return Pair.create((PsiMethodCallExpression)methodCall.replace(elementFactory.createExpressionFromText(newExpressionText, null)), true); + } + else { + final FluentIterableMethodTransformer transformer = METHOD_INDEX.get(name); + LOG.assertTrue(transformer != null, name); + final PsiMethodCallExpression transformedExpression = transformer.transform(methodCall, elementFactory); + return Pair.create(transformedExpression, false); + } + } + + private static PsiMethodCallExpression convertToCollection(final PsiMethodCallExpression methodCall, + final String methodName, + final PsiElementFactory elementFactory) { + final PsiExpression[] expressions = methodCall.getArgumentList().getExpressions(); + assert expressions.length < 2; + String template = TO_OTHER_COLLECTION_METHODS.get(methodName); + if (expressions.length == 1) { + template = String.format(template, expressions[0].getText()); + } + final PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression(); + if (qualifier == null) { + return null; + } + final String text = qualifier.getText() + "." + template; + final PsiExpression expression = elementFactory.createExpressionFromText(text, null); + return (PsiMethodCallExpression)methodCall.replace(expression); + } +} diff --git a/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaFunctionAndPredicateConverter.java b/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaFunctionAndPredicateConverter.java new file mode 100644 index 000000000000..c0e5d4d3c9d6 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaFunctionAndPredicateConverter.java @@ -0,0 +1,70 @@ +/* + * 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.AnonymousCanBeLambdaInspection; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.psi.*; +import com.intellij.psi.util.InheritanceUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Dmitry Batkovich + */ +public class GuavaFunctionAndPredicateConverter { + private final static Logger LOG = Logger.getInstance(GuavaFunctionAndPredicateConverter.class); + + @Nullable + public static Boolean isClassConditionPredicate(final PsiExpression expression) { + final PsiType type = expression.getType(); + if (type instanceof PsiClassType) { + final PsiClass resolvedClass = ((PsiClassType)type).resolve(); + if (InheritanceUtil.isInheritor(resolvedClass, "com.google.common.base.Function") || + InheritanceUtil.isInheritor(resolvedClass, "com.google.common.base.Predicate")) { + return Boolean.FALSE; + } + else if (InheritanceUtil.isInheritor(resolvedClass, CommonClassNames.JAVA_LANG_CLASS)) { + return Boolean.TRUE; + } + } + return null; + } + + @NotNull + public static String convertFunctionOrPredicateParameter(final @NotNull PsiExpression expression, + final boolean role) { + if (role) { + final String pattern = expression instanceof PsiMethodCallExpression || expression instanceof PsiReferenceExpression + ? "%s::isInstance" + : "(%s)::isInstance"; + return String.format(pattern, expression.getText()); + } + if (expression instanceof PsiNewExpression) { + final PsiAnonymousClass anonymousClass = ((PsiNewExpression)expression).getAnonymousClass(); + if (anonymousClass != null && AnonymousCanBeLambdaInspection.canBeConvertedToLambda(anonymousClass, true)) { + final PsiLambdaExpression lambdaExpression = AnonymousCanBeLambdaInspection.replacePsiElementWithLambda(expression, true); + LOG.assertTrue(lambdaExpression != null); + return lambdaExpression.getText(); + } + } + String qualifierText = expression.getText(); + if (!(expression instanceof PsiMethodCallExpression) && !(expression instanceof PsiReferenceExpression)) { + qualifierText = "(" + qualifierText + ")"; + } + return qualifierText + "::apply"; + } +} diff --git a/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaOptionalConverter.java b/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaOptionalConverter.java new file mode 100644 index 000000000000..673f25d08fd3 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInspection/java18StreamApi/GuavaOptionalConverter.java @@ -0,0 +1,84 @@ +/* + * 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.openapi.diagnostic.Logger; +import com.intellij.psi.*; +import com.intellij.util.containers.hash.HashMap; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +/** + * @author Dmitry Batkovich + */ +public class GuavaOptionalConverter { + private static final Logger LOG = Logger.getInstance(GuavaOptionalConverter.class); + private static final Map METHODS_CONVERSION = new HashMap(); + private static final String OR_METHOD = "or"; + + static { + METHODS_CONVERSION.put("isPresent", new FluentIterableMethodTransformer.ParameterlessMethodTransformer("isPresent")); + METHODS_CONVERSION.put("get", new FluentIterableMethodTransformer.ParameterlessMethodTransformer("get")); + METHODS_CONVERSION.put(OR_METHOD, new FluentIterableMethodTransformer.OneParameterMethodTransformer("orElse(%s)")); + METHODS_CONVERSION.put("orNull", new FluentIterableMethodTransformer.ParameterlessMethodTransformer("orElse(null)")); + } + + public static boolean isConvertibleIfOption(PsiMethodCallExpression methodCallExpression) { + final PsiMethod method = methodCallExpression.resolveMethod(); + if (method == null) { + return false; + } + final PsiClass aClass = method.getContainingClass(); + if (aClass == null) { + return false; + } + if (!GuavaFluentIterableInspection.GUAVA_OPTIONAL.equals(aClass.getQualifiedName())) { + return true; + } + return METHODS_CONVERSION.containsKey(method.getName()); + } + + @NotNull + public static PsiMethodCallExpression convertGuavaOptionalToJava(final PsiMethodCallExpression methodCall, + final PsiElementFactory elementFactory) { + final String methodName = methodCall.getMethodExpression().getReferenceName(); + if (methodName == OR_METHOD) { + final PsiExpression[] arguments = methodCall.getArgumentList().getExpressions(); + if (arguments.length != 1) { + return methodCall; + } + final PsiExpression argument = arguments[0]; + final PsiType type = argument.getType(); + if (type instanceof PsiClassType) { + final PsiClass resolvedClass = ((PsiClassType)type).resolve(); + if (resolvedClass != null) { + final String qName = resolvedClass.getQualifiedName(); + if (GuavaFluentIterableInspection.GUAVA_OPTIONAL.equals(qName) || "com.google.common.base.Supplier".equals(qName)) { + return methodCall; + } + } + } + } + final FluentIterableMethodTransformer conversion = METHODS_CONVERSION.get(methodName); + if (conversion == null) { + return methodCall; + } + final PsiMethodCallExpression transformed = conversion.transform(methodCall, elementFactory); + LOG.assertTrue(transformed != null); + return transformed; + } +} diff --git a/resources-en/src/inspectionDescriptions/GuavaFluentIterable.html b/resources-en/src/inspectionDescriptions/GuavaFluentIterable.html new file mode 100644 index 000000000000..e99075b50dbb --- /dev/null +++ b/resources-en/src/inspectionDescriptions/GuavaFluentIterable.html @@ -0,0 +1,5 @@ + + +Inspection detects most of cases when Guava's Fluent Iterable can be replaced by Java Stream API. + + \ No newline at end of file diff --git a/resources/src/META-INF/IdeaPlugin.xml b/resources/src/META-INF/IdeaPlugin.xml index db6e2910474f..99ba2f251ef9 100644 --- a/resources/src/META-INF/IdeaPlugin.xml +++ b/resources/src/META-INF/IdeaPlugin.xml @@ -740,7 +740,11 @@ groupName="Performance issues" enabledByDefault="false" level="WARNING" implementationClass="com.intellij.codeInspection.CollectionAddAllCanBeReplacedWithConstructorInspection" displayName="Collection.addAll() can be replaced with parametrized constructor"/> - + com.intellij.codeInsight.daemon.quickFix.RedundantLambdaParameterTypeIntention Java/Declaration