/*******************************************************************************
* Copyright (c) 2000, 2008 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.wst.jsdt.internal.corext.dom.fragments;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.text.edits.TextEditGroup;
import org.eclipse.wst.jsdt.core.IJavaScriptUnit;
import org.eclipse.wst.jsdt.core.JavaScriptModelException;
import org.eclipse.wst.jsdt.core.dom.ASTNode;
import org.eclipse.wst.jsdt.core.dom.JavaScriptUnit;
import org.eclipse.wst.jsdt.core.dom.Expression;
import org.eclipse.wst.jsdt.core.dom.InfixExpression;
import org.eclipse.wst.jsdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.wst.jsdt.internal.corext.SourceRange;
import org.eclipse.wst.jsdt.internal.corext.dom.ASTNodeFactory;
import org.eclipse.wst.jsdt.internal.corext.dom.GenericVisitor;
import org.eclipse.wst.jsdt.internal.corext.dom.JdtASTMatcher;
class AssociativeInfixExpressionFragment extends ASTFragment implements IExpressionFragment {
private final List/*<Expression>*/ fOperands;
private final InfixExpression fGroupRoot;
public static IExpressionFragment createSubPartFragmentBySourceRange(InfixExpression node, SourceRange range, IJavaScriptUnit cu) throws JavaScriptModelException {
Assert.isNotNull(node);
Assert.isNotNull(range);
Assert.isTrue(!range.covers(node));
Assert.isTrue(new SourceRange(node).covers(range));
if(!isAssociativeInfix(node))
return null;
InfixExpression groupRoot= findGroupRoot(node);
Assert.isTrue(isAGroupRoot(groupRoot));
List/*<Expression>*/ groupMembers= AssociativeInfixExpressionFragment.findGroupMembersInOrderFor(groupRoot);
List/*<Expression>*/ subGroup= findSubGroupForSourceRange(groupMembers, range);
if(subGroup.isEmpty() || rangeIncludesExtraNonWhitespace(range, subGroup, cu))
return null;
return new AssociativeInfixExpressionFragment(groupRoot, subGroup);
}
public static IExpressionFragment createFragmentForFullSubtree(InfixExpression node) {
Assert.isNotNull(node);
if(!isAssociativeInfix(node))
return null;
InfixExpression groupRoot= findGroupRoot(node);
Assert.isTrue(isAGroupRoot(groupRoot));
List/*<Expression>*/ groupMembers= AssociativeInfixExpressionFragment.findGroupMembersInOrderFor(node);
return new AssociativeInfixExpressionFragment(groupRoot, groupMembers);
}
private static InfixExpression findGroupRoot(InfixExpression node) {
Assert.isTrue(isAssociativeInfix(node));
while(!isAGroupRoot(node)) {
ASTNode parent= node.getParent();
Assert.isNotNull(parent);
Assert.isTrue(isAssociativeInfix(parent));
Assert.isTrue(((InfixExpression) parent).getOperator() == node.getOperator());
node= (InfixExpression) parent;
}
return node;
}
private static List findSubGroupForSourceRange(List/*<Expression>*/ group, SourceRange range) {
Assert.isTrue(!group.isEmpty());
List subGroup= new ArrayList();
boolean entered= false, exited= false;
if(range.getOffset() == ((ASTNode)group.get(0)).getStartPosition())
entered= true;
for(int i= 0; i < group.size() - 1; i++) {
ASTNode member= (ASTNode) group.get(i);
ASTNode nextMember= (ASTNode) group.get(i + 1);
if(entered) {
subGroup.add(member);
if(rangeEndsBetween(range, member, nextMember)) {
exited= true;
break;
}
} else {
if(rangeStartsBetween(range, member, nextMember))
entered= true;
}
}
ASTNode lastGroupMember= (ASTNode)group.get(group.size() - 1);
if(range.getEndExclusive() == new SourceRange(lastGroupMember).getEndExclusive()) {
subGroup.add(lastGroupMember);
exited= true;
}
if(!exited)
return new ArrayList(0);
return subGroup;
}
private static boolean rangeStartsBetween(SourceRange range, ASTNode first, ASTNode next) {
int pos= range.getOffset();
return first.getStartPosition() + first.getLength() <= pos
&& pos <= next.getStartPosition();
}
private static boolean rangeEndsBetween(SourceRange range, ASTNode first, ASTNode next) {
int pos= range.getEndExclusive();
return first.getStartPosition() + first.getLength() <= pos
&& pos <= next.getStartPosition();
}
private static boolean rangeIncludesExtraNonWhitespace(SourceRange range, List/*<Expression>*/ operands, IJavaScriptUnit cu) throws JavaScriptModelException {
return Util.rangeIncludesNonWhitespaceOutsideRange(range, getRangeOfOperands(operands), cu.getBuffer());
}
private static SourceRange getRangeOfOperands(List/*<Expression>*/ operands) {
Expression first= (Expression) operands.get(0);
Expression last= (Expression) operands.get(operands.size() - 1);
return new SourceRange(first.getStartPosition(), last.getStartPosition() + last.getLength() - first.getStartPosition());
}
public IASTFragment[] getMatchingFragmentsWithNode(ASTNode node) {
IASTFragment fragmentForNode= ASTFragmentFactory.createFragmentForFullSubtree(node);
if (fragmentForNode instanceof AssociativeInfixExpressionFragment) {
AssociativeInfixExpressionFragment kin= (AssociativeInfixExpressionFragment)fragmentForNode;
return kin.getSubFragmentsWithMyNodeMatching(this);
} else {
return new IASTFragment[0];
}
}
/**
* Returns List of Lists of <code>ASTNode</code>s
*/
private static List getMatchingContiguousNodeSubsequences(List source, List toMatch) {
//naive implementation:
List subsequences= new ArrayList();
for(int i= 0; i < source.size();) {
if(matchesAt(i, source, toMatch)) {
subsequences.add(source.subList(i, i + toMatch.size()));
i += toMatch.size();
} else
i++;
}
return subsequences;
}
private static boolean matchesAt(int index, List subject, List toMatch) {
if(index + toMatch.size() > subject.size())
return false;
for(int i= 0; i < toMatch.size(); i++, index++) {
if(!JdtASTMatcher.doNodesMatch(
(ASTNode) subject.get(index), (ASTNode) toMatch.get(i)
)
)
return false;
}
return true;
}
private static boolean isAGroupRoot(ASTNode node) {
Assert.isNotNull(node);
return isAssociativeInfix(node)
&& !isParentInfixWithSameOperator((InfixExpression) node);
}
private static boolean isAssociativeInfix(ASTNode node) {
return node instanceof InfixExpression && isOperatorAssociative(((InfixExpression)node).getOperator());
}
private static boolean isParentInfixWithSameOperator(InfixExpression node) {
return node.getParent() instanceof InfixExpression
&& ((InfixExpression) node.getParent()).getOperator() == node.getOperator();
}
private static boolean isOperatorAssociative(InfixExpression.Operator operator) {
return operator == InfixExpression.Operator.PLUS
|| operator == InfixExpression.Operator.TIMES
|| operator == InfixExpression.Operator.XOR
|| operator == InfixExpression.Operator.OR
|| operator == InfixExpression.Operator.AND
|| operator == InfixExpression.Operator.CONDITIONAL_OR
|| operator == InfixExpression.Operator.CONDITIONAL_AND;
}
private AssociativeInfixExpressionFragment(InfixExpression groupRoot, List/*<Expression>*/ operands) {
Assert.isTrue(isAGroupRoot(groupRoot));
Assert.isTrue(operands.size() >= 2);
fGroupRoot= groupRoot;
fOperands= Collections.unmodifiableList(operands);
}
public boolean matches(IASTFragment other) {
if (! other.getClass().equals(getClass()))
return false;
AssociativeInfixExpressionFragment otherOfKind= (AssociativeInfixExpressionFragment) other;
return getOperator() == otherOfKind.getOperator()
&& doOperandsMatch(otherOfKind);
}
private boolean doOperandsMatch(AssociativeInfixExpressionFragment other) {
if (getOperands().size() != other.getOperands().size())
return false;
Iterator myOperands= getOperands().iterator();
Iterator othersOperands= other.getOperands().iterator();
while (myOperands.hasNext() && othersOperands.hasNext()) {
ASTNode myOperand= (ASTNode) myOperands.next();
ASTNode othersOperand= (ASTNode) othersOperands.next();
if (! JdtASTMatcher.doNodesMatch(myOperand, othersOperand))
return false;
}
return true;
}
public IASTFragment[] getSubFragmentsMatching(IASTFragment toMatch) {
return union(
getSubFragmentsWithMyNodeMatching(toMatch),
getSubFragmentsWithAnotherNodeMatching(toMatch)
);
}
private IASTFragment[] getSubFragmentsWithMyNodeMatching(IASTFragment toMatch) {
if(toMatch.getClass() != getClass())
return new IASTFragment[0];
AssociativeInfixExpressionFragment kinToMatch= (AssociativeInfixExpressionFragment) toMatch;
if(kinToMatch.getOperator() != getOperator())
return new IASTFragment[0];
List matchingSubsequences=
getMatchingContiguousNodeSubsequences(
getOperands(),
kinToMatch.getOperands()
);
IASTFragment[] matches= new IASTFragment[matchingSubsequences.size()];
for(int i= 0; i < matchingSubsequences.size(); i++) {
IASTFragment match= new AssociativeInfixExpressionFragment(
fGroupRoot,
(List) matchingSubsequences.get(i)
);
Assert.isTrue(match.matches(toMatch) || toMatch.matches(match));
matches[i]= match;
}
return matches;
}
private IASTFragment[] getSubFragmentsWithAnotherNodeMatching(IASTFragment toMatch) {
IASTFragment[] result= new IASTFragment[0];
for (Iterator iter= getOperands().iterator(); iter.hasNext();) {
ASTNode operand= (ASTNode) iter.next();
result= union(result, ASTMatchingFragmentFinder.findMatchingFragments(operand, (ASTFragment)toMatch));
}
return result;
}
private static IASTFragment[] union(IASTFragment[] a1, IASTFragment[] a2) {
IASTFragment[] union= new IASTFragment[a1.length + a2.length];
System.arraycopy(a1, 0, union, 0, a1.length);
System.arraycopy(a2, 0, union, a1.length, a2.length);
return union;
//TODO: this would be a REAL union...:
// ArrayList union= new ArrayList();
// for (int i= 0; i < a1.length; i++) {
// union.add(a1[i]);
// }
// for (int i= 0; i < a2.length; i++) {
// if (! union.contains(a2[i]))
// union.add(a2[i]);
// }
// return (IASTFragment[]) union.toArray(new IASTFragment[union.size()]);
}
/**
* Note that this fragment does not directly
* represent this expression node, but rather
* a part of it.
*/
public Expression getAssociatedExpression() {
return fGroupRoot;
}
/**
* Note that this fragment does not directly
* represent this node, but rather a particular sort of
* part of its subtree.
*/
public ASTNode getAssociatedNode() {
return fGroupRoot;
}
public InfixExpression getGroupRoot() {
return fGroupRoot;
}
public int getLength() {
return getEndPositionExclusive() - getStartPosition();
}
private int getEndPositionExclusive() {
List operands= getOperands();
ASTNode lastNode= (ASTNode) operands.get(operands.size() - 1);
return lastNode.getStartPosition() + lastNode.getLength();
}
public int getStartPosition() {
return ((ASTNode) getOperands().get(0)).getStartPosition();
}
public List getOperands() {
return fOperands;
}
public InfixExpression.Operator getOperator() {
return fGroupRoot.getOperator();
}
public Expression createCopyTarget(ASTRewrite rewrite) throws JavaScriptModelException {
List allOperands= findGroupMembersInOrderFor(fGroupRoot);
if (allOperands.size() == fOperands.size()) {
return (Expression) rewrite.createCopyTarget(fGroupRoot);
}
JavaScriptUnit root= (JavaScriptUnit) fGroupRoot.getRoot();
IJavaScriptUnit cu= (IJavaScriptUnit) root.getJavaElement();
String source= cu.getBuffer().getText(getStartPosition(), getLength());
return (Expression) rewrite.createStringPlaceholder(source, ASTNode.INFIX_EXPRESSION);
// //Todo: see whether we could copy bigger chunks of the original selection
// // (probably only possible from extendedOperands list or from nested InfixExpressions)
// InfixExpression result= rewrite.getAST().newInfixExpression();
// result.setOperator(getOperator());
// Expression first= (Expression) fOperands.get(0);
// Expression second= (Expression) fOperands.get(1);
// result.setLeftOperand((Expression) rewrite.createCopyTarget(first));
// result.setRightOperand((Expression) rewrite.createCopyTarget(second));
// for (int i= 2; i < fOperands.size(); i++) {
// Expression next= (Expression) fOperands.get(i);
// result.extendedOperands().add(rewrite.createCopyTarget(next));
// }
// return result;
}
public void replace(ASTRewrite rewrite, ASTNode replacement, TextEditGroup textEditGroup) {
List allOperands= findGroupMembersInOrderFor(fGroupRoot);
if (allOperands.size() == fOperands.size()) {
rewrite.replace(fGroupRoot, replacement, textEditGroup);
return;
}
// Could maybe be done with less edits.
// Problem is that the nodes to replace may not be all in the same InfixExpression.
int first= allOperands.indexOf(fOperands.get(0));
int after= first + fOperands.size();
ArrayList newOperands= new ArrayList();
for (int i= 0; i < allOperands.size(); i++) {
if (i < first || after <= i) {
newOperands.add(rewrite.createCopyTarget((Expression) allOperands.get(i)));
} else /* i == first */ {
newOperands.add(replacement);
i= after - 1;
}
}
Expression newExpression= ASTNodeFactory.newInfixExpression(rewrite.getAST(), getOperator(), newOperands);
rewrite.replace(getGroupRoot(), newExpression, textEditGroup);
}
private static ArrayList/*<Expression>*/ findGroupMembersInOrderFor(InfixExpression groupRoot) {
return new GroupMemberFinder(groupRoot).fMembersInOrder;
}
private static class GroupMemberFinder extends GenericVisitor {
private ArrayList/*<Expression>*/ fMembersInOrder= new ArrayList();
private InfixExpression fGroupRoot;
public GroupMemberFinder(InfixExpression groupRoot) {
super(true);
Assert.isTrue(isAssociativeInfix(groupRoot));
fGroupRoot= groupRoot;
fGroupRoot.accept(this);
}
protected boolean visitNode(ASTNode node) {
if (node instanceof InfixExpression && ((InfixExpression) node).getOperator() == fGroupRoot.getOperator())
return true;
fMembersInOrder.add(node);
return false;
}
}
public int hashCode() {
return fGroupRoot.hashCode();
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AssociativeInfixExpressionFragment other= (AssociativeInfixExpressionFragment) obj;
return fGroupRoot.equals(other.fGroupRoot) && fOperands.equals(other.fOperands);
}
}