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