1 /*******************************************************************************
2  * Copyright (c) 2020 Fabrice TIERCELIN and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     Fabrice TIERCELIN - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.jdt.internal.ui.fix;
15 
16 import java.util.ArrayList;
17 import java.util.Collections;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.concurrent.atomic.AtomicInteger;
21 
22 import org.eclipse.core.runtime.CoreException;
23 
24 import org.eclipse.jdt.core.ICompilationUnit;
25 import org.eclipse.jdt.core.dom.AST;
26 import org.eclipse.jdt.core.dom.ASTMatcher;
27 import org.eclipse.jdt.core.dom.ASTVisitor;
28 import org.eclipse.jdt.core.dom.CompilationUnit;
29 import org.eclipse.jdt.core.dom.Expression;
30 import org.eclipse.jdt.core.dom.IfStatement;
31 import org.eclipse.jdt.core.dom.InfixExpression;
32 import org.eclipse.jdt.core.dom.Statement;
33 import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
34 
35 import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;
36 import org.eclipse.jdt.internal.corext.dom.ASTNodes;
37 import org.eclipse.jdt.internal.corext.fix.CleanUpConstants;
38 import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix;
39 import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix.CompilationUnitRewriteOperation;
40 import org.eclipse.jdt.internal.corext.fix.LinkedProposalModel;
41 import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
42 
43 import org.eclipse.jdt.ui.cleanup.CleanUpRequirements;
44 import org.eclipse.jdt.ui.cleanup.ICleanUpFix;
45 import org.eclipse.jdt.ui.text.java.IProblemLocation;
46 
47 /**
48  * A fix that merge conditions of if/else if/else that have the same blocks.
49  */
50 public class MergeConditionalBlocksCleanUp extends AbstractMultiFix {
MergeConditionalBlocksCleanUp()51 	public MergeConditionalBlocksCleanUp() {
52 		this(Collections.emptyMap());
53 	}
54 
MergeConditionalBlocksCleanUp(Map<String, String> options)55 	public MergeConditionalBlocksCleanUp(Map<String, String> options) {
56 		super(options);
57 	}
58 
59 	@Override
getRequirements()60 	public CleanUpRequirements getRequirements() {
61 		boolean requireAST= isEnabled(CleanUpConstants.MERGE_CONDITIONAL_BLOCKS);
62 		return new CleanUpRequirements(requireAST, false, false, null);
63 	}
64 
65 	@Override
getStepDescriptions()66 	public String[] getStepDescriptions() {
67 		if (isEnabled(CleanUpConstants.MERGE_CONDITIONAL_BLOCKS)) {
68 			return new String[] { MultiFixMessages.MergeConditionalBlocksCleanup_description };
69 		}
70 
71 		return new String[0];
72 	}
73 
74 	@Override
getPreview()75 	public String getPreview() {
76 		StringBuilder bld= new StringBuilder();
77 
78 		if (isEnabled(CleanUpConstants.MERGE_CONDITIONAL_BLOCKS)) {
79 			bld.append("if ((i == 0) || (i == 1)) {\n"); //$NON-NLS-1$
80 		} else {
81 			bld.append("if (i == 0) {\n"); //$NON-NLS-1$
82 			bld.append("    System.out.println(\"Duplicate\");\n"); //$NON-NLS-1$
83 			bld.append("} else if (i == 1) {\n"); //$NON-NLS-1$
84 		}
85 
86 		bld.append("    System.out.println(\"Duplicate\");\n"); //$NON-NLS-1$
87 		bld.append("} else {\n"); //$NON-NLS-1$
88 		bld.append("    System.out.println(\"Different\");\n"); //$NON-NLS-1$
89 		bld.append("}\n"); //$NON-NLS-1$
90 
91 		return bld.toString();
92 	}
93 
94 	@Override
createFix(CompilationUnit unit)95 	protected ICleanUpFix createFix(CompilationUnit unit) throws CoreException {
96 		if (!isEnabled(CleanUpConstants.MERGE_CONDITIONAL_BLOCKS)) {
97 			return null;
98 		}
99 
100 		final List<CompilationUnitRewriteOperation> rewriteOperations= new ArrayList<>();
101 
102 		unit.accept(new ASTVisitor() {
103 			@Override
104 			public boolean visit(final IfStatement node) {
105 				if (node.getElseStatement() != null) {
106 					List<IfStatement> duplicateIfBlocks= new ArrayList<>(4);
107 					List<Boolean> isThenStatement= new ArrayList<>(4);
108 					AtomicInteger operandCount= new AtomicInteger(ASTNodes.getNbOperands(node.getExpression()));
109 					duplicateIfBlocks.add(node);
110 					isThenStatement.add(Boolean.TRUE);
111 
112 					while (addOneMoreIf(duplicateIfBlocks, isThenStatement, operandCount)) {
113 						// OK continue
114 					}
115 
116 					if (duplicateIfBlocks.size() > 1) {
117 						rewriteOperations.add(new MergeConditionalBlocksOperation(duplicateIfBlocks, isThenStatement));
118 						return false;
119 					}
120 				}
121 
122 				return true;
123 			}
124 
125 			private boolean addOneMoreIf(final List<IfStatement> duplicateIfBlocks, final List<Boolean> isThenStatement, final AtomicInteger operandCount) {
126 				IfStatement lastBlock= getLast(duplicateIfBlocks);
127 				Statement previousStatement= getLast(isThenStatement) ? lastBlock.getThenStatement() : lastBlock.getElseStatement();
128 				Statement nextStatement= getLast(isThenStatement) ? lastBlock.getElseStatement() : lastBlock.getThenStatement();
129 
130 				if (nextStatement != null) {
131 					IfStatement nextElse= ASTNodes.as(nextStatement, IfStatement.class);
132 
133 					if (nextElse != null
134 							&& operandCount.get() + ASTNodes.getNbOperands(nextElse.getExpression()) < ASTNodes.EXCESSIVE_OPERAND_NUMBER) {
135 						if (match(previousStatement, nextElse.getThenStatement())) {
136 							operandCount.addAndGet(ASTNodes.getNbOperands(nextElse.getExpression()));
137 							duplicateIfBlocks.add(nextElse);
138 							isThenStatement.add(Boolean.TRUE);
139 							return true;
140 						}
141 
142 						if (nextElse.getElseStatement() != null
143 								&& match(previousStatement, nextElse.getElseStatement())) {
144 							operandCount.addAndGet(ASTNodes.getNbOperands(nextElse.getExpression()));
145 							duplicateIfBlocks.add(nextElse);
146 							isThenStatement.add(Boolean.FALSE);
147 							return true;
148 						}
149 					}
150 				}
151 
152 				return false;
153 			}
154 
155 			private boolean match(final Statement expectedStatement, final Statement actualStatement) {
156 				ASTMatcher matcher= new ASTMatcher();
157 				List<Statement> expectedStatements= ASTNodes.asList(expectedStatement);
158 				List<Statement> actualStatements= ASTNodes.asList(actualStatement);
159 
160 				if (expectedStatements.size() != actualStatements.size()) {
161 					return false;
162 				}
163 
164 				for (int codeLine= 0; codeLine < expectedStatements.size(); codeLine++) {
165 					if (!matcher.safeSubtreeMatch(expectedStatements.get(codeLine), actualStatements.get(codeLine))) {
166 						return false;
167 					}
168 				}
169 
170 				return true;
171 			}
172 		});
173 
174 		if (rewriteOperations.isEmpty()) {
175 			return null;
176 		}
177 
178 		return new CompilationUnitRewriteOperationsFix(MultiFixMessages.MergeConditionalBlocksCleanup_description, unit,
179 				rewriteOperations.toArray(new CompilationUnitRewriteOperation[0]));
180 	}
181 
182 	@Override
canFix(final ICompilationUnit compilationUnit, final IProblemLocation problem)183 	public boolean canFix(final ICompilationUnit compilationUnit, final IProblemLocation problem) {
184 		return false;
185 	}
186 
187 	@Override
createFix(final CompilationUnit unit, final IProblemLocation[] problems)188 	protected ICleanUpFix createFix(final CompilationUnit unit, final IProblemLocation[] problems) throws CoreException {
189 		return null;
190 	}
191 
192 	private static class MergeConditionalBlocksOperation extends CompilationUnitRewriteOperation {
193 		private final List<IfStatement> duplicateIfBlocks;
194 		private final List<Boolean> isThenStatement;
195 
MergeConditionalBlocksOperation(final List<IfStatement> duplicateIfBlocks, final List<Boolean> isThenStatement)196 		public MergeConditionalBlocksOperation(final List<IfStatement> duplicateIfBlocks, final List<Boolean> isThenStatement) {
197 			this.duplicateIfBlocks= duplicateIfBlocks;
198 			this.isThenStatement= isThenStatement;
199 		}
200 
201 		@Override
rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModel linkedModel)202 		public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModel linkedModel) throws CoreException {
203 			ASTRewrite rewrite= cuRewrite.getASTRewrite();
204 			AST ast= cuRewrite.getRoot().getAST();
205 
206 			List<Expression> newConditions= new ArrayList<>(duplicateIfBlocks.size());
207 
208 			for (int i= 0; i < duplicateIfBlocks.size(); i++) {
209 				if (isThenStatement.get(i)) {
210 					newConditions.add(ASTNodeFactory.parenthesizeIfNeeded(ast, ASTNodes.createMoveTarget(rewrite, duplicateIfBlocks.get(i).getExpression())));
211 				} else {
212 					newConditions.add(ASTNodeFactory.parenthesizeIfNeeded(ast, ASTNodeFactory.negate(ast, rewrite, duplicateIfBlocks.get(i).getExpression(), true)));
213 				}
214 			}
215 
216 			IfStatement lastBlock= getLast(duplicateIfBlocks);
217 			Statement remainingStatement= getLast(isThenStatement) ? lastBlock.getElseStatement() : lastBlock.getThenStatement();
218 			InfixExpression newCondition= ast.newInfixExpression();
219 			newCondition.setOperator(InfixExpression.Operator.CONDITIONAL_OR);
220 			newCondition.setLeftOperand(newConditions.remove(0));
221 			newCondition.setRightOperand(newConditions.remove(0));
222 			newCondition.extendedOperands().addAll(newConditions);
223 
224 			rewrite.replace(duplicateIfBlocks.get(0).getExpression(), newCondition, null);
225 
226 			if (remainingStatement != null) {
227 				rewrite.replace(duplicateIfBlocks.get(0).getElseStatement(), ASTNodes.createMoveTarget(rewrite, remainingStatement), null);
228 			} else if (duplicateIfBlocks.get(0).getElseStatement() != null) {
229 				rewrite.remove(duplicateIfBlocks.get(0).getElseStatement(), null);
230 			}
231 		}
232 	}
233 
getLast(final List<E> list)234 	private static <E> E getLast(final List<E> list) {
235 		return list.get(list.size() - 1);
236 	}
237 }
238