blob: 610f91609e029c38ad52a56f675c69f7705e3dac [file] [log] [blame]
package com.github.javaparser.printer.lexicalpreservation;
import com.github.javaparser.GeneratedJavaParserConstants;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.comments.Comment;
import com.github.javaparser.ast.type.PrimitiveType;
import com.github.javaparser.TokenTypes;
import com.github.javaparser.printer.concretesyntaxmodel.*;
import com.github.javaparser.printer.lexicalpreservation.LexicalDifferenceCalculator.CsmChild;
import java.util.*;
import java.util.stream.Collectors;
import static com.github.javaparser.GeneratedJavaParserConstants.*;
/**
* A Difference should give me a sequence of elements I should find (to indicate the context) followed by a list of elements
* to remove or to add and follow by another sequence of elements.
*
* I should later be able to apply such difference to a nodeText.
*/
public class Difference {
private static final int STANDARD_INDENTATION_SIZE = 4;
private final List<DifferenceElement> elements;
private Difference(List<DifferenceElement> elements) {
this.elements = elements;
}
interface DifferenceElement {
static DifferenceElement added(CsmElement element) {
return new Added(element);
}
static DifferenceElement removed(CsmElement element) {
return new Removed(element);
}
static DifferenceElement kept(CsmElement element) {
return new Kept(element);
}
/**
* Return the CsmElement considered in this DifferenceElement.
*/
CsmElement getElement();
boolean isAdded();
}
private static class Added implements DifferenceElement {
final CsmElement element;
public Added(CsmElement element) {
this.element = element;
}
@Override
public String toString() {
return "Added{" + element + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Added added = (Added) o;
return element.equals(added.element);
}
@Override
public int hashCode() {
return element.hashCode();
}
@Override
public CsmElement getElement() {
return element;
}
@Override
public boolean isAdded() {
return true;
}
}
/**
* Elements in a CsmMix have been reshuffled. It could also mean that
* some new elements have been added or removed to the mix.
*/
private static class Reshuffled implements DifferenceElement {
final CsmMix previousOrder;
final CsmMix element;
public Reshuffled(CsmMix previousOrder, CsmMix element) {
this.previousOrder = previousOrder;
this.element = element;
}
@Override
public String toString() {
return "Reshuffled{" + element + ", previous="+ previousOrder+ '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Reshuffled that = (Reshuffled) o;
if (!previousOrder.equals(that.previousOrder)) return false;
return element.equals(that.element);
}
@Override
public int hashCode() {
int result = previousOrder.hashCode();
result = 31 * result + element.hashCode();
return result;
}
@Override
public CsmMix getElement() {
return element;
}
@Override
public boolean isAdded() {
return false;
}
}
private static class Kept implements DifferenceElement {
final CsmElement element;
public Kept(CsmElement element) {
this.element = element;
}
@Override
public String toString() {
return "Kept{" + element + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Kept kept = (Kept) o;
return element.equals(kept.element);
}
@Override
public int hashCode() {
return element.hashCode();
}
@Override
public CsmElement getElement() {
return element;
}
@Override
public boolean isAdded() {
return false;
}
}
private static class Removed implements DifferenceElement {
final CsmElement element;
public Removed(CsmElement element) {
this.element = element;
}
@Override
public String toString() {
return "Removed{" + element + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Removed removed = (Removed) o;
return element.equals(removed.element);
}
@Override
public int hashCode() {
return element.hashCode();
}
@Override
public CsmElement getElement() {
return element;
}
@Override
public boolean isAdded() {
return false;
}
}
private static boolean matching(CsmElement a, CsmElement b) {
if (a instanceof CsmChild) {
if (b instanceof CsmChild) {
CsmChild childA = (CsmChild) a;
CsmChild childB = (CsmChild) b;
return childA.getChild().equals(childB.getChild());
} else if (b instanceof CsmToken) {
return false;
} else if (b instanceof CsmIndent) {
return false;
} else if (b instanceof CsmUnindent) {
return false;
} else {
throw new UnsupportedOperationException(a.getClass().getSimpleName()+ " "+b.getClass().getSimpleName());
}
} else if (a instanceof CsmToken) {
if (b instanceof CsmToken) {
CsmToken childA = (CsmToken)a;
CsmToken childB = (CsmToken)b;
return childA.getTokenType() == childB.getTokenType();
} else if (b instanceof CsmChild) {
return false;
} else if (b instanceof CsmIndent) {
return false;
} else if (b instanceof CsmUnindent) {
return false;
} else {
throw new UnsupportedOperationException(a.getClass().getSimpleName()+ " "+b.getClass().getSimpleName());
}
} else if (a instanceof CsmIndent) {
return b instanceof CsmIndent;
} else if (a instanceof CsmUnindent) {
return b instanceof CsmUnindent;
}
throw new UnsupportedOperationException(a.getClass().getSimpleName()+ " "+b.getClass().getSimpleName());
}
private static boolean replacement(CsmElement a, CsmElement b) {
if (a instanceof CsmIndent || b instanceof CsmIndent || a instanceof CsmUnindent || b instanceof CsmUnindent) {
return false;
}
if (a instanceof CsmChild) {
if (b instanceof CsmChild) {
CsmChild childA = (CsmChild) a;
CsmChild childB = (CsmChild) b;
return childA.getChild().getClass().equals(childB.getChild().getClass());
} else if (b instanceof CsmToken) {
return false;
} else {
throw new UnsupportedOperationException(a.getClass().getSimpleName()+ " "+b.getClass().getSimpleName());
}
} else if (a instanceof CsmToken) {
if (b instanceof CsmToken) {
CsmToken childA = (CsmToken)a;
CsmToken childB = (CsmToken)b;
return childA.getTokenType() == childB.getTokenType();
} else if (b instanceof CsmChild) {
return false;
}
}
throw new UnsupportedOperationException(a.getClass().getSimpleName()+ " "+b.getClass().getSimpleName());
}
/**
* Find the positions of all the given children.
*/
private static Map<Node, Integer> findChildrenPositions(LexicalDifferenceCalculator.CalculatedSyntaxModel calculatedSyntaxModel) {
Map<Node, Integer> positions = new HashMap<>();
for (int i=0;i<calculatedSyntaxModel.elements.size();i++) {
CsmElement element = calculatedSyntaxModel.elements.get(i);
if (element instanceof CsmChild) {
positions.put(((CsmChild)element).getChild(), i);
}
}
return positions;
}
/**
* Calculate the Difference between two CalculatedSyntaxModel elements, determining which elements were kept,
* which were added and which were removed.
*/
static Difference calculate(LexicalDifferenceCalculator.CalculatedSyntaxModel original, LexicalDifferenceCalculator.CalculatedSyntaxModel after) {
// For performance reasons we use the positions of matching children
// to guide the calculation of the difference
//
// Suppose we have:
// qwerty[A]uiop
// qwer[A]uiop
//
// with [A] being a child and lowercase letters being tokens
//
// We would calculate the Difference between "qwerty" and "qwer" then we know the A is kep, and then we
// would calculate the difference between "uiop" and "uiop"
Map<Node, Integer> childrenInOriginal = findChildrenPositions(original);
Map<Node, Integer> childrenInAfter = findChildrenPositions(after);
List<Node> commonChildren = new LinkedList<>(childrenInOriginal.keySet());
commonChildren.retainAll(childrenInAfter.keySet());
commonChildren.sort(Comparator.comparingInt(childrenInOriginal::get));
List<DifferenceElement> elements = new LinkedList<>();
int originalIndex = 0;
int afterIndex = 0;
int commonChildrenIndex = 0;
while (commonChildrenIndex < commonChildren.size()) {
Node child = commonChildren.get(commonChildrenIndex++);
int posOfNextChildInOriginal = childrenInOriginal.get(child);
int posOfNextChildInAfter = childrenInAfter.get(child);
if (originalIndex < posOfNextChildInOriginal || afterIndex < posOfNextChildInAfter) {
elements.addAll(calculateImpl(original.sub(originalIndex, posOfNextChildInOriginal), after.sub(afterIndex, posOfNextChildInAfter)).elements);
}
elements.add(new Kept(new CsmChild(child)));
originalIndex = posOfNextChildInOriginal + 1;
afterIndex = posOfNextChildInAfter + 1;
}
if (originalIndex < original.elements.size() || afterIndex < after.elements.size()) {
elements.addAll(calculateImpl(original.sub(originalIndex, original.elements.size()), after.sub(afterIndex, after.elements.size())).elements);
}
return new Difference(elements);
}
private static Difference calculateImpl(LexicalDifferenceCalculator.CalculatedSyntaxModel original, LexicalDifferenceCalculator.CalculatedSyntaxModel after) {
List<DifferenceElement> elements = new LinkedList<>();
int originalIndex = 0;
int afterIndex = 0;
// We move through the two CalculatedSyntaxModel, moving both forward when we have a match
// and moving just one side forward when we have an element kept or removed
do {
if (originalIndex < original.elements.size() && afterIndex >= after.elements.size()) {
elements.add(new Removed(original.elements.get(originalIndex)));
originalIndex++;
} else if (originalIndex >= original.elements.size() && afterIndex < after.elements.size()) {
elements.add(new Added(after.elements.get(afterIndex)));
afterIndex++;
} else {
CsmElement nextOriginal = original.elements.get(originalIndex);
CsmElement nextAfter = after.elements.get(afterIndex);
if ((nextOriginal instanceof CsmMix) && (nextAfter instanceof CsmMix)) {
if (((CsmMix) nextAfter).getElements().equals(((CsmMix) nextOriginal).getElements())) {
// No reason to deal with a reshuffled, we are just going to keep everything as it is
((CsmMix) nextAfter).getElements().forEach(el -> elements.add(new Kept(el)));
} else {
elements.add(new Reshuffled((CsmMix)nextOriginal, (CsmMix)nextAfter));
}
originalIndex++;
afterIndex++;
} else if (matching(nextOriginal, nextAfter)) {
elements.add(new Kept(nextOriginal));
originalIndex++;
afterIndex++;
} else if (replacement(nextOriginal, nextAfter)) {
elements.add(new Removed(nextOriginal));
elements.add(new Added(nextAfter));
originalIndex++;
afterIndex++;
} else {
// We can try to remove the element or add it and look which one leads to the lower difference
Difference adding = calculate(original.from(originalIndex), after.from(afterIndex + 1));
Difference removing = null;
if (adding.cost() > 0) {
removing = calculate(original.from(originalIndex + 1), after.from(afterIndex));
}
if (removing == null || removing.cost() > adding.cost()) {
elements.add(new Added(nextAfter));
afterIndex++;
} else {
elements.add(new Removed(nextOriginal));
originalIndex++;
}
}
}
} while (originalIndex < original.elements.size() || afterIndex < after.elements.size());
return new Difference(elements);
}
private TextElement toTextElement(CsmElement csmElement) {
if (csmElement instanceof CsmChild) {
return new ChildTextElement(((CsmChild) csmElement).getChild());
} else if (csmElement instanceof CsmToken) {
return new TokenTextElement(((CsmToken) csmElement).getTokenType(), ((CsmToken) csmElement).getContent(null));
} else {
throw new UnsupportedOperationException(csmElement.getClass().getSimpleName());
}
}
private List<TextElement> processIndentation(List<TokenTextElement> indentation, List<TextElement> prevElements) {
List<TextElement> res = new LinkedList<>();
res.addAll(indentation);
boolean afterNl = false;
for (TextElement e : prevElements) {
if (e.isNewline() || e.isToken(SINGLE_LINE_COMMENT)) {
res.clear();
afterNl = true;
} else {
if (afterNl && e instanceof TokenTextElement && TokenTypes.isWhitespace(((TokenTextElement)e).getTokenKind())) {
res.add(e);
} else {
afterNl = false;
}
}
}
return res;
}
private List<TextElement> indentationBlock() {
List<TextElement> res = new LinkedList<>();
res.add(new TokenTextElement(SPACE));
res.add(new TokenTextElement(SPACE));
res.add(new TokenTextElement(SPACE));
res.add(new TokenTextElement(SPACE));
return res;
}
private int considerCleaningTheLine(NodeText nodeText, int nodeTextIndex) {
while (nodeTextIndex >=1 && nodeText.getElements().get(nodeTextIndex - 1).isWhiteSpace() && !nodeText.getElements().get(nodeTextIndex - 1).isNewline()) {
nodeText.removeElement(nodeTextIndex - 1);
nodeTextIndex--;
}
return nodeTextIndex;
}
private boolean isAfterLBrace(NodeText nodeText, int nodeTextIndex) {
if (nodeTextIndex > 0 && nodeText.getElements().get(nodeTextIndex - 1).isToken(LBRACE)) {
return true;
}
if (nodeTextIndex > 0 && nodeText.getElements().get(nodeTextIndex - 1).isWhiteSpace() && !nodeText.getElements().get(nodeTextIndex - 1).isNewline()) {
return isAfterLBrace(nodeText, nodeTextIndex - 1);
}
return false;
}
/**
* If we are at the beginning of a line, with just spaces or tabs before us we should force the space to be
* the same as the indentation.
*/
private int considerEnforcingIndentation(NodeText nodeText, int nodeTextIndex) {
boolean hasOnlyWsBefore = true;
for (int i=nodeTextIndex; i >= 0 && hasOnlyWsBefore && i < nodeText.getElements().size(); i--) {
if (nodeText.getElements().get(i).isNewline()) {
break;
}
if (!nodeText.getElements().get(i).isSpaceOrTab()) {
hasOnlyWsBefore = false;
}
}
if (hasOnlyWsBefore) {
for (int i=nodeTextIndex; i >= 0 && i < nodeText.getElements().size(); i--) {
if (nodeText.getElements().get(i).isNewline()) {
break;
}
nodeText.removeElement(i);
}
}
return nodeTextIndex;
}
/**
* Node that we have calculate the Difference we can apply to a concrete NodeText, modifying it according
* to the difference (adding and removing the elements provided).
*/
void apply(NodeText nodeText, Node node) {
if (nodeText == null) {
throw new NullPointerException();
}
boolean addedIndentation = false;
List<TokenTextElement> indentation = LexicalPreservingPrinter.findIndentation(node);
int diffIndex = 0;
int nodeTextIndex = 0;
do {
if (diffIndex < this.elements.size() && nodeTextIndex >= nodeText.getElements().size()) {
DifferenceElement diffEl = elements.get(diffIndex);
if (diffEl instanceof Kept) {
Kept kept = (Kept) diffEl;
if (kept.element instanceof CsmToken) {
CsmToken csmToken = (CsmToken) kept.element;
if (TokenTypes.isWhitespaceOrComment(csmToken.getTokenType())) {
diffIndex++;
} else {
throw new IllegalStateException("Cannot keep element because we reached the end of nodetext: "
+ nodeText + ". Difference: " + this);
}
} else {
throw new IllegalStateException("Cannot keep element because we reached the end of nodetext: "
+ nodeText + ". Difference: " + this);
}
} else if (diffEl instanceof Added) {
nodeText.addElement(nodeTextIndex, toTextElement(((Added) diffEl).element));
nodeTextIndex++;
diffIndex++;
} else {
throw new UnsupportedOperationException(diffEl.getClass().getSimpleName());
}
} else if (diffIndex >= this.elements.size() && nodeTextIndex < nodeText.getElements().size()) {
TextElement nodeTextEl = nodeText.getElements().get(nodeTextIndex);
if (nodeTextEl.isWhiteSpaceOrComment()) {
nodeTextIndex++;
} else {
throw new UnsupportedOperationException("NodeText: " + nodeText + ". Difference: "
+ this + " " + nodeTextEl);
}
} else {
DifferenceElement diffEl = elements.get(diffIndex);
TextElement nodeTextEl = nodeText.getElements().get(nodeTextIndex);
if (diffEl instanceof Added) {
CsmElement addedElement = ((Added) diffEl).element;
if (addedElement instanceof CsmIndent) {
for (int i=0;i<STANDARD_INDENTATION_SIZE;i++){
indentation.add(new TokenTextElement(GeneratedJavaParserConstants.SPACE));
}
addedIndentation = true;
diffIndex++;
continue;
}
if (addedElement instanceof CsmUnindent) {
for (int i=0;i<STANDARD_INDENTATION_SIZE && !indentation.isEmpty();i++){
indentation.remove(indentation.size() - 1);
}
addedIndentation = false;
diffIndex++;
continue;
}
TextElement textElement = toTextElement(addedElement);
boolean used = false;
if (nodeTextIndex > 0 && nodeText.getElements().get(nodeTextIndex - 1).isNewline()) {
for (TextElement e : processIndentation(indentation, nodeText.getElements().subList(0, nodeTextIndex - 1))) {
nodeText.addElement(nodeTextIndex++, e);
}
} else if (isAfterLBrace(nodeText, nodeTextIndex) && !isAReplacement(diffIndex)) {
if (textElement.isNewline()) {
used = true;
}
nodeText.addElement(nodeTextIndex++, new TokenTextElement(TokenTypes.eolTokenKind()));
// This remove the space in "{ }" when adding a new line
while (nodeText.getElements().get(nodeTextIndex).isSpaceOrTab()) {
nodeText.getElements().remove(nodeTextIndex);
}
for (TextElement e : processIndentation(indentation, nodeText.getElements().subList(0, nodeTextIndex - 1))) {
nodeText.addElement(nodeTextIndex++, e);
}
// Indentation is painful...
// Sometimes we want to force indentation: this is the case when indentation was expected but
// was actually not there. For example if we have "{ }" we would expect indentation but it is
// not there, so when adding new elements we force it. However if the indentation has been
// inserted by us in this transformation we do not want to insert it again
if (!addedIndentation) {
for (TextElement e : indentationBlock()) {
nodeText.addElement(nodeTextIndex++, e);
}
}
}
if (!used) {
nodeText.addElement(nodeTextIndex, textElement);
nodeTextIndex++;
}
if (textElement.isNewline()) {
boolean followedByUnindent = (diffIndex + 1) < elements.size()
&& elements.get(diffIndex + 1).isAdded()
&& elements.get(diffIndex + 1).getElement() instanceof CsmUnindent;
nodeTextIndex = adjustIndentation(indentation, nodeText, nodeTextIndex, followedByUnindent/* && !addedIndentation*/);
}
diffIndex++;
} else if (diffEl instanceof Kept) {
Kept kept = (Kept)diffEl;
if (nodeTextEl.isComment()) {
nodeTextIndex++;
} else if ((kept.element instanceof CsmChild) && nodeTextEl instanceof ChildTextElement) {
diffIndex++;
nodeTextIndex++;
} else if ((kept.element instanceof CsmChild) && nodeTextEl instanceof TokenTextElement) {
if (nodeTextEl.isWhiteSpaceOrComment()) {
nodeTextIndex++;
} else {
if (kept.element instanceof CsmChild) {
CsmChild keptChild = (CsmChild)kept.element;
if (keptChild.getChild() instanceof PrimitiveType) {
nodeTextIndex++;
diffIndex++;
} else {
throw new UnsupportedOperationException("kept " + kept.element + " vs " + nodeTextEl);
}
} else {
throw new UnsupportedOperationException("kept " + kept.element + " vs " + nodeTextEl);
}
}
} else if ((kept.element instanceof CsmToken) && nodeTextEl instanceof TokenTextElement) {
CsmToken csmToken = (CsmToken) kept.element;
TokenTextElement nodeTextToken = (TokenTextElement) nodeTextEl;
if (csmToken.getTokenType() == nodeTextToken.getTokenKind()) {
nodeTextIndex++;
diffIndex++;
} else if (TokenTypes.isWhitespaceOrComment(csmToken.getTokenType())) {
diffIndex++;
} else if (nodeTextToken.isWhiteSpaceOrComment()) {
nodeTextIndex++;
} else {
throw new UnsupportedOperationException("Csm token " + csmToken + " NodeText TOKEN " + nodeTextToken);
}
} else if ((kept.element instanceof CsmToken) && ((CsmToken) kept.element).isWhiteSpace()) {
diffIndex++;
} else if (kept.element instanceof CsmIndent) {
// Nothing to do
diffIndex++;
} else if (kept.element instanceof CsmUnindent) {
// Nothing to do, beside considering indentation
diffIndex++;
for (int i = 0; i < STANDARD_INDENTATION_SIZE && nodeTextIndex >= 1 && nodeText.getTextElement(nodeTextIndex - 1).isSpaceOrTab(); i++) {
nodeText.removeElement(--nodeTextIndex);
}
} else {
throw new UnsupportedOperationException("kept " + kept.element + " vs " + nodeTextEl);
}
} else if (diffEl instanceof Removed) {
Removed removed = (Removed)diffEl;
if ((removed.element instanceof CsmChild) && nodeTextEl instanceof ChildTextElement) {
ChildTextElement actualChild = (ChildTextElement)nodeTextEl;
if (actualChild.isComment()) {
CsmChild csmChild = (CsmChild)removed.element;
// We expected to remove a proper node but we found a comment in between.
// If the comment is associated to the node we want to remove we remove it as well, otherwise we keep it
Comment comment = (Comment)actualChild.getChild();
if (!comment.isOrphan() && comment.getCommentedNode().isPresent() && comment.getCommentedNode().get().equals(csmChild.getChild())) {
nodeText.removeElement(nodeTextIndex);
} else {
nodeTextIndex++;
}
} else {
nodeText.removeElement(nodeTextIndex);
if (nodeTextIndex < nodeText.getElements().size() && nodeText.getElements().get(nodeTextIndex).isNewline()) {
nodeTextIndex = considerCleaningTheLine(nodeText, nodeTextIndex);
} else {
if (diffIndex + 1 >= this.getElements().size() || !(this.getElements().get(diffIndex + 1) instanceof Added)) {
nodeTextIndex = considerEnforcingIndentation(nodeText, nodeTextIndex);
}
// If in front we have one space and before also we had space let's drop one space
if (nodeText.getElements().size() > nodeTextIndex && nodeTextIndex > 0) {
if (nodeText.getElements().get(nodeTextIndex).isWhiteSpace()
&& nodeText.getElements().get(nodeTextIndex - 1).isWhiteSpace()) {
// However we do not want to do that when we are about to adding or removing elements
if ((diffIndex + 1) == this.elements.size() || (elements.get(diffIndex + 1) instanceof Kept)) {
nodeText.getElements().remove(nodeTextIndex--);
}
}
}
}
diffIndex++;
}
} else if ((removed.element instanceof CsmToken) && nodeTextEl instanceof TokenTextElement
&& ((CsmToken)removed.element).getTokenType() == ((TokenTextElement)nodeTextEl).getTokenKind()) {
nodeText.removeElement(nodeTextIndex);
diffIndex++;
} else if (nodeTextEl instanceof TokenTextElement
&& nodeTextEl.isWhiteSpaceOrComment()) {
nodeTextIndex++;
} else if (removed.element instanceof CsmChild
&& ((CsmChild)removed.element).getChild() instanceof PrimitiveType) {
if (isPrimitiveType(nodeTextEl)) {
nodeText.removeElement(nodeTextIndex);
diffIndex++;
} else {
throw new UnsupportedOperationException("removed " + removed.element + " vs " + nodeTextEl);
}
} else if (removed.element instanceof CsmToken && ((CsmToken)removed.element).isWhiteSpace()) {
diffIndex++;
} else if (nodeTextEl.isWhiteSpace()) {
nodeTextIndex++;
} else {
throw new UnsupportedOperationException("removed " + removed.element + " vs " + nodeTextEl);
}
} else if (diffEl instanceof Reshuffled) {
// First, let's see how many tokens we need to attribute to the previous version of the of the CsmMix
Reshuffled reshuffled = (Reshuffled)diffEl;
CsmMix elementsFromPreviousOrder = reshuffled.previousOrder;
CsmMix elementsFromNextOrder = reshuffled.element;
// This contains indexes from elementsFromNextOrder to indexes from elementsFromPreviousOrder
Map<Integer, Integer> correspondanceBetweenNextOrderAndPreviousOrder = new HashMap<>();
for (int ni=0;ni<elementsFromNextOrder.getElements().size();ni++) {
boolean found = false;
CsmElement ne = elementsFromNextOrder.getElements().get(ni);
for (int pi=0;pi<elementsFromPreviousOrder.getElements().size() && !found;pi++) {
CsmElement pe = elementsFromPreviousOrder.getElements().get(pi);
if (!correspondanceBetweenNextOrderAndPreviousOrder.values().contains(pi)
&& matching(ne, pe)) {
found = true;
correspondanceBetweenNextOrderAndPreviousOrder.put(ni, pi);
}
}
}
// We now find out which Node Text elements corresponds to the elements in the original CSM
final int startNodeTextIndex = nodeTextIndex;
final Set<Integer> usedIndexes = new HashSet<>();
List<Integer> nodeTextIndexOfPreviousElements = elementsFromPreviousOrder.getElements().stream()
.map(it -> findIndexOfCorrespondingNodeTextElement(it, nodeText, startNodeTextIndex, usedIndexes, node))
.collect(Collectors.toList());
Map<Integer, Integer> nodeTextIndexToPreviousCSMIndex = new HashMap<>();
for (int i=0;i<nodeTextIndexOfPreviousElements.size();i++) {
int value = nodeTextIndexOfPreviousElements.get(i);
if (value != -1) {
nodeTextIndexToPreviousCSMIndex.put(value, i);
}
}
int lastNodeTextIndex = nodeTextIndexOfPreviousElements.stream().max(Integer::compareTo).orElse(-1);
// Elements to be added at the end
List<CsmElement> elementsToBeAddedAtTheEnd = new LinkedList<>();
Map<Integer, List<CsmElement>> elementsToAddBeforeGivenOriginalCSMElement = new HashMap<>();
for (int ni=0;ni<elementsFromNextOrder.getElements().size();ni++) {
// If it has a mapping, then it is kept
if (!correspondanceBetweenNextOrderAndPreviousOrder.containsKey(ni)) {
// Ok, it is something new. Where to put it? Let's see what is the first following
// element that has a mapping
int originalCsmIndex = -1;
for (int nj=ni + 1;nj<elementsFromNextOrder.getElements().size() && originalCsmIndex==-1;nj++) {
if (correspondanceBetweenNextOrderAndPreviousOrder.containsKey(nj)) {
originalCsmIndex = correspondanceBetweenNextOrderAndPreviousOrder.get(nj);
if (!elementsToAddBeforeGivenOriginalCSMElement.containsKey(originalCsmIndex)){
elementsToAddBeforeGivenOriginalCSMElement.put(originalCsmIndex, new LinkedList<>());
}
elementsToAddBeforeGivenOriginalCSMElement.get(originalCsmIndex).add(elementsFromNextOrder.getElements().get(ni));
}
}
// it does not preceed anything, so it goes at the end
if (originalCsmIndex == -1) {
elementsToBeAddedAtTheEnd.add(elementsFromNextOrder.getElements().get(ni));
}
}
}
// We go over the original node text elements, in the order they appear in the NodeText.
// Considering an original node text element (ONE)
// * we verify if it corresponds to a CSM element. If it does not we just move on, otherwise
// we find the correspond OCE (Original CSM Element)
// * we first add new elements that are marked to be added before OCE
// * if OCE is marked to be present also in the "after" CSM we add a kept element,
// otherwise we add a removed element
this.getElements().remove(diffIndex);
int diffElIterator = diffIndex;
if (lastNodeTextIndex != -1) {
for (int ntIndex = startNodeTextIndex; ntIndex<=lastNodeTextIndex; ntIndex++) {
if (nodeTextIndexToPreviousCSMIndex.containsKey(ntIndex)) {
int indexOfOriginalCSMElement = nodeTextIndexToPreviousCSMIndex.get(ntIndex);
if (elementsToAddBeforeGivenOriginalCSMElement.containsKey(indexOfOriginalCSMElement)) {
for (CsmElement elementToAdd : elementsToAddBeforeGivenOriginalCSMElement.get(indexOfOriginalCSMElement)) {
elements.add(diffElIterator++, new Added(elementToAdd));
}
}
CsmElement originalCSMElement = elementsFromPreviousOrder.getElements().get(indexOfOriginalCSMElement);
boolean toBeKept = correspondanceBetweenNextOrderAndPreviousOrder.containsValue(indexOfOriginalCSMElement);
if (toBeKept) {
elements.add(diffElIterator++, new Kept(originalCSMElement));
} else {
elements.add(diffElIterator++, new Removed(originalCSMElement));
}
}
// else we have a simple node text element, without associated csm element, just keep ignore it
}
}
// Finally we look for the remaining new elements that were not yet added and
// add all of them
for (CsmElement elementToAdd : elementsToBeAddedAtTheEnd) {
elements.add(diffElIterator++, new Added(elementToAdd));
}
} else {
throw new UnsupportedOperationException("" + diffEl + " vs " + nodeTextEl);
}
}
} while (diffIndex < this.elements.size() || nodeTextIndex < nodeText.getElements().size());
}
private int findIndexOfCorrespondingNodeTextElement(CsmElement csmElement, NodeText nodeText, int startIndex, Set<Integer> usedIndexes, Node node) {
for (int i=startIndex;i<nodeText.getElements().size();i++){
if (!usedIndexes.contains(i)) {
TextElement textElement = nodeText.getTextElement(i);
if (csmElement instanceof CsmToken) {
CsmToken csmToken = (CsmToken)csmElement;
if (textElement instanceof TokenTextElement) {
TokenTextElement tokenTextElement = (TokenTextElement)textElement;
if (tokenTextElement.getTokenKind() == csmToken.getTokenType() && tokenTextElement.getText().equals(csmToken.getContent(node))) {
usedIndexes.add(i);
return i;
}
}
} else if (csmElement instanceof CsmChild) {
CsmChild csmChild = (CsmChild)csmElement;
if (textElement instanceof ChildTextElement) {
ChildTextElement childTextElement = (ChildTextElement)textElement;
if (childTextElement.getChild() == csmChild.getChild()) {
usedIndexes.add(i);
return i;
}
}
} else {
throw new UnsupportedOperationException();
}
}
}
return -1;
}
private int adjustIndentation(List<TokenTextElement> indentation, NodeText nodeText, int nodeTextIndex, boolean followedByUnindent) {
List<TextElement> indentationAdj = processIndentation(indentation, nodeText.getElements().subList(0, nodeTextIndex - 1));
if (nodeTextIndex < nodeText.getElements().size() && nodeText.getElements().get(nodeTextIndex).isToken(RBRACE)) {
indentationAdj = indentationAdj.subList(0, indentationAdj.size() - Math.min(STANDARD_INDENTATION_SIZE, indentationAdj.size()));
} else if (followedByUnindent) {
indentationAdj = indentationAdj.subList(0, Math.max(0, indentationAdj.size() - STANDARD_INDENTATION_SIZE));
}
for (TextElement e : indentationAdj) {
if ((nodeTextIndex<nodeText.getElements().size()) && nodeText.getElements().get(nodeTextIndex).isSpaceOrTab()) {
nodeTextIndex++;
} else {
nodeText.getElements().add(nodeTextIndex++, e);
}
}
return nodeTextIndex;
}
private boolean isAReplacement(int diffIndex) {
return (diffIndex > 0) && getElements().get(diffIndex) instanceof Added && getElements().get(diffIndex - 1) instanceof Removed;
}
private boolean isPrimitiveType(TextElement textElement) {
if (textElement instanceof TokenTextElement) {
TokenTextElement tokenTextElement = (TokenTextElement)textElement;
int tokenKind = tokenTextElement.getTokenKind();
return tokenKind == BYTE
|| tokenKind == CHAR
|| tokenKind == SHORT
|| tokenKind == INT
|| tokenKind == LONG
|| tokenKind == FLOAT
|| tokenKind == DOUBLE;
} else {
return false;
}
}
private long cost() {
return elements.stream().filter(e -> !(e instanceof Kept)).count();
}
@Override
public String toString() {
return "Difference{" + elements + '}';
}
public List<DifferenceElement> getElements() {
return elements;
}
/**
* Remove from the difference all the elements related to indentation.
* This is mainly intended for test purposes.
*/
void removeIndentationElements() {
elements.removeIf(el -> el.getElement() instanceof CsmIndent || el.getElement() instanceof CsmUnindent);
}
}