/* * Copyright 2000-2016 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; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.psi.*; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.psi.codeStyle.JavaCodeStyleManager; import com.intellij.psi.codeStyle.SuggestedNameInfo; import com.intellij.psi.codeStyle.VariableKind; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiTypesUtil; import com.siyeh.ig.psiutils.EquivalenceChecker; import com.siyeh.ig.psiutils.MethodCallUtils; import one.util.streamex.StreamEx; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collection; public class ComparatorCombinatorsInspection extends BaseJavaBatchLocalInspectionTool { private static final Logger LOG = Logger.getInstance(ComparatorCombinatorsInspection.class); @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) { return new JavaElementVisitor() { @SuppressWarnings("DialogTitleCapitalization") @Override public void visitLambdaExpression(PsiLambdaExpression lambda) { super.visitLambdaExpression(lambda); PsiType type = lambda.getFunctionalInterfaceType(); PsiParameter[] parameters = lambda.getParameterList().getParameters(); if (parameters.length != 2 || !(type instanceof PsiClassType) || !((PsiClassType)type).rawType().equalsToText(CommonClassNames.JAVA_UTIL_COMPARATOR)) { return; } PsiElement body = LambdaUtil.extractSingleExpressionFromBody(lambda.getBody()); String methodName = null; if (body instanceof PsiMethodCallExpression) { PsiMethodCallExpression methodCall = (PsiMethodCallExpression)body; PsiExpression[] args = methodCall.getArgumentList().getExpressions(); if (args.length == 1 && MethodCallUtils.isCompareToCall(methodCall)) { PsiExpression left = methodCall.getMethodExpression().getQualifierExpression(); if (left == null) { return; } PsiExpression right = args[0]; if (left instanceof PsiReferenceExpression && right instanceof PsiReferenceExpression) { PsiElement leftElement = ((PsiReferenceExpression)left).resolve(); PsiElement rightElement = ((PsiReferenceExpression)right).resolve(); if (leftElement == parameters[0] && rightElement == parameters[1]) { methodName = "naturalOrder"; } else if (leftElement == parameters[1] && rightElement == parameters[0]) { methodName = "reverseOrder"; } } if (methodName == null && areEquivalent(parameters, left, right)) { methodName = "comparing"; } } else { PsiMethod method = methodCall.resolveMethod(); if (args.length == 2 && method != null && method.getName().equals("compare")) { PsiClass compareClass = method.getContainingClass(); if (compareClass != null) { methodName = getComparingMethodName(compareClass.getQualifiedName()); if (!areEquivalent(parameters, args[0], args[1])) return; } } } } else if (body instanceof PsiBinaryExpression) { PsiBinaryExpression binOp = (PsiBinaryExpression)body; if (binOp.getOperationTokenType().equals(JavaTokenType.MINUS) && areEquivalent(parameters, binOp.getLOperand(), binOp.getROperand())) { PsiType opType = binOp.getLOperand().getType(); if(opType == null) return; methodName = getComparingMethodName(opType.getCanonicalText()); } } if (methodName != null) { holder .registerProblem(lambda, "Can be replaced with Comparator." + methodName, ProblemHighlightType.LIKE_UNUSED_SYMBOL, new ReplaceWithComparatorFix(methodName)); } } }; } @Contract(value = "null -> null", pure = true) @Nullable private static String getComparingMethodName(String type) { if(type == null) return null; switch(PsiTypesUtil.unboxIfPossible(type)) { case "int": case "short": case "byte": case "char": return "comparingInt"; case "long": return "comparingLong"; case "double": return "comparingDouble"; } return null; } @Contract("_, null, _ -> false; _, !null, null -> false") private static boolean areEquivalent(@NotNull PsiParameter[] parameters, @Nullable PsiExpression left, @Nullable PsiExpression right) { if (left == null || right == null) return false; if (!PsiTreeUtil.processElements(left, e -> !(e instanceof PsiReferenceExpression) || ((PsiReferenceExpression)e).resolve() != parameters[1]) || !PsiTreeUtil.processElements(right, e -> !(e instanceof PsiReferenceExpression) || ((PsiReferenceExpression)e).resolve() != parameters[0])) { return false; } PsiExpression copy = (PsiExpression)right.copy(); PsiElement[] rightRefs = PsiTreeUtil.collectElements(copy, e -> e instanceof PsiReferenceExpression && ((PsiReferenceExpression)e).resolve() == parameters[1]); PsiElementFactory factory = JavaPsiFacade.getElementFactory(left.getProject()); String paramName = parameters[0].getName(); if (paramName == null) return false; for (PsiElement ref : rightRefs) { PsiElement nameElement = ((PsiReferenceExpression)ref).getReferenceNameElement(); LOG.assertTrue(nameElement != null); nameElement.replace(factory.createIdentifier(paramName)); } return EquivalenceChecker.getCanonicalPsiEquivalence().expressionsAreEquivalent(left, copy); } static class ReplaceWithComparatorFix implements LocalQuickFix { private final String myMethodName; public ReplaceWithComparatorFix(String methodName) { myMethodName = methodName; } @Nls @NotNull @Override public String getName() { return "Replace with Comparator." + myMethodName; } @Nls @NotNull @Override public String getFamilyName() { return "Simplify comparator using Comparator static methods"; } @Override public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { PsiElement element = descriptor.getStartElement(); if (!(element instanceof PsiLambdaExpression)) return; PsiLambdaExpression lambda = (PsiLambdaExpression)element; PsiParameter[] parameters = lambda.getParameterList().getParameters(); if (parameters.length != 2) return; PsiElement body = LambdaUtil.extractSingleExpressionFromBody(lambda.getBody()); PsiExpression keyExtractor = null; String methodName = null; if (body instanceof PsiMethodCallExpression) { PsiMethodCallExpression methodCall = (PsiMethodCallExpression)body; if (MethodCallUtils.isCompareToCall(methodCall)) { methodName = "comparing"; keyExtractor = methodCall.getMethodExpression().getQualifierExpression(); if (keyExtractor instanceof PsiReferenceExpression) { PsiElement keyElement = ((PsiReferenceExpression)keyExtractor).resolve(); if (keyElement == parameters[0]) { methodName = "naturalOrder"; } else if (keyElement == parameters[1]) { methodName = "reverseOrder"; } } } else { PsiMethod method = methodCall.resolveMethod(); if (method != null && method.getName().equals("compare")) { PsiClass containingClass = method.getContainingClass(); if (containingClass != null) { String className = containingClass.getQualifiedName(); if (className != null) { PsiExpression[] args = methodCall.getArgumentList().getExpressions(); if (args.length != 2) return; keyExtractor = args[0]; methodName = getComparingMethodName(className); } } } } } else if(body instanceof PsiBinaryExpression) { PsiBinaryExpression binOp = (PsiBinaryExpression)body; if(!binOp.getOperationTokenType().equals(JavaTokenType.MINUS)) return; keyExtractor = binOp.getLOperand(); PsiType type = keyExtractor.getType(); if(type == null) return; methodName = getComparingMethodName(type.getCanonicalText()); } if (methodName == null || keyExtractor == null) return; String parameterName = parameters[0].getName(); PsiElementFactory factory = JavaPsiFacade.getElementFactory(project); PsiElement result; if (!methodName.startsWith("comparing")) { result = lambda.replace(factory.createExpressionFromText("java.util.Comparator." + methodName + "()", element)); } else { String newLambda = parameterName + " -> " + keyExtractor.getText(); PsiExpression replacement = factory.createExpressionFromText("java.util.Comparator." + methodName + "(" + newLambda + ")", element); result = lambda.replace(replacement); normalizeLambda(((PsiMethodCallExpression)result).getArgumentList().getExpressions()[0], factory); } CodeStyleManager.getInstance(project).reformat(JavaCodeStyleManager.getInstance(project).shortenClassReferences(result)); } private static void normalizeLambda(PsiExpression expression, PsiElementFactory factory) { if (!(expression instanceof PsiLambdaExpression)) return; PsiLambdaExpression lambda = (PsiLambdaExpression)expression; PsiParameter[] parameters = lambda.getParameterList().getParameters(); PsiElement body = lambda.getBody(); if (body == null) return; if (LambdaCanBeMethodReferenceInspection.replaceLambdaWithMethodReference(lambda) == lambda) { PsiParameter parameter = parameters[0]; String name = parameter.getName(); SuggestedNameInfo nameCandidate = null; JavaCodeStyleManager codeStyleManager = JavaCodeStyleManager.getInstance(expression.getProject()); if (name != null) { if (name.length() > 1 && name.endsWith("1")) { nameCandidate = codeStyleManager.suggestVariableName(VariableKind.PARAMETER, name.substring(0, name.length() - 1), null, parameter.getType(), true); } else if (name.equals("first")) { nameCandidate = codeStyleManager.suggestVariableName(VariableKind.PARAMETER, null, null, parameter.getType(), true); } } if (nameCandidate != null) { String[] names = codeStyleManager.suggestUniqueVariableName(nameCandidate, lambda, true).names; if (names.length > 0) { String newName = names[0]; Collection<PsiReferenceExpression> references = PsiTreeUtil.collectElementsOfType(body, PsiReferenceExpression.class); StreamEx.of(references).filter(ref -> ref.resolve() == parameter).map(PsiJavaCodeReferenceElement::getReferenceNameElement) .nonNull().forEach(nameElement -> nameElement.replace(factory.createIdentifier(newName))); parameter.setName(newName); } } } } } }