| /******************************************************************************* |
| * Copyright (c) 2021 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.corext.util; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| import org.eclipse.jdt.core.dom.ConditionalExpression; |
| import org.eclipse.jdt.core.dom.Expression; |
| import org.eclipse.jdt.core.dom.IfStatement; |
| import org.eclipse.jdt.core.dom.InfixExpression; |
| import org.eclipse.jdt.core.dom.PrefixExpression; |
| import org.eclipse.jdt.core.dom.ReturnStatement; |
| import org.eclipse.jdt.core.dom.Statement; |
| |
| import org.eclipse.jdt.internal.corext.dom.ASTNodes; |
| import org.eclipse.jdt.internal.corext.dom.AbortSearchException; |
| |
| /** |
| * Implementation of the control workflow builder. |
| * |
| * The aim is to match a conditional part of code like: |
| * |
| * <pre> |
| * if (c1) { |
| * if (c2) { |
| * return A; |
| * } else { |
| * return B; |
| * } |
| * } else { |
| * if (c3) { |
| * return C; |
| * } else { |
| * return D; |
| * } |
| * } |
| * </pre> |
| * |
| * Any cases should be handled (no remaining cases). It is expressed this way |
| * <ul> |
| * <li>One case (c1, c2) goes to A</li> |
| * <li>One case (c1, not c2) goes to B</li> |
| * <li>One case (not c1, c3) goes to C</li> |
| * <li>One case (not c1, not c3) goes to D</li> |
| * </ul> |
| * |
| * It can handle as many conditions as you want. |
| * The conditions and the expressions are analyzed as ordinary matcher using <code>org.eclipse.jdt.internal.corext.dom.ASTSemanticMatcher</code>. |
| * All the remaining part of the structure may have lots of different forms: |
| * |
| * <pre> |
| * if (c1 && c2) { |
| * return A; |
| * } else if (c1 && not c2) { |
| * return B; |
| * } else if (not c1 && not c3) { |
| * return D; |
| * } else if (not c1 && c3) { |
| * return C; |
| * } |
| * </pre> |
| * |
| * <pre> |
| * if (c1) { |
| * if (c2) { |
| * return A; |
| * } |
| * return B; |
| * } else { |
| * if (c3) { |
| * return C; |
| * } |
| * return D; |
| * } |
| * </pre> |
| * |
| * <pre> |
| * if (c1) { |
| * if (not c2) { |
| * return B; |
| * } else { |
| * return A; |
| * } |
| * } else { |
| * if (not c3) { |
| * return D; |
| * } else { |
| * return C; |
| * } |
| * } |
| * </pre> |
| * |
| * <pre> |
| * if (c1) { |
| * return c2 ? A : B; |
| * } else { |
| * return c3 ? C :D; |
| * } |
| * |
| * if (not c1) { |
| * return c3 ? C :D; |
| * } |
| * |
| * return not c2 ? A : B; |
| * </pre> |
| */ |
| public final class ControlWorkflowMatcher implements ControlWorkflowMatcherCompletable, ControlWorkflowMatcherRunnable { |
| /** |
| * Id generator. |
| */ |
| private int idGenerator= 1; |
| |
| /** |
| * Built for each analyzed code. |
| */ |
| private class ControlWorkflowNode { |
| /** |
| * Unique id. |
| */ |
| private final int id= idGenerator++; |
| |
| /** |
| * Only finalStatement, returnedValue or condition can be not null. |
| */ |
| private Statement finalStatement; |
| |
| /** |
| * Only finalStatement, returnedValue or condition can be not null. |
| */ |
| private Expression returnedValue; |
| |
| /** |
| * Only finalStatement, returnedValue or condition can be not null. |
| */ |
| private Expression condition; |
| |
| /** |
| * Not null only if the condition is not null. |
| */ |
| private ControlWorkflowNode thenNode; |
| |
| /** |
| * Not null only if the condition is not null. |
| */ |
| private ControlWorkflowNode elseNode; |
| |
| public Statement getFinalStatement() { |
| return finalStatement; |
| } |
| |
| public void setFinalStatement(final Statement finalStatement) { |
| this.finalStatement= finalStatement; |
| } |
| |
| public Expression getReturnedValue() { |
| return returnedValue; |
| } |
| |
| public void setReturnedValue(final Expression returnedValue) { |
| this.returnedValue= returnedValue; |
| } |
| |
| public Expression getCondition() { |
| return condition; |
| } |
| |
| public void setCondition(final Expression condition) { |
| this.condition= condition; |
| } |
| |
| public ControlWorkflowNode getThenNode() { |
| return thenNode; |
| } |
| |
| public void setThenNode(final ControlWorkflowNode thenNode) { |
| this.thenNode= thenNode; |
| } |
| |
| public ControlWorkflowNode getElseNode() { |
| return elseNode; |
| } |
| |
| public void setElseNode(final ControlWorkflowNode elseNode) { |
| this.elseNode= elseNode; |
| } |
| |
| public int getId() { |
| return id; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(id); |
| } |
| |
| @Override |
| public boolean equals(final Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| |
| if (obj == null || getClass() != obj.getClass()) { |
| return false; |
| } |
| |
| ControlWorkflowNode other= (ControlWorkflowNode) obj; |
| return id == other.id; |
| } |
| } |
| |
| private int nbWorkflow; |
| |
| private List<List<NodeMatcher<Expression>>> conditionsByWorkflow= new ArrayList<>(); |
| private List<List<NodeMatcher<Statement>>> statementsByWorkflow= new ArrayList<>(); |
| private List<NodeMatcher<Expression>> returnedValuesByWorkflow= new ArrayList<>(); |
| |
| private ControlWorkflowMatcher() { |
| // Forbidden |
| } |
| |
| /** |
| * Factory. |
| * |
| * @return an instance. |
| */ |
| public static ControlWorkflowMatcherRunnable createControlWorkflowMatcher() { |
| return new ControlWorkflowMatcher(); |
| } |
| |
| @Override |
| public ControlWorkflowMatcherCompletable addWorkflow(final NodeMatcher<Expression> condition) { |
| nbWorkflow++; |
| |
| List<NodeMatcher<Expression>> conditions= new ArrayList<>(); |
| conditions.add(condition); |
| conditionsByWorkflow.add(conditions); |
| statementsByWorkflow.add(new ArrayList<NodeMatcher<Statement>>()); |
| returnedValuesByWorkflow.add(null); |
| |
| return this; |
| } |
| |
| @Override |
| public ControlWorkflowMatcherCompletable condition(final NodeMatcher<Expression> condition) { |
| conditionsByWorkflow.get(nbWorkflow - 1).add(condition); |
| return this; |
| } |
| |
| @Override |
| public ControlWorkflowMatcherCompletable statement(final NodeMatcher<Statement> statement) { |
| statementsByWorkflow.get(nbWorkflow - 1).add(statement); |
| return this; |
| } |
| |
| @Override |
| public ControlWorkflowMatcherRunnable returnedValue(final NodeMatcher<Expression> returnedValue) { |
| returnedValuesByWorkflow.set(nbWorkflow - 1, returnedValue); |
| return this; |
| } |
| |
| @Override |
| public boolean isMatching(final Statement statement) { |
| return isMatching(Arrays.asList(statement)); |
| } |
| |
| @Override |
| public boolean isMatching(final List<Statement> actualStatements) { |
| try { |
| ControlWorkflowNode actualNode= buildActualNodes(actualStatements); |
| |
| expandActualNode(actualNode); |
| |
| Set<Integer> actualLastNodes= new HashSet<>(); |
| collectActualLastNodes(actualNode, actualLastNodes); |
| |
| if (actualLastNodes.size() != nbWorkflow) { |
| return false; |
| } |
| |
| boolean isPassive= isPassive(actualNode); |
| |
| for (int i= 0; i < nbWorkflow; i++) { |
| if (!doMatching(actualNode, i, actualLastNodes, isPassive)) { |
| return false; |
| } |
| } |
| |
| return actualLastNodes.isEmpty(); |
| } catch (AbortSearchException e) { |
| return false; |
| } |
| } |
| |
| private boolean doMatching(final ControlWorkflowNode actualNode, final int i, final Set<Integer> actualLastNodes, final boolean isPassive) { |
| ControlWorkflowNode currentActualNode= actualNode; |
| List<NodeMatcher<Expression>> remainingConditions= new ArrayList<>(conditionsByWorkflow.get(i)); |
| |
| while (!remainingConditions.isEmpty()) { |
| if (currentActualNode == null || currentActualNode.getCondition() == null) { |
| return false; |
| } |
| |
| Boolean matching= null; |
| |
| for (Iterator<NodeMatcher<Expression>> iterator= remainingConditions.iterator(); iterator.hasNext();) { |
| NodeMatcher<Expression> nodeMatcher= iterator.next(); |
| matching= nodeMatcher.isMatching(currentActualNode.getCondition()); |
| |
| if (matching != null) { |
| iterator.remove(); |
| break; |
| } |
| |
| if (!isPassive) { |
| return false; |
| } |
| } |
| |
| if (matching == null) { |
| return false; |
| } |
| |
| if (matching) { |
| currentActualNode= currentActualNode.getThenNode(); |
| } else { |
| currentActualNode= currentActualNode.getElseNode(); |
| } |
| } |
| |
| if (currentActualNode.getCondition() != null |
| || currentActualNode.getFinalStatement() != null == isEmpty(statementsByWorkflow.get(i)) |
| || currentActualNode.getReturnedValue() != null ^ returnedValuesByWorkflow.get(i) != null) { |
| return false; |
| } |
| |
| if (!isEmpty(statementsByWorkflow.get(i))) { |
| if (Boolean.TRUE.equals(statementsByWorkflow.get(i).get(0).isMatching(currentActualNode.getFinalStatement())) |
| && actualLastNodes.contains(currentActualNode.getId())) { |
| actualLastNodes.remove(currentActualNode.getId()); |
| return true; |
| } |
| } else if (returnedValuesByWorkflow.get(i) != null |
| && Boolean.TRUE.equals(returnedValuesByWorkflow.get(i).isMatching(currentActualNode.getReturnedValue())) |
| && actualLastNodes.contains(currentActualNode.getId())) { |
| actualLastNodes.remove(currentActualNode.getId()); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private void collectActualLastNodes(final ControlWorkflowNode actualNode, final Set<Integer> actualLastNodes) { |
| if (actualNode.getCondition() == null) { |
| if (actualNode.getThenNode() != null || actualNode.getElseNode() != null || (actualNode.getReturnedValue() == null && actualNode.getFinalStatement() == null)) { |
| throw new AbortSearchException(); |
| } |
| |
| actualLastNodes.add(actualNode.getId()); |
| } else { |
| if (actualNode.getThenNode() == null || actualNode.getElseNode() == null || actualNode.getReturnedValue() != null || actualNode.getFinalStatement() != null) { |
| throw new AbortSearchException(); |
| } |
| |
| collectActualLastNodes(actualNode.getThenNode(), actualLastNodes); |
| collectActualLastNodes(actualNode.getElseNode(), actualLastNodes); |
| } |
| } |
| |
| private boolean isPassive(final ControlWorkflowNode actualNode) { |
| return actualNode.getCondition() == null || (ASTNodes.isPassive(actualNode.getCondition()) && isPassive(actualNode.getThenNode()) && isPassive(actualNode.getElseNode())); |
| } |
| |
| private void expandActualNode(final ControlWorkflowNode actualNode) { |
| if (actualNode.getCondition() == null) { |
| return; |
| } |
| |
| PrefixExpression prefixExpression= ASTNodes.as(actualNode.getCondition(), PrefixExpression.class); |
| ConditionalExpression ternaryExpression= ASTNodes.as(actualNode.getCondition(), ConditionalExpression.class); |
| InfixExpression infixExpression= ASTNodes.as(actualNode.getCondition(), InfixExpression.class); |
| |
| if (prefixExpression != null) { |
| if (ASTNodes.hasOperator(prefixExpression, PrefixExpression.Operator.NOT)) { |
| ControlWorkflowNode oppositeNode= actualNode.getThenNode(); |
| actualNode.setThenNode(actualNode.getElseNode()); |
| actualNode.setElseNode(oppositeNode); |
| actualNode.setCondition(prefixExpression.getOperand()); |
| |
| expandActualNode(actualNode); |
| return; |
| } |
| } else if (ternaryExpression != null) { |
| ControlWorkflowNode node1= new ControlWorkflowNode(); |
| node1.setCondition(ternaryExpression.getThenExpression()); |
| node1.setThenNode(cloneNode(actualNode.getThenNode())); |
| node1.setElseNode(cloneNode(actualNode.getElseNode())); |
| |
| ControlWorkflowNode node2= new ControlWorkflowNode(); |
| node2.setCondition(ternaryExpression.getElseExpression()); |
| node2.setThenNode(cloneNode(actualNode.getThenNode())); |
| node2.setElseNode(cloneNode(actualNode.getElseNode())); |
| |
| actualNode.setCondition(ternaryExpression.getExpression()); |
| actualNode.setThenNode(node1); |
| actualNode.setElseNode(node2); |
| |
| expandActualNode(actualNode); |
| return; |
| } else if (infixExpression != null && ASTNodes.hasOperator(infixExpression, InfixExpression.Operator.CONDITIONAL_AND, InfixExpression.Operator.AND, InfixExpression.Operator.CONDITIONAL_OR, InfixExpression.Operator.OR)) { |
| List<Expression> allOperands= ASTNodes.allOperands(infixExpression); |
| Expression firstOperand= allOperands.remove(0); |
| ControlWorkflowNode currentNode= actualNode; |
| |
| for (Expression operand : allOperands) { |
| ControlWorkflowNode subNode= new ControlWorkflowNode(); |
| subNode.setCondition(operand); |
| subNode.setThenNode(cloneNode(currentNode.getThenNode())); |
| subNode.setElseNode(cloneNode(currentNode.getElseNode())); |
| currentNode.setCondition(firstOperand); |
| |
| if (ASTNodes.hasOperator(infixExpression, InfixExpression.Operator.CONDITIONAL_AND, InfixExpression.Operator.AND)) { |
| currentNode.setThenNode(subNode); |
| } else { |
| currentNode.setElseNode(subNode); |
| } |
| |
| currentNode= subNode; |
| } |
| |
| expandActualNode(actualNode); |
| return; |
| } else if (infixExpression != null && !infixExpression.hasExtendedOperands() && ASTNodes.hasOperator(infixExpression, InfixExpression.Operator.XOR)) { |
| ControlWorkflowNode subNode1= new ControlWorkflowNode(); |
| subNode1.setCondition(infixExpression.getRightOperand()); |
| subNode1.setThenNode(cloneNode(actualNode.getElseNode())); |
| subNode1.setElseNode(cloneNode(actualNode.getThenNode())); |
| |
| ControlWorkflowNode subNode2= new ControlWorkflowNode(); |
| subNode2.setCondition(infixExpression.getRightOperand()); |
| subNode2.setThenNode(cloneNode(actualNode.getThenNode())); |
| subNode2.setElseNode(cloneNode(actualNode.getElseNode())); |
| |
| actualNode.setCondition(infixExpression.getLeftOperand()); |
| actualNode.setThenNode(subNode1); |
| actualNode.setElseNode(subNode2); |
| |
| expandActualNode(actualNode); |
| return; |
| } |
| |
| expandActualNode(actualNode.getThenNode()); |
| expandActualNode(actualNode.getElseNode()); |
| } |
| |
| private ControlWorkflowNode cloneNode(final ControlWorkflowNode actualNode) { |
| if (actualNode == null) { |
| return null; |
| } |
| |
| ControlWorkflowNode clone= new ControlWorkflowNode(); |
| clone.setCondition(actualNode.getCondition()); |
| clone.setReturnedValue(actualNode.getReturnedValue()); |
| clone.setFinalStatement(actualNode.getFinalStatement()); |
| clone.setThenNode(cloneNode(actualNode.getThenNode())); |
| clone.setElseNode(cloneNode(actualNode.getElseNode())); |
| |
| return clone; |
| } |
| |
| private ControlWorkflowNode buildActualNodes(final Statement actualStatement) { |
| if (actualStatement == null) { |
| throw new AbortSearchException(); |
| } |
| |
| return buildActualNodes(ASTNodes.asList(actualStatement)); |
| } |
| |
| private ControlWorkflowNode buildActualNodes(final List<Statement> actualStatements) { |
| if (isEmpty(actualStatements)) { |
| throw new AbortSearchException(); |
| } |
| |
| IfStatement ifStatement= ASTNodes.as(actualStatements.get(0), IfStatement.class); |
| ReturnStatement returnStatement= ASTNodes.as(actualStatements.get(0), ReturnStatement.class); |
| |
| if (ifStatement != null) { |
| ControlWorkflowNode node= new ControlWorkflowNode(); |
| node.setCondition(ifStatement.getExpression()); |
| node.setThenNode(buildActualNodes(ifStatement.getThenStatement())); |
| |
| if (ifStatement.getElseStatement() != null && actualStatements.size() == 1) { |
| node.setElseNode(buildActualNodes(ifStatement.getElseStatement())); |
| return node; |
| } |
| |
| if (ifStatement.getElseStatement() != null || actualStatements.size() == 1 || !ASTNodes.fallsThrough(ifStatement.getThenStatement())) { |
| throw new AbortSearchException(); |
| } |
| |
| List<Statement> elseStmts= new ArrayList<>(actualStatements); |
| elseStmts.remove(0); |
| node.setElseNode(buildActualNodes(elseStmts)); |
| return node; |
| } |
| |
| if (returnStatement != null) { |
| Expression condition= returnStatement.getExpression(); |
| return buildActualNodes(condition); |
| } |
| |
| if (actualStatements.size() == 1) { |
| ControlWorkflowNode node= new ControlWorkflowNode(); |
| node.setFinalStatement(actualStatements.get(0)); |
| return node; |
| } |
| |
| throw new AbortSearchException(); |
| } |
| |
| private ControlWorkflowNode buildActualNodes(final Expression condition) { |
| ControlWorkflowNode actualNode= new ControlWorkflowNode(); |
| ConditionalExpression ternaryExpression= ASTNodes.as(condition, ConditionalExpression.class); |
| |
| if (ternaryExpression != null) { |
| actualNode.setCondition(ternaryExpression.getExpression()); |
| actualNode.setThenNode(buildActualNodes(ternaryExpression.getThenExpression())); |
| actualNode.setElseNode(buildActualNodes(ternaryExpression.getElseExpression())); |
| } else { |
| actualNode.setReturnedValue(condition); |
| } |
| |
| return actualNode; |
| } |
| |
| private static boolean isEmpty(final Collection<?> collection) { |
| return collection == null || collection.isEmpty(); |
| } |
| } |