|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
|
|
package com.github.javaparser.printer.lexicalpreservation; |
|
|
|
import static com.github.javaparser.GeneratedJavaParserConstants.LBRACE; |
|
import static com.github.javaparser.GeneratedJavaParserConstants.RBRACE; |
|
import static com.github.javaparser.GeneratedJavaParserConstants.SPACE; |
|
|
|
import java.util.ArrayList; |
|
import java.util.Comparator; |
|
import java.util.EnumMap; |
|
import java.util.HashMap; |
|
import java.util.LinkedList; |
|
import java.util.List; |
|
import java.util.ListIterator; |
|
import java.util.Map; |
|
import java.util.Optional; |
|
|
|
import com.github.javaparser.GeneratedJavaParserConstants; |
|
import com.github.javaparser.JavaToken; |
|
import com.github.javaparser.JavaToken.Kind; |
|
import com.github.javaparser.TokenTypes; |
|
import com.github.javaparser.ast.Node; |
|
import com.github.javaparser.ast.NodeList; |
|
import com.github.javaparser.ast.comments.Comment; |
|
import com.github.javaparser.ast.nodeTypes.NodeWithTypeArguments; |
|
import com.github.javaparser.ast.type.ArrayType; |
|
import com.github.javaparser.ast.type.Type; |
|
import com.github.javaparser.printer.concretesyntaxmodel.CsmElement; |
|
import com.github.javaparser.printer.concretesyntaxmodel.CsmIndent; |
|
import com.github.javaparser.printer.concretesyntaxmodel.CsmMix; |
|
import com.github.javaparser.printer.concretesyntaxmodel.CsmToken; |
|
import com.github.javaparser.printer.concretesyntaxmodel.CsmUnindent; |
|
import com.github.javaparser.printer.lexicalpreservation.LexicalDifferenceCalculator.CsmChild; |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public class Difference { |
|
|
|
public static final int STANDARD_INDENTATION_SIZE = 4; |
|
|
|
private final NodeText nodeText; |
|
private final Node node; |
|
|
|
private final List<DifferenceElement> diffElements; |
|
private final List<TextElement> originalElements; |
|
private int originalIndex = 0; |
|
private int diffIndex = 0; |
|
|
|
private final List<TokenTextElement> indentation; |
|
private boolean addedIndentation = false; |
|
|
|
Difference(List<DifferenceElement> diffElements, NodeText nodeText, Node node) { |
|
if (nodeText == null) { |
|
throw new NullPointerException("nodeText can not be null"); |
|
} |
|
|
|
this.nodeText = nodeText; |
|
this.node = node; |
|
this.diffElements = diffElements; |
|
this.originalElements = nodeText.getElements(); |
|
|
|
this.indentation = LexicalPreservingPrinter.findIndentation(node); |
|
} |
|
|
|
private List<TextElement> processIndentation(List<TokenTextElement> indentation, List<TextElement> prevElements) { |
|
List<TextElement> res = new LinkedList<>(indentation); |
|
boolean afterNl = false; |
|
for (TextElement e : prevElements) { |
|
if (e.isNewline()) { |
|
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 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).isSpaceOrTab()) { |
|
return isAfterLBrace(nodeText, nodeTextIndex - 1); |
|
} |
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
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; |
|
} |
|
} |
|
int res = nodeTextIndex; |
|
if (hasOnlyWsBefore) { |
|
for (int i = nodeTextIndex; i >= 0 && i < nodeText.getElements().size(); i--) { |
|
if (nodeText.getElements().get(i).isNewline()) { |
|
break; |
|
} |
|
nodeText.removeElement(i); |
|
res = i; |
|
} |
|
} |
|
if (res < 0) { |
|
throw new IllegalStateException(); |
|
} |
|
return res; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
void apply() { |
|
extractReshuffledDiffElements(diffElements); |
|
Map<Removed, RemovedGroup> removedGroups = combineRemovedElementsToRemovedGroups(); |
|
|
|
do { |
|
boolean isLeftOverDiffElement = applyLeftOverDiffElements(); |
|
boolean isLeftOverOriginalElement = applyLeftOverOriginalElements(); |
|
|
|
if (!isLeftOverDiffElement && !isLeftOverOriginalElement){ |
|
DifferenceElement diffElement = diffElements.get(diffIndex); |
|
|
|
if (diffElement instanceof Added) { |
|
applyAddedDiffElement((Added) diffElement); |
|
} else { |
|
TextElement originalElement = originalElements.get(originalIndex); |
|
boolean originalElementIsChild = originalElement instanceof ChildTextElement; |
|
boolean originalElementIsToken = originalElement instanceof TokenTextElement; |
|
|
|
if (diffElement instanceof Kept) { |
|
applyKeptDiffElement((Kept) diffElement, originalElement, originalElementIsChild, originalElementIsToken); |
|
} else if (diffElement instanceof Removed) { |
|
Removed removed = (Removed) diffElement; |
|
applyRemovedDiffElement(removedGroups.get(removed), removed, originalElement, originalElementIsChild, originalElementIsToken); |
|
} else { |
|
throw new UnsupportedOperationException("" + diffElement + " vs " + originalElement); |
|
} |
|
} |
|
} |
|
} while (diffIndex < diffElements.size() || originalIndex < originalElements.size()); |
|
} |
|
|
|
private boolean applyLeftOverOriginalElements() { |
|
boolean isLeftOverElement = false; |
|
if (diffIndex >= diffElements.size() && originalIndex < originalElements.size()) { |
|
TextElement originalElement = originalElements.get(originalIndex); |
|
|
|
if (originalElement.isWhiteSpaceOrComment()) { |
|
originalIndex++; |
|
} else { |
|
throw new UnsupportedOperationException("NodeText: " + nodeText + ". Difference: " |
|
+ this + " " + originalElement); |
|
} |
|
|
|
isLeftOverElement = true; |
|
} |
|
return isLeftOverElement; |
|
} |
|
|
|
private boolean applyLeftOverDiffElements() { |
|
boolean isLeftOverElement = false; |
|
if (diffIndex < diffElements.size() && originalIndex >= originalElements.size()) { |
|
DifferenceElement diffElement = diffElements.get(diffIndex); |
|
if (diffElement instanceof Kept) { |
|
Kept kept = (Kept) diffElement; |
|
|
|
if (kept.isWhiteSpaceOrComment() || kept.isIndent() || kept.isUnindent()) { |
|
diffIndex++; |
|
} else { |
|
throw new IllegalStateException("Cannot keep element because we reached the end of nodetext: " |
|
+ nodeText + ". Difference: " + this); |
|
} |
|
} else if (diffElement instanceof Added) { |
|
Added addedElement = (Added) diffElement; |
|
|
|
nodeText.addElement(originalIndex, addedElement.toTextElement()); |
|
originalIndex++; |
|
diffIndex++; |
|
} else { |
|
throw new UnsupportedOperationException(diffElement.getClass().getSimpleName()); |
|
} |
|
|
|
isLeftOverElement = true; |
|
} |
|
|
|
return isLeftOverElement; |
|
} |
|
|
|
private void extractReshuffledDiffElements(List<DifferenceElement> diffElements) { |
|
for (int index = 0; index < diffElements.size(); index++) { |
|
DifferenceElement diffElement = diffElements.get(index); |
|
if (diffElement instanceof Reshuffled) { |
|
Reshuffled reshuffled = (Reshuffled) diffElement; |
|
|
|
|
|
CsmMix elementsFromPreviousOrder = reshuffled.getPreviousOrder(); |
|
CsmMix elementsFromNextOrder = reshuffled.getNextOrder(); |
|
|
|
|
|
Map<Integer, Integer> correspondanceBetweenNextOrderAndPreviousOrder = getCorrespondanceBetweenNextOrderAndPreviousOrder(elementsFromPreviousOrder, elementsFromNextOrder); |
|
|
|
|
|
List<Integer> nodeTextIndexOfPreviousElements = findIndexOfCorrespondingNodeTextElement(elementsFromPreviousOrder.getElements(), nodeText, originalIndex, node); |
|
|
|
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); |
|
|
|
|
|
List<CsmElement> elementsToBeAddedAtTheEnd = new LinkedList<>(); |
|
List<CsmElement> nextOrderElements = elementsFromNextOrder.getElements(); |
|
|
|
Map<Integer, List<CsmElement>> elementsToAddBeforeGivenOriginalCSMElement = new HashMap<>(); |
|
for (int ni = 0; ni < nextOrderElements.size(); ni++) { |
|
|
|
if (!correspondanceBetweenNextOrderAndPreviousOrder.containsKey(ni)) { |
|
// Ok, it is something new. Where to put it? Let's see what is the first following |
|
|
|
int originalCsmIndex = -1; |
|
for (int nj = ni + 1; nj < nextOrderElements.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(nextOrderElements.get(ni)); |
|
} |
|
} |
|
|
|
if (originalCsmIndex == -1) { |
|
elementsToBeAddedAtTheEnd.add(nextOrderElements.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 |
|
|
|
|
|
diffElements.remove(index); |
|
|
|
int diffElIterator = index; |
|
if (lastNodeTextIndex != -1) { |
|
for (int ntIndex = originalIndex; ntIndex <= lastNodeTextIndex; ntIndex++) { |
|
|
|
if (nodeTextIndexToPreviousCSMIndex.containsKey(ntIndex)) { |
|
int indexOfOriginalCSMElement = nodeTextIndexToPreviousCSMIndex.get(ntIndex); |
|
if (elementsToAddBeforeGivenOriginalCSMElement.containsKey(indexOfOriginalCSMElement)) { |
|
for (CsmElement elementToAdd : elementsToAddBeforeGivenOriginalCSMElement.get(indexOfOriginalCSMElement)) { |
|
diffElements.add(diffElIterator++, new Added(elementToAdd)); |
|
} |
|
} |
|
|
|
CsmElement originalCSMElement = elementsFromPreviousOrder.getElements().get(indexOfOriginalCSMElement); |
|
boolean toBeKept = correspondanceBetweenNextOrderAndPreviousOrder.containsValue(indexOfOriginalCSMElement); |
|
if (toBeKept) { |
|
diffElements.add(diffElIterator++, new Kept(originalCSMElement)); |
|
} else { |
|
diffElements.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 |
|
|
|
for (CsmElement elementToAdd : elementsToBeAddedAtTheEnd) { |
|
diffElements.add(diffElIterator++, new Added(elementToAdd)); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private Map<Removed, RemovedGroup> combineRemovedElementsToRemovedGroups() { |
|
Map<Integer, List<Removed>> removedElementsMap = groupConsecutiveRemovedElements(); |
|
|
|
List<RemovedGroup> removedGroups = new ArrayList<>(); |
|
for (Map.Entry<Integer, List<Removed>> entry : removedElementsMap.entrySet()) { |
|
removedGroups.add(RemovedGroup.of(entry.getKey(), entry.getValue())); |
|
} |
|
|
|
Map<Removed, RemovedGroup> map = new HashMap<>(); |
|
for (RemovedGroup removedGroup : removedGroups){ |
|
for (Removed index : removedGroup) { |
|
map.put(index, removedGroup); |
|
} |
|
} |
|
|
|
return map; |
|
} |
|
|
|
private Map<Integer, List<Removed>> groupConsecutiveRemovedElements() { |
|
Map<Integer, List<Removed>> removedElementsMap = new HashMap<>(); |
|
|
|
Integer firstElement = null; |
|
for (int i = 0; i < diffElements.size(); i++) { |
|
DifferenceElement diffElement = diffElements.get(i); |
|
if (diffElement instanceof Removed) { |
|
if (firstElement == null) { |
|
firstElement = i; |
|
} |
|
|
|
removedElementsMap.computeIfAbsent(firstElement, key -> new ArrayList<>()) |
|
.add((Removed) diffElement); |
|
} else { |
|
firstElement = null; |
|
} |
|
} |
|
return removedElementsMap; |
|
} |
|
|
|
private void applyRemovedDiffElement(RemovedGroup removedGroup, Removed removed, TextElement originalElement, boolean originalElementIsChild, boolean originalElementIsToken) { |
|
if (removed.isChild() && originalElementIsChild) { |
|
ChildTextElement originalElementChild = (ChildTextElement) originalElement; |
|
if (originalElementChild.isComment()) { |
|
// We expected to remove a proper node but we found a comment in between. |
|
|
|
Comment comment = (Comment) originalElementChild.getChild(); |
|
if (!comment.isOrphan() && comment.getCommentedNode().isPresent() && comment.getCommentedNode().get().equals(removed.getChild())) { |
|
nodeText.removeElement(originalIndex); |
|
} else { |
|
originalIndex++; |
|
} |
|
} else { |
|
nodeText.removeElement(originalIndex); |
|
|
|
if ((diffIndex + 1 >= diffElements.size() || !(diffElements.get(diffIndex + 1) instanceof Added)) |
|
&& !removedGroup.isACompleteLine()) { |
|
originalIndex = considerEnforcingIndentation(nodeText, originalIndex); |
|
} |
|
|
|
if (originalElements.size() > originalIndex && originalIndex > 0) { |
|
if (originalElements.get(originalIndex).isWhiteSpace() |
|
&& originalElements.get(originalIndex - 1).isWhiteSpace()) { |
|
|
|
if ((diffIndex + 1) == diffElements.size() || (diffElements.get(diffIndex + 1) instanceof Kept)) { |
|
originalElements.remove(originalIndex--); |
|
} |
|
} |
|
} |
|
|
|
diffIndex++; |
|
} |
|
} else if (removed.isToken() && originalElementIsToken && |
|
(removed.getTokenType() == ((TokenTextElement) originalElement).getTokenKind() |
|
// handle EOLs separately as their token kind might not be equal. This is because the 'removed' |
|
|
|
|| (((TokenTextElement) originalElement).getToken().getCategory().isEndOfLine() |
|
&& removed.isNewLine()))) { |
|
nodeText.removeElement(originalIndex); |
|
diffIndex++; |
|
} else if (originalElementIsToken && originalElement.isWhiteSpaceOrComment()) { |
|
originalIndex++; |
|
} else if (originalElement.isLiteral()) { |
|
nodeText.removeElement(originalIndex); |
|
diffIndex++; |
|
} else if (removed.isPrimitiveType()) { |
|
if (originalElement.isPrimitive()) { |
|
nodeText.removeElement(originalIndex); |
|
diffIndex++; |
|
} else { |
|
throw new UnsupportedOperationException("removed " + removed.getElement() + " vs " + originalElement); |
|
} |
|
} else if (removed.isWhiteSpace() || removed.getElement() instanceof CsmIndent || removed.getElement() instanceof CsmUnindent) { |
|
diffIndex++; |
|
} else if (originalElement.isWhiteSpace()) { |
|
originalIndex++; |
|
} else { |
|
throw new UnsupportedOperationException("removed " + removed.getElement() + " vs " + originalElement); |
|
} |
|
|
|
cleanTheLineOfLeftOverSpace(removedGroup, removed); |
|
} |
|
|
|
|
|
|
|
*/ |
|
private void cleanTheLineOfLeftOverSpace(RemovedGroup removedGroup, Removed removed) { |
|
if (originalIndex >= originalElements.size()) { |
|
|
|
return; |
|
} |
|
|
|
if (!removedGroup.isProcessed() |
|
&& removedGroup.getLastElement() == removed |
|
&& removedGroup.isACompleteLine()) { |
|
Integer lastElementIndex = removedGroup.getLastElementIndex(); |
|
Optional<Integer> indentation = removedGroup.getIndentation(); |
|
|
|
if (indentation.isPresent() && !isReplaced(lastElementIndex)) { |
|
for (int i = 0; i < indentation.get(); i++) { |
|
if (originalElements.get(originalIndex).isSpaceOrTab()) { |
|
|
|
nodeText.removeElement(originalIndex); |
|
} else if (originalIndex >= 1 && originalElements.get(originalIndex - 1).isSpaceOrTab()) { |
|
|
|
nodeText.removeElement(originalIndex - 1); |
|
originalIndex--; |
|
} |
|
} |
|
} |
|
|
|
|
|
removedGroup.processed(); |
|
} |
|
} |
|
|
|
// note: |
|
// increment originalIndex if we want to keep the original element |
|
|
|
private void applyKeptDiffElement(Kept kept, TextElement originalElement, boolean originalElementIsChild, boolean originalElementIsToken) { |
|
if (originalElement.isComment()) { |
|
originalIndex++; |
|
} else if (kept.isChild() && ((CsmChild)kept.getElement()).getChild() instanceof Comment ) { |
|
diffIndex++; |
|
} else if (kept.isChild() && originalElementIsChild) { |
|
diffIndex++; |
|
originalIndex++; |
|
} else if (kept.isChild() && originalElementIsToken) { |
|
if (originalElement.isWhiteSpaceOrComment()) { |
|
originalIndex++; |
|
} else if (originalElement.isIdentifier() && isNodeWithTypeArguments(kept)) { |
|
diffIndex++; |
|
// skip all token related to node with type argument declaration |
|
// for example: |
|
// List i : in this case originalElement is "List" and the next token is space. There is nothing to skip. in the originalElements list. |
|
// List<String> i : in this case originalElement is "List" and the next token is |
|
// "<" so we have to skip all the tokens which are used in the typed argument declaration [<][String][>](3 tokens) in the originalElements list. |
|
// List<List<String>> i : in this case originalElement is "List" and the next |
|
|
|
int step = getIndexToNextTokenElement((TokenTextElement) originalElement, 0); |
|
originalIndex += step; |
|
originalIndex++; |
|
} else if ((originalElement.isIdentifier() || originalElement.isKeyword()) && isArrayType(kept)) { |
|
int tokenToSkip = getIndexToNextTokenElementInArrayType((TokenTextElement)originalElement, getArrayLevel(kept)); |
|
diffIndex++; |
|
originalIndex += tokenToSkip; |
|
originalIndex++; |
|
} else if (originalElement.isIdentifier()) { |
|
originalIndex++; |
|
diffIndex++; |
|
} else { |
|
if (kept.isPrimitiveType()) { |
|
originalIndex++; |
|
diffIndex++; |
|
} else { |
|
throw new UnsupportedOperationException("kept " + kept.getElement() + " vs " + originalElement); |
|
} |
|
} |
|
} else if (kept.isToken() && originalElementIsToken) { |
|
TokenTextElement originalTextToken = (TokenTextElement) originalElement; |
|
|
|
if (kept.getTokenType() == originalTextToken.getTokenKind()) { |
|
originalIndex++; |
|
diffIndex++; |
|
} else if (kept.isNewLine() && originalTextToken.isNewline()) { |
|
originalIndex++; |
|
diffIndex++; |
|
} else if (kept.isNewLine() && originalTextToken.isSpaceOrTab()) { |
|
originalIndex++; |
|
diffIndex++; |
|
} else if (kept.isWhiteSpaceOrComment()) { |
|
diffIndex++; |
|
} else if (originalTextToken.isWhiteSpaceOrComment()) { |
|
originalIndex++; |
|
} else if (!kept.isNewLine() && originalTextToken.isSeparator()) { |
|
// case where originalTextToken is a separator like ";" and |
|
// kept is not a new line or whitespace for example "}" |
|
|
|
originalIndex++; |
|
} else { |
|
throw new UnsupportedOperationException("Csm token " + kept.getElement() + " NodeText TOKEN " + originalTextToken); |
|
} |
|
} else if (kept.isToken() && originalElementIsChild) { |
|
diffIndex++; |
|
} else if (kept.isWhiteSpace()) { |
|
diffIndex++; |
|
} else if (kept.isIndent()) { |
|
diffIndex++; |
|
} else if (kept.isUnindent()) { |
|
// Nothing to do, beside considering indentation |
|
// However we want to consider the case in which the indentation was not applied, like when we have |
|
// just a left brace followed by space |
|
|
|
diffIndex++; |
|
if (!openBraceWasOnSameLine()) { |
|
for (int i = 0; i < STANDARD_INDENTATION_SIZE && originalIndex >= 1 && nodeText.getTextElement(originalIndex - 1).isSpaceOrTab(); i++) { |
|
nodeText.removeElement(--originalIndex); |
|
} |
|
} |
|
} else { |
|
throw new UnsupportedOperationException("kept " + kept.getElement() + " vs " + originalElement); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private int getArrayLevel(DifferenceElement element) { |
|
CsmElement csmElem = element.getElement(); |
|
if (csmElem instanceof LexicalDifferenceCalculator.CsmChild && |
|
((LexicalDifferenceCalculator.CsmChild) csmElem).getChild() instanceof ArrayType) { |
|
Node child = ((LexicalDifferenceCalculator.CsmChild) csmElem).getChild(); |
|
return ((ArrayType)child).getArrayLevel(); |
|
} |
|
return 0; |
|
} |
|
|
|
|
|
|
|
*/ |
|
private boolean isArrayType(DifferenceElement element) { |
|
CsmElement csmElem = element.getElement(); |
|
return csmElem instanceof LexicalDifferenceCalculator.CsmChild && |
|
((LexicalDifferenceCalculator.CsmChild) csmElem).getChild() instanceof ArrayType; |
|
} |
|
|
|
|
|
|
|
*/ |
|
private boolean isNodeWithTypeArguments(DifferenceElement element) { |
|
CsmElement csmElem = element.getElement(); |
|
if (!CsmChild.class.isAssignableFrom(csmElem.getClass())) |
|
return false; |
|
CsmChild child = (CsmChild) csmElem; |
|
if (!NodeWithTypeArguments.class.isAssignableFrom(child.getChild().getClass())) |
|
return false; |
|
Optional<NodeList<Type>> typeArgs = ((NodeWithTypeArguments) child.getChild()).getTypeArguments(); |
|
return typeArgs.isPresent() && typeArgs.get().size() > 0; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private int getIndexToNextTokenElement(TokenTextElement element, int nestedDiamondOperator) { |
|
int step = 0; |
|
Optional<JavaToken> next = element.getToken().getNextToken(); |
|
if (!next.isPresent()) return step; |
|
|
|
step++; |
|
|
|
JavaToken nextToken = next.get(); |
|
Kind kind = Kind.valueOf(nextToken.getKind()); |
|
if (isDiamondOperator(kind)) { |
|
if (kind.GT.equals(kind)) |
|
nestedDiamondOperator--; |
|
else |
|
nestedDiamondOperator++; |
|
} |
|
// manage the fact where the first token is not a diamond operator but a whitespace |
|
// and the end of the token sequence to skip |
|
|
|
if (nestedDiamondOperator == 0 && !nextToken.getCategory().isWhitespace()) |
|
return step; |
|
|
|
return step += getIndexToNextTokenElement(new TokenTextElement(nextToken), nestedDiamondOperator); |
|
} |
|
|
|
|
|
|
|
*/ |
|
private int getIndexToNextTokenElementInArrayType(TokenTextElement element, int arrayLevel) { |
|
int step = 0; |
|
Optional<JavaToken> next = element.getToken().getNextToken(); |
|
if (!next.isPresent()) return step; |
|
|
|
step++; |
|
|
|
JavaToken nextToken = next.get(); |
|
Kind kind = Kind.valueOf(nextToken.getKind()); |
|
if (isBracket(kind)) { |
|
if (kind.RBRACKET.equals(kind)) |
|
arrayLevel--; |
|
} |
|
// manage the fact where the first token is not a diamond operator but a whitespace |
|
// and the end of the token sequence to skip |
|
|
|
if (arrayLevel == 0 && !nextToken.getCategory().isWhitespace()) |
|
return step; |
|
|
|
return step += getIndexToNextTokenElementInArrayType(new TokenTextElement(nextToken), arrayLevel); |
|
} |
|
|
|
|
|
|
|
*/ |
|
private boolean isDiamondOperator(Kind kind) { |
|
return kind.GT.equals(kind) || kind.LT.equals(kind); |
|
} |
|
|
|
|
|
|
|
*/ |
|
private boolean isBracket(Kind kind) { |
|
return kind.LBRACKET.equals(kind) || kind.RBRACKET.equals(kind); |
|
} |
|
|
|
private boolean openBraceWasOnSameLine() { |
|
int index = originalIndex; |
|
while (index >= 0 && !nodeText.getTextElement(index).isNewline()) { |
|
if (nodeText.getTextElement(index).isToken(LBRACE)) { |
|
return true; |
|
} |
|
index--; |
|
} |
|
return false; |
|
} |
|
|
|
private boolean wasSpaceBetweenBraces() { |
|
return nodeText.getTextElement(originalIndex).isToken(RBRACE) |
|
&& doWeHaveLeftBraceFollowedBySpace(originalIndex - 1) |
|
&& (diffIndex < 2 || !diffElements.get(diffIndex - 2).isRemoved()); |
|
} |
|
|
|
private boolean doWeHaveLeftBraceFollowedBySpace(int index) { |
|
index = rewindSpace(index); |
|
return nodeText.getElements().get(index).isToken(LBRACE); |
|
} |
|
|
|
private int rewindSpace(int index) { |
|
if (index <= 0) { |
|
return index; |
|
} |
|
if (nodeText.getElements().get(index).isWhiteSpace()) { |
|
return rewindSpace(index - 1); |
|
} else { |
|
return index; |
|
} |
|
} |
|
|
|
private boolean nextIsRightBrace(int index) { |
|
List<TextElement> elements = originalElements.subList(index, originalElements.size()); |
|
for(TextElement element : elements) { |
|
if (!element.isSpaceOrTab()) { |
|
return element.isToken(RBRACE); |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
private void applyAddedDiffElement(Added added) { |
|
if (added.isIndent()) { |
|
for (int i=0;i<STANDARD_INDENTATION_SIZE;i++){ |
|
indentation.add(new TokenTextElement(GeneratedJavaParserConstants.SPACE)); |
|
} |
|
addedIndentation = true; |
|
diffIndex++; |
|
return; |
|
} |
|
if (added.isUnindent()) { |
|
for (int i = 0; i<STANDARD_INDENTATION_SIZE && !indentation.isEmpty(); i++){ |
|
indentation.remove(indentation.size() - 1); |
|
} |
|
addedIndentation = false; |
|
diffIndex++; |
|
return; |
|
} |
|
|
|
TextElement addedTextElement = added.toTextElement(); |
|
boolean used = false; |
|
boolean isPreviousElementNewline = (originalIndex > 0) && originalElements.get(originalIndex - 1).isNewline(); |
|
if (isPreviousElementNewline) { |
|
List<TextElement> elements = processIndentation(indentation, originalElements.subList(0, originalIndex - 1)); |
|
boolean nextIsRightBrace = nextIsRightBrace(originalIndex); |
|
for (TextElement e : elements) { |
|
if (!nextIsRightBrace |
|
&& e instanceof TokenTextElement |
|
&& originalElements.get(originalIndex).isToken(((TokenTextElement)e).getTokenKind())) { |
|
originalIndex++; |
|
} else { |
|
nodeText.addElement(originalIndex++, e); |
|
} |
|
} |
|
} else if (isAfterLBrace(nodeText, originalIndex) && !isAReplacement(diffIndex)) { |
|
if (addedTextElement.isNewline()) { |
|
used = true; |
|
} |
|
nodeText.addElement(originalIndex++, new TokenTextElement(TokenTypes.eolTokenKind())); |
|
|
|
while (originalIndex >= 2 && originalElements.get(originalIndex - 2).isSpaceOrTab()) { |
|
originalElements.remove(originalIndex - 2); |
|
originalIndex--; |
|
} |
|
for (TextElement e : processIndentation(indentation, originalElements.subList(0, originalIndex - 1))) { |
|
nodeText.addElement(originalIndex++, 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 |
|
|
|
if (!addedIndentation) { |
|
for (TextElement e : indentationBlock()) { |
|
nodeText.addElement(originalIndex++, e); |
|
} |
|
} |
|
} |
|
|
|
if (!used) { |
|
|
|
boolean sufficientTokensRemainToSkip = nodeText.numberOfElements() > originalIndex + 2; |
|
boolean currentIsAComment = nodeText.getTextElement(originalIndex).isComment(); |
|
boolean previousIsAComment = originalIndex > 0 && nodeText.getTextElement(originalIndex - 1).isComment(); |
|
boolean currentIsNewline = nodeText.getTextElement(originalIndex).isNewline(); |
|
boolean isFirstElement = originalIndex == 0; |
|
boolean previousIsWhiteSpace = originalIndex > 0 && nodeText.getTextElement(originalIndex - 1).isWhiteSpace(); |
|
|
|
if (sufficientTokensRemainToSkip && currentIsAComment) { |
|
// Need to get behind the comment: |
|
originalIndex += 2; |
|
nodeText.addElement(originalIndex, addedTextElement); |
|
|
|
|
|
originalIndex = adjustIndentation(indentation, nodeText, originalIndex, false); |
|
originalIndex++; |
|
} else if (currentIsNewline && previousIsAComment) { |
|
/* |
|
* Manage the case where we want to add an element, after an expression which is followed by a comment on the same line. |
|
* This is not the same case as the one who handles the trailing comments, because in this case the node text element is a new line (not a comment) |
|
* For example : {@code private String a; // this is a } |
|
*/ |
|
originalIndex++; |
|
|
|
|
|
originalIndex = adjustIndentation(indentation, nodeText, originalIndex, false); |
|
nodeText.addElement(originalIndex, addedTextElement); |
|
originalIndex++; |
|
} else if (currentIsNewline && addedTextElement.isChild()) { |
|
// here we want to place the new child element after the current new line character. |
|
// Except if indentation has been inserted just before this step (in the case where isPreviousElementNewline is true) |
|
// or if the previous character is a space (it could be the case if we want to replace a statement) |
|
// Example 1 : if we insert a statement (a duplicated method call expression ) after this one <code> value();\n\n</code> |
|
// we want to have this result <code> value();\n value();\n</code> not <code> value();\n \nvalue();</code> |
|
// Example 2 : if we want to insert a statement after this one <code> \n</code> we want to have <code> value();\n</code> |
|
|
|
if (!isPreviousElementNewline && !isFirstElement && !previousIsWhiteSpace) { |
|
originalIndex++; |
|
} |
|
nodeText.addElement(originalIndex, addedTextElement); |
|
originalIndex++; |
|
} else { |
|
nodeText.addElement(originalIndex, addedTextElement); |
|
originalIndex++; |
|
} |
|
} |
|
|
|
if (addedTextElement.isNewline()) { |
|
boolean followedByUnindent = isFollowedByUnindent(diffElements, diffIndex); |
|
boolean nextIsRightBrace = nextIsRightBrace(originalIndex); |
|
boolean nextIsNewLine = nodeText.getTextElement(originalIndex).isNewline(); |
|
if ((!nextIsNewLine && !nextIsRightBrace) || followedByUnindent) { |
|
originalIndex = adjustIndentation(indentation, nodeText, originalIndex, followedByUnindent); |
|
} |
|
} |
|
|
|
diffIndex++; |
|
} |
|
|
|
private String tokenDescription(int kind) { |
|
return GeneratedJavaParserConstants.tokenImage[kind]; |
|
} |
|
|
|
private Map<Integer, Integer> getCorrespondanceBetweenNextOrderAndPreviousOrder(CsmMix elementsFromPreviousOrder, CsmMix elementsFromNextOrder) { |
|
Map<Integer, Integer> correspondanceBetweenNextOrderAndPreviousOrder = new HashMap<>(); |
|
|
|
List<CsmElement> nextOrderElements = elementsFromNextOrder.getElements(); |
|
List<CsmElement> previousOrderElements = elementsFromPreviousOrder.getElements(); |
|
WrappingRangeIterator piNext = new WrappingRangeIterator(previousOrderElements.size()); |
|
|
|
for (int ni = 0; ni < nextOrderElements.size(); ni++) { |
|
boolean found = false; |
|
CsmElement ne = nextOrderElements.get(ni); |
|
|
|
for (int counter = 0; counter < previousOrderElements.size() && !found; counter++) { |
|
Integer pi = piNext.next(); |
|
CsmElement pe = previousOrderElements.get(pi); |
|
if (!correspondanceBetweenNextOrderAndPreviousOrder.values().contains(pi) |
|
&& DifferenceElementCalculator.matching(ne, pe)) { |
|
found = true; |
|
correspondanceBetweenNextOrderAndPreviousOrder.put(ni, pi); |
|
} |
|
} |
|
} |
|
|
|
return correspondanceBetweenNextOrderAndPreviousOrder; |
|
} |
|
|
|
private boolean isFollowedByUnindent(List<DifferenceElement> diffElements, int diffIndex) { |
|
return (diffIndex + 1) < diffElements.size() |
|
&& diffElements.get(diffIndex + 1).isAdded() |
|
&& diffElements.get(diffIndex + 1).getElement() instanceof CsmUnindent; |
|
} |
|
|
|
private List<Integer> findIndexOfCorrespondingNodeTextElement(List<CsmElement> elements, NodeText nodeText, int startIndex, Node node) { |
|
List<Integer> correspondingIndices = new ArrayList<>(); |
|
for (ListIterator<CsmElement> csmElementListIterator = elements.listIterator(); csmElementListIterator.hasNext(); ) { |
|
|
|
int previousCsmElementIndex = csmElementListIterator.previousIndex(); |
|
CsmElement csmElement = csmElementListIterator.next(); |
|
int nextCsmElementIndex = csmElementListIterator.nextIndex(); |
|
|
|
Map<MatchClassification, Integer> potentialMatches = new EnumMap<>(MatchClassification.class); |
|
for (int i = startIndex; i < nodeText.getElements().size(); i++){ |
|
if (!correspondingIndices.contains(i)) { |
|
TextElement textElement = nodeText.getTextElement(i); |
|
|
|
boolean isCorresponding = isCorrespondingElement(textElement, csmElement, node); |
|
|
|
if (isCorresponding) { |
|
boolean hasSamePreviousElement = false; |
|
if (i > 0 && previousCsmElementIndex > -1) { |
|
TextElement previousTextElement = nodeText.getTextElement(i - 1); |
|
|
|
hasSamePreviousElement = isCorrespondingElement(previousTextElement, elements.get(previousCsmElementIndex), node); |
|
} |
|
|
|
boolean hasSameNextElement = false; |
|
if (i < nodeText.getElements().size() - 1 && nextCsmElementIndex < elements.size()) { |
|
TextElement nextTextElement = nodeText.getTextElement(i + 1); |
|
|
|
hasSameNextElement = isCorrespondingElement(nextTextElement, elements.get(nextCsmElementIndex), node); |
|
} |
|
|
|
if (hasSamePreviousElement && hasSameNextElement) { |
|
potentialMatches.putIfAbsent(MatchClassification.ALL, i); |
|
} else if (hasSamePreviousElement) { |
|
potentialMatches.putIfAbsent(MatchClassification.PREVIOUS_AND_SAME, i); |
|
} else if (hasSameNextElement) { |
|
potentialMatches.putIfAbsent(MatchClassification.NEXT_AND_SAME, i); |
|
} else { |
|
potentialMatches.putIfAbsent(MatchClassification.SAME_ONLY, i); |
|
} |
|
} else if (isAlmostCorrespondingElement(textElement, csmElement, node)) { |
|
potentialMatches.putIfAbsent(MatchClassification.ALMOST, i); |
|
} |
|
} |
|
} |
|
|
|
|
|
Optional<MatchClassification> bestMatchKey = potentialMatches.keySet().stream() |
|
.min(Comparator.comparing(MatchClassification::getPriority)); |
|
|
|
if (bestMatchKey.isPresent()) { |
|
correspondingIndices.add(potentialMatches.get(bestMatchKey.get())); |
|
} else { |
|
correspondingIndices.add(-1); |
|
} |
|
} |
|
|
|
return correspondingIndices; |
|
} |
|
|
|
private enum MatchClassification { |
|
ALL(1), PREVIOUS_AND_SAME(2), NEXT_AND_SAME(3), SAME_ONLY(4), ALMOST(5); |
|
|
|
private final int priority; |
|
|
|
MatchClassification(int priority) { |
|
this.priority = priority; |
|
} |
|
|
|
int getPriority() { |
|
return priority; |
|
} |
|
} |
|
|
|
private boolean isCorrespondingElement(TextElement textElement, CsmElement csmElement, Node node) { |
|
if (csmElement instanceof CsmToken) { |
|
CsmToken csmToken = (CsmToken)csmElement; |
|
if (textElement instanceof TokenTextElement) { |
|
TokenTextElement tokenTextElement = (TokenTextElement)textElement; |
|
return tokenTextElement.getTokenKind() == csmToken.getTokenType() && tokenTextElement.getText().equals(csmToken.getContent(node)); |
|
} |
|
} else if (csmElement instanceof CsmChild) { |
|
CsmChild csmChild = (CsmChild)csmElement; |
|
if (textElement instanceof ChildTextElement) { |
|
ChildTextElement childTextElement = (ChildTextElement)textElement; |
|
return childTextElement.getChild() == csmChild.getChild(); |
|
} |
|
} else { |
|
throw new UnsupportedOperationException(); |
|
} |
|
|
|
return false; |
|
} |
|
|
|
private boolean isAlmostCorrespondingElement(TextElement textElement, CsmElement csmElement, Node node) { |
|
if (isCorrespondingElement(textElement, csmElement, node)) { |
|
return false; |
|
} |
|
return textElement.isWhiteSpace() && csmElement instanceof CsmToken && ((CsmToken)csmElement).isWhiteSpace(); |
|
} |
|
|
|
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); |
|
} |
|
} |
|
if (nodeTextIndex < 0) { |
|
throw new IllegalStateException(); |
|
} |
|
return nodeTextIndex; |
|
} |
|
|
|
private boolean isAReplacement(int diffIndex) { |
|
return (diffIndex > 0) && diffElements.get(diffIndex) instanceof Added && diffElements.get(diffIndex - 1) instanceof Removed; |
|
} |
|
|
|
private boolean isReplaced(int diffIndex) { |
|
return (diffIndex < diffElements.size() - 1) && diffElements.get(diffIndex + 1) instanceof Added && diffElements.get(diffIndex) instanceof Removed; |
|
} |
|
|
|
@Override |
|
public String toString() { |
|
return "Difference{" + diffElements + '}'; |
|
} |
|
} |