| /******************************************************************************* |
| * Copyright (c) 2020 Fabrice TIERCELIN and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Fabrice TIERCELIN - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.jdt.internal.ui.fix; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.eclipse.core.runtime.CoreException; |
| |
| import org.eclipse.text.edits.TextEditGroup; |
| |
| import org.eclipse.jdt.core.ICompilationUnit; |
| import org.eclipse.jdt.core.dom.AST; |
| import org.eclipse.jdt.core.dom.ASTNode; |
| import org.eclipse.jdt.core.dom.ASTVisitor; |
| import org.eclipse.jdt.core.dom.Block; |
| import org.eclipse.jdt.core.dom.ClassInstanceCreation; |
| import org.eclipse.jdt.core.dom.CompilationUnit; |
| import org.eclipse.jdt.core.dom.CreationReference; |
| import org.eclipse.jdt.core.dom.Expression; |
| import org.eclipse.jdt.core.dom.ExpressionMethodReference; |
| import org.eclipse.jdt.core.dom.FieldAccess; |
| import org.eclipse.jdt.core.dom.IMethodBinding; |
| import org.eclipse.jdt.core.dom.ITypeBinding; |
| import org.eclipse.jdt.core.dom.LambdaExpression; |
| import org.eclipse.jdt.core.dom.MethodInvocation; |
| import org.eclipse.jdt.core.dom.Modifier; |
| import org.eclipse.jdt.core.dom.NumberLiteral; |
| import org.eclipse.jdt.core.dom.ReturnStatement; |
| import org.eclipse.jdt.core.dom.SimpleName; |
| import org.eclipse.jdt.core.dom.StringLiteral; |
| import org.eclipse.jdt.core.dom.SuperFieldAccess; |
| import org.eclipse.jdt.core.dom.SuperMethodInvocation; |
| import org.eclipse.jdt.core.dom.SuperMethodReference; |
| import org.eclipse.jdt.core.dom.ThisExpression; |
| import org.eclipse.jdt.core.dom.Type; |
| import org.eclipse.jdt.core.dom.TypeMethodReference; |
| import org.eclipse.jdt.core.dom.VariableDeclarationFragment; |
| import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; |
| import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; |
| import org.eclipse.jdt.core.dom.rewrite.ImportRewrite.ImportRewriteContext; |
| |
| import org.eclipse.jdt.internal.corext.codemanipulation.ContextSensitiveImportRewriteContext; |
| import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory; |
| import org.eclipse.jdt.internal.corext.dom.ASTNodes; |
| import org.eclipse.jdt.internal.corext.dom.Bindings; |
| import org.eclipse.jdt.internal.corext.fix.CleanUpConstants; |
| import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix; |
| import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix.CompilationUnitRewriteOperation; |
| import org.eclipse.jdt.internal.corext.fix.LinkedProposalModel; |
| import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite; |
| import org.eclipse.jdt.internal.corext.util.JavaModelUtil; |
| |
| import org.eclipse.jdt.ui.cleanup.CleanUpRequirements; |
| import org.eclipse.jdt.ui.cleanup.ICleanUpFix; |
| import org.eclipse.jdt.ui.text.java.IProblemLocation; |
| |
| /** |
| * A fix that simplifies the lambda expression and the method reference syntax: |
| * <ul> |
| * <li>Parenthesis are not needed for a single untyped parameter,</li> |
| * <li>Return statement is not needed for a single expression,</li> |
| * <li>Brackets are not needed for a single statement,</li> |
| * <li>A lambda expression can be replaced by a creation or a method reference in some cases.</li> |
| * </ul> |
| */ |
| public class LambdaExpressionAndMethodRefCleanUp extends AbstractMultiFix { |
| public LambdaExpressionAndMethodRefCleanUp() { |
| this(Collections.emptyMap()); |
| } |
| |
| public LambdaExpressionAndMethodRefCleanUp(final Map<String, String> options) { |
| super(options); |
| } |
| |
| @Override |
| public CleanUpRequirements getRequirements() { |
| boolean requireAST= isEnabled(CleanUpConstants.SIMPLIFY_LAMBDA_EXPRESSION_AND_METHOD_REF); |
| Map<String, String> requiredOptions= null; |
| return new CleanUpRequirements(requireAST, false, false, requiredOptions); |
| } |
| |
| @Override |
| public String[] getStepDescriptions() { |
| if (isEnabled(CleanUpConstants.SIMPLIFY_LAMBDA_EXPRESSION_AND_METHOD_REF)) { |
| return new String[] { MultiFixMessages.LambdaExpressionAndMethodRefCleanUp_description }; |
| } |
| return new String[0]; |
| } |
| |
| @Override |
| public String getPreview() { |
| if (isEnabled(CleanUpConstants.SIMPLIFY_LAMBDA_EXPRESSION_AND_METHOD_REF)) { |
| return "" //$NON-NLS-1$ |
| + "someString -> someString.trim().toLowerCase();\n" //$NON-NLS-1$ |
| + "someString -> someString.trim().toLowerCase();\n" //$NON-NLS-1$ |
| + "someString -> (someString.trim().toLowerCase() + \"bar\");\n" //$NON-NLS-1$ |
| + "ArrayList::new;\n" //$NON-NLS-1$ |
| + "Date::getTime;\n"; //$NON-NLS-1$ |
| } |
| |
| return "" //$NON-NLS-1$ |
| + "(someString) -> someString.trim().toLowerCase();\n" //$NON-NLS-1$ |
| + "someString -> {return someString.trim().toLowerCase();};\n" //$NON-NLS-1$ |
| + "someString -> {return someString.trim().toLowerCase() + \"bar\";};\n" //$NON-NLS-1$ |
| + "() -> new ArrayList<>();\n" //$NON-NLS-1$ |
| + "date -> date.getTime();\n"; //$NON-NLS-1$ |
| } |
| |
| private enum ActionType { DO_NOTHING, REMOVE_RETURN, CLASS_INSTANCE_REF, TYPE_REF, SUPER_METHOD_REF, METHOD_REF } |
| |
| @Override |
| protected ICleanUpFix createFix(CompilationUnit unit) throws CoreException { |
| if (!isEnabled(CleanUpConstants.SIMPLIFY_LAMBDA_EXPRESSION_AND_METHOD_REF) || !JavaModelUtil.is1d8OrHigher(unit.getJavaElement().getJavaProject())) { |
| return null; |
| } |
| |
| final List<CompilationUnitRewriteOperation> rewriteOperations= new ArrayList<>(); |
| |
| unit.accept(new ASTVisitor() { |
| @Override |
| public boolean visit(final LambdaExpression node) { |
| ActionType actionType= ActionType.DO_NOTHING; |
| ITypeBinding classBinding= null; |
| |
| boolean removeParamParentheses= hasToRemoveParamParentheses(node); |
| Expression bodyExpression= null; |
| |
| if (node.getBody() instanceof Block) { |
| ReturnStatement returnStatement= ASTNodes.as((Block) node.getBody(), ReturnStatement.class); |
| |
| if (returnStatement != null) { |
| bodyExpression= returnStatement.getExpression(); |
| actionType= ActionType.REMOVE_RETURN; |
| } |
| } else if (node.getBody() instanceof Expression) { |
| bodyExpression= (Expression) node.getBody(); |
| } |
| |
| if (bodyExpression instanceof ClassInstanceCreation) { |
| ClassInstanceCreation classInstanceCreation= (ClassInstanceCreation) bodyExpression; |
| List<Expression> arguments= classInstanceCreation.arguments(); |
| |
| if (node.parameters().size() == arguments.size() |
| && areSameIdentifiers(node, arguments) |
| && classInstanceCreation.getAnonymousClassDeclaration() == null) { |
| actionType= ActionType.CLASS_INSTANCE_REF; |
| } |
| } else if (bodyExpression instanceof SuperMethodInvocation) { |
| SuperMethodInvocation superMethodInvocation= (SuperMethodInvocation) bodyExpression; |
| List<Expression> arguments= superMethodInvocation.arguments(); |
| |
| if (node.parameters().size() == arguments.size() && areSameIdentifiers(node, arguments)) { |
| actionType= ActionType.SUPER_METHOD_REF; |
| } |
| } else if (bodyExpression instanceof MethodInvocation) { |
| MethodInvocation methodInvocation= (MethodInvocation) bodyExpression; |
| Expression calledExpression= methodInvocation.getExpression(); |
| List<Expression> arguments= methodInvocation.arguments(); |
| |
| if (node.parameters().size() == arguments.size()) { |
| if (areSameIdentifiers(node, arguments)) { |
| |
| IMethodBinding methodBinding= methodInvocation.resolveMethodBinding(); |
| |
| if (Boolean.TRUE.equals(ASTNodes.isStatic(methodInvocation))) { |
| boolean valid= true; |
| |
| if (methodBinding != null && methodBinding.getDeclaringClass() != null) { |
| ITypeBinding calledType= methodBinding.getDeclaringClass(); |
| |
| if (!arguments.isEmpty()) { |
| String[] remainingParams= new String[arguments.size() - 1]; |
| |
| for (int i= 0; i < arguments.size() - 1; i++) { |
| ITypeBinding resolveTypeBinding= arguments.get(i + 1).resolveTypeBinding(); |
| |
| if (resolveTypeBinding == null) { |
| valid= false; |
| break; |
| } |
| |
| remainingParams[i]= resolveTypeBinding.getQualifiedName(); |
| } |
| |
| if (valid) { |
| for (IMethodBinding declaredMethodBinding : calledType.getDeclaredMethods()) { |
| if ((declaredMethodBinding.getModifiers() & Modifier.STATIC) == 0 && ASTNodes.usesGivenSignature(declaredMethodBinding, |
| calledType.getQualifiedName(), methodInvocation.getName().getIdentifier(), remainingParams)) { |
| valid= false; |
| break; |
| } |
| } |
| } |
| } |
| |
| if (valid) { |
| actionType= ActionType.TYPE_REF; |
| classBinding= calledType; |
| } |
| } |
| } |
| |
| if (calledExpression == null) { |
| if (methodBinding != null) { |
| ITypeBinding calledType= methodBinding.getDeclaringClass(); |
| ITypeBinding enclosingType= Bindings.getBindingOfParentType(node); |
| |
| if (calledType != null && Bindings.isSuperType(calledType, enclosingType)) { |
| actionType= ActionType.METHOD_REF; |
| } |
| } |
| } else if (calledExpression instanceof StringLiteral || calledExpression instanceof NumberLiteral |
| || calledExpression instanceof ThisExpression) { |
| actionType= ActionType.METHOD_REF; |
| } else if (calledExpression instanceof FieldAccess) { |
| FieldAccess fieldAccess= (FieldAccess) calledExpression; |
| |
| if (fieldAccess.resolveFieldBinding() != null && fieldAccess.resolveFieldBinding().isEffectivelyFinal()) { |
| actionType= ActionType.METHOD_REF; |
| } |
| } else if (calledExpression instanceof SuperFieldAccess) { |
| SuperFieldAccess fieldAccess= (SuperFieldAccess) calledExpression; |
| |
| if (fieldAccess.resolveFieldBinding() != null && fieldAccess.resolveFieldBinding().isEffectivelyFinal()) { |
| actionType= ActionType.METHOD_REF; |
| } |
| } |
| } |
| } else if (calledExpression instanceof SimpleName && node.parameters().size() == arguments.size() + 1) { |
| SimpleName calledObject= (SimpleName) calledExpression; |
| |
| if (isSameIdentifier(node, 0, calledObject)) { |
| boolean valid= true; |
| |
| for (int i= 0; i < arguments.size(); i++) { |
| ASTNode expression= ASTNodes.getUnparenthesedExpression(arguments.get(i)); |
| |
| if (!(expression instanceof SimpleName) || !isSameIdentifier(node, i + 1, (SimpleName) expression)) { |
| valid= false; |
| break; |
| } |
| } |
| |
| if (valid) { |
| ITypeBinding clazz= null; |
| |
| if (calledExpression.resolveTypeBinding() != null) { |
| clazz= calledExpression.resolveTypeBinding(); |
| } else if (methodInvocation.resolveMethodBinding() != null && methodInvocation.resolveMethodBinding().getDeclaringClass() != null) { |
| clazz= methodInvocation.resolveMethodBinding().getDeclaringClass(); |
| } |
| |
| if (clazz != null) { |
| String[] cumulativeParams= new String[arguments.size() + 1]; |
| cumulativeParams[0]= clazz.getQualifiedName(); |
| |
| for (int i= 0; i < arguments.size(); i++) { |
| ITypeBinding resolveTypeBinding= arguments.get(i).resolveTypeBinding(); |
| |
| if (resolveTypeBinding == null) { |
| valid= false; |
| break; |
| } |
| |
| cumulativeParams[i + 1]= resolveTypeBinding.getQualifiedName(); |
| } |
| |
| if (valid) { |
| for (IMethodBinding declaredMethodBinding : clazz.getDeclaredMethods()) { |
| if ((declaredMethodBinding.getModifiers() & Modifier.STATIC) > 0 && ASTNodes.usesGivenSignature(declaredMethodBinding, |
| clazz.getQualifiedName(), methodInvocation.getName().getIdentifier(), cumulativeParams)) { |
| valid= false; |
| break; |
| } |
| } |
| } |
| |
| if (valid) { |
| actionType= ActionType.TYPE_REF; |
| classBinding= clazz; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| if (removeParamParentheses || actionType != ActionType.DO_NOTHING) { |
| rewriteOperations.add(new ReplaceLambdaOperation(node, removeParamParentheses, actionType, bodyExpression, classBinding)); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| private boolean hasToRemoveParamParentheses(final LambdaExpression node) { |
| if (node.hasParentheses() && node.parameters().size() == 1 |
| && node.parameters().get(0) instanceof VariableDeclarationFragment) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * In order to make parameters implicit, those ones should be the same in the same |
| * order. |
| * |
| * @param node the lambda expression |
| * @param arguments The arguments in the expression |
| * @return true if the parameters are obvious |
| */ |
| private boolean areSameIdentifiers(LambdaExpression node, List<Expression> arguments) { |
| for (int i= 0; i < node.parameters().size(); i++) { |
| Expression expression= ASTNodes.getUnparenthesedExpression(arguments.get(i)); |
| |
| if (!(expression instanceof SimpleName) || !isSameIdentifier(node, i, (SimpleName) expression)) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| private boolean isSameIdentifier(final LambdaExpression node, final int i, final SimpleName argument) { |
| Object param0= node.parameters().get(i); |
| |
| if (param0 instanceof VariableDeclarationFragment) { |
| VariableDeclarationFragment variableDeclarationFragment= (VariableDeclarationFragment) param0; |
| return variableDeclarationFragment.getName().getIdentifier().equals(argument.getIdentifier()); |
| } |
| |
| return false; |
| } |
| }); |
| |
| if (rewriteOperations.isEmpty()) { |
| return null; |
| } |
| |
| return new CompilationUnitRewriteOperationsFix(MultiFixMessages.LambdaExpressionAndMethodRefCleanUp_description, unit, |
| rewriteOperations.toArray(new CompilationUnitRewriteOperation[0])); |
| } |
| |
| @Override |
| public boolean canFix(final ICompilationUnit compilationUnit, final IProblemLocation problem) { |
| return false; |
| } |
| |
| @Override |
| protected ICleanUpFix createFix(final CompilationUnit unit, IProblemLocation[] problems) throws CoreException { |
| return null; |
| } |
| |
| private static class ReplaceLambdaOperation extends CompilationUnitRewriteOperation { |
| private final LambdaExpression node; |
| private final boolean removeParentheses; |
| private final ActionType action; |
| private final Expression bodyExpression; |
| private final ITypeBinding classBinding; |
| |
| public ReplaceLambdaOperation(final LambdaExpression node, final boolean removeParentheses, final ActionType action, final Expression bodyExpression, final ITypeBinding classBinding) { |
| this.node= node; |
| this.removeParentheses= removeParentheses; |
| this.action= action; |
| this.bodyExpression= bodyExpression; |
| this.classBinding= classBinding; |
| } |
| |
| @Override |
| public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModel linkedModel) throws CoreException { |
| ASTRewrite rewrite= cuRewrite.getASTRewrite(); |
| AST ast= cuRewrite.getRoot().getAST(); |
| TextEditGroup group= createTextEditGroup(MultiFixMessages.LambdaExpressionAndMethodRefCleanUp_description, cuRewrite); |
| |
| switch (action) { |
| case REMOVE_RETURN: |
| LambdaExpression copyOfLambdaExpression2= ast.newLambdaExpression(); |
| copyParameters(rewrite, node, copyOfLambdaExpression2); |
| |
| if (removeParentheses) { |
| copyOfLambdaExpression2.setParentheses(false); |
| } |
| |
| copyOfLambdaExpression2.setBody(ASTNodeFactory.parenthesizeIfNeeded(ast, ASTNodes.createMoveTarget(rewrite, bodyExpression))); |
| ASTNodes.replaceButKeepComment(rewrite, node, copyOfLambdaExpression2, group); |
| break; |
| |
| case TYPE_REF: |
| TypeMethodReference typeMethodRef= ast.newTypeMethodReference(); |
| MethodInvocation typeMethodInvocation= (MethodInvocation) bodyExpression; |
| |
| typeMethodRef.setType(copyType(cuRewrite, ast, typeMethodInvocation, classBinding)); |
| typeMethodRef.setName(ASTNodes.createMoveTarget(rewrite, typeMethodInvocation.getName())); |
| ASTNodes.replaceButKeepComment(rewrite, node, typeMethodRef, group); |
| break; |
| |
| case METHOD_REF: |
| ExpressionMethodReference methodRef= ast.newExpressionMethodReference(); |
| MethodInvocation methodInvocation= (MethodInvocation) bodyExpression; |
| |
| if (methodInvocation.getExpression() != null) { |
| methodRef.setExpression(ASTNodes.createMoveTarget(rewrite, methodInvocation.getExpression())); |
| } else { |
| methodRef.setExpression(ast.newThisExpression()); |
| } |
| |
| methodRef.setName(ASTNodes.createMoveTarget(rewrite, methodInvocation.getName())); |
| ASTNodes.replaceButKeepComment(rewrite, node, methodRef, group); |
| break; |
| |
| case SUPER_METHOD_REF: |
| SuperMethodReference superMethodRef= ast.newSuperMethodReference(); |
| SuperMethodInvocation superMethodInvocation= (SuperMethodInvocation) bodyExpression; |
| |
| superMethodRef.setName(ASTNodes.createMoveTarget(rewrite, superMethodInvocation.getName())); |
| ASTNodes.replaceButKeepComment(rewrite, node, superMethodRef, group); |
| break; |
| |
| case CLASS_INSTANCE_REF: |
| CreationReference creationRef= ast.newCreationReference(); |
| ClassInstanceCreation classInstanceCreation= (ClassInstanceCreation) bodyExpression; |
| |
| creationRef.setType(copyType(cuRewrite, ast, classInstanceCreation, classInstanceCreation.resolveTypeBinding())); |
| ASTNodes.replaceButKeepComment(rewrite, node, creationRef, group); |
| break; |
| |
| case DO_NOTHING: |
| default: |
| LambdaExpression copyOfLambdaExpression= ast.newLambdaExpression(); |
| copyParameters(rewrite, node, copyOfLambdaExpression); |
| |
| if (removeParentheses) { |
| copyOfLambdaExpression.setParentheses(false); |
| } |
| |
| copyOfLambdaExpression.setBody(rewrite.createMoveTarget(node.getBody())); |
| ASTNodes.replaceButKeepComment(rewrite, node, copyOfLambdaExpression, group); |
| break; |
| |
| } |
| } |
| |
| private void copyParameters(final ASTRewrite rewrite, final LambdaExpression oldLambdaExpression, final LambdaExpression copyOfLambdaExpression) { |
| for (Object oldParameter : oldLambdaExpression.parameters()) { |
| ASTNode copyOfParameter= ASTNodes.createMoveTarget(rewrite, (ASTNode) oldParameter); |
| copyOfLambdaExpression.parameters().add(copyOfParameter); |
| } |
| |
| copyOfLambdaExpression.setParentheses(oldLambdaExpression.hasParentheses()); |
| } |
| |
| private static Type copyType(final CompilationUnitRewrite cuRewrite, final AST ast, final ASTNode node, final ITypeBinding typeBinding) { |
| ImportRewrite importRewrite= cuRewrite.getImportRewrite(); |
| ImportRewriteContext importContext= new ContextSensitiveImportRewriteContext(node, importRewrite); |
| ITypeBinding modifiedType; |
| |
| if (typeBinding.getTypeParameters().length == 0) { |
| modifiedType= typeBinding.getErasure(); |
| } else { |
| modifiedType= typeBinding; |
| } |
| |
| return ASTNodeFactory.newCreationType(ast, modifiedType, importRewrite, importContext); |
| } |
| } |
| } |