blob: 4ef642a18e7461d1079f9f4756b220518cf463d7 [file] [log] [blame]
/*******************************************************************************
* 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();
}
}