aboutsummaryrefslogtreecommitdiffstats
path: root/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation
diff options
context:
space:
mode:
Diffstat (limited to 'javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation')
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/ChildTextElement.java104
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Difference.java885
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalDifferenceCalculator.java332
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalPreservingPrinter.java507
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/NodeText.java217
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PhantomNodeLogic.java74
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElement.java62
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementIteratorsFactory.java189
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatcher.java28
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatchers.java45
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TokenTextElement.java115
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/Change.java29
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/ListAdditionChange.java37
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/ListRemovalChange.java35
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/ListReplacementChange.java44
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/NoChange.java15
-rw-r--r--javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/PropertyChange.java40
17 files changed, 2758 insertions, 0 deletions
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/ChildTextElement.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/ChildTextElement.java
new file mode 100644
index 000000000..ad890c489
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/ChildTextElement.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2016 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer.lexicalpreservation;
+
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.comments.Comment;
+
+/**
+ * Represent the position of a child node in the NodeText of its parent.
+ */
+class ChildTextElement extends TextElement {
+ private final Node child;
+
+ ChildTextElement(Node child) {
+ this.child = child;
+ }
+
+ String expand() {
+ return LexicalPreservingPrinter.print(child);
+ }
+
+ Node getChild() {
+ return child;
+ }
+
+ @Override
+ boolean isToken(int tokenKind) {
+ return false;
+ }
+
+ @Override
+ boolean isNode(Node node) {
+ return node == child;
+ }
+
+ NodeText getNodeTextForWrappedNode() {
+ return LexicalPreservingPrinter.getOrCreateNodeText(child);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ChildTextElement that = (ChildTextElement) o;
+
+ return child.equals(that.child);
+
+ }
+
+ @Override
+ public int hashCode() {
+ return child.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "ChildTextElement{" + child + '}';
+ }
+
+ @Override
+ public boolean isWhiteSpace() {
+ return false;
+ }
+
+ @Override
+ public boolean isSpaceOrTab() {
+ return false;
+ }
+
+ @Override
+ public boolean isNewline() {
+ return false;
+ }
+
+ @Override
+ public boolean isComment() {
+ return child instanceof Comment;
+ }
+
+ @Override
+ public boolean isChildOfClass(Class<? extends Node> nodeClass) {
+ return nodeClass.isInstance(child);
+ }
+}
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);
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalDifferenceCalculator.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalDifferenceCalculator.java
new file mode 100644
index 000000000..9263dfd8a
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalDifferenceCalculator.java
@@ -0,0 +1,332 @@
+package com.github.javaparser.printer.lexicalpreservation;
+
+import com.github.javaparser.GeneratedJavaParserConstants;
+import com.github.javaparser.ast.Modifier;
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.NodeList;
+import com.github.javaparser.ast.expr.StringLiteralExpr;
+import com.github.javaparser.ast.observer.ObservableProperty;
+import com.github.javaparser.printer.ConcreteSyntaxModel;
+import com.github.javaparser.printer.Printable;
+import com.github.javaparser.printer.SourcePrinter;
+import com.github.javaparser.printer.concretesyntaxmodel.*;
+import com.github.javaparser.printer.lexicalpreservation.changes.*;
+
+import java.util.*;
+
+class LexicalDifferenceCalculator {
+
+ /**
+ * The ConcreteSyntaxModel represents the general format. This model is a calculated version of the ConcreteSyntaxModel,
+ * with no condition, no lists, just tokens and node children.
+ */
+ static class CalculatedSyntaxModel {
+ final List<CsmElement> elements;
+
+ CalculatedSyntaxModel(List<CsmElement> elements) {
+ this.elements = elements;
+ }
+
+ public CalculatedSyntaxModel from(int index) {
+ List<CsmElement> newList = new LinkedList<>();
+ newList.addAll(elements.subList(index, elements.size()));
+ return new CalculatedSyntaxModel(newList);
+ }
+
+ @Override
+ public String toString() {
+ return "CalculatedSyntaxModel{" +
+ "elements=" + elements +
+ '}';
+ }
+
+ CalculatedSyntaxModel sub(int start, int end) {
+ return new CalculatedSyntaxModel(elements.subList(start, end));
+ }
+
+ void removeIndentationElements() {
+ elements.removeIf(el -> el instanceof CsmIndent || el instanceof CsmUnindent);
+ }
+ }
+
+ static class CsmChild implements CsmElement {
+ private final Node child;
+
+ public Node getChild() {
+ return child;
+ }
+
+ CsmChild(Node child) {
+ this.child = child;
+ }
+
+ @Override
+ public void prettyPrint(Node node, SourcePrinter printer) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ return "child(" + child.getClass().getSimpleName()+")";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ CsmChild csmChild = (CsmChild) o;
+
+ return child.equals(csmChild.child);
+ }
+
+ @Override
+ public int hashCode() {
+ return child.hashCode();
+ }
+ }
+
+ Difference calculateListRemovalDifference(ObservableProperty observableProperty, NodeList nodeList, int index) {
+ Node container = nodeList.getParentNodeForChildren();
+ CsmElement element = ConcreteSyntaxModel.forClass(container.getClass());
+ CalculatedSyntaxModel original = calculatedSyntaxModelForNode(element, container);
+ CalculatedSyntaxModel after = calculatedSyntaxModelAfterListRemoval(element, observableProperty, nodeList, index);
+ return Difference.calculate(original, after);
+ }
+
+ Difference calculateListAdditionDifference(ObservableProperty observableProperty, NodeList nodeList, int index, Node nodeAdded) {
+ Node container = nodeList.getParentNodeForChildren();
+ CsmElement element = ConcreteSyntaxModel.forClass(container.getClass());
+ CalculatedSyntaxModel original = calculatedSyntaxModelForNode(element, container);
+ CalculatedSyntaxModel after = calculatedSyntaxModelAfterListAddition(element, observableProperty, nodeList, index, nodeAdded);
+ return Difference.calculate(original, after);
+ }
+
+ Difference calculateListReplacementDifference(ObservableProperty observableProperty, NodeList nodeList, int index, Node newValue) {
+ Node container = nodeList.getParentNodeForChildren();
+ CsmElement element = ConcreteSyntaxModel.forClass(container.getClass());
+ CalculatedSyntaxModel original = calculatedSyntaxModelForNode(element, container);
+ CalculatedSyntaxModel after = calculatedSyntaxModelAfterListReplacement(element, observableProperty, nodeList, index, newValue);
+ return Difference.calculate(original, after);
+ }
+
+ public void calculatePropertyChange(NodeText nodeText, Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
+ if (nodeText == null) {
+ throw new NullPointerException();
+ }
+ CsmElement element = ConcreteSyntaxModel.forClass(observedNode.getClass());
+ CalculatedSyntaxModel original = calculatedSyntaxModelForNode(element, observedNode);
+ CalculatedSyntaxModel after = calculatedSyntaxModelAfterPropertyChange(element, observedNode, property, oldValue, newValue);
+ Difference difference = Difference.calculate(original, after);
+ difference.apply(nodeText, observedNode);
+ }
+
+ // Visible for testing
+ CalculatedSyntaxModel calculatedSyntaxModelForNode(CsmElement csm, Node node) {
+ List<CsmElement> elements = new LinkedList<>();
+ calculatedSyntaxModelForNode(csm, node, elements, new NoChange());
+ return new CalculatedSyntaxModel(elements);
+ }
+
+ CalculatedSyntaxModel calculatedSyntaxModelForNode(Node node) {
+ return calculatedSyntaxModelForNode(ConcreteSyntaxModel.forClass(node.getClass()), node);
+ }
+
+ private void calculatedSyntaxModelForNode(CsmElement csm, Node node, List<CsmElement> elements, Change change) {
+ if (csm instanceof CsmSequence) {
+ CsmSequence csmSequence = (CsmSequence) csm;
+ csmSequence.getElements().forEach(e -> calculatedSyntaxModelForNode(e, node, elements, change));
+ } else if (csm instanceof CsmComment) {
+ // nothing to do
+ } else if (csm instanceof CsmSingleReference) {
+ CsmSingleReference csmSingleReference = (CsmSingleReference)csm;
+ Node child;
+ if (change instanceof PropertyChange && ((PropertyChange)change).getProperty() == csmSingleReference.getProperty()) {
+ child = (Node)((PropertyChange)change).getNewValue();
+ } else {
+ child = csmSingleReference.getProperty().getValueAsSingleReference(node);
+ }
+ if (child != null) {
+ elements.add(new CsmChild(child));
+ }
+ } else if (csm instanceof CsmNone) {
+ // nothing to do
+ } else if (csm instanceof CsmToken) {
+ elements.add(csm);
+ } else if (csm instanceof CsmOrphanCommentsEnding) {
+ // nothing to do
+ } else if (csm instanceof CsmList) {
+ CsmList csmList = (CsmList) csm;
+ if (csmList.getProperty().isAboutNodes()) {
+ Object rawValue = change.getValue(csmList.getProperty(), node);
+ NodeList nodeList;
+ if (rawValue instanceof Optional) {
+ Optional optional = (Optional)rawValue;
+ if (optional.isPresent()) {
+ if (!(optional.get() instanceof NodeList)) {
+ throw new IllegalStateException("Expected NodeList, found " + optional.get().getClass().getCanonicalName());
+ }
+ nodeList = (NodeList) optional.get();
+ } else {
+ nodeList = new NodeList();
+ }
+ } else {
+ if (!(rawValue instanceof NodeList)) {
+ throw new IllegalStateException("Expected NodeList, found " + rawValue.getClass().getCanonicalName());
+ }
+ nodeList = (NodeList) rawValue;
+ }
+ if (!nodeList.isEmpty()) {
+ calculatedSyntaxModelForNode(csmList.getPreceeding(), node, elements, change);
+ for (int i = 0; i < nodeList.size(); i++) {
+ if (i != 0) {
+ calculatedSyntaxModelForNode(csmList.getSeparatorPre(), node, elements, change);
+ }
+ elements.add(new CsmChild(nodeList.get(i)));
+ if (i != (nodeList.size() - 1)) {
+ calculatedSyntaxModelForNode(csmList.getSeparatorPost(), node, elements, change);
+ }
+
+ }
+ calculatedSyntaxModelForNode(csmList.getFollowing(), node, elements, change);
+ }
+ } else {
+ Collection collection = (Collection) change.getValue(csmList.getProperty(), node);
+ if (!collection.isEmpty()) {
+ calculatedSyntaxModelForNode(csmList.getPreceeding(), node, elements, change);
+
+ boolean first = true;
+ for (Iterator it = collection.iterator(); it.hasNext(); ) {
+ if (!first) {
+ calculatedSyntaxModelForNode(csmList.getSeparatorPre(), node, elements, change);
+ }
+ Object value = it.next();
+ if (value instanceof Modifier) {
+ Modifier modifier = (Modifier)value;
+ elements.add(new CsmToken(toToken(modifier)));
+ } else {
+ throw new UnsupportedOperationException(it.next().getClass().getSimpleName());
+ }
+ if (it.hasNext()) {
+ calculatedSyntaxModelForNode(csmList.getSeparatorPost(), node, elements, change);
+ }
+ first = false;
+ }
+ calculatedSyntaxModelForNode(csmList.getFollowing(), node, elements, change);
+ }
+ }
+ } else if (csm instanceof CsmConditional) {
+ CsmConditional csmConditional = (CsmConditional) csm;
+ boolean satisfied = change.evaluate(csmConditional, node);
+ if (satisfied) {
+ calculatedSyntaxModelForNode(csmConditional.getThenElement(), node, elements, change);
+ } else {
+ calculatedSyntaxModelForNode(csmConditional.getElseElement(), node, elements, change);
+ }
+ } else if (csm instanceof CsmIndent) {
+ elements.add(csm);
+ } else if (csm instanceof CsmUnindent) {
+ elements.add(csm);
+ } else if (csm instanceof CsmAttribute) {
+ CsmAttribute csmAttribute = (CsmAttribute) csm;
+ Object value = change.getValue(csmAttribute.getProperty(), node);
+ String text = value.toString();
+ if (value instanceof Printable) {
+ text = ((Printable) value).asString();
+ }
+ elements.add(new CsmToken(csmAttribute.getTokenType(node, value.toString()), text));
+ } else if ((csm instanceof CsmString) && (node instanceof StringLiteralExpr)) {
+ elements.add(new CsmToken(GeneratedJavaParserConstants.STRING_LITERAL,
+ "\"" + ((StringLiteralExpr) node).getValue() + "\""));
+ } else if (csm instanceof CsmMix) {
+ CsmMix csmMix = (CsmMix)csm;
+ List<CsmElement> mixElements = new LinkedList<>();
+ csmMix.getElements().forEach(e -> calculatedSyntaxModelForNode(e, node, mixElements, change));
+ elements.add(new CsmMix(mixElements));
+ } else {
+ throw new UnsupportedOperationException(csm.getClass().getSimpleName()+ " " + csm);
+ }
+ }
+
+ private int toToken(Modifier modifier) {
+ switch (modifier) {
+ case PUBLIC:
+ return GeneratedJavaParserConstants.PUBLIC;
+ case PRIVATE:
+ return GeneratedJavaParserConstants.PRIVATE;
+ case PROTECTED:
+ return GeneratedJavaParserConstants.PROTECTED;
+ case STATIC:
+ return GeneratedJavaParserConstants.STATIC;
+ case FINAL:
+ return GeneratedJavaParserConstants.FINAL;
+ case ABSTRACT:
+ return GeneratedJavaParserConstants.ABSTRACT;
+ default:
+ throw new UnsupportedOperationException(modifier.name());
+ }
+ }
+
+ ///
+ /// Methods that calculate CalculatedSyntaxModel
+ ///
+
+ // Visible for testing
+ CalculatedSyntaxModel calculatedSyntaxModelAfterPropertyChange(Node node, ObservableProperty property, Object oldValue, Object newValue) {
+ return calculatedSyntaxModelAfterPropertyChange(ConcreteSyntaxModel.forClass(node.getClass()), node, property, oldValue, newValue);
+ }
+
+ // Visible for testing
+ CalculatedSyntaxModel calculatedSyntaxModelAfterPropertyChange(CsmElement csm, Node node, ObservableProperty property, Object oldValue, Object newValue) {
+ List<CsmElement> elements = new LinkedList<>();
+ calculatedSyntaxModelForNode(csm, node, elements, new PropertyChange(property, oldValue, newValue));
+ return new CalculatedSyntaxModel(elements);
+ }
+
+ // Visible for testing
+ CalculatedSyntaxModel calculatedSyntaxModelAfterListRemoval(CsmElement csm, ObservableProperty observableProperty, NodeList nodeList, int index) {
+ List<CsmElement> elements = new LinkedList<>();
+ Node container = nodeList.getParentNodeForChildren();
+ calculatedSyntaxModelForNode(csm, container, elements, new ListRemovalChange(observableProperty, index));
+ return new CalculatedSyntaxModel(elements);
+ }
+
+ // Visible for testing
+ CalculatedSyntaxModel calculatedSyntaxModelAfterListAddition(CsmElement csm, ObservableProperty observableProperty, NodeList nodeList, int index, Node nodeAdded) {
+ List<CsmElement> elements = new LinkedList<>();
+ Node container = nodeList.getParentNodeForChildren();
+ calculatedSyntaxModelForNode(csm, container, elements, new ListAdditionChange(observableProperty, index, nodeAdded));
+ return new CalculatedSyntaxModel(elements);
+ }
+
+ // Visible for testing
+ CalculatedSyntaxModel calculatedSyntaxModelAfterListAddition(Node container, ObservableProperty observableProperty, int index, Node nodeAdded) {
+ CsmElement csm = ConcreteSyntaxModel.forClass(container.getClass());
+ Object rawValue = observableProperty.getRawValue(container);
+ if (!(rawValue instanceof NodeList)) {
+ throw new IllegalStateException("Expected NodeList, found " + rawValue.getClass().getCanonicalName());
+ }
+ NodeList nodeList = (NodeList)rawValue;
+ return calculatedSyntaxModelAfterListAddition(csm, observableProperty, nodeList, index, nodeAdded);
+ }
+
+ // Visible for testing
+ CalculatedSyntaxModel calculatedSyntaxModelAfterListRemoval(Node container, ObservableProperty observableProperty, int index) {
+ CsmElement csm = ConcreteSyntaxModel.forClass(container.getClass());
+ Object rawValue = observableProperty.getRawValue(container);
+ if (!(rawValue instanceof NodeList)) {
+ throw new IllegalStateException("Expected NodeList, found " + rawValue.getClass().getCanonicalName());
+ }
+ NodeList nodeList = (NodeList)rawValue;
+ return calculatedSyntaxModelAfterListRemoval(csm, observableProperty, nodeList, index);
+ }
+
+ // Visible for testing
+ private CalculatedSyntaxModel calculatedSyntaxModelAfterListReplacement(CsmElement csm, ObservableProperty observableProperty, NodeList nodeList, int index, Node newValue) {
+ List<CsmElement> elements = new LinkedList<>();
+ Node container = nodeList.getParentNodeForChildren();
+ calculatedSyntaxModelForNode(csm, container, elements, new ListReplacementChange(observableProperty, index, newValue));
+ return new CalculatedSyntaxModel(elements);
+ }
+
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalPreservingPrinter.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalPreservingPrinter.java
new file mode 100644
index 000000000..ef647fbf0
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalPreservingPrinter.java
@@ -0,0 +1,507 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2016 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer.lexicalpreservation;
+
+import com.github.javaparser.*;
+import com.github.javaparser.ast.DataKey;
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.NodeList;
+import com.github.javaparser.ast.body.VariableDeclarator;
+import com.github.javaparser.ast.comments.Comment;
+import com.github.javaparser.ast.comments.JavadocComment;
+import com.github.javaparser.ast.nodeTypes.NodeWithVariables;
+import com.github.javaparser.ast.observer.AstObserver;
+import com.github.javaparser.ast.observer.ObservableProperty;
+import com.github.javaparser.ast.observer.PropagatingAstObserver;
+import com.github.javaparser.ast.type.PrimitiveType;
+import com.github.javaparser.ast.visitor.TreeVisitor;
+import com.github.javaparser.printer.ConcreteSyntaxModel;
+import com.github.javaparser.printer.concretesyntaxmodel.CsmElement;
+import com.github.javaparser.printer.concretesyntaxmodel.CsmMix;
+import com.github.javaparser.printer.concretesyntaxmodel.CsmToken;
+import com.github.javaparser.utils.Pair;
+import com.github.javaparser.utils.Utils;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.github.javaparser.GeneratedJavaParserConstants.*;
+import static com.github.javaparser.TokenTypes.eolTokenKind;
+import static com.github.javaparser.utils.Utils.assertNotNull;
+import static com.github.javaparser.utils.Utils.decapitalize;
+import static java.util.Comparator.*;
+
+/**
+ * A Lexical Preserving Printer is used to capture all the lexical information while parsing, update them when
+ * operating on the AST and then used them to reproduce the source code
+ * in its original formatting including the AST changes.
+ */
+public class LexicalPreservingPrinter {
+
+ /**
+ * The nodetext for a node is stored in the node's data field. This is the key to set and retrieve it.
+ */
+ public static final DataKey<NodeText> NODE_TEXT_DATA = new DataKey<NodeText>() {
+ };
+
+ //
+ // Factory methods
+ //
+
+ /**
+ * Parse the code and setup the LexicalPreservingPrinter.
+ *
+ * @deprecated use setup(Node) and the static methods on this class.
+ */
+ public static <N extends Node> Pair<ParseResult<N>, LexicalPreservingPrinter> setup(ParseStart<N> parseStart,
+ Provider provider) {
+ ParseResult<N> parseResult = new JavaParser().parse(parseStart, provider);
+ if (!parseResult.isSuccessful()) {
+ throw new RuntimeException("Parsing failed, unable to setup the lexical preservation printer: "
+ + parseResult.getProblems());
+ }
+ LexicalPreservingPrinter lexicalPreservingPrinter = new LexicalPreservingPrinter(parseResult.getResult().get());
+ return new Pair<>(parseResult, lexicalPreservingPrinter);
+ }
+
+ /**
+ * Prepares the node so it can be used in the print methods.
+ * The correct order is:
+ * <ol>
+ * <li>Parse some code</li>
+ * <li>Call this setup method on the result</li>
+ * <li>Make changes to the AST as desired</li>
+ * <li>Use one of the print methods on this class to print out the original source code with your changes added</li>
+ * </ol>
+ *
+ * @return the node passed as a parameter for your convenience.
+ */
+ public static <N extends Node> N setup(N node) {
+ assertNotNull(node);
+
+ node.getTokenRange().ifPresent(r -> {
+ storeInitialText(node);
+
+ // Setup observer
+ AstObserver observer = createObserver();
+
+ node.registerForSubtree(observer);
+ });
+ return node;
+ }
+
+ //
+ // Constructor and setup
+ //
+
+ /**
+ * @deprecated use setup(Node) to prepare a node for lexical preservation,
+ * then use the static methods on this class to print it.
+ */
+ @Deprecated
+ public LexicalPreservingPrinter(Node node) {
+ setup(node);
+ }
+
+ private static AstObserver createObserver() {
+ return new PropagatingAstObserver() {
+ @Override
+ public void concretePropertyChange(Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
+ // Not really a change, ignoring
+ if ((oldValue != null && oldValue.equals(newValue)) || (oldValue == null && newValue == null)) {
+ return;
+ }
+ if (property == ObservableProperty.RANGE || property == ObservableProperty.COMMENTED_NODE) {
+ return;
+ }
+ if (property == ObservableProperty.COMMENT) {
+ if (!observedNode.getParentNode().isPresent()) {
+ throw new IllegalStateException();
+ }
+ NodeText nodeText = getOrCreateNodeText(observedNode.getParentNode().get());
+ if (oldValue == null) {
+ // Find the position of the comment node and put in front of it the comment and a newline
+ int index = nodeText.findChild(observedNode);
+ nodeText.addChild(index, (Comment) newValue);
+ nodeText.addToken(index + 1, eolTokenKind(), Utils.EOL);
+ } else if (newValue == null) {
+ if (oldValue instanceof JavadocComment) {
+ JavadocComment javadocComment = (JavadocComment) oldValue;
+ List<TokenTextElement> matchingTokens = nodeText.getElements().stream().filter(e -> e.isToken(JAVADOC_COMMENT)
+ && ((TokenTextElement) e).getText().equals("/**" + javadocComment.getContent() + "*/")).map(e -> (TokenTextElement) e).collect(Collectors.toList());
+ if (matchingTokens.size() != 1) {
+ throw new IllegalStateException();
+ }
+ int index = nodeText.findElement(matchingTokens.get(0));
+ nodeText.removeElement(index);
+ if (nodeText.getElements().get(index).isNewline()) {
+ nodeText.removeElement(index);
+ }
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ } else {
+ if (oldValue instanceof JavadocComment) {
+ JavadocComment oldJavadocComment = (JavadocComment) oldValue;
+ List<TokenTextElement> matchingTokens = nodeText.getElements().stream().filter(e -> e.isToken(JAVADOC_COMMENT)
+ && ((TokenTextElement) e).getText().equals("/**" + oldJavadocComment.getContent() + "*/")).map(e -> (TokenTextElement) e).collect(Collectors.toList());
+ if (matchingTokens.size() != 1) {
+ throw new IllegalStateException();
+ }
+ JavadocComment newJavadocComment = (JavadocComment) newValue;
+ nodeText.replace(matchingTokens.get(0), new TokenTextElement(JAVADOC_COMMENT, "/**" + newJavadocComment.getContent() + "*/"));
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+ NodeText nodeText = getOrCreateNodeText(observedNode);
+
+ if (nodeText == null) {
+ throw new NullPointerException(observedNode.getClass().getSimpleName());
+ }
+
+ new LexicalDifferenceCalculator().calculatePropertyChange(nodeText, observedNode, property, oldValue, newValue);
+ }
+
+ @Override
+ public void concreteListChange(NodeList changedList, ListChangeType type, int index, Node nodeAddedOrRemoved) {
+ NodeText nodeText = getOrCreateNodeText(changedList.getParentNodeForChildren());
+ if (type == ListChangeType.REMOVAL) {
+ new LexicalDifferenceCalculator().calculateListRemovalDifference(findNodeListName(changedList), changedList, index).apply(nodeText, changedList.getParentNodeForChildren());
+ } else if (type == ListChangeType.ADDITION) {
+ new LexicalDifferenceCalculator().calculateListAdditionDifference(findNodeListName(changedList), changedList, index, nodeAddedOrRemoved).apply(nodeText, changedList.getParentNodeForChildren());
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public void concreteListReplacement(NodeList changedList, int index, Node oldValue, Node newValue) {
+ NodeText nodeText = getOrCreateNodeText(changedList.getParentNodeForChildren());
+ new LexicalDifferenceCalculator().calculateListReplacementDifference(findNodeListName(changedList), changedList, index, newValue).apply(nodeText, changedList.getParentNodeForChildren());
+ }
+ };
+ }
+
+ private static void storeInitialText(Node root) {
+ Map<Node, List<JavaToken>> tokensByNode = new IdentityHashMap<>();
+
+ // We go over tokens and find to which nodes they belong. Note that we do not traverse the tokens as they were
+ // on a list but as they were organized in a tree. At each time we select only the branch corresponding to the
+ // range of interest and ignore all other branches
+ for (JavaToken token : root.getTokenRange().get()) {
+ Range tokenRange = token.getRange().orElseThrow(() -> new RuntimeException("Token without range: " + token));
+ Node owner = findNodeForToken(root, tokenRange);
+ if (owner == null) {
+ throw new RuntimeException("Token without node owning it: " + token);
+ }
+ if (!tokensByNode.containsKey(owner)) {
+ tokensByNode.put(owner, new LinkedList<>());
+ }
+ tokensByNode.get(owner).add(token);
+ }
+
+ // Now that we know the tokens we use them to create the initial NodeText for each node
+ new TreeVisitor() {
+ @Override
+ public void process(Node node) {
+ if (!PhantomNodeLogic.isPhantomNode(node)) {
+ LexicalPreservingPrinter.storeInitialTextForOneNode(node, tokensByNode.get(node));
+ }
+ }
+ }.visitBreadthFirst(root);
+ }
+
+ private static Node findNodeForToken(Node node, Range tokenRange) {
+ if (PhantomNodeLogic.isPhantomNode(node)) {
+ return null;
+ }
+ if (node.getRange().get().contains(tokenRange)) {
+ for (Node child : node.getChildNodes()) {
+ Node found = findNodeForToken(child, tokenRange);
+ if (found != null) {
+ return found;
+ }
+ }
+ return node;
+ } else {
+ return null;
+ }
+ }
+
+ private static void storeInitialTextForOneNode(Node node, List<JavaToken> nodeTokens) {
+ if (nodeTokens == null) {
+ nodeTokens = Collections.emptyList();
+ }
+ List<Pair<Range, TextElement>> elements = new LinkedList<>();
+ for (Node child : node.getChildNodes()) {
+ if (!PhantomNodeLogic.isPhantomNode(child)) {
+ if (!child.getRange().isPresent()) {
+ throw new RuntimeException("Range not present on node " + child);
+ }
+ elements.add(new Pair<>(child.getRange().get(), new ChildTextElement(child)));
+ }
+ }
+ for (JavaToken token : nodeTokens) {
+ elements.add(new Pair<>(token.getRange().get(), new TokenTextElement(token)));
+ }
+ elements.sort(comparing(e -> e.a.begin));
+ node.setData(NODE_TEXT_DATA, new NodeText(elements.stream().map(p -> p.b).collect(Collectors.toList())));
+ }
+
+ //
+ // Iterators
+ //
+
+ private static Iterator<TokenTextElement> tokensPreceeding(final Node node) {
+ if (!node.getParentNode().isPresent()) {
+ return new TextElementIteratorsFactory.EmptyIterator<>();
+ }
+ // There is the awfully painful case of the fake types involved in variable declarators and
+ // fields or variable declaration that are, of course, an exception...
+
+ NodeText parentNodeText = getOrCreateNodeText(node.getParentNode().get());
+ int index = parentNodeText.tryToFindChild(node);
+ if (index == NodeText.NOT_FOUND) {
+ if (node.getParentNode().get() instanceof VariableDeclarator) {
+ return tokensPreceeding(node.getParentNode().get());
+ } else {
+ throw new IllegalArgumentException(
+ String.format("I could not find child '%s' in parent '%s'. parentNodeText: %s",
+ node, node.getParentNode().get(), parentNodeText));
+ }
+ }
+
+ return new TextElementIteratorsFactory.CascadingIterator<>(
+ TextElementIteratorsFactory.partialReverseIterator(parentNodeText, index - 1),
+ () -> tokensPreceeding(node.getParentNode().get()));
+ }
+
+ //
+ // Printing methods
+ //
+
+ /**
+ * Print a Node into a String, preserving the lexical information.
+ */
+ public static String print(Node node) {
+ StringWriter writer = new StringWriter();
+ try {
+ print(node, writer);
+ } catch (IOException e) {
+ throw new RuntimeException("Unexpected IOException on a StringWriter", e);
+ }
+ return writer.toString();
+ }
+
+ /**
+ * Print a Node into a Writer, preserving the lexical information.
+ */
+ public static void print(Node node, Writer writer) throws IOException {
+ if (!node.containsData(NODE_TEXT_DATA)) {
+ getOrCreateNodeText(node);
+ }
+ final NodeText text = node.getData(NODE_TEXT_DATA);
+ writer.append(text.expand());
+ }
+
+ //
+ // Methods to handle transformations
+ //
+
+ private static void prettyPrintingTextNode(Node node, NodeText nodeText) {
+ if (node instanceof PrimitiveType) {
+ PrimitiveType primitiveType = (PrimitiveType) node;
+ switch (primitiveType.getType()) {
+ case BOOLEAN:
+ nodeText.addToken(BOOLEAN, node.toString());
+ break;
+ case CHAR:
+ nodeText.addToken(CHAR, node.toString());
+ break;
+ case BYTE:
+ nodeText.addToken(BYTE, node.toString());
+ break;
+ case SHORT:
+ nodeText.addToken(SHORT, node.toString());
+ break;
+ case INT:
+ nodeText.addToken(INT, node.toString());
+ break;
+ case LONG:
+ nodeText.addToken(LONG, node.toString());
+ break;
+ case FLOAT:
+ nodeText.addToken(FLOAT, node.toString());
+ break;
+ case DOUBLE:
+ nodeText.addToken(DOUBLE, node.toString());
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ return;
+ }
+ if (node instanceof JavadocComment) {
+ nodeText.addToken(JAVADOC_COMMENT, "/**" + ((JavadocComment) node).getContent() + "*/");
+ return;
+ }
+
+ interpret(node, ConcreteSyntaxModel.forClass(node.getClass()), nodeText);
+ }
+
+ private static NodeText interpret(Node node, CsmElement csm, NodeText nodeText) {
+ LexicalDifferenceCalculator.CalculatedSyntaxModel calculatedSyntaxModel = new LexicalDifferenceCalculator().calculatedSyntaxModelForNode(csm, node);
+
+ List<TokenTextElement> indentation = findIndentation(node);
+
+ boolean pendingIndentation = false;
+ for (CsmElement element : calculatedSyntaxModel.elements) {
+ if (pendingIndentation && !(element instanceof CsmToken && ((CsmToken) element).isNewLine())) {
+ indentation.forEach(nodeText::addElement);
+ }
+ pendingIndentation = false;
+ if (element instanceof LexicalDifferenceCalculator.CsmChild) {
+ nodeText.addChild(((LexicalDifferenceCalculator.CsmChild) element).getChild());
+ } else if (element instanceof CsmToken) {
+ CsmToken csmToken = (CsmToken) element;
+ nodeText.addToken(csmToken.getTokenType(), csmToken.getContent(node));
+ if (csmToken.isNewLine()) {
+ pendingIndentation = true;
+ }
+ } else if (element instanceof CsmMix) {
+ CsmMix csmMix = (CsmMix) element;
+ csmMix.getElements().forEach(e -> interpret(node, e, nodeText));
+ } else {
+ throw new UnsupportedOperationException(element.getClass().getSimpleName());
+ }
+ }
+ // Array brackets are a pain... we do not have a way to represent them explicitly in the AST
+ // so they have to be handled in a special way
+ if (node instanceof VariableDeclarator) {
+ VariableDeclarator variableDeclarator = (VariableDeclarator) node;
+ variableDeclarator.getParentNode().ifPresent(parent ->
+ ((NodeWithVariables<?>) parent).getMaximumCommonType().ifPresent(mct -> {
+ int extraArrayLevels = variableDeclarator.getType().getArrayLevel() - mct.getArrayLevel();
+ for (int i = 0; i < extraArrayLevels; i++) {
+ nodeText.addElement(new TokenTextElement(LBRACKET));
+ nodeText.addElement(new TokenTextElement(RBRACKET));
+ }
+ })
+ );
+ }
+ return nodeText;
+ }
+
+ // Visible for testing
+ static NodeText getOrCreateNodeText(Node node) {
+ if (!node.containsData(NODE_TEXT_DATA)) {
+ NodeText nodeText = new NodeText();
+ node.setData(NODE_TEXT_DATA, nodeText);
+ prettyPrintingTextNode(node, nodeText);
+ }
+ return node.getData(NODE_TEXT_DATA);
+ }
+
+ // Visible for testing
+ static List<TokenTextElement> findIndentation(Node node) {
+ List<TokenTextElement> followingNewlines = new LinkedList<>();
+ Iterator<TokenTextElement> it = tokensPreceeding(node);
+ while (it.hasNext()) {
+ TokenTextElement tte = it.next();
+ if (tte.getTokenKind() == SINGLE_LINE_COMMENT
+ || tte.isNewline()) {
+ break;
+ } else {
+ followingNewlines.add(tte);
+ }
+ }
+ Collections.reverse(followingNewlines);
+ for (int i = 0; i < followingNewlines.size(); i++) {
+ if (!followingNewlines.get(i).isSpaceOrTab()) {
+ return followingNewlines.subList(0, i);
+ }
+ }
+ return followingNewlines;
+ }
+
+ //
+ // Helper methods
+ //
+
+ private static boolean isReturningOptionalNodeList(Method m) {
+ if (!m.getReturnType().getCanonicalName().equals(Optional.class.getCanonicalName())) {
+ return false;
+ }
+ if (!(m.getGenericReturnType() instanceof ParameterizedType)) {
+ return false;
+ }
+ ParameterizedType parameterizedType = (ParameterizedType) m.getGenericReturnType();
+ java.lang.reflect.Type optionalArgument = parameterizedType.getActualTypeArguments()[0];
+ return (optionalArgument.getTypeName().startsWith(NodeList.class.getCanonicalName()));
+ }
+
+ private static ObservableProperty findNodeListName(NodeList nodeList) {
+ Node parent = nodeList.getParentNodeForChildren();
+ for (Method m : parent.getClass().getMethods()) {
+ if (m.getParameterCount() == 0 && m.getReturnType().getCanonicalName().equals(NodeList.class.getCanonicalName())) {
+ try {
+ Object raw = m.invoke(parent);
+ if (!(raw instanceof NodeList)) {
+ throw new IllegalStateException("Expected NodeList, found " + raw.getClass().getCanonicalName());
+ }
+ NodeList result = (NodeList) raw;
+ if (result == nodeList) {
+ String name = m.getName();
+ if (name.startsWith("get")) {
+ name = name.substring("get".length());
+ }
+ return ObservableProperty.fromCamelCaseName(decapitalize(name));
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ } else if (m.getParameterCount() == 0 && isReturningOptionalNodeList(m)) {
+ try {
+ Optional<NodeList<?>> raw = (Optional<NodeList<?>>) m.invoke(parent);
+ if (raw.isPresent() && raw.get() == nodeList) {
+ String name = m.getName();
+ if (name.startsWith("get")) {
+ name = name.substring("get".length());
+ }
+ return ObservableProperty.fromCamelCaseName(decapitalize(name));
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ throw new IllegalArgumentException("Cannot find list name of NodeList of size " + nodeList.size());
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/NodeText.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/NodeText.java
new file mode 100644
index 000000000..1be264e88
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/NodeText.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2016 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer.lexicalpreservation;
+
+import com.github.javaparser.ast.Node;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * This contains the lexical information for a single node.
+ * It is basically a list of tokens and children.
+ */
+class NodeText {
+ private final List<TextElement> elements;
+
+ public static final int NOT_FOUND = -1;
+
+ enum Option {
+ REMOVE_SPACE_IMMEDIATELY_AFTER,
+ EXCLUDE_START,
+ EXCLUDE_END
+ }
+
+ //
+ // Constructors
+ //
+
+ NodeText(List<TextElement> elements) {
+ this.elements = elements;
+ }
+
+ /**
+ * Initialize with an empty list of elements.
+ */
+ NodeText() {
+ this(new LinkedList<>());
+ }
+
+ //
+ // Adding elements
+ //
+
+ /**
+ * Add an element at the end.
+ */
+ void addElement(TextElement nodeTextElement) {
+ this.elements.add(nodeTextElement);
+ }
+
+ /**
+ * Add an element at the given position.
+ */
+ void addElement(int index, TextElement nodeTextElement) {
+ this.elements.add(index, nodeTextElement);
+ }
+
+ void addChild(Node child) {
+ addElement(new ChildTextElement(child));
+ }
+
+ void addChild(int index, Node child) {
+ addElement(index, new ChildTextElement(child));
+ }
+
+ void addToken(int tokenKind, String text) {
+ elements.add(new TokenTextElement(tokenKind, text));
+ }
+
+ void addToken(int index, int tokenKind, String text) {
+ elements.add(index, new TokenTextElement(tokenKind, text));
+ }
+
+ //
+ // Finding elements
+ //
+
+ int findElement(TextElementMatcher matcher) {
+ return findElement(matcher, 0);
+ }
+
+ int findElement(TextElementMatcher matcher, int from) {
+ int res = tryToFindElement(matcher, from);
+ if (res == NOT_FOUND) {
+ throw new IllegalArgumentException(
+ String.format("I could not find child '%s' from position %d. Elements: %s",
+ matcher, from, elements));
+ } else {
+ return res;
+ }
+ }
+
+ int tryToFindElement(TextElementMatcher matcher, int from) {
+ for (int i=from; i<elements.size(); i++) {
+ TextElement element = elements.get(i);
+ if (matcher.match(element)) {
+ return i;
+ }
+ }
+ return NOT_FOUND;
+ }
+
+ int findChild(Node child) {
+ return findChild(child, 0);
+ }
+
+ int findChild(Node child, int from) {
+ return findElement(TextElementMatchers.byNode(child), from);
+ }
+
+ int tryToFindChild(Node child) {
+ return tryToFindChild(child, 0);
+ }
+
+ int tryToFindChild(Node child, int from) {
+ return tryToFindElement(TextElementMatchers.byNode(child), from);
+ }
+
+ //
+ // Removing single elements
+ //
+
+ void remove(TextElementMatcher matcher) {
+ elements.removeIf(matcher::match);
+ }
+
+ public void remove(TextElementMatcher matcher, boolean potentiallyFollowingWhitespace) {
+ int i=0;
+ for (TextElement e : elements) {
+ if (matcher.match(e)) {
+ elements.remove(e);
+ if (potentiallyFollowingWhitespace) {
+ if (i < elements.size()) {
+ if (elements.get(i).isWhiteSpace()) {
+ elements.remove(i);
+ }
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ return;
+ }
+ }
+ throw new IllegalArgumentException();
+ }
+
+ //
+ // Removing sequences
+ //
+
+ void removeElement(int index) {
+ elements.remove(index);
+ }
+
+ //
+ // Replacing elements
+ //
+
+ void replace(TextElementMatcher position, TextElement newElement) {
+ int index = findElement(position, 0);
+ elements.remove(index);
+ elements.add(index, newElement);
+ }
+
+ //
+ // Other methods
+ //
+
+ /**
+ * Generate the corresponding string.
+ */
+ String expand() {
+ StringBuffer sb = new StringBuffer();
+
+ elements.forEach(e -> sb.append(e.expand()));
+ return sb.toString();
+ }
+
+ // Visible for testing
+ int numberOfElements() {
+ return elements.size();
+ }
+
+ // Visible for testing
+ TextElement getTextElement(int index) {
+ return elements.get(index);
+ }
+
+ // Visible for testing
+ List<TextElement> getElements() {
+ return elements;
+ }
+
+ @Override
+ public String toString() {
+ return "NodeText{" + elements + '}';
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PhantomNodeLogic.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PhantomNodeLogic.java
new file mode 100644
index 000000000..d17f76835
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PhantomNodeLogic.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2016 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer.lexicalpreservation;
+
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.observer.AstObserver;
+import com.github.javaparser.ast.observer.AstObserverAdapter;
+import com.github.javaparser.ast.type.UnknownType;
+
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+import static java.util.Collections.synchronizedMap;
+
+/**
+ * We want to recognize and ignore "phantom" nodes, like the fake type of variable in FieldDeclaration
+ */
+class PhantomNodeLogic {
+
+ private static final int LEVELS_TO_EXPLORE = 3;
+
+ private static final Map<Node, Boolean> isPhantomNodeCache = synchronizedMap(new IdentityHashMap<>());
+
+ private static final AstObserver cacheCleaner = new AstObserverAdapter() {
+ @Override
+ public void parentChange(Node observedNode, Node previousParent, Node newParent) {
+ isPhantomNodeCache.remove(observedNode);
+ }
+ };
+
+ static boolean isPhantomNode(Node node) {
+ if (isPhantomNodeCache.containsKey(node)) {
+ return isPhantomNodeCache.get(node);
+ } else {
+ if (node instanceof UnknownType) {
+ return true;
+ }
+ boolean res = (node.getParentNode().isPresent() &&
+ !node.getParentNode().get().getRange().get().contains(node.getRange().get())
+ || inPhantomNode(node, LEVELS_TO_EXPLORE));
+ isPhantomNodeCache.put(node, res);
+ node.register(cacheCleaner);
+ return res;
+ }
+ }
+
+ /**
+ * A node contained in a phantom node is also a phantom node. We limit how many levels up we check just for performance reasons.
+ */
+ private static boolean inPhantomNode(Node node, int levels) {
+ return node.getParentNode().isPresent() &&
+ (isPhantomNode(node.getParentNode().get())
+ || inPhantomNode(node.getParentNode().get(), levels - 1));
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElement.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElement.java
new file mode 100644
index 000000000..987b8f228
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElement.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2016 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer.lexicalpreservation;
+
+import com.github.javaparser.GeneratedJavaParserConstants;
+import com.github.javaparser.ast.Node;
+
+public abstract class TextElement implements TextElementMatcher {
+
+ abstract String expand();
+
+ abstract boolean isToken(int tokenKind);
+
+ final boolean isCommentToken() {
+ return isToken(GeneratedJavaParserConstants.JAVADOC_COMMENT)
+ || isToken(GeneratedJavaParserConstants.SINGLE_LINE_COMMENT)
+ || isToken(GeneratedJavaParserConstants.MULTI_LINE_COMMENT);
+ }
+
+ @Override
+ public boolean match(TextElement textElement) {
+ return this.equals(textElement);
+ }
+
+ abstract boolean isNode(Node node);
+
+ public abstract boolean isWhiteSpace();
+
+ public abstract boolean isSpaceOrTab();
+
+ public abstract boolean isNewline();
+
+ public abstract boolean isComment();
+
+ public final boolean isWhiteSpaceOrComment() {
+ return isWhiteSpace() || isComment();
+ }
+
+ /**
+ * Is this TextElement representing a child of the given class?
+ */
+ public abstract boolean isChildOfClass(Class<? extends Node> nodeClass);
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementIteratorsFactory.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementIteratorsFactory.java
new file mode 100644
index 000000000..b2e29b41b
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementIteratorsFactory.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2016 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer.lexicalpreservation;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+class TextElementIteratorsFactory {
+
+ static class CascadingIterator<E> implements Iterator<E> {
+ interface Provider<E> {
+ Iterator<E> provide();
+ }
+
+ private final Provider<E> nextProvider;
+ private Iterator<E> current;
+ private Iterator<E> next;
+ private boolean lastReturnedFromCurrent = false;
+ private boolean lastReturnedFromNext = false;
+
+ public CascadingIterator(Iterator<E> current, Provider<E> nextProvider) {
+ this.nextProvider = nextProvider;
+ this.current = current;
+ }
+
+
+ @Override
+ public boolean hasNext() {
+ if (current.hasNext()) {
+ return true;
+ }
+ if (next == null) {
+ next = nextProvider.provide();
+ }
+ return next.hasNext();
+ }
+
+ @Override
+ public E next() {
+ if (current.hasNext()) {
+ lastReturnedFromCurrent = true;
+ lastReturnedFromNext = false;
+ return current.next();
+ }
+ if (next == null) {
+ next = nextProvider.provide();
+ }
+ lastReturnedFromCurrent = false;
+ lastReturnedFromNext = true;
+ return next.next();
+ }
+
+ @Override
+ public void remove() {
+ if (lastReturnedFromCurrent) {
+ current.remove();
+ return;
+ }
+ if (lastReturnedFromNext) {
+ next.remove();
+ return;
+ }
+ throw new IllegalArgumentException();
+ }
+ }
+
+ static class EmptyIterator<E> implements Iterator<E> {
+ @Override
+ public boolean hasNext() {
+ return false;
+ }
+
+ @Override
+ public E next() {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ private static class SingleElementIterator<E> implements Iterator<E> {
+ private final E element;
+ private boolean returned;
+
+ SingleElementIterator(E element) {
+ this.element = element;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return !returned;
+ }
+
+ @Override
+ public E next() {
+ returned = true;
+ return element;
+ }
+
+ @Override
+ public void remove() {
+
+ }
+ }
+
+ static class ComposedIterator<E> implements Iterator<E> {
+ private final List<Iterator<E>> elements;
+ private int currIndex;
+
+ ComposedIterator(List<Iterator<E>> elements) {
+ this.elements = elements;
+ currIndex = 0;
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (currIndex >= elements.size()) {
+ return false;
+ }
+ if (elements.get(currIndex).hasNext()){
+ return true;
+ }
+ currIndex++;
+ return hasNext();
+ }
+
+ @Override
+ public E next() {
+ if (!hasNext()) {
+ throw new IllegalArgumentException();
+ }
+ return elements.get(currIndex).next();
+ }
+
+ @Override
+ public void remove() {
+ elements.get(currIndex).remove();
+ }
+ }
+
+ private static Iterator<TokenTextElement> reverseIterator(NodeText nodeText, int index) {
+ TextElement textElement = nodeText.getTextElement(index);
+ if (textElement instanceof TokenTextElement) {
+ return new SingleElementIterator<TokenTextElement>((TokenTextElement)textElement) {
+ @Override
+ public void remove() {
+ nodeText.removeElement(index);
+ }
+ };
+ } else if (textElement instanceof ChildTextElement) {
+ ChildTextElement childTextElement = (ChildTextElement)textElement;
+ NodeText textForChild = childTextElement.getNodeTextForWrappedNode();
+ return reverseIterator(textForChild);
+ } else {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ public static Iterator<TokenTextElement> reverseIterator(NodeText nodeText) {
+ return partialReverseIterator(nodeText, nodeText.numberOfElements() - 1);
+ }
+
+ public static Iterator<TokenTextElement> partialReverseIterator(NodeText nodeText, int fromIndex) {
+ List<Iterator<TokenTextElement>> elements = new LinkedList<>();
+ for (int i=fromIndex;i>=0;i--) {
+ elements.add(reverseIterator(nodeText, i));
+ }
+ return new ComposedIterator<>(elements);
+ }
+
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatcher.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatcher.java
new file mode 100644
index 000000000..e52200849
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatcher.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2016 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer.lexicalpreservation;
+
+public interface TextElementMatcher {
+
+ boolean match(TextElement textElement);
+
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatchers.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatchers.java
new file mode 100644
index 000000000..f3e95b754
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatchers.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2016 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer.lexicalpreservation;
+
+import com.github.javaparser.ast.Node;
+
+class TextElementMatchers {
+
+ static TextElementMatcher byTokenType(int tokenType) {
+ return textElement -> textElement.isToken(tokenType);
+ }
+
+ static TextElementMatcher byNode(final Node node) {
+ return new TextElementMatcher() {
+ @Override
+ public boolean match(TextElement textElement) {
+ return textElement.isNode(node);
+ }
+
+ @Override
+ public String toString() {
+ return "match node " + node;
+ }
+ };
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TokenTextElement.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TokenTextElement.java
new file mode 100644
index 000000000..e57b214ab
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TokenTextElement.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2016 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer.lexicalpreservation;
+
+import com.github.javaparser.JavaToken;
+import com.github.javaparser.TokenTypes;
+import com.github.javaparser.ast.Node;
+
+class TokenTextElement extends TextElement {
+ private final JavaToken token;
+
+ TokenTextElement(JavaToken token) {
+ this.token = token;
+ }
+
+ TokenTextElement(int tokenKind, String text) {
+ this(new JavaToken(tokenKind, text));
+ }
+
+ TokenTextElement(int tokenKind) {
+ this(new JavaToken(tokenKind));
+ }
+
+ @Override
+ String expand() {
+ return token.getText();
+ }
+
+ // Visible for testing
+ String getText() {
+ return token.getText();
+ }
+
+ public int getTokenKind() {
+ return token.getKind();
+ }
+
+ public JavaToken getToken() {
+ return token;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ TokenTextElement that = (TokenTextElement) o;
+
+ return token.equals(that.token);
+ }
+
+ @Override
+ public int hashCode() {
+ return token.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return token.toString();
+ }
+
+ @Override
+ boolean isToken(int tokenKind) {
+ return token.getKind() == tokenKind;
+ }
+
+ @Override
+ boolean isNode(Node node) {
+ return false;
+ }
+
+ @Override
+ public boolean isWhiteSpace() {
+ return token.getCategory().isWhitespace();
+ }
+
+ @Override
+ public boolean isSpaceOrTab() {
+ return token.getCategory().isWhitespaceButNotEndOfLine();
+ }
+
+ @Override
+ public boolean isComment() {
+ return token.getCategory().isComment();
+ }
+
+ @Override
+ public boolean isNewline() {
+ return token.getCategory().isEndOfLine();
+ }
+
+ @Override
+ public boolean isChildOfClass(Class<? extends Node> nodeClass) {
+ return false;
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/Change.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/Change.java
new file mode 100644
index 000000000..23fe09dce
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/Change.java
@@ -0,0 +1,29 @@
+package com.github.javaparser.printer.lexicalpreservation.changes;
+
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.observer.ObservableProperty;
+import com.github.javaparser.printer.concretesyntaxmodel.CsmConditional;
+import com.github.javaparser.utils.Utils;
+
+/**
+ * This represent a change happened to a specific Node.
+ */
+public interface Change {
+
+ default boolean evaluate(CsmConditional csmConditional, Node node) {
+ switch (csmConditional.getCondition()) {
+ case FLAG:
+ return (Boolean) getValue(csmConditional.getProperty(), node);
+ case IS_NOT_EMPTY:
+ return !Utils.valueIsNullOrEmpty(getValue(csmConditional.getProperty(), node));
+ case IS_EMPTY:
+ return Utils.valueIsNullOrEmpty(getValue(csmConditional.getProperty(), node));
+ case IS_PRESENT:
+ return !Utils.valueIsNullOrEmpty(getValue(csmConditional.getProperty(), node));
+ default:
+ throw new UnsupportedOperationException("" + csmConditional.getProperty() + " " + csmConditional.getCondition());
+ }
+ }
+
+ Object getValue(ObservableProperty property, Node node);
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/ListAdditionChange.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/ListAdditionChange.java
new file mode 100644
index 000000000..ab15cc52f
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/ListAdditionChange.java
@@ -0,0 +1,37 @@
+package com.github.javaparser.printer.lexicalpreservation.changes;
+
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.NodeList;
+import com.github.javaparser.ast.observer.ObservableProperty;
+
+/**
+ * The Addition of an element to a list.
+ */
+public class ListAdditionChange implements Change {
+ private final ObservableProperty observableProperty;
+ private final int index;
+ private final Node nodeAdded;
+
+ public ListAdditionChange(ObservableProperty observableProperty, int index, Node nodeAdded) {
+ this.observableProperty = observableProperty;
+ this.index = index;
+ this.nodeAdded = nodeAdded;
+ }
+
+ @Override
+ public Object getValue(ObservableProperty property, Node node) {
+ if (property == observableProperty) {
+ NodeList<Node> nodeList = new NodeList<>();
+ Object currentRawValue = new NoChange().getValue(property, node);
+ if (!(currentRawValue instanceof NodeList)){
+ throw new IllegalStateException("Expected NodeList, found " + currentRawValue.getClass().getCanonicalName());
+ }
+ NodeList<?> currentNodeList = (NodeList<?>)(currentRawValue);
+ nodeList.addAll(currentNodeList);
+ nodeList.add(index, nodeAdded);
+ return nodeList;
+ } else {
+ return new NoChange().getValue(property, node);
+ }
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/ListRemovalChange.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/ListRemovalChange.java
new file mode 100644
index 000000000..c99efd265
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/ListRemovalChange.java
@@ -0,0 +1,35 @@
+package com.github.javaparser.printer.lexicalpreservation.changes;
+
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.NodeList;
+import com.github.javaparser.ast.observer.ObservableProperty;
+
+/**
+ * The removal of an element in a list.
+ */
+public class ListRemovalChange implements Change {
+ private final ObservableProperty observableProperty;
+ private final int index;
+
+ public ListRemovalChange(ObservableProperty observableProperty, int index) {
+ this.observableProperty = observableProperty;
+ this.index = index;
+ }
+
+ @Override
+ public Object getValue(ObservableProperty property, Node node) {
+ if (property == observableProperty) {
+ NodeList<Node> nodeList = new NodeList<>();
+ Object currentRawValue = new NoChange().getValue(property, node);
+ if (!(currentRawValue instanceof NodeList)){
+ throw new IllegalStateException("Expected NodeList, found " + currentRawValue.getClass().getCanonicalName());
+ }
+ NodeList<?> currentNodeList = (NodeList<?>)currentRawValue;
+ nodeList.addAll(currentNodeList);
+ nodeList.remove(index);
+ return nodeList;
+ } else {
+ return new NoChange().getValue(property, node);
+ }
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/ListReplacementChange.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/ListReplacementChange.java
new file mode 100644
index 000000000..ccb0eada5
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/ListReplacementChange.java
@@ -0,0 +1,44 @@
+package com.github.javaparser.printer.lexicalpreservation.changes;
+
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.NodeList;
+import com.github.javaparser.ast.observer.ObservableProperty;
+import com.github.javaparser.utils.Pair;
+
+import java.util.Optional;
+
+/**
+ * The replacement of an element in a list.
+ */
+public class ListReplacementChange implements Change {
+ private final ObservableProperty observableProperty;
+ private final int index;
+ private final Node newValue;
+
+ public ListReplacementChange(ObservableProperty observableProperty, int index, Node newValue) {
+ this.observableProperty = observableProperty;
+ this.index = index;
+ this.newValue = newValue;
+ }
+
+ @Override
+ public Object getValue(ObservableProperty property, Node node) {
+ if (property == observableProperty) {
+ NodeList nodeList = new NodeList();
+ Object currentRawValue = new NoChange().getValue(property, node);
+ if (currentRawValue instanceof Optional) {
+ Optional optional = (Optional)currentRawValue;
+ currentRawValue = optional.orElseGet(null);
+ }
+ if (!(currentRawValue instanceof NodeList)){
+ throw new IllegalStateException("Expected NodeList, found " + currentRawValue.getClass().getCanonicalName());
+ }
+ NodeList currentNodeList = (NodeList)currentRawValue;
+ nodeList.addAll(currentNodeList);
+ nodeList.set(index, newValue);
+ return nodeList;
+ } else {
+ return new NoChange().getValue(property, node);
+ }
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/NoChange.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/NoChange.java
new file mode 100644
index 000000000..6b29e3eb5
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/NoChange.java
@@ -0,0 +1,15 @@
+package com.github.javaparser.printer.lexicalpreservation.changes;
+
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.observer.ObservableProperty;
+
+/**
+ * No change. The Node is not mutated.
+ */
+public class NoChange implements Change {
+
+ @Override
+ public Object getValue(ObservableProperty property, Node node) {
+ return property.getRawValue(node);
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/PropertyChange.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/PropertyChange.java
new file mode 100644
index 000000000..37b3a6f93
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/changes/PropertyChange.java
@@ -0,0 +1,40 @@
+package com.github.javaparser.printer.lexicalpreservation.changes;
+
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.observer.ObservableProperty;
+
+/**
+ * The change in value of a property.
+ */
+public class PropertyChange implements Change {
+ private final ObservableProperty property;
+ private final Object oldValue;
+ private final Object newValue;
+
+ public ObservableProperty getProperty() {
+ return property;
+ }
+
+ public Object getOldValue() {
+ return oldValue;
+ }
+
+ public Object getNewValue() {
+ return newValue;
+ }
+
+ public PropertyChange(ObservableProperty property, Object oldValue, Object newValue) {
+ this.property = property;
+ this.oldValue = oldValue;
+ this.newValue = newValue;
+ }
+
+ @Override
+ public Object getValue(ObservableProperty property, Node node) {
+ if (property == this.property) {
+ return newValue;
+ } else {
+ return property.getRawValue(node);
+ }
+ }
+}