aboutsummaryrefslogtreecommitdiffstats
path: root/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Difference.java
diff options
context:
space:
mode:
Diffstat (limited to 'javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Difference.java')
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Difference.java885
1 files changed, 885 insertions, 0 deletions
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Difference.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Difference.java
new file mode 100644
index 000000000..610f91609
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Difference.java
@@ -0,0 +1,885 @@
+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);
+ }
+}