• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.github.javaparser.printer.lexicalpreservation;
2 
3 import static com.github.javaparser.GeneratedJavaParserConstants.*;
4 
5 import java.util.*;
6 
7 import com.github.javaparser.GeneratedJavaParserConstants;
8 import com.github.javaparser.TokenTypes;
9 import com.github.javaparser.ast.Node;
10 import com.github.javaparser.ast.comments.Comment;
11 import com.github.javaparser.printer.concretesyntaxmodel.CsmElement;
12 import com.github.javaparser.printer.concretesyntaxmodel.CsmIndent;
13 import com.github.javaparser.printer.concretesyntaxmodel.CsmMix;
14 import com.github.javaparser.printer.concretesyntaxmodel.CsmToken;
15 import com.github.javaparser.printer.concretesyntaxmodel.CsmUnindent;
16 import com.github.javaparser.printer.lexicalpreservation.LexicalDifferenceCalculator.CsmChild;
17 
18 /**
19  * A Difference should give me a sequence of elements I should find (to indicate the context) followed by a list of elements
20  * to remove or to add and follow by another sequence of elements.
21  *
22  * I should later be able to apply such difference to a nodeText.
23  */
24 public class Difference {
25 
26     public static final int STANDARD_INDENTATION_SIZE = 4;
27 
28     private final NodeText nodeText;
29     private final Node node;
30 
31     private final List<DifferenceElement> diffElements;
32     private final List<TextElement> originalElements;
33     private int originalIndex = 0;
34     private int diffIndex = 0;
35 
36     private final List<TokenTextElement> indentation;
37     private boolean addedIndentation = false;
38 
Difference(List<DifferenceElement> diffElements, NodeText nodeText, Node node)39     Difference(List<DifferenceElement> diffElements, NodeText nodeText, Node node) {
40         if (nodeText == null) {
41             throw new NullPointerException("nodeText can not be null");
42         }
43 
44         this.nodeText = nodeText;
45         this.node = node;
46         this.diffElements = diffElements;
47         this.originalElements = nodeText.getElements();
48 
49         this.indentation = LexicalPreservingPrinter.findIndentation(node);
50     }
51 
processIndentation(List<TokenTextElement> indentation, List<TextElement> prevElements)52     private List<TextElement> processIndentation(List<TokenTextElement> indentation, List<TextElement> prevElements) {
53         List<TextElement> res = new LinkedList<>(indentation);
54         boolean afterNl = false;
55         for (TextElement e : prevElements) {
56             if (e.isNewline()) {
57                 res.clear();
58                 afterNl = true;
59             } else {
60                 if (afterNl && e instanceof TokenTextElement && TokenTypes.isWhitespace(((TokenTextElement)e).getTokenKind())) {
61                     res.add(e);
62                 } else {
63                     afterNl = false;
64                 }
65             }
66         }
67         return res;
68     }
69 
indentationBlock()70     private List<TextElement> indentationBlock() {
71         List<TextElement> res = new LinkedList<>();
72         res.add(new TokenTextElement(SPACE));
73         res.add(new TokenTextElement(SPACE));
74         res.add(new TokenTextElement(SPACE));
75         res.add(new TokenTextElement(SPACE));
76         return res;
77     }
78 
isAfterLBrace(NodeText nodeText, int nodeTextIndex)79     private boolean isAfterLBrace(NodeText nodeText, int nodeTextIndex) {
80         if (nodeTextIndex > 0 && nodeText.getElements().get(nodeTextIndex - 1).isToken(LBRACE)) {
81             return true;
82         }
83         if (nodeTextIndex > 0 && nodeText.getElements().get(nodeTextIndex - 1).isSpaceOrTab()) {
84             return isAfterLBrace(nodeText, nodeTextIndex - 1);
85         }
86         return false;
87     }
88 
89     /**
90      * If we are at the beginning of a line, with just spaces or tabs before us we should force the space to be
91      * the same as the indentation.
92      */
considerEnforcingIndentation(NodeText nodeText, int nodeTextIndex)93     private int considerEnforcingIndentation(NodeText nodeText, int nodeTextIndex) {
94         boolean hasOnlyWsBefore = true;
95         for (int i = nodeTextIndex; i >= 0 && hasOnlyWsBefore && i < nodeText.getElements().size(); i--) {
96             if (nodeText.getElements().get(i).isNewline()) {
97                 break;
98             }
99             if (!nodeText.getElements().get(i).isSpaceOrTab()) {
100                 hasOnlyWsBefore = false;
101             }
102         }
103         int res = nodeTextIndex;
104         if (hasOnlyWsBefore) {
105             for (int i = nodeTextIndex; i >= 0 && i < nodeText.getElements().size(); i--) {
106                 if (nodeText.getElements().get(i).isNewline()) {
107                     break;
108                 }
109                 nodeText.removeElement(i);
110                 res = i;
111             }
112         }
113         if (res < 0) {
114             throw new IllegalStateException();
115         }
116         return res;
117     }
118 
119     /**
120      * Node that we have calculate the Difference we can apply to a concrete NodeText, modifying it according
121      * to the difference (adding and removing the elements provided).
122      */
apply()123     void apply() {
124         extractReshuffledDiffElements(diffElements);
125         Map<Removed, RemovedGroup> removedGroups = combineRemovedElementsToRemovedGroups();
126 
127         do {
128             boolean isLeftOverDiffElement = applyLeftOverDiffElements();
129             boolean isLeftOverOriginalElement = applyLeftOverOriginalElements();
130 
131             if (!isLeftOverDiffElement && !isLeftOverOriginalElement){
132                 DifferenceElement diffElement = diffElements.get(diffIndex);
133 
134                 if (diffElement instanceof Added) {
135                     applyAddedDiffElement((Added) diffElement);
136                 } else {
137                     TextElement originalElement = originalElements.get(originalIndex);
138                     boolean originalElementIsChild = originalElement instanceof ChildTextElement;
139                     boolean originalElementIsToken = originalElement instanceof TokenTextElement;
140 
141                     if (diffElement instanceof Kept) {
142                         applyKeptDiffElement((Kept) diffElement, originalElement, originalElementIsChild, originalElementIsToken);
143                     } else if (diffElement instanceof Removed) {
144                         Removed removed = (Removed) diffElement;
145                         applyRemovedDiffElement(removedGroups.get(removed), removed, originalElement, originalElementIsChild, originalElementIsToken);
146                     } else {
147                         throw new UnsupportedOperationException("" + diffElement + " vs " + originalElement);
148                     }
149                 }
150             }
151         } while (diffIndex < diffElements.size() || originalIndex < originalElements.size());
152     }
153 
applyLeftOverOriginalElements()154     private boolean applyLeftOverOriginalElements() {
155         boolean isLeftOverElement = false;
156         if (diffIndex >= diffElements.size() && originalIndex < originalElements.size()) {
157             TextElement originalElement = originalElements.get(originalIndex);
158 
159             if (originalElement.isWhiteSpaceOrComment()) {
160                 originalIndex++;
161             } else {
162                 throw new UnsupportedOperationException("NodeText: " + nodeText + ". Difference: "
163                         + this + " " + originalElement);
164             }
165 
166             isLeftOverElement = true;
167         }
168         return isLeftOverElement;
169     }
170 
applyLeftOverDiffElements()171     private boolean applyLeftOverDiffElements() {
172         boolean isLeftOverElement = false;
173         if (diffIndex < diffElements.size() && originalIndex >= originalElements.size()) {
174             DifferenceElement diffElement = diffElements.get(diffIndex);
175             if (diffElement instanceof Kept) {
176                 Kept kept = (Kept) diffElement;
177 
178                 if (kept.isWhiteSpaceOrComment() || kept.isIndent() || kept.isUnindent()) {
179                     diffIndex++;
180                 } else {
181                     throw new IllegalStateException("Cannot keep element because we reached the end of nodetext: "
182                             + nodeText + ". Difference: " + this);
183                 }
184             } else if (diffElement instanceof Added) {
185                 Added addedElement = (Added) diffElement;
186 
187                 nodeText.addElement(originalIndex, addedElement.toTextElement());
188                 originalIndex++;
189                 diffIndex++;
190             } else {
191                 throw new UnsupportedOperationException(diffElement.getClass().getSimpleName());
192             }
193 
194             isLeftOverElement = true;
195         }
196 
197         return isLeftOverElement;
198     }
199 
extractReshuffledDiffElements(List<DifferenceElement> diffElements)200     private void extractReshuffledDiffElements(List<DifferenceElement> diffElements) {
201         for (int index = 0; index < diffElements.size(); index++) {
202             DifferenceElement diffElement = diffElements.get(index);
203             if (diffElement instanceof Reshuffled) {
204                 Reshuffled reshuffled = (Reshuffled) diffElement;
205 
206                 // First, let's see how many tokens we need to attribute to the previous version of the of the CsmMix
207                 CsmMix elementsFromPreviousOrder = reshuffled.getPreviousOrder();
208                 CsmMix elementsFromNextOrder = reshuffled.getNextOrder();
209 
210                 // This contains indexes from elementsFromNextOrder to indexes from elementsFromPreviousOrder
211                 Map<Integer, Integer> correspondanceBetweenNextOrderAndPreviousOrder = getCorrespondanceBetweenNextOrderAndPreviousOrder(elementsFromPreviousOrder, elementsFromNextOrder);
212 
213                 // We now find out which Node Text elements corresponds to the elements in the original CSM
214                 List<Integer> nodeTextIndexOfPreviousElements = findIndexOfCorrespondingNodeTextElement(elementsFromPreviousOrder.getElements(), nodeText, originalIndex, node);
215 
216                 Map<Integer, Integer> nodeTextIndexToPreviousCSMIndex = new HashMap<>();
217                 for (int i = 0; i < nodeTextIndexOfPreviousElements.size(); i++) {
218                     int value = nodeTextIndexOfPreviousElements.get(i);
219                     if (value != -1) {
220                         nodeTextIndexToPreviousCSMIndex.put(value, i);
221                     }
222                 }
223                 int lastNodeTextIndex = nodeTextIndexOfPreviousElements.stream().max(Integer::compareTo).orElse(-1);
224 
225                 // Elements to be added at the end
226                 List<CsmElement> elementsToBeAddedAtTheEnd = new LinkedList<>();
227                 List<CsmElement> nextOrderElements = elementsFromNextOrder.getElements();
228 
229                 Map<Integer, List<CsmElement>> elementsToAddBeforeGivenOriginalCSMElement = new HashMap<>();
230                 for (int ni = 0; ni < nextOrderElements.size(); ni++) {
231                     // If it has a mapping, then it is kept
232                     if (!correspondanceBetweenNextOrderAndPreviousOrder.containsKey(ni)) {
233                         // Ok, it is something new. Where to put it? Let's see what is the first following
234                         // element that has a mapping
235                         int originalCsmIndex = -1;
236                         for (int nj = ni + 1; nj < nextOrderElements.size() && originalCsmIndex == -1; nj++) {
237                             if (correspondanceBetweenNextOrderAndPreviousOrder.containsKey(nj)) {
238                                 originalCsmIndex = correspondanceBetweenNextOrderAndPreviousOrder.get(nj);
239                                 if (!elementsToAddBeforeGivenOriginalCSMElement.containsKey(originalCsmIndex)) {
240                                     elementsToAddBeforeGivenOriginalCSMElement.put(originalCsmIndex, new LinkedList<>());
241                                 }
242                                 elementsToAddBeforeGivenOriginalCSMElement.get(originalCsmIndex).add(nextOrderElements.get(ni));
243                             }
244                         }
245                         // it does not preceed anything, so it goes at the end
246                         if (originalCsmIndex == -1) {
247                             elementsToBeAddedAtTheEnd.add(nextOrderElements.get(ni));
248                         }
249                     }
250                 }
251 
252                 // We go over the original node text elements, in the order they appear in the NodeText.
253                 // Considering an original node text element (ONE)
254                 // * we verify if it corresponds to a CSM element. If it does not we just move on, otherwise
255                 //   we find the correspond OCE (Original CSM Element)
256                 // * we first add new elements that are marked to be added before OCE
257                 // * if OCE is marked to be present also in the "after" CSM we add a kept element,
258                 //   otherwise we add a removed element
259 
260                 // Remove the whole Reshuffled element
261                 diffElements.remove(index);
262 
263                 int diffElIterator = index;
264                 if (lastNodeTextIndex != -1) {
265                     for (int ntIndex = originalIndex; ntIndex <= lastNodeTextIndex; ntIndex++) {
266 
267                         if (nodeTextIndexToPreviousCSMIndex.containsKey(ntIndex)) {
268                             int indexOfOriginalCSMElement = nodeTextIndexToPreviousCSMIndex.get(ntIndex);
269                             if (elementsToAddBeforeGivenOriginalCSMElement.containsKey(indexOfOriginalCSMElement)) {
270                                 for (CsmElement elementToAdd : elementsToAddBeforeGivenOriginalCSMElement.get(indexOfOriginalCSMElement)) {
271                                     diffElements.add(diffElIterator++, new Added(elementToAdd));
272                                 }
273                             }
274 
275                             CsmElement originalCSMElement = elementsFromPreviousOrder.getElements().get(indexOfOriginalCSMElement);
276                             boolean toBeKept = correspondanceBetweenNextOrderAndPreviousOrder.containsValue(indexOfOriginalCSMElement);
277                             if (toBeKept) {
278                                 diffElements.add(diffElIterator++, new Kept(originalCSMElement));
279                             } else {
280                                 diffElements.add(diffElIterator++, new Removed(originalCSMElement));
281                             }
282                         }
283                         // else we have a simple node text element, without associated csm element, just keep ignore it
284                     }
285                 }
286 
287                 // Finally we look for the remaining new elements that were not yet added and
288                 // add all of them
289                 for (CsmElement elementToAdd : elementsToBeAddedAtTheEnd) {
290                     diffElements.add(diffElIterator++, new Added(elementToAdd));
291                 }
292             }
293         }
294     }
295 
296     /**
297      * Maps all Removed elements as keys to their corresponding RemovedGroup.
298      * A RemovedGroup contains all consecutive Removed elements.
299      * <br/>
300      * Example:
301      * <pre>
302      * Elements: Kept|Removed1|Removed2|Kept|Removed3|Added|Removed4
303      * Groups:        <----Group1---->       Group2         Group3
304      * Keys:          Removed1+Removed2      Removed3       Removed4
305      * </pre>
306      *
307      * @return Map with all Removed elements as keys to their corresponding RemovedGroup
308      */
combineRemovedElementsToRemovedGroups()309     private Map<Removed, RemovedGroup> combineRemovedElementsToRemovedGroups() {
310         Map<Integer, List<Removed>> removedElementsMap = groupConsecutiveRemovedElements();
311 
312         List<RemovedGroup> removedGroups = new ArrayList<>();
313         for (Map.Entry<Integer, List<Removed>> entry : removedElementsMap.entrySet()) {
314             removedGroups.add(RemovedGroup.of(entry.getKey(), entry.getValue()));
315         }
316 
317         Map<Removed, RemovedGroup> map = new HashMap<>();
318         for (RemovedGroup removedGroup : removedGroups){
319             for (Removed index : removedGroup) {
320                 map.put(index, removedGroup);
321             }
322         }
323 
324         return map;
325     }
326 
groupConsecutiveRemovedElements()327     private Map<Integer, List<Removed>> groupConsecutiveRemovedElements() {
328         Map<Integer, List<Removed>> removedElementsMap = new HashMap<>();
329 
330         Integer firstElement = null;
331         for (int i = 0; i < diffElements.size(); i++) {
332             DifferenceElement diffElement = diffElements.get(i);
333             if (diffElement instanceof Removed) {
334                 if (firstElement == null) {
335                     firstElement = i;
336                 }
337 
338                 removedElementsMap.computeIfAbsent(firstElement, key -> new ArrayList<>())
339                         .add((Removed) diffElement);
340             } else {
341                 firstElement = null;
342             }
343         }
344         return removedElementsMap;
345     }
346 
applyRemovedDiffElement(RemovedGroup removedGroup, Removed removed, TextElement originalElement, boolean originalElementIsChild, boolean originalElementIsToken)347     private void applyRemovedDiffElement(RemovedGroup removedGroup, Removed removed, TextElement originalElement, boolean originalElementIsChild, boolean originalElementIsToken) {
348         if (removed.isChild() && originalElementIsChild) {
349             ChildTextElement originalElementChild = (ChildTextElement) originalElement;
350             if (originalElementChild.isComment()) {
351                 // We expected to remove a proper node but we found a comment in between.
352                 // If the comment is associated to the node we want to remove we remove it as well, otherwise we keep it
353                 Comment comment = (Comment) originalElementChild.getChild();
354                 if (!comment.isOrphan() && comment.getCommentedNode().isPresent() && comment.getCommentedNode().get().equals(removed.getChild())) {
355                     nodeText.removeElement(originalIndex);
356                 } else {
357                     originalIndex++;
358                 }
359             } else {
360                 nodeText.removeElement(originalIndex);
361 
362                 if ((diffIndex + 1 >= diffElements.size() || !(diffElements.get(diffIndex + 1) instanceof Added))
363                         && !removedGroup.isACompleteLine()) {
364                     originalIndex = considerEnforcingIndentation(nodeText, originalIndex);
365                 }
366                 // If in front we have one space and before also we had space let's drop one space
367                 if (originalElements.size() > originalIndex && originalIndex > 0) {
368                     if (originalElements.get(originalIndex).isWhiteSpace()
369                             && originalElements.get(originalIndex - 1).isWhiteSpace()) {
370                         // However we do not want to do that when we are about to adding or removing elements
371                         if ((diffIndex + 1) == diffElements.size() || (diffElements.get(diffIndex + 1) instanceof Kept)) {
372                             originalElements.remove(originalIndex--);
373                         }
374                     }
375                 }
376 
377                 diffIndex++;
378             }
379         } else if (removed.isToken() && originalElementIsToken &&
380                 (removed.getTokenType() == ((TokenTextElement) originalElement).getTokenKind()
381                         // handle EOLs separately as their token kind might not be equal. This is because the 'removed'
382                         // element always has the current operating system's EOL as type
383                         || (((TokenTextElement) originalElement).getToken().getCategory().isEndOfLine()
384                                 && removed.isNewLine()))) {
385             nodeText.removeElement(originalIndex);
386             diffIndex++;
387         } else if (originalElementIsToken && originalElement.isWhiteSpaceOrComment()) {
388             originalIndex++;
389         } else if (removed.isPrimitiveType()) {
390             if (isPrimitiveType(originalElement)) {
391                 nodeText.removeElement(originalIndex);
392                 diffIndex++;
393             } else {
394                 throw new UnsupportedOperationException("removed " + removed.getElement() + " vs " + originalElement);
395             }
396         } else if (removed.isWhiteSpace() || removed.getElement() instanceof CsmIndent || removed.getElement() instanceof CsmUnindent) {
397             diffIndex++;
398         } else if (originalElement.isWhiteSpace()) {
399             originalIndex++;
400         } else {
401             throw new UnsupportedOperationException("removed " + removed.getElement() + " vs " + originalElement);
402         }
403 
404         cleanTheLineOfLeftOverSpace(removedGroup, removed);
405     }
406 
407     /**
408      * Cleans the line of left over space if there is unnecessary indentation and the element will not be replaced
409      */
cleanTheLineOfLeftOverSpace(RemovedGroup removedGroup, Removed removed)410     private void cleanTheLineOfLeftOverSpace(RemovedGroup removedGroup, Removed removed) {
411         if (originalIndex >= originalElements.size()) {
412             // if all elements were already processed there is nothing to do
413             return;
414         }
415 
416         if (!removedGroup.isProcessed()
417                 && removedGroup.getLastElement() == removed
418                 && removedGroup.isACompleteLine()) {
419             Integer lastElementIndex = removedGroup.getLastElementIndex();
420             Optional<Integer> indentation = removedGroup.getIndentation();
421 
422             if (indentation.isPresent() && !isReplaced(lastElementIndex)) {
423                 for (int i = 0; i < indentation.get(); i++) {
424                     if (originalElements.get(originalIndex).isSpaceOrTab()) {
425                         // If the current element is a space, remove it
426                         nodeText.removeElement(originalIndex);
427                     } else if (originalIndex >= 1 && originalElements.get(originalIndex - 1).isSpaceOrTab()) {
428                         // If the current element is not a space itself we remove the space in front of it
429                         nodeText.removeElement(originalIndex - 1);
430                         originalIndex--;
431                     }
432                 }
433             }
434 
435             // Mark RemovedGroup as processed
436             removedGroup.processed();
437         }
438     }
439 
applyKeptDiffElement(Kept kept, TextElement originalElement, boolean originalElementIsChild, boolean originalElementIsToken)440     private void applyKeptDiffElement(Kept kept, TextElement originalElement, boolean originalElementIsChild, boolean originalElementIsToken) {
441         if (originalElement.isComment()) {
442             originalIndex++;
443         } else if (kept.isChild() && originalElementIsChild) {
444             diffIndex++;
445             originalIndex++;
446         } else if (kept.isChild() && originalElementIsToken) {
447             if (originalElement.isWhiteSpaceOrComment()) {
448                 originalIndex++;
449             } else {
450                 if (kept.isPrimitiveType()) {
451                     originalIndex++;
452                     diffIndex++;
453                 } else {
454                     throw new UnsupportedOperationException("kept " + kept.getElement() + " vs " + originalElement);
455                 }
456             }
457         } else if (kept.isToken() && originalElementIsToken) {
458             TokenTextElement originalTextToken = (TokenTextElement) originalElement;
459 
460             if (kept.getTokenType() == originalTextToken.getTokenKind()) {
461                 originalIndex++;
462                 diffIndex++;
463             } else if (kept.isNewLine() && originalTextToken.isSpaceOrTab()) {
464                 originalIndex++;
465                 diffIndex++;
466             } else if (kept.isWhiteSpaceOrComment()) {
467                 diffIndex++;
468             } else if (originalTextToken.isWhiteSpaceOrComment()) {
469                 originalIndex++;
470             } else {
471                 throw new UnsupportedOperationException("Csm token " + kept.getElement() + " NodeText TOKEN " + originalTextToken);
472             }
473         } else if (kept.isWhiteSpace()) {
474             diffIndex++;
475         } else if (kept.isIndent()) {
476             diffIndex++;
477         } else if (kept.isUnindent()) {
478             // Nothing to do, beside considering indentation
479             // However we want to consider the case in which the indentation was not applied, like when we have
480             // just a left brace followed by space
481 
482             diffIndex++;
483             if (!openBraceWasOnSameLine()) {
484                 for (int i = 0; i < STANDARD_INDENTATION_SIZE && originalIndex >= 1 && nodeText.getTextElement(originalIndex - 1).isSpaceOrTab(); i++) {
485                     nodeText.removeElement(--originalIndex);
486                 }
487             }
488         } else {
489             throw new UnsupportedOperationException("kept " + kept.getElement() + " vs " + originalElement);
490         }
491     }
492 
openBraceWasOnSameLine()493     private boolean openBraceWasOnSameLine() {
494         int index = originalIndex;
495         while (index >= 0 && !nodeText.getTextElement(index).isNewline()) {
496             if (nodeText.getTextElement(index).isToken(LBRACE)) {
497                 return true;
498             }
499             index--;
500         }
501         return false;
502     }
503 
wasSpaceBetweenBraces()504     private boolean wasSpaceBetweenBraces() {
505         return nodeText.getTextElement(originalIndex).isToken(RBRACE)
506                 && doWeHaveLeftBraceFollowedBySpace(originalIndex - 1)
507                 && (diffIndex < 2 || !diffElements.get(diffIndex - 2).isRemoved());
508     }
509 
doWeHaveLeftBraceFollowedBySpace(int index)510     private boolean doWeHaveLeftBraceFollowedBySpace(int index) {
511         index = rewindSpace(index);
512         return nodeText.getElements().get(index).isToken(LBRACE);
513     }
514 
rewindSpace(int index)515     private int rewindSpace(int index) {
516         if (index <= 0) {
517             return index;
518         }
519         if (nodeText.getElements().get(index).isWhiteSpace()) {
520             return rewindSpace(index - 1);
521         } else {
522             return index;
523         }
524     }
525 
nextIsRightBrace(int index)526     private boolean nextIsRightBrace(int index) {
527         List<TextElement> elements = originalElements.subList(index, originalElements.size());
528         for(TextElement element : elements) {
529             if (!element.isSpaceOrTab()) {
530                 return element.isToken(RBRACE);
531             }
532         }
533         return false;
534     }
535 
applyAddedDiffElement(Added added)536     private void applyAddedDiffElement(Added added) {
537         if (added.isIndent()) {
538             for (int i=0;i<STANDARD_INDENTATION_SIZE;i++){
539                 indentation.add(new TokenTextElement(GeneratedJavaParserConstants.SPACE));
540             }
541             addedIndentation = true;
542             diffIndex++;
543             return;
544         }
545         if (added.isUnindent()) {
546             for (int i = 0; i<STANDARD_INDENTATION_SIZE && !indentation.isEmpty(); i++){
547                 indentation.remove(indentation.size() - 1);
548             }
549             addedIndentation = false;
550             diffIndex++;
551             return;
552         }
553 
554         TextElement addedTextElement = added.toTextElement();
555         boolean used = false;
556         if (originalIndex > 0 && originalElements.get(originalIndex - 1).isNewline()) {
557             List<TextElement> elements = processIndentation(indentation, originalElements.subList(0, originalIndex - 1));
558             boolean nextIsRightBrace = nextIsRightBrace(originalIndex);
559             for (TextElement e : elements) {
560                 if (!nextIsRightBrace
561                         && e instanceof TokenTextElement
562                         && originalElements.get(originalIndex).isToken(((TokenTextElement)e).getTokenKind())) {
563                     originalIndex++;
564                 } else {
565                     nodeText.addElement(originalIndex++, e);
566                 }
567             }
568         } else if (isAfterLBrace(nodeText, originalIndex) && !isAReplacement(diffIndex)) {
569             if (addedTextElement.isNewline()) {
570                 used = true;
571             }
572             nodeText.addElement(originalIndex++, new TokenTextElement(TokenTypes.eolTokenKind()));
573             // This remove the space in "{ }" when adding a new line
574             while (originalIndex >= 2 && originalElements.get(originalIndex - 2).isSpaceOrTab()) {
575                 originalElements.remove(originalIndex - 2);
576                 originalIndex--;
577             }
578             for (TextElement e : processIndentation(indentation, originalElements.subList(0, originalIndex - 1))) {
579                 nodeText.addElement(originalIndex++, e);
580             }
581             // Indentation is painful...
582             // Sometimes we want to force indentation: this is the case when indentation was expected but
583             // was actually not there. For example if we have "{ }" we would expect indentation but it is
584             // not there, so when adding new elements we force it. However if the indentation has been
585             // inserted by us in this transformation we do not want to insert it again
586             if (!addedIndentation) {
587                 for (TextElement e : indentationBlock()) {
588                     nodeText.addElement(originalIndex++, e);
589                 }
590             }
591         }
592 
593         if (!used) {
594             // Handling trailing comments
595             if(nodeText.numberOfElements() > originalIndex + 1 &&
596                     nodeText.getTextElement(originalIndex).isComment()) {
597                 // Need to get behind the comment:
598                 originalIndex += 2;
599                 nodeText.addElement(originalIndex, addedTextElement); // Defer originalIndex increment
600                 // We want to adjust the indentation while considering the new element that we added
601                 originalIndex = adjustIndentation(indentation, nodeText, originalIndex, false);
602                 originalIndex++; // Now we can increment
603             } else {
604                 nodeText.addElement(originalIndex, addedTextElement);
605                 originalIndex++;
606             }
607         }
608 
609         if (addedTextElement.isNewline()) {
610             boolean followedByUnindent = isFollowedByUnindent(diffElements, diffIndex);
611             boolean nextIsRightBrace = nextIsRightBrace(originalIndex);
612             boolean nextIsNewLine = nodeText.getTextElement(originalIndex).isNewline();
613             if ((!nextIsNewLine && !nextIsRightBrace) || followedByUnindent) {
614                 originalIndex = adjustIndentation(indentation, nodeText, originalIndex, followedByUnindent);
615             }
616         }
617 
618         diffIndex++;
619     }
620 
getCorrespondanceBetweenNextOrderAndPreviousOrder(CsmMix elementsFromPreviousOrder, CsmMix elementsFromNextOrder)621     private Map<Integer, Integer> getCorrespondanceBetweenNextOrderAndPreviousOrder(CsmMix elementsFromPreviousOrder, CsmMix elementsFromNextOrder) {
622         Map<Integer, Integer> correspondanceBetweenNextOrderAndPreviousOrder = new HashMap<>();
623 
624         List<CsmElement> nextOrderElements = elementsFromNextOrder.getElements();
625         List<CsmElement> previousOrderElements = elementsFromPreviousOrder.getElements();
626         WrappingRangeIterator piNext = new WrappingRangeIterator(previousOrderElements.size());
627 
628         for (int ni = 0; ni < nextOrderElements.size(); ni++) {
629             boolean found = false;
630             CsmElement ne = nextOrderElements.get(ni);
631 
632             for (int counter = 0; counter < previousOrderElements.size() && !found; counter++) {
633                 Integer pi = piNext.next();
634                 CsmElement pe = previousOrderElements.get(pi);
635                 if (!correspondanceBetweenNextOrderAndPreviousOrder.values().contains(pi)
636                         && DifferenceElementCalculator.matching(ne, pe)) {
637                     found = true;
638                     correspondanceBetweenNextOrderAndPreviousOrder.put(ni, pi);
639                 }
640             }
641         }
642 
643         return correspondanceBetweenNextOrderAndPreviousOrder;
644     }
645 
isFollowedByUnindent(List<DifferenceElement> diffElements, int diffIndex)646     private boolean isFollowedByUnindent(List<DifferenceElement> diffElements, int diffIndex) {
647         return (diffIndex + 1) < diffElements.size()
648                 && diffElements.get(diffIndex + 1).isAdded()
649                 && diffElements.get(diffIndex + 1).getElement() instanceof CsmUnindent;
650     }
651 
findIndexOfCorrespondingNodeTextElement(List<CsmElement> elements, NodeText nodeText, int startIndex, Node node)652     private List<Integer> findIndexOfCorrespondingNodeTextElement(List<CsmElement> elements, NodeText nodeText, int startIndex, Node node) {
653         List<Integer> correspondingIndices = new ArrayList<>();
654         for (ListIterator<CsmElement> csmElementListIterator = elements.listIterator(); csmElementListIterator.hasNext(); ) {
655 
656             int previousCsmElementIndex = csmElementListIterator.previousIndex();
657             CsmElement csmElement = csmElementListIterator.next();
658             int nextCsmElementIndex = csmElementListIterator.nextIndex();
659 
660             Map<MatchClassification, Integer> potentialMatches = new EnumMap<>(MatchClassification.class);
661             for (int i = startIndex; i < nodeText.getElements().size(); i++){
662                 if (!correspondingIndices.contains(i)) {
663                     TextElement textElement = nodeText.getTextElement(i);
664 
665                     boolean isCorresponding = isCorrespondingElement(textElement, csmElement, node);
666 
667                     if (isCorresponding) {
668                         boolean hasSamePreviousElement = false;
669                         if (i > 0 && previousCsmElementIndex > -1) {
670                             TextElement previousTextElement = nodeText.getTextElement(i - 1);
671 
672                             hasSamePreviousElement = isCorrespondingElement(previousTextElement, elements.get(previousCsmElementIndex), node);
673                         }
674 
675                         boolean hasSameNextElement = false;
676                         if (i < nodeText.getElements().size() - 1 && nextCsmElementIndex < elements.size()) {
677                             TextElement nextTextElement = nodeText.getTextElement(i + 1);
678 
679                             hasSameNextElement = isCorrespondingElement(nextTextElement, elements.get(nextCsmElementIndex), node);
680                         }
681 
682                         if (hasSamePreviousElement && hasSameNextElement) {
683                             potentialMatches.putIfAbsent(MatchClassification.ALL, i);
684                         } else if (hasSamePreviousElement) {
685                             potentialMatches.putIfAbsent(MatchClassification.PREVIOUS_AND_SAME, i);
686                         } else if (hasSameNextElement) {
687                             potentialMatches.putIfAbsent(MatchClassification.NEXT_AND_SAME, i);
688                         } else {
689                             potentialMatches.putIfAbsent(MatchClassification.SAME_ONLY, i);
690                         }
691                     } else if (isAlmostCorrespondingElement(textElement, csmElement, node)) {
692                         potentialMatches.putIfAbsent(MatchClassification.ALMOST, i);
693                     }
694                 }
695             }
696 
697             // Prioritize the matches from best to worst
698             Optional<MatchClassification> bestMatchKey = potentialMatches.keySet().stream()
699                     .min(Comparator.comparing(MatchClassification::getPriority));
700 
701             if (bestMatchKey.isPresent()) {
702                 correspondingIndices.add(potentialMatches.get(bestMatchKey.get()));
703             } else {
704                 correspondingIndices.add(-1);
705             }
706         }
707 
708         return correspondingIndices;
709     }
710 
711     private enum MatchClassification {
712         ALL(1), PREVIOUS_AND_SAME(2), NEXT_AND_SAME(3), SAME_ONLY(4), ALMOST(5);
713 
714         private final int priority;
MatchClassification(int priority)715         MatchClassification(int priority) {
716             this.priority = priority;
717         }
718 
getPriority()719         int getPriority() {
720             return priority;
721         }
722     }
723 
isCorrespondingElement(TextElement textElement, CsmElement csmElement, Node node)724     private boolean isCorrespondingElement(TextElement textElement, CsmElement csmElement, Node node) {
725         if (csmElement instanceof CsmToken) {
726             CsmToken csmToken = (CsmToken)csmElement;
727             if (textElement instanceof TokenTextElement) {
728                 TokenTextElement tokenTextElement = (TokenTextElement)textElement;
729                 return tokenTextElement.getTokenKind() == csmToken.getTokenType() && tokenTextElement.getText().equals(csmToken.getContent(node));
730             }
731         } else if (csmElement instanceof CsmChild) {
732             CsmChild csmChild = (CsmChild)csmElement;
733             if (textElement instanceof ChildTextElement) {
734                 ChildTextElement childTextElement = (ChildTextElement)textElement;
735                 return childTextElement.getChild() == csmChild.getChild();
736             }
737         } else {
738             throw new UnsupportedOperationException();
739         }
740 
741         return false;
742     }
743 
isAlmostCorrespondingElement(TextElement textElement, CsmElement csmElement, Node node)744     private boolean isAlmostCorrespondingElement(TextElement textElement, CsmElement csmElement, Node node) {
745         if (isCorrespondingElement(textElement, csmElement, node)) {
746             return false;
747         }
748         return textElement.isWhiteSpace() && csmElement instanceof CsmToken && ((CsmToken)csmElement).isWhiteSpace();
749     }
750 
adjustIndentation(List<TokenTextElement> indentation, NodeText nodeText, int nodeTextIndex, boolean followedByUnindent)751     private int adjustIndentation(List<TokenTextElement> indentation, NodeText nodeText, int nodeTextIndex, boolean followedByUnindent) {
752         List<TextElement> indentationAdj = processIndentation(indentation, nodeText.getElements().subList(0, nodeTextIndex - 1));
753         if (nodeTextIndex < nodeText.getElements().size() && nodeText.getElements().get(nodeTextIndex).isToken(RBRACE)) {
754             indentationAdj = indentationAdj.subList(0, indentationAdj.size() - Math.min(STANDARD_INDENTATION_SIZE, indentationAdj.size()));
755         } else if (followedByUnindent) {
756             indentationAdj = indentationAdj.subList(0, Math.max(0, indentationAdj.size() - STANDARD_INDENTATION_SIZE));
757         }
758         for (TextElement e : indentationAdj) {
759             if ((nodeTextIndex< nodeText.getElements().size()) && nodeText.getElements().get(nodeTextIndex).isSpaceOrTab()) {
760                 nodeTextIndex++;
761             } else {
762                 nodeText.getElements().add(nodeTextIndex++, e);
763             }
764         }
765         if (nodeTextIndex < 0) {
766             throw new IllegalStateException();
767         }
768         return nodeTextIndex;
769     }
770 
isAReplacement(int diffIndex)771     private boolean isAReplacement(int diffIndex) {
772         return (diffIndex > 0) && diffElements.get(diffIndex) instanceof Added && diffElements.get(diffIndex - 1) instanceof Removed;
773     }
774 
isReplaced(int diffIndex)775     private boolean isReplaced(int diffIndex) {
776         return (diffIndex < diffElements.size() - 1) && diffElements.get(diffIndex + 1) instanceof Added && diffElements.get(diffIndex) instanceof Removed;
777     }
778 
isPrimitiveType(TextElement textElement)779     private boolean isPrimitiveType(TextElement textElement) {
780         if (textElement instanceof TokenTextElement) {
781             TokenTextElement tokenTextElement = (TokenTextElement)textElement;
782             int tokenKind = tokenTextElement.getTokenKind();
783             return tokenKind == BYTE
784                 || tokenKind == CHAR
785                 || tokenKind == SHORT
786                 || tokenKind == INT
787                 || tokenKind == LONG
788                 || tokenKind == FLOAT
789                 || tokenKind == DOUBLE;
790         } else {
791             return false;
792         }
793     }
794     @Override
toString()795     public String toString() {
796         return "Difference{" + diffElements + '}';
797     }
798 }
799