diff --git a/javaparser-core-testing/src/test/java/com/github/javaparser/printer/lexicalpreservation/PeekingIteratorTest.java b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/lexicalpreservation/PeekingIteratorTest.java new file mode 100755 index 0000000000..3e77c97a95 --- /dev/null +++ b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/lexicalpreservation/PeekingIteratorTest.java @@ -0,0 +1,126 @@ +package com.github.javaparser.printer.lexicalpreservation; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; + +import org.junit.jupiter.api.*; + +class PeekingIteratorTest { + + private static final List EMPTY_LIST = new ArrayList(); + + private static List NON_EMPTY_LIST ; + + private PeekingIterator peekingIterator; + + @BeforeAll + static void setUpBeforeClass() throws Exception { + } + + @AfterAll + static void tearDownAfterClass() throws Exception { + } + + @BeforeEach + void setUp() throws Exception { + NON_EMPTY_LIST = Arrays.asList("A", "B", "C"); + } + + @AfterEach + void tearDown() throws Exception { + } + + @Test + void testHasNext() { + peekingIterator = new PeekingIterator(EMPTY_LIST.listIterator()); + assertFalse(peekingIterator.hasNext()); + peekingIterator = new PeekingIterator(NON_EMPTY_LIST.listIterator()); + assertTrue(peekingIterator.hasNext()); + } + + @Test + void testPeek() { + peekingIterator = new PeekingIterator(EMPTY_LIST.listIterator()); + assertEquals(null, peekingIterator.peek()); + peekingIterator = new PeekingIterator(NON_EMPTY_LIST.listIterator()); + assertEquals("A", peekingIterator.peek()); + assertEquals("A", peekingIterator.next()); + } + + @Test + void testElement() { + peekingIterator = new PeekingIterator(EMPTY_LIST.listIterator()); + assertEquals(null, peekingIterator.peek()); + peekingIterator = new PeekingIterator(NON_EMPTY_LIST.listIterator()); + assertEquals("A", peekingIterator.peek()); + assertEquals(1, peekingIterator.nextIndex()); + } + + @Test + void testNext() { + peekingIterator = new PeekingIterator(EMPTY_LIST.listIterator()); + assertThrows(NoSuchElementException.class, () -> peekingIterator.next()); + peekingIterator = new PeekingIterator(NON_EMPTY_LIST.listIterator()); + assertEquals("A", peekingIterator.next()); + } + + @Test + void testRemove() { + peekingIterator = new PeekingIterator(EMPTY_LIST.listIterator()); + assertThrows(IllegalStateException.class, () -> peekingIterator.remove()); + peekingIterator = new PeekingIterator(NON_EMPTY_LIST.listIterator()); + assertThrows(IllegalStateException.class, () -> peekingIterator.remove()); + String result = peekingIterator.next(); + assertThrows(UnsupportedOperationException.class, () -> peekingIterator.remove()); + } + + @Test + void testHasPrevious() { + peekingIterator = new PeekingIterator(EMPTY_LIST.listIterator()); + assertFalse(peekingIterator.hasPrevious()); + peekingIterator = new PeekingIterator(NON_EMPTY_LIST.listIterator()); + assertFalse(peekingIterator.hasPrevious()); + String result = peekingIterator.next(); + assertTrue(peekingIterator.hasPrevious()); + } + + @Test + void testPrevious() { + peekingIterator = new PeekingIterator(EMPTY_LIST.listIterator()); + assertThrows(NoSuchElementException.class, () -> peekingIterator.previous()); + peekingIterator = new PeekingIterator(NON_EMPTY_LIST.listIterator()); + assertThrows(NoSuchElementException.class, () -> peekingIterator.previous()); + } + + @Test + void testNextIndex() { + peekingIterator = new PeekingIterator(EMPTY_LIST.listIterator()); + assertEquals(0, peekingIterator.nextIndex()); + peekingIterator = new PeekingIterator(NON_EMPTY_LIST.listIterator()); + assertEquals(0, peekingIterator.nextIndex()); + } + + @Test + void testPreviousIndex() { + peekingIterator = new PeekingIterator(EMPTY_LIST.listIterator()); + assertEquals(-1, peekingIterator.previousIndex()); + peekingIterator = new PeekingIterator(NON_EMPTY_LIST.listIterator()); + assertEquals(-1, peekingIterator.previousIndex()); + } + + @Test + void testSet() { + peekingIterator = new PeekingIterator(EMPTY_LIST.listIterator()); + assertThrows(IllegalStateException.class, () -> peekingIterator.set("D")); + peekingIterator = new PeekingIterator(NON_EMPTY_LIST.listIterator()); + assertThrows(IllegalStateException.class, () -> peekingIterator.set("D")); + peekingIterator.next(); + peekingIterator.set("D"); + assertEquals(3, NON_EMPTY_LIST.size()); + } + +} diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LookaheadIterator.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LookaheadIterator.java new file mode 100755 index 0000000000..9847fe9268 --- /dev/null +++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LookaheadIterator.java @@ -0,0 +1,30 @@ +package com.github.javaparser.printer.lexicalpreservation; + +import java.util.NoSuchElementException; + +public interface LookaheadIterator { + + /** + * Returns the next element in iteration without advancing the underlying iterator. + * If the iterator is already exhausted, null will be returned. + *

+ * Note: this method does not throw a {@link NoSuchElementException} if the iterator + * is already exhausted. If you want such a behavior, use {@link #element()} instead. + *

+ * The rationale behind this is to follow the {@link java.util.Queue} interface + * which uses the same terminology. + * + * @return the next element from the iterator + */ + public E peek(); + + /** + * Returns the next element in iteration without advancing the underlying iterator. + * If the iterator is already exhausted, null will be returned. + * + * @return the next element from the iterator + * @throws NoSuchElementException if the iterator is already exhausted according to {@link #hasNext()} + */ + public E element(); + +} diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PeekingIterator.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PeekingIterator.java new file mode 100755 index 0000000000..98fd0f5ad8 --- /dev/null +++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PeekingIterator.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2011, 2013-2023 The JavaParser Team. + * + * This file is part of JavaParser. + * + * JavaParser can be used either under the terms of + * a) the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * b) the terms of the Apache License + * + * You should have received a copy of both licenses in LICENCE.LGPL and + * LICENCE.APACHE. Please refer to those files for details. + * + * JavaParser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + */ +package com.github.javaparser.printer.lexicalpreservation; + +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.Objects; + +/** + * Decorates an iterator to support one-element lookahead while iterating. + *

+ * The decorator supports the removal operation, but an {@link IllegalStateException} + * will be thrown if {@link #remove()}, {@link #add()}, {@link #set()}} is called directly after a call to + * {@link #peek()} or {@link #element()}. + * + * @param the type of elements returned by this iterator. + * @since 4.0 + */ +public class PeekingIterator implements ListIterator, LookaheadIterator { + + /** The iterator being decorated. */ + private final ListIterator iterator; + + /** Indicates that the decorated iterator is exhausted. */ + private boolean exhausted; + + /** Indicates if the lookahead slot is filled. */ + private boolean slotFilled; + + /** The current slot for lookahead. */ + private E slot; + + /** + * Decorates the specified iterator to support one-element lookahead. + *

+ * If the iterator is already a {@link PeekingIterator} it is returned directly. + * + * @param the element type + * @param iterator the iterator to decorate + * @return a new peeking iterator + * @throws NullPointerException if the iterator is null + */ + public PeekingIterator peekingIterator(final ListIterator iterator) { + Objects.requireNonNull(iterator, "iterator"); + if (iterator instanceof PeekingIterator) { + final PeekingIterator it = (PeekingIterator) iterator; + return it; + } + return new PeekingIterator<>(iterator); + } + + + /** + * Constructor. + * + * @param iterator the iterator to decorate + */ + public PeekingIterator(final ListIterator iterator) { + this.iterator = iterator; + } + + /** + * Constructor. + * + * @param list the provider of the iterator to decorate + */ + public PeekingIterator(final List list) { + this.iterator = list.listIterator(); + } + + private void fill() { + if (exhausted || slotFilled) { + return; + } + if (iterator.hasNext()) { + slot = iterator.next(); + slotFilled = true; + } else { + exhausted = true; + slot = null; + slotFilled = false; + } + } + + @Override + public boolean hasNext() { + if (exhausted) { + return false; + } + return slotFilled || iterator.hasNext(); + } + + /** + * Returns the next element in iteration without advancing the underlying iterator. + * If the iterator is already exhausted, null will be returned. + *

+ * Note: this method does not throw a {@link NoSuchElementException} if the iterator + * is already exhausted. If you want such a behavior, use {@link #element()} instead. + *

+ * The rationale behind this is to follow the {@link java.util.Queue} interface + * which uses the same terminology. + * + * @return the next element from the iterator + */ + @Override + public E peek() { + fill(); + return exhausted ? null : slot; + } + + /** + * Returns the next element in iteration without advancing the underlying iterator. + * If the iterator is already exhausted, null will be returned. + * + * @return the next element from the iterator + * @throws NoSuchElementException if the iterator is already exhausted according to {@link #hasNext()} + */ + @Override + public E element() { + fill(); + if (exhausted) { + throw new NoSuchElementException(); + } + return slot; + } + + @Override + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + final E x = slotFilled ? slot : iterator.next(); + // reset the lookahead slot + slot = null; + slotFilled = false; + return x; + } + + /** + * {@inheritDoc} + * + * @throws IllegalStateException if {@link #peek()} or {@link #element()} has been called + * prior to the call to {@link #remove()} + */ + @Override + public void remove() { + if (slotFilled) { + throw new IllegalStateException("peek() or element() called before remove()"); + } + iterator.remove(); + } + + + @Override + public boolean hasPrevious() { + return iterator.hasPrevious(); + } + + + @Override + public E previous() { + return iterator.previous(); + } + + + @Override + public int nextIndex() { + return iterator.nextIndex(); + } + + /* + * Returns the index of the element that would be returned by the last call to next. + * Returns list size - 1 if the listiterator is at the end of the list. + * Returns -1 if the listiterator is at the beginning of the list. + */ + public int currentIndex() { + if (!hasPrevious()) return previousIndex(); + return nextIndex() - 1; + } + + + @Override + public int previousIndex() { + return iterator.previousIndex(); + } + + + @Override + public void set(E e) { + if (slotFilled) { + throw new IllegalStateException("peek() or element() called before set()"); + } + iterator.set(e); + } + + + @Override + public void add(E e) { + if (slotFilled) { + throw new IllegalStateException("peek() or element() called before add()"); + } + iterator.add(e); + } + +} diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/ReshuffledDiffElementExtractor.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/ReshuffledDiffElementExtractor.java index cc3ad5b11d..041ad5ef7e 100755 --- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/ReshuffledDiffElementExtractor.java +++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/ReshuffledDiffElementExtractor.java @@ -47,11 +47,12 @@ public void extract(List diffElements) { Map correspondanceBetweenNextOrderAndPreviousOrder = getCorrespondanceBetweenNextOrderAndPreviousOrder(elementsFromPreviousOrder, elementsFromNextOrder); // We now find out which Node Text elements corresponds to the elements in the original CSM List nodeTextIndexOfPreviousElements = findIndexOfCorrespondingNodeTextElement(elementsFromPreviousOrder.getElements(), nodeText); + PeekingIterator nodeTextIndexOfPreviousElementsIterator = new PeekingIterator<>(nodeTextIndexOfPreviousElements); Map nodeTextIndexToPreviousCSMIndex = new HashMap<>(); - for (int i = 0; i < nodeTextIndexOfPreviousElements.size(); i++) { - int value = nodeTextIndexOfPreviousElements.get(i); + while (nodeTextIndexOfPreviousElementsIterator.hasNext()) { + int value = nodeTextIndexOfPreviousElementsIterator.next(); if (value != -1) { - nodeTextIndexToPreviousCSMIndex.put(value, i); + nodeTextIndexToPreviousCSMIndex.put(value, nodeTextIndexOfPreviousElementsIterator.currentIndex()); } } int lastNodeTextIndex = nodeTextIndexOfPreviousElements.stream().max(Integer::compareTo).orElse(-1); @@ -159,38 +160,41 @@ private Map getCorrespondanceBetweenNextOrderAndPreviousOrder( private List findIndexOfCorrespondingNodeTextElement(List elements, NodeText nodeText) { List correspondingIndices = new ArrayList<>(); - ListIterator csmElementListIterator = elements.listIterator(); + PeekingIterator csmElementListIterator = new PeekingIterator<>(elements); while ( csmElementListIterator.hasNext() ) { + boolean isFirstIterationOnCsmElements = !csmElementListIterator.hasPrevious(); int previousCsmElementIndex = csmElementListIterator.previousIndex(); CsmElement csmElement = csmElementListIterator.next(); - int nextCsmElementIndex = csmElementListIterator.nextIndex(); Map potentialMatches = new EnumMap<>(MatchClassification.class); - for (int i = 0; i < nodeText.numberOfElements(); i++) { - if (!correspondingIndices.contains(i)) { - TextElement textElement = nodeText.getTextElement(i); + PeekingIterator nodeTextListIterator = new PeekingIterator<>(nodeText.getElements()); + while (nodeTextListIterator.hasNext()) { + boolean isFirstIterationOnNodeTextElements = !nodeTextListIterator.hasPrevious(); + TextElement textElement = nodeTextListIterator.next(); + int currentTextElementIndex = nodeTextListIterator.currentIndex(); + if (!correspondingIndices.contains(currentTextElementIndex)) { boolean isCorresponding = csmElement.isCorrespondingElement(textElement); if (isCorresponding) { boolean hasSamePreviousElement = false; - if (i > 0 && previousCsmElementIndex > -1) { - TextElement previousTextElement = nodeText.getTextElement(i - 1); + if (!isFirstIterationOnNodeTextElements && !isFirstIterationOnCsmElements) { + TextElement previousTextElement = nodeText.getTextElement(currentTextElementIndex - 1); hasSamePreviousElement = elements.get(previousCsmElementIndex).isCorrespondingElement(previousTextElement); } boolean hasSameNextElement = false; - if (i < nodeText.numberOfElements() - 1 && nextCsmElementIndex < elements.size()) { - TextElement nextTextElement = nodeText.getTextElement(i + 1); - hasSameNextElement = elements.get(nextCsmElementIndex).isCorrespondingElement(nextTextElement); + if (csmElementListIterator.hasNext()) { + TextElement nextTextElement = nodeTextListIterator.peek(); + hasSameNextElement = elements.get(csmElementListIterator.nextIndex()).isCorrespondingElement(nextTextElement); } if (hasSamePreviousElement && hasSameNextElement) { - potentialMatches.putIfAbsent(MatchClassification.ALL, i); + potentialMatches.putIfAbsent(MatchClassification.ALL, currentTextElementIndex); } else if (hasSamePreviousElement) { - potentialMatches.putIfAbsent(MatchClassification.PREVIOUS_AND_SAME, i); + potentialMatches.putIfAbsent(MatchClassification.PREVIOUS_AND_SAME, currentTextElementIndex); } else if (hasSameNextElement) { - potentialMatches.putIfAbsent(MatchClassification.NEXT_AND_SAME, i); + potentialMatches.putIfAbsent(MatchClassification.NEXT_AND_SAME, currentTextElementIndex); } else { - potentialMatches.putIfAbsent(MatchClassification.SAME_ONLY, i); + potentialMatches.putIfAbsent(MatchClassification.SAME_ONLY, currentTextElementIndex); } } else if (isAlmostCorrespondingElement(textElement, csmElement)) { - potentialMatches.putIfAbsent(MatchClassification.ALMOST, i); + potentialMatches.putIfAbsent(MatchClassification.ALMOST, currentTextElementIndex); } } }