diff options
Diffstat (limited to 'javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation')
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); + } + } +} |