blob: b89c7c73250b9a9f05a499967cab3f10d427353e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2020, 2021 Red Hat Inc. 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:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.corext.fix;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
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.IBuffer;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTMatcher;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BreakStatement;
import org.eclipse.jdt.core.dom.Comment;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ContinueStatement;
import org.eclipse.jdt.core.dom.DoStatement;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.ForStatement;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.Modifier.ModifierKeyword;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.SwitchCase;
import org.eclipse.jdt.core.dom.SwitchExpression;
import org.eclipse.jdt.core.dom.SwitchStatement;
import org.eclipse.jdt.core.dom.ThrowStatement;
import org.eclipse.jdt.core.dom.TryStatement;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.WhileStatement;
import org.eclipse.jdt.core.dom.YieldStatement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.core.manipulation.ICleanUpFixCore;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
public class SwitchExpressionsFixCore extends CompilationUnitRewriteOperationsFixCore {
public static final class SwitchStatementsFinder extends ASTVisitor {
private List<SwitchExpressionsFixOperation> fResult;
public SwitchStatementsFinder(List<SwitchExpressionsFixOperation> ops) {
fResult= ops;
}
@Override
public boolean visit(SwitchStatement node) {
SwitchExpressionsFixOperation operation= getOperation(node);
if (operation != null) {
fResult.add(operation);
}
return true;
}
private boolean isInvalidStatement(Statement statement) {
return statement instanceof ContinueStatement
|| statement instanceof ForStatement
|| statement instanceof ReturnStatement
|| statement instanceof IfStatement
|| statement instanceof DoStatement
|| statement instanceof EnhancedForStatement
|| statement instanceof SwitchStatement
|| statement instanceof YieldStatement
|| statement instanceof TryStatement
|| statement instanceof WhileStatement;
}
private SwitchExpressionsFixOperation getOperation(SwitchStatement switchStatement) {
final List<SwitchCase> throwList= new ArrayList<>();
boolean defaultFound= false;
List<Statement> currentBlock= null;
SwitchCase currentCase= null;
Map<SwitchCase, List<Statement>> caseMap= new LinkedHashMap<>();
for (Iterator<Statement> iter= switchStatement.statements().iterator(); iter.hasNext();) {
Statement statement= iter.next();
if (statement instanceof SwitchCase) {
SwitchCase switchCase= (SwitchCase)statement;
if (switchCase.isDefault()) {
defaultFound= true;
}
if (currentBlock != null && !currentBlock.isEmpty()) {
return null;
}
if (currentCase != null) {
caseMap.put(currentCase, currentBlock);
}
currentBlock= new ArrayList<>();
currentCase= switchCase;
} else if (isInvalidStatement(statement)) {
return null;
} else if (statement instanceof BreakStatement) {
if (currentBlock != null && currentBlock.isEmpty()) {
return null;
}
if (currentCase != null) {
caseMap.put(currentCase, currentBlock);
}
currentBlock= null;
currentCase= null;
} else if (statement instanceof ThrowStatement) {
throwList.add(currentCase);
if (currentBlock == null) {
return null;
}
currentBlock.add(statement);
caseMap.put(currentCase, currentBlock);
currentBlock= null;
currentCase= null;
} else {
if (currentBlock == null) {
return null;
}
if (statement instanceof Block) {
Block block= (Block)statement;
// allow one level of block with no invalid statements inside
for (Iterator<Statement> blockIter= block.statements().iterator(); blockIter.hasNext();) {
Statement blockStatement= blockIter.next();
if (isInvalidStatement(blockStatement) || blockStatement instanceof Block) {
return null;
}
if (blockStatement instanceof ThrowStatement) {
throwList.add(currentCase);
}
}
}
currentBlock.add(statement);
}
}
// look for final case that has implicit break statement
if (currentCase != null) {
if (currentBlock != null && currentBlock.isEmpty()) {
return null;
}
caseMap.put(currentCase, currentBlock);
}
String commonAssignmentName= null;
IBinding assignmentBinding= null;
for (Map.Entry<SwitchCase, List<Statement>> entry : caseMap.entrySet()) {
SwitchCase entryCase= entry.getKey();
List<Statement> entryStatements= entry.getValue();
if (throwList.contains(entryCase) || entryStatements.size() == 0) {
continue;
}
Statement lastStatement= entryStatements.get(entryStatements.size() - 1);
if (lastStatement instanceof Block) {
@SuppressWarnings("rawtypes")
List blockStatements= ((Block)lastStatement).statements();
if (blockStatements.isEmpty()) {
continue;
}
lastStatement= (Statement)(blockStatements.get(blockStatements.size() - 1));
}
// case must end in an assignment
if (!(lastStatement instanceof ExpressionStatement) || !(((ExpressionStatement)lastStatement).getExpression() instanceof Assignment)) {
return null;
}
Assignment assignment= (Assignment)((ExpressionStatement) lastStatement).getExpression();
// must be simple assign operator
if (assignment.getOperator() != Assignment.Operator.ASSIGN) {
return null;
}
if (commonAssignmentName == null) {
Expression exp= assignment.getLeftHandSide();
if (exp instanceof Name) {
commonAssignmentName= ((Name)exp).getFullyQualifiedName();
assignmentBinding= ((Name) exp).resolveBinding();
}
} else {
Expression exp= assignment.getLeftHandSide();
if (exp instanceof Name) {
Name name= (Name)exp;
if (!name.getFullyQualifiedName().equals(commonAssignmentName)) {
return null;
}
}
}
}
if (assignmentBinding == null) {
return null;
}
// ensure either we have default case or else expression is enum and all constants specified
ITypeBinding binding= switchStatement.getExpression().resolveTypeBinding();
if (binding != null && binding.isEnum()) {
IVariableBinding[] fields= binding.getDeclaredFields();
int enumCount= 0;
for (IVariableBinding field : fields) {
if (field.isEnumConstant()) {
++enumCount;
}
}
if (enumCount != caseMap.size() && !defaultFound) {
return null;
}
} else if (!defaultFound) {
return null;
}
return new SwitchExpressionsFixOperation(switchStatement, caseMap, commonAssignmentName, assignmentBinding);
}
}
public static class SwitchExpressionsFixOperation extends CompilationUnitRewriteOperation {
private final SwitchStatement switchStatement;
private final Map<SwitchCase, List<Statement>> caseMap;
private final String varName;
private final IBinding assignmentBinding;
public SwitchExpressionsFixOperation(SwitchStatement switchStatement, Map<SwitchCase, List<Statement>> caseMap,
String varName, IBinding assignmentBinding) {
this.switchStatement= switchStatement;
this.caseMap= caseMap;
this.varName= varName;
this.assignmentBinding= assignmentBinding;
}
@SuppressWarnings("rawtypes")
@Override
public void rewriteAST(CompilationUnitRewrite cuRewrite, LinkedProposalModelCore linkedModel) throws CoreException {
final ASTRewrite rewrite= cuRewrite.getASTRewrite();
final AST ast= rewrite.getAST();
TextEditGroup group= createTextEditGroup(FixMessages.SwitchExpressionsFix_convert_to_switch_expression, cuRewrite);
SwitchExpression newSwitchExpression= ast.newSwitchExpression();
Expression newSwitchExpressionExpression= (Expression)rewrite.createCopyTarget(switchStatement.getExpression());
newSwitchExpression.setExpression(newSwitchExpressionExpression);
SwitchCase lastSwitchCase= null;
// build switch expression
for (Map.Entry<SwitchCase, List<Statement>> entry : caseMap.entrySet()) {
SwitchCase oldSwitchCase= entry.getKey();
List<Statement> oldStatements= entry.getValue();
if (oldStatements.isEmpty()) {
// fall-through, want all fall-through labels in single case
if (lastSwitchCase == null) {
lastSwitchCase= ast.newSwitchCase();
lastSwitchCase.setSwitchLabeledRule(true);
newSwitchExpression.statements().add(lastSwitchCase);
}
for (Object obj : oldSwitchCase.expressions()) {
Expression oldExpression= (Expression)obj;
Expression newExpression= (Expression)rewrite.createCopyTarget(oldExpression);
lastSwitchCase.expressions().add(newExpression);
}
continue;
}
SwitchCase switchCase= null;
if (lastSwitchCase == null) {
SwitchCase newSwitchCase= ast.newSwitchCase();
newSwitchExpression.statements().add(newSwitchCase);
newSwitchCase.setSwitchLabeledRule(true);
switchCase= newSwitchCase;
} else {
switchCase= lastSwitchCase;
}
lastSwitchCase= null;
for (Object obj : oldSwitchCase.expressions()) {
Expression oldExpression= (Expression)obj;
Expression newExpression= (Expression)rewrite.createCopyTarget(oldExpression);
switchCase.expressions().add(newExpression);
}
if (oldStatements.size() == 1 && oldStatements.get(0) instanceof Block) {
oldStatements= ((Block)oldStatements.get(0)).statements();
}
if (oldStatements.size() == 1) {
Statement oldStatement= oldStatements.get(0);
Statement newStatement= null;
if (oldStatement instanceof ThrowStatement) {
ThrowStatement throwStatement= (ThrowStatement)oldStatement;
newStatement= (Statement)rewrite.createCopyTarget(throwStatement);
} else {
ExpressionStatement oldExpStatement= (ExpressionStatement)oldStatement;
Assignment oldAssignment= (Assignment)oldExpStatement.getExpression();
Expression rhs= oldAssignment.getRightHandSide();
// Ugly hack to tack on trailing comments
IBuffer buffer= cuRewrite.getCu().getBuffer();
StringBuilder b= new StringBuilder();
b.append(buffer.getText(rhs.getStartPosition(), rhs.getLength()) + ";"); //$NON-NLS-1$
List<Comment> trailingComments= ASTNodes.getTrailingComments(oldExpStatement);
for (Comment comment : trailingComments) {
b.append(" " + buffer.getText(comment.getStartPosition(), comment.getLength())); //$NON-NLS-1$
}
newStatement= (Statement) rewrite.createStringPlaceholder(b.toString(), ASTNode.EXPRESSION_STATEMENT);
}
newSwitchExpression.statements().add(newStatement);
} else {
Block newBlock= ast.newBlock();
int statementsLen= oldStatements.size();
for (int i= 0; i < statementsLen - 1; ++i) {
Statement oldSwitchCaseStatement= oldStatements.get(i);
newBlock.statements().add(rewrite.createCopyTarget(oldSwitchCaseStatement));
}
ExpressionStatement oldExpStatement= (ExpressionStatement)oldStatements.get(statementsLen - 1);
Assignment oldAssignment= (Assignment)oldExpStatement.getExpression();
Expression rhs= oldAssignment.getRightHandSide();
IBuffer buffer= cuRewrite.getCu().getBuffer();
StringBuilder b= new StringBuilder();
List<Comment> leadingComments= ASTNodes.getLeadingComments(oldExpStatement);
for (Comment comment : leadingComments) {
b.append(buffer.getText(comment.getStartPosition(), comment.getLength()) + "\n"); //$NON-NLS-1$
}
b.append("yield "); //$NON-NLS-1$
List<Comment> trailingComments= ASTNodes.getTrailingComments(oldExpStatement);
b.append(buffer.getText(rhs.getStartPosition(), rhs.getLength()) + ";"); //$NON-NLS-1$
for (Comment comment : trailingComments) {
b.append(" " + buffer.getText(comment.getStartPosition(), comment.getLength())); //$NON-NLS-1$
}
YieldStatement newYield = (YieldStatement)rewrite.createStringPlaceholder(b.toString(), ASTNode.YIELD_STATEMENT);
Expression newYieldExpression= (Expression) rewrite.createStringPlaceholder(b.toString(), rhs.getNodeType());
newYield.setExpression(newYieldExpression);
newBlock.statements().add(newYield);
newSwitchExpression.statements().add(newBlock);
}
}
// see if we can make new switch expression the initializer of assignment variable
if (assignmentBinding instanceof IVariableBinding) {
VariableDeclarationStatement varDeclarationStatement= null;
int varIndex= -2;
IVariableBinding binding= (IVariableBinding)assignmentBinding;
if (!binding.isField() && !binding.isParameter() && !binding.isSynthetic()) {
ASTNode parent= switchStatement.getParent();
if (parent instanceof Block) {
Block block= (Block)parent;
List statements= block.statements();
ListRewrite listRewrite= rewrite.getListRewrite(block, Block.STATEMENTS_PROPERTY);
for (int i= 0; i < statements.size(); ++i) {
Statement statement= (Statement)statements.get(i);
if (statement instanceof VariableDeclarationStatement) {
VariableDeclarationStatement decl= (VariableDeclarationStatement)statement;
List fragments= decl.fragments();
if (fragments.size() == 1) { // must be single var declaration
VariableDeclarationFragment fragment= (VariableDeclarationFragment)fragments.get(0);
if (fragment.getInitializer() == null) { // must not already be initialized
IVariableBinding fragBinding= fragment.resolveBinding();
if (fragBinding != null && fragBinding.isEqualTo(binding)) {
varDeclarationStatement= decl;
varIndex= i;
}
}
}
} else if (statement instanceof SwitchStatement) {
if (statement.subtreeMatch(new ASTMatcher(), switchStatement)) {
// if previous statement declares assignment variable, we can set initializer
if (varIndex == i - 1) {
VariableDeclarationFragment newVarFragment= ast.newVariableDeclarationFragment();
newVarFragment.setName(ast.newSimpleName(varName));
newVarFragment.setInitializer(newSwitchExpression);
VariableDeclarationStatement newVar= ast.newVariableDeclarationStatement(newVarFragment);
ImportRewrite importRewrite= cuRewrite.getImportRewrite();
newVar.setType(importRewrite.addImport(((IVariableBinding) assignmentBinding).getType(), ast));
if (varDeclarationStatement != null && Modifier.isFinal(varDeclarationStatement.getModifiers())) {
newVar.modifiers().add(ast.newModifier(ModifierKeyword.FINAL_KEYWORD));
}
replaceWithLeadingComments(cuRewrite, listRewrite, varDeclarationStatement, group, newVar);
listRewrite.remove(switchStatement, group);
return;
}
break;
}
}
}
}
}
}
// otherwise just assign new switch expression to varName
Assignment newAssignment= ast.newAssignment();
ExpressionStatement newExpressionStatement= ast.newExpressionStatement(newAssignment);
newAssignment.setLeftHandSide(ast.newName(varName));
newAssignment.setRightHandSide(newSwitchExpression);
ASTNode parent= switchStatement.getParent();
if (parent instanceof Block) {
ListRewrite listRewrite= rewrite.getListRewrite(parent, Block.STATEMENTS_PROPERTY);
replaceWithLeadingComments(cuRewrite, listRewrite, switchStatement, group, newExpressionStatement);
} else {
rewrite.replace(switchStatement, newExpressionStatement, group);
}
}
private void replaceWithLeadingComments(CompilationUnitRewrite cuRewrite, ListRewrite listRewrite,
ASTNode oldNode, TextEditGroup group, ASTNode newNode) throws JavaModelException {
ASTRewrite rewrite= cuRewrite.getASTRewrite();
List<Comment> comments= ASTNodes.getLeadingComments(oldNode);
if (!comments.isEmpty()) {
Comment firstComment= comments.get(0);
String commentString= cuRewrite.getCu().getBuffer().getText(firstComment.getStartPosition(), firstComment.getLength());
ASTNode lastComment= rewrite.createStringPlaceholder(commentString, firstComment.isBlockComment() ? ASTNode.BLOCK_COMMENT : ASTNode.LINE_COMMENT);
listRewrite.replace(oldNode, lastComment, group);
for (int j= 1; j < comments.size(); ++j) {
Comment comment= comments.get(j);
commentString= cuRewrite.getCu().getBuffer().getText(comment.getStartPosition(), comment.getLength());
ASTNode newComment= rewrite.createStringPlaceholder(commentString, comment.isBlockComment() ? ASTNode.BLOCK_COMMENT : ASTNode.LINE_COMMENT);
listRewrite.insertAfter(newComment, lastComment, group);
lastComment= newComment;
}
listRewrite.insertAfter(newNode, lastComment, group);
} else {
listRewrite.replace(oldNode, newNode, group);
}
}
}
public static ICleanUpFixCore createCleanUp(CompilationUnit compilationUnit) {
if (!JavaModelUtil.is14OrHigher(compilationUnit.getJavaElement().getJavaProject()))
return null;
List<SwitchExpressionsFixOperation> operations= new ArrayList<>();
SwitchStatementsFinder finder= new SwitchStatementsFinder(operations);
compilationUnit.accept(finder);
if (operations.isEmpty())
return null;
CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation[] ops= operations.toArray(new CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation[operations.size()]);
return new SwitchExpressionsFixCore(FixMessages.SwitchExpressionsFix_convert_to_switch_expression, compilationUnit, ops);
}
protected SwitchExpressionsFixCore(String name, CompilationUnit compilationUnit, CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation[] fixRewriteOperations) {
super(name, compilationUnit, fixRewriteOperations);
}
}