newIndentation) {
+ elements.clear();
+ elements.addAll(newIndentation);
+ }
+
+ @Override
+ public String toString() {
+ return "IndentationContext{size=" + elements.size() + ", elements=" + elements + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ IndentationContext that = (IndentationContext) o;
+ return elements.equals(that.elements);
+ }
+
+ @Override
+ public int hashCode() {
+ return elements.hashCode();
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/IndexTrackingIterator.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/IndexTrackingIterator.java
new file mode 100755
index 0000000000..a5a3f9bf77
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/IndexTrackingIterator.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2026 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;
+
+/**
+ * A generic iterator that tracks the index of the current element.
+ *
+ * This iterator wraps a standard {@link ListIterator} and maintains
+ * the index of the most recently read element, which can be accessed via
+ * {@link #currentIndex()}.
+ *
+ *
This class provides a generic implementation that can be used with any
+ * type of list elements. It correctly handles bidirectional iteration by
+ * tracking the current index internally.
+ *
+ *
This iterator is particularly useful for operations that need to know
+ * the position of elements during iteration, such as calculating
+ * correspondences between lists or tracking modifications.
+ *
+ * @param the type of elements in the list
+ * @since 3.28.0
+ */
+public class IndexTrackingIterator implements ListIterator {
+
+ private final ListIterator delegate;
+
+ private int currentIndex;
+
+ /**
+ * Creates an iterator starting at the beginning of the list.
+ *
+ * @param elements the list to iterate over
+ */
+ public IndexTrackingIterator(List elements) {
+ this(elements, 0);
+ }
+
+ /**
+ * Creates an iterator starting at the specified index.
+ * The current index is initialized to -1, indicating that no element
+ * has been read yet.
+ *
+ * @param elements the list to iterate over
+ * @param fromIndex the starting index (cursor position)
+ * @throws IndexOutOfBoundsException if fromIndex is out of range
+ */
+ public IndexTrackingIterator(List elements, int fromIndex) {
+ this.delegate = elements.listIterator(fromIndex);
+ // No element read yet
+ this.currentIndex = -1;
+ }
+
+ /**
+ * Returns the index of the element that was returned by the most recent call
+ * to {@link #next()} or {@link #previous()}.
+ *
+ * This method can be called multiple times without side effects - it will
+ * always return the same value until the next call to {@link #next()},
+ * {@link #previous()}, or {@link #remove()}.
+ *
+ *
Important: If neither {@link #next()} nor {@link #previous()} has
+ * been called yet, or if {@link #remove()} was called after the last call to
+ * {@link #next()} or {@link #previous()}, this method returns -1.
+ *
+ *
Note: In the legacy {@code ArrayIterator} class, this method was
+ * named {@code index()}. It has been renamed to {@code currentIndex()} for
+ * better clarity and consistency.
+ *
+ * @return the index of the current element, or -1 if no element has been read
+ * or the current element was removed
+ */
+ public int currentIndex() {
+ return currentIndex;
+ }
+
+ // === LISTITERATOR METHODS WITH INDEX TRACKING ===
+ @Override
+ public boolean hasNext() {
+ return delegate.hasNext();
+ }
+
+ @Override
+ public T next() {
+ T result = delegate.next();
+ // After next(), previousIndex() points to the element we just read
+ currentIndex = delegate.previousIndex();
+ return result;
+ }
+
+ @Override
+ public boolean hasPrevious() {
+ return delegate.hasPrevious();
+ }
+
+ @Override
+ public T previous() {
+ T result = delegate.previous();
+ // After previous(), nextIndex() points to the element we just read
+ currentIndex = delegate.nextIndex();
+ return result;
+ }
+
+ @Override
+ public int nextIndex() {
+ return delegate.nextIndex();
+ }
+
+ @Override
+ public int previousIndex() {
+ return delegate.previousIndex();
+ }
+
+ @Override
+ public void remove() {
+ delegate.remove();
+ // After remove, there is no current element
+ currentIndex = -1;
+ }
+
+ @Override
+ public void set(T element) {
+ delegate.set(element);
+ // Current index doesn't change when replacing an element
+ }
+
+ @Override
+ public void add(T element) {
+ delegate.add(element);
+ // After add(), the added element becomes the current element
+ currentIndex = delegate.previousIndex();
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Kept.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Kept.java
index 47e80befe4..28358de13f 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Kept.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Kept.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalDifferenceCalculator.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalDifferenceCalculator.java
index 0b04499feb..d8bce07ef6 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalDifferenceCalculator.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalDifferenceCalculator.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalPreservingPrinter.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalPreservingPrinter.java
index fa00f905ad..e11e049531 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalPreservingPrinter.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalPreservingPrinter.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
@@ -148,6 +148,24 @@ public void concretePropertyChange(
if (property == ObservableProperty.RANGE || property == ObservableProperty.COMMENTED_NODE) {
return;
}
+ // Handle node replacements with TokenOwnerDetector
+ if (oldValue instanceof Node && newValue instanceof Node) {
+ Node oldNode = (Node) oldValue;
+ Node newNode = (Node) newValue;
+ // Find the actual token owner
+ Node tokenOwner = TokenOwnerDetector.findTokenOwner(oldNode);
+ // Check if we need to regenerate the token owner instead of just the parent
+ if (TokenOwnerDetector.needsRegeneration(observedNode, tokenOwner, oldNode)) {
+ NodeText tokenOwnerText = getOrCreateNodeText(tokenOwner);
+ if (tokenOwnerText != null) {
+ // Regenerate the token owner's NodeText instead of the observed node
+ LEXICAL_DIFFERENCE_CALCULATOR.calculatePropertyChange(
+ tokenOwnerText, tokenOwner, property, oldValue, newValue);
+ // Early exit - we've handled the change
+ return;
+ }
+ }
+ }
if (property == ObservableProperty.COMMENT) {
Optional parentNode = observedNode.getParentNode();
NodeText nodeText = parentNode
@@ -177,13 +195,26 @@ public void concretePropertyChange(
* }
* }
*/
- fixIndentOfAddedNode(nodeText, index - 1);
+ // Extract the existing indentation of the line where we'll insert the comment
+ List precedingElements = nodeText.getElements().subList(0, index);
+ List existingIndent =
+ IndentationCalculator.computeFromPrecedingElements(precedingElements);
+ // Insert the comment (WITHOUT adding indentation before, we'll add it after the EOL)
LineSeparator lineSeparator = observedNode.getLineEndingStyleOrDefault(LineSeparator.SYSTEM);
for (TokenTextElement element : makeCommentTokens((Comment) newValue)) {
nodeText.addElement(index++, element);
}
+ // Insert EOL
nodeText.addToken(index, eolTokenKind(lineSeparator), lineSeparator.asRawString());
- // code indentation after inserting an eol token may be wrong
+ index++;
+ // Restore the indentation for the node that follows
+ for (TextElement indentElement : existingIndent) {
+ if (indentElement instanceof TokenTextElement) {
+ TokenTextElement tokenElement = (TokenTextElement) indentElement;
+ nodeText.addElement(
+ index++, new TokenTextElement(tokenElement.getTokenKind(), tokenElement.getText()));
+ }
+ }
} else if (newValue == null) {
// this case corresponds to a deletion of a comment
if (oldValue instanceof Comment) {
@@ -512,32 +543,33 @@ private void fixIndentOfAddedNode(NodeText nodeText, int index) {
if (index <= 0) {
return;
}
- TextElement currentSpaceCandidate = null;
+ // finds the existing indentation
+ List existingIndent = IndentationCalculator.computeFromPrecedingElements(
+ nodeText.getElements().subList(0, index + 1));
+ if (existingIndent.isEmpty()) {
+ return;
+ }
+ // Find the last newline before index
+ int lastNewlineIndex = -1;
for (int i = index; i >= 0; i--) {
- TextElement spaceCandidate = nodeText.getTextElement(i);
- if (spaceCandidate.isSpaceOrTab()) {
- // save the current indentation char
- currentSpaceCandidate = nodeText.getTextElement(i);
- }
- if (!spaceCandidate.isSpaceOrTab()) {
- if (spaceCandidate.isNewline() && i != index) {
- int numberOfIndentationCharacters = index - i;
- for (int j = 0; j < numberOfIndentationCharacters; j++) {
- if (currentSpaceCandidate != null) {
- // use the current (or last) indentation character
- nodeText.addElement(
- index,
- new TokenTextElement(
- JavaToken.Kind.SPACE.getKind(), currentSpaceCandidate.expand()));
- } else {
- // use the default indentation character
- nodeText.addElement(index, new TokenTextElement(JavaToken.Kind.SPACE.getKind()));
- }
- }
- }
+ if (nodeText.getTextElement(i).isNewline()) {
+ lastNewlineIndex = i;
break;
}
}
+ // If there is no newline or if the newline is just before the index, do nothing.
+ if (lastNewlineIndex == -1 || lastNewlineIndex == index - 1) {
+ return;
+ }
+ // Apply the computed indentation
+ // The indentation elements are inserted at the given index.
+ for (TextElement indentElement : existingIndent) {
+ if (indentElement instanceof TokenTextElement) {
+ TokenTextElement tokenElement = (TokenTextElement) indentElement;
+ nodeText.addElement(
+ index, new TokenTextElement(tokenElement.getTokenKind(), tokenElement.getText()));
+ }
+ }
}
@Override
@@ -728,7 +760,8 @@ private static void prettyPrintingTextNode(Node node, NodeText nodeText) {
private static NodeText interpret(Node node, CsmElement csm, NodeText nodeText) {
LexicalDifferenceCalculator.CalculatedSyntaxModel calculatedSyntaxModel =
new LexicalDifferenceCalculator().calculatedSyntaxModelForNode(csm, node);
- List indentation = findIndentation(node);
+ List initialIndentation = findIndentation(node);
+ IndentationContext indentationContext = new IndentationContext(initialIndentation);
boolean pendingIndentation = false;
// Add a comment and line separator if necessary
node.getComment().ifPresent(comment -> {
@@ -745,17 +778,13 @@ private static NodeText interpret(Node node, CsmElement csm, NodeText nodeText)
int indexCurrentElement = calculatedSyntaxModel.elements.indexOf(element);
if (calculatedSyntaxModel.elements.size() > indexCurrentElement
&& !(calculatedSyntaxModel.elements.get(indexCurrentElement + 1) instanceof CsmUnindent)) {
- for (int i = 0; i < Difference.STANDARD_INDENTATION_SIZE; i++) {
- indentation.add(new TokenTextElement(SPACE, " "));
- }
+ indentationContext.increase();
}
} else if (element instanceof CsmUnindent) {
- for (int i = 0; i < Difference.STANDARD_INDENTATION_SIZE && indentation.size() > 0; i++) {
- indentation.remove(indentation.size() - 1);
- }
+ indentationContext.decrease();
}
if (pendingIndentation && !(element instanceof CsmToken && ((CsmToken) element).isNewLine())) {
- indentation.forEach(nodeText::addElement);
+ indentationContext.getCurrent().forEach(nodeText::addElement);
}
pendingIndentation = false;
if (element instanceof LexicalDifferenceCalculator.CsmChild) {
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalPreservingVisitor.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalPreservingVisitor.java
index 46eeb36fe9..51f7301c99 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalPreservingVisitor.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/LexicalPreservingVisitor.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013-2024 The JavaParser Team.
+ * Copyright (C) 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
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
index 0cece8c009..a3ab58bcc0 100644
--- 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
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013-2024 The JavaParser Team.
+ * Copyright (C) 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/NodeText.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/NodeText.java
index 642ee82109..b88e078728 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/NodeText.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/NodeText.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
@@ -28,18 +28,28 @@
/**
* This contains the lexical information for a single node.
* It is basically a list of tokens and children.
+ *
+ * This class has been refactored to use {@link TextElementList} internally
+ * for better code reuse and maintainability, while preserving the exact same
+ * external API and behavior.
*/
class NodeText {
- private final List elements;
+ // Changed from List to TextElementList for internal optimization
+ private final TextElementList elements;
public static final int NOT_FOUND = -1;
//
// Constructors
//
+ /**
+ * Creates a NodeText wrapping the given list of elements.
+ *
+ * @param elements the list to wrap (will be wrapped in TextElementList)
+ */
NodeText(List elements) {
- this.elements = elements;
+ this.elements = new TextElementList(elements);
}
/**
@@ -56,14 +66,14 @@ class NodeText {
* Add an element at the end.
*/
void addElement(TextElement nodeTextElement) {
- this.elements.add(nodeTextElement);
+ elements.insert(elements.size(), nodeTextElement);
}
/**
* Add an element at the given position.
*/
void addElement(int index, TextElement nodeTextElement) {
- this.elements.add(index, nodeTextElement);
+ elements.insert(index, nodeTextElement);
}
void addChild(Node child) {
@@ -75,51 +85,96 @@ void addChild(int index, Node child) {
}
void addToken(int tokenKind, String text) {
- elements.add(new TokenTextElement(tokenKind, text));
+ addElement(new TokenTextElement(tokenKind, text));
}
void addToken(int index, int tokenKind, String text) {
- elements.add(index, new TokenTextElement(tokenKind, text));
+ addElement(index, new TokenTextElement(tokenKind, text));
}
//
// Finding elements
//
+ /**
+ * Finds the first element matching the given matcher.
+ *
+ * @param matcher the matcher to use
+ * @return the index of the first matching element
+ * @throws IllegalArgumentException if no matching element is found
+ */
int findElement(TextElementMatcher matcher) {
return findElement(matcher, 0);
}
+ /**
+ * Finds the first element matching the given matcher, starting from the given index.
+ *
+ * @param matcher the matcher to use
+ * @param from the starting index (inclusive)
+ * @return the index of the first matching element
+ * @throws IllegalArgumentException if no matching element is found
+ */
int findElement(TextElementMatcher matcher, int from) {
int res = tryToFindElement(matcher, from);
if (res == NOT_FOUND) {
throw new IllegalArgumentException(String.format(
- "I could not find child '%s' from position %d. Elements: %s", matcher, from, elements));
+ "I could not find child '%s' from position %d. Elements: %s", matcher, from, elements.toList()));
}
return res;
}
+ /**
+ * Tries to find an element matching the given matcher, starting from the given index.
+ * Returns NOT_FOUND if no matching element is found.
+ *
+ * @param matcher the matcher to use
+ * @param from the starting index (inclusive)
+ * @return the index of the first matching element, or NOT_FOUND
+ */
int tryToFindElement(TextElementMatcher matcher, int from) {
- for (int i = from; i < elements.size(); i++) {
- TextElement element = elements.get(i);
- if (matcher.match(element)) {
- return i;
- }
- }
- return NOT_FOUND;
+ return elements.findNext(from, matcher::match);
}
+ /**
+ * Finds the first occurrence of the given child node.
+ *
+ * @param child the child to find
+ * @return the index of the child
+ * @throws IllegalArgumentException if child is not found
+ */
int findChild(Node child) {
return findChild(child, 0);
}
+ /**
+ * Finds the first occurrence of the given child node, starting from the given index.
+ *
+ * @param child the child to find
+ * @param from the starting index (inclusive)
+ * @return the index of the child
+ * @throws IllegalArgumentException if child is not found
+ */
int findChild(Node child, int from) {
return findElement(TextElementMatchers.byNode(child), from);
}
+ /**
+ * Tries to find the first occurrence of the given child node.
+ *
+ * @param child the child to find
+ * @return the index of the child, or NOT_FOUND
+ */
int tryToFindChild(Node child) {
return tryToFindChild(child, 0);
}
+ /**
+ * Tries to find the first occurrence of the given child node, starting from the given index.
+ *
+ * @param child the child to find
+ * @param from the starting index (inclusive)
+ * @return the index of the child, or NOT_FOUND
+ */
int tryToFindChild(Node child, int from) {
return tryToFindElement(TextElementMatchers.byNode(child), from);
}
@@ -127,29 +182,43 @@ int tryToFindChild(Node child, int from) {
//
// Removing single elements
//
+ /**
+ * Removes the first element matching the given matcher.
+ * Optionally removes following whitespace.
+ *
+ * @param matcher the matcher to use
+ * @param potentiallyFollowingWhitespace if true, removes following whitespace element
+ * @throws IllegalArgumentException if no matching element is found
+ * @throws UnsupportedOperationException if whitespace removal is requested but no element follows
+ */
public void remove(TextElementMatcher matcher, boolean potentiallyFollowingWhitespace) {
- int i = 0;
- for (TextElement e : elements) {
- if (matcher.match(e)) {
- elements.remove(e);
- if (potentiallyFollowingWhitespace) {
- if (i < elements.size()) {
- if (elements.get(i).isWhiteSpace()) {
- elements.remove(i);
- }
- } else {
- throw new UnsupportedOperationException("There is no element to remove!");
- }
+ // Find the matching element using our optimized search
+ int index = tryToFindElement(matcher, 0);
+ if (index == NOT_FOUND) {
+ throw new IllegalArgumentException("No matching element found");
+ }
+ // Remove the element
+ elements.remove(index);
+ // Optionally remove following whitespace
+ if (potentiallyFollowingWhitespace) {
+ if (index < elements.size()) {
+ if (elements.get(index).isWhiteSpace()) {
+ elements.remove(index);
}
- return;
+ } else {
+ throw new UnsupportedOperationException("There is no element to remove!");
}
}
- throw new IllegalArgumentException();
}
//
// Removing sequences
//
+ /**
+ * Removes the element at the given index.
+ *
+ * @param index the index of the element to remove
+ */
void removeElement(int index) {
elements.remove(index);
}
@@ -157,47 +226,85 @@ void removeElement(int index) {
//
// Replacing elements
//
+ /**
+ * Replaces the element at the position matched by the given matcher
+ * with the given new element.
+ *
+ * @param position the matcher to find the element to replace
+ * @param newElement the new element
+ * @throws IllegalArgumentException if no matching element is found
+ */
void replace(TextElementMatcher position, TextElement newElement) {
int index = findElement(position, 0);
elements.remove(index);
- elements.add(index, newElement);
+ elements.insert(index, newElement);
}
+ /**
+ * Replaces the element at the position matched by the given matcher
+ * with the given collection of new elements.
+ *
+ * @param position the matcher to find the element to replace
+ * @param newElements the new elements
+ * @throws IllegalArgumentException if no matching element is found
+ */
void replace(TextElementMatcher position, Collection extends TextElement> newElements) {
int index = findElement(position, 0);
elements.remove(index);
- elements.addAll(index, newElements);
+ elements.insertAll(index, (List) newElements);
}
//
// Other methods
//
/**
- * Generate the corresponding string.
+ * Generate the corresponding string by expanding all elements.
+ *
+ * @return the expanded string representation
*/
String expand() {
- StringBuffer sb = new StringBuffer();
- elements.forEach(e -> sb.append(e.expand()));
+ StringBuilder sb = new StringBuilder();
+ // Use the underlying list's forEach for efficiency
+ elements.toList().forEach(e -> sb.append(e.expand()));
return sb.toString();
}
- // Visible for testing
+ /**
+ * Returns the number of elements.
+ * Visible for testing.
+ *
+ * @return the number of elements
+ */
int numberOfElements() {
return elements.size();
}
- // Visible for testing
+ /**
+ * Returns the element at the given index.
+ * Visible for testing.
+ *
+ * @param index the index
+ * @return the element at that index
+ */
TextElement getTextElement(int index) {
return elements.get(index);
}
- // Visible for testing
+ /**
+ * Returns the underlying list of elements.
+ * Visible for testing.
+ *
+ * IMPORTANT: This returns the internal mutable list.
+ * External modifications will affect this NodeText.
+ *
+ * @return the list of elements (mutable)
+ */
List getElements() {
- return elements;
+ return elements.toMutableList();
}
@Override
public String toString() {
- return "NodeText{" + elements + '}';
+ return "NodeText{" + elements.toList() + '}';
}
}
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
index 42c0a1e2ce..dd92ebb440 100755
--- 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
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PhantomNodeLogic.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PhantomNodeLogic.java
index 6e8a2605a0..4f7bd15615 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PhantomNodeLogic.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PhantomNodeLogic.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PrintableTextElement.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PrintableTextElement.java
index 944438d5af..3a3377ef34 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PrintableTextElement.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/PrintableTextElement.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013-2024 The JavaParser Team.
+ * Copyright (C) 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Removed.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Removed.java
index 6b40c43fe0..05e27dac76 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Removed.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Removed.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/RemovedGroup.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/RemovedGroup.java
index 18af690ba9..383e6cc2a6 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/RemovedGroup.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/RemovedGroup.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Reshuffled.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Reshuffled.java
index 4d8669fe55..35732b56da 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Reshuffled.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/Reshuffled.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
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 d2deffa021..916b47fee8 100644
--- 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
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013-2024 The JavaParser Team.
+ * Copyright (C) 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
@@ -22,7 +22,6 @@
import com.github.javaparser.printer.concretesyntaxmodel.CsmElement;
import com.github.javaparser.printer.concretesyntaxmodel.CsmMix;
import com.github.javaparser.printer.concretesyntaxmodel.CsmToken;
-import com.github.javaparser.printer.lexicalpreservation.Difference.ArrayIterator;
import java.util.*;
public class ReshuffledDiffElementExtractor {
@@ -56,7 +55,7 @@ private ReshuffledDiffElementExtractor(NodeText nodeText) {
}
public void extract(List diffElements) {
- ArrayIterator iterator = new ArrayIterator<>(diffElements);
+ IndexTrackingIterator iterator = new IndexTrackingIterator<>(diffElements);
while (iterator.hasNext()) {
DifferenceElement diffElement = iterator.next();
if (diffElement instanceof Reshuffled) {
@@ -170,21 +169,21 @@ public void extract(List diffElements) {
private Map getCorrespondanceBetweenNextOrderAndPreviousOrder(
CsmMix elementsFromPreviousOrder, CsmMix elementsFromNextOrder) {
Map correspondanceBetweenNextOrderAndPreviousOrder = new HashMap<>();
- ArrayIterator previousOrderElementsIterator =
- new ArrayIterator<>(elementsFromPreviousOrder.getElements());
+ IndexTrackingIterator previousOrderElementsIterator =
+ new IndexTrackingIterator<>(elementsFromPreviousOrder.getElements());
int syncNextIndex = 0;
while (previousOrderElementsIterator.hasNext()) {
CsmElement pe = previousOrderElementsIterator.next();
- ArrayIterator nextOrderElementsIterator =
- new ArrayIterator<>(elementsFromNextOrder.getElements(), syncNextIndex);
+ IndexTrackingIterator nextOrderElementsIterator =
+ new IndexTrackingIterator<>(elementsFromNextOrder.getElements(), syncNextIndex);
while (nextOrderElementsIterator.hasNext()) {
CsmElement ne = nextOrderElementsIterator.next();
if (!correspondanceBetweenNextOrderAndPreviousOrder
.values()
- .contains(previousOrderElementsIterator.index())
+ .contains(previousOrderElementsIterator.currentIndex())
&& DifferenceElementCalculator.matching(ne, pe)) {
correspondanceBetweenNextOrderAndPreviousOrder.put(
- nextOrderElementsIterator.index(), previousOrderElementsIterator.index());
+ nextOrderElementsIterator.currentIndex(), previousOrderElementsIterator.currentIndex());
// set the position to start on the next {@code nextOrderElementsIterator} iteration
syncNextIndex = nextOrderElementsIterator.nextIndex();
break;
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElement.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElement.java
index 06d7bfdfcc..22c3193ae6 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElement.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElement.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementIterator.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementIterator.java
new file mode 100755
index 0000000000..9a37b172d3
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementIterator.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2011, 2013-2026 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;
+
+/**
+ * Iterator that tracks its current position in a TextElement list.
+ *
+ * Unlike standard {@link ListIterator}, this class provides direct access
+ * to the current index via {@link #currentIndex()}, which returns the index of
+ * the element that was returned by the most recent call to {@link #next()} or
+ * {@link #previous()}.
+ *
+ *
This iterator is a replacement for the internal {@code ArrayIterator} class,
+ * with clearer semantics and better naming. It correctly handles bidirectional
+ * iteration by maintaining the current index internally.
+ *
+ * @since 3.28.0
+ */
+public class TextElementIterator implements ListIterator {
+
+ private final ListIterator delegate;
+
+ private int currentIndex;
+
+ /**
+ * Creates an iterator starting at the specified index.
+ * The current index is initialized to -1, indicating that no element
+ * has been read yet.
+ *
+ * @param elements the list to iterate over
+ * @param fromIndex the starting index (cursor position)
+ * @throws IndexOutOfBoundsException if fromIndex is out of range
+ */
+ public TextElementIterator(List elements, int fromIndex) {
+ this.delegate = elements.listIterator(fromIndex);
+ // No element read yet
+ this.currentIndex = -1;
+ }
+
+ /**
+ * Returns the index of the element that was returned by the most recent call
+ * to {@link #next()} or {@link #previous()}.
+ *
+ * This method can be called multiple times without side effects - it will
+ * always return the same value until the next call to {@link #next()},
+ * {@link #previous()}, or {@link #remove()}.
+ *
+ *
Important: If neither {@link #next()} nor {@link #previous()} has
+ * been called yet, or if {@link #remove()} was called after the last call to
+ * {@link #next()} or {@link #previous()}, this method returns -1.
+ *
+ * @return the index of the current element, or -1 if no element has been read
+ * or the current element was removed
+ */
+ public int currentIndex() {
+ return currentIndex;
+ }
+
+ // === LISTITERATOR METHODS WITH INDEX TRACKING ===
+ @Override
+ public boolean hasNext() {
+ return delegate.hasNext();
+ }
+
+ @Override
+ public TextElement next() {
+ TextElement result = delegate.next();
+ // After next(), previousIndex() points to the element we just read
+ currentIndex = delegate.previousIndex();
+ return result;
+ }
+
+ @Override
+ public boolean hasPrevious() {
+ return delegate.hasPrevious();
+ }
+
+ @Override
+ public TextElement previous() {
+ TextElement result = delegate.previous();
+ // After previous(), nextIndex() points to the element we just read
+ currentIndex = delegate.nextIndex();
+ return result;
+ }
+
+ @Override
+ public int nextIndex() {
+ return delegate.nextIndex();
+ }
+
+ @Override
+ public int previousIndex() {
+ return delegate.previousIndex();
+ }
+
+ @Override
+ public void remove() {
+ delegate.remove();
+ // After remove, there is no current element
+ currentIndex = -1;
+ }
+
+ @Override
+ public void set(TextElement element) {
+ delegate.set(element);
+ // Current index doesn't change when replacing an element
+ }
+
+ @Override
+ public void add(TextElement element) {
+ delegate.add(element);
+ // After add(), the added element becomes the current element
+ currentIndex = delegate.previousIndex();
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementIteratorsFactory.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementIteratorsFactory.java
index 1d4b8abedc..0920a748da 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementIteratorsFactory.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementIteratorsFactory.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementList.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementList.java
new file mode 100755
index 0000000000..698891a2a6
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementList.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2026 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.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+/**
+ * Default mutable implementation of {@link TextElementSequence}.
+ *
+ *
This class wraps an existing {@code List} without copying it.
+ * All mutations directly affect the underlying list.
+ *
+ * The implementation provides optimized search operations and clear semantics
+ * for index-based manipulations required by lexical preservation operations.
+ *
+ * @since 3.28.0
+ */
+public class TextElementList implements TextElementSequence {
+
+ private final List elements;
+
+ /**
+ * Creates a wrapper around the given list.
+ * The list is NOT copied, mutations affect the original.
+ *
+ * @param elements the list to wrap
+ * @throws NullPointerException if elements is null
+ */
+ public TextElementList(List elements) {
+ this.elements = Objects.requireNonNull(elements, "elements cannot be null");
+ }
+
+ /**
+ * Creates a new TextElementList containing the given elements.
+ *
+ * @param elements varargs of elements
+ * @return a new list
+ */
+ public static TextElementList of(TextElement... elements) {
+ return new TextElementList(new ArrayList<>(Arrays.asList(elements)));
+ }
+
+ /**
+ * Creates a new TextElementList wrapping the given list.
+ *
+ * IMPORTANT: This method wraps the list directly without copying.
+ * Modifications to the TextElementList will affect the original list.
+ * Use {@link #copyOf(List)} if you need an independent copy.
+ *
+ *
This method is useful for chaining operations:
+ *
{@code
+ * List result = TextElementList.of(list.subList(0, 10))
+ * .takeWhile(TextElement::isSpaceOrTab);
+ * }
+ *
+ * @param elements the list to wrap (not copied)
+ * @return a new TextElementList wrapping the given list
+ * @throws NullPointerException if elements is null
+ */
+ public static TextElementList of(List elements) {
+ return new TextElementList(elements);
+ }
+
+ /**
+ * Creates an empty mutable TextElementList.
+ *
+ * @return an empty list
+ */
+ public static TextElementList empty() {
+ return new TextElementList(new ArrayList<>());
+ }
+
+ /**
+ * Creates a new TextElementList with a copy of the given list.
+ *
+ * @param elements the list to copy
+ * @return a new list with copied elements
+ * @throws NullPointerException if elements is null
+ */
+ public static TextElementList copyOf(List elements) {
+ return new TextElementList(new ArrayList<>(elements));
+ }
+
+ // === SEARCH BY PREDICATE ===
+ @Override
+ public int findFirst(Predicate predicate) {
+ Objects.requireNonNull(predicate, "predicate cannot be null");
+ for (int i = 0; i < elements.size(); i++) {
+ if (predicate.test(elements.get(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int findLast(Predicate predicate) {
+ Objects.requireNonNull(predicate, "predicate cannot be null");
+ for (int i = elements.size() - 1; i >= 0; i--) {
+ if (predicate.test(elements.get(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int findNext(int fromIndex, Predicate predicate) {
+ Objects.requireNonNull(predicate, "predicate cannot be null");
+ if (!isValidIndex(fromIndex)) {
+ return -1;
+ }
+ for (int i = fromIndex; i < elements.size(); i++) {
+ if (predicate.test(elements.get(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int findPrevious(int fromIndex, Predicate predicate) {
+ Objects.requireNonNull(predicate, "predicate cannot be null");
+ if (fromIndex < 0 || fromIndex >= elements.size()) {
+ return -1;
+ }
+ for (int i = fromIndex; i >= 0; i--) {
+ if (predicate.test(elements.get(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ // === FILTERING ===
+ @Override
+ public List takeWhile(Predicate predicate) {
+ Objects.requireNonNull(predicate, "predicate cannot be null");
+ List result = new ArrayList<>();
+ for (TextElement element : elements) {
+ if (!predicate.test(element)) {
+ break;
+ }
+ result.add(element);
+ }
+ return result;
+ }
+
+ @Override
+ public List subList(int fromIndex, int toIndex) {
+ return elements.subList(fromIndex, toIndex);
+ }
+
+ // === MUTATION ===
+ @Override
+ public void insert(int index, TextElement element) {
+ Objects.requireNonNull(element, "element cannot be null");
+ elements.add(index, element);
+ }
+
+ @Override
+ public void insertAll(int index, List elementsToInsert) {
+ Objects.requireNonNull(elementsToInsert, "elements cannot be null");
+ elements.addAll(index, elementsToInsert);
+ }
+
+ @Override
+ public void remove(int index) {
+ elements.remove(index);
+ }
+
+ @Override
+ public void removeRange(int fromIndex, int toIndex) {
+ if (!isValidIndex(fromIndex) || !isValidIndex(toIndex) || fromIndex > toIndex) {
+ throw new IndexOutOfBoundsException(
+ "Invalid range: [" + fromIndex + ", " + toIndex + "] for size " + elements.size());
+ }
+ // toIndex is inclusive, subList.clear() expects exclusive upper bound
+ elements.subList(fromIndex, toIndex + 1).clear();
+ }
+
+ // === ACCESS ===
+ @Override
+ public TextElement get(int index) {
+ return elements.get(index);
+ }
+
+ @Override
+ public boolean isValidIndex(int index) {
+ return index >= 0 && index < elements.size();
+ }
+
+ @Override
+ public int size() {
+ return elements.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return elements.isEmpty();
+ }
+
+ @Override
+ public List toList() {
+ return Collections.unmodifiableList(elements);
+ }
+
+ @Override
+ public List toMutableList() {
+ return elements;
+ }
+
+ // === MATCHING (TERMINAL OPERATIONS) ===
+ @Override
+ public boolean anyMatch(Predicate predicate) {
+ Objects.requireNonNull(predicate, "predicate cannot be null");
+ for (TextElement element : elements) {
+ if (predicate.test(element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean allMatch(Predicate predicate) {
+ Objects.requireNonNull(predicate, "predicate cannot be null");
+ for (TextElement element : elements) {
+ if (!predicate.test(element)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean noneMatch(Predicate predicate) {
+ Objects.requireNonNull(predicate, "predicate cannot be null");
+ return !anyMatch(predicate);
+ }
+
+ // === ITERATION ===
+ @Override
+ public TextElementIterator iterator(int fromIndex) {
+ return new TextElementIterator(elements, fromIndex);
+ }
+
+ @Override
+ public String toString() {
+ return "TextElementList{size=" + elements.size() + "}";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof TextElementList)) return false;
+ TextElementList other = (TextElementList) obj;
+ return elements.equals(other.elements);
+ }
+
+ @Override
+ public int hashCode() {
+ return elements.hashCode();
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatcher.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatcher.java
index 696d998e70..20c05fdef2 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatcher.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatcher.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatchers.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatchers.java
index 3a21ddac4c..fee837ff68 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatchers.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementMatchers.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementSequence.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementSequence.java
new file mode 100755
index 0000000000..6128d23f7b
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TextElementSequence.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2026 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.Objects;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+/**
+ * Contract for a specialized sequence of TextElements with enhanced search
+ * and modification capabilities for lexical preservation operations.
+ *
+ * Unlike standard {@link List}, this interface provides:
+ *
+ * - Index-based search with predicates (findFirst, findLast, findNext, findPrevious)
+ * - Element-based search (indexOf, lastIndexOf with overloads)
+ * - Controlled mutations (insert, remove) where caller manages index adjustments
+ *
+ *
+ * Thread safety: Implementations are not required to be thread-safe.
+ *
+ *
Index management: Mutation operations modify the underlying list directly.
+ * Callers are responsible for tracking index changes after mutations.
+ *
+ * @since 3.28.0
+ */
+public interface TextElementSequence {
+
+ // === SEARCH BY PREDICATE ===
+ /**
+ * Finds the first index where the predicate matches, searching forward from index 0.
+ *
+ * @param predicate the condition to test
+ * @return the first matching index, or -1 if no match found
+ * @throws NullPointerException if predicate is null
+ */
+ int findFirst(Predicate predicate);
+
+ /**
+ * Finds the last index where the predicate matches, searching backward from the end.
+ *
+ * @param predicate the condition to test
+ * @return the last matching index, or -1 if no match found
+ * @throws NullPointerException if predicate is null
+ */
+ int findLast(Predicate predicate);
+
+ /**
+ * Finds the next index where the predicate matches, searching forward from fromIndex (inclusive).
+ *
+ * @param fromIndex the starting index (inclusive)
+ * @param predicate the condition to test
+ * @return the next matching index, or -1 if no match found
+ * @throws NullPointerException if predicate is null
+ */
+ int findNext(int fromIndex, Predicate predicate);
+
+ /**
+ * Finds the previous index where the predicate matches, searching backward from fromIndex (inclusive).
+ *
+ * @param fromIndex the starting index (inclusive)
+ * @param predicate the condition to test
+ * @return the previous matching index, or -1 if no match found
+ * @throws NullPointerException if predicate is null
+ */
+ int findPrevious(int fromIndex, Predicate predicate);
+
+ // === MATCHING (TERMINAL OPERATIONS) ===
+ /**
+ * Tests whether any element in this sequence matches the given predicate.
+ *
+ * This is a short-circuiting terminal operation: it stops as soon as
+ * a matching element is found and returns true immediately.
+ *
+ *
Examples:
+ *
{@code
+ * // Check if list contains any comment
+ * boolean hasComment = list.anyMatch(TextElement::isComment);
+ *
+ * // Check if list contains any token with specific text
+ * boolean hasIdentifier = list.anyMatch(el ->
+ * el instanceof TokenTextElement &&
+ * ((TokenTextElement) el).getText().equals("myVar")
+ * );
+ * }
+ *
+ * @param predicate the predicate to test elements against
+ * @return true if any element matches the predicate, false otherwise
+ * (returns false for empty sequences)
+ * @throws NullPointerException if predicate is null
+ */
+ boolean anyMatch(Predicate predicate);
+
+ /**
+ * Tests whether all elements in this sequence match the given predicate.
+ *
+ * This is a short-circuiting terminal operation: it stops as soon as
+ * a non-matching element is found and returns false immediately.
+ *
+ *
Returns true for empty sequences (vacuous truth).
+ *
+ *
Examples:
+ *
{@code
+ * // Check if all elements are whitespace
+ * boolean allWhitespace = list.allMatch(TextElement::isSpaceOrTab);
+ *
+ * // Check if all elements are comments
+ * boolean allComments = list.allMatch(TextElement::isComment);
+ * }
+ *
+ * @param predicate the predicate to test elements against
+ * @return true if all elements match the predicate (or sequence is empty),
+ * false otherwise
+ * @throws NullPointerException if predicate is null
+ */
+ boolean allMatch(Predicate predicate);
+
+ /**
+ * Tests whether no elements in this sequence match the given predicate.
+ *
+ * This is a short-circuiting terminal operation: it stops as soon as
+ * a matching element is found and returns false immediately.
+ *
+ *
Returns true for empty sequences.
+ *
+ *
Equivalent to {@code !anyMatch(predicate)}.
+ *
+ *
Examples:
+ *
{@code
+ * // Check if list has no comments
+ * boolean noComments = list.noneMatch(TextElement::isComment);
+ *
+ * // Check if list has no newlines
+ * boolean noNewlines = list.noneMatch(TextElement::isNewline);
+ * }
+ *
+ * @param predicate the predicate to test elements against
+ * @return true if no elements match the predicate (or sequence is empty),
+ * false otherwise
+ * @throws NullPointerException if predicate is null
+ */
+ boolean noneMatch(Predicate predicate);
+
+ // === SEARCH BY ELEMENT ===
+ /**
+ * Finds the first occurrence of the specified element.
+ * Equivalent to {@code findFirst(e -> Objects.equals(e, element))}.
+ *
+ * @param element the element to search for (may be null)
+ * @return the first occurrence index, or -1 if not found
+ */
+ default int indexOf(TextElement element) {
+ return findFirst(e -> Objects.equals(e, element));
+ }
+
+ /**
+ * Finds the last occurrence of the specified element.
+ * Equivalent to {@code findLast(e -> Objects.equals(e, element))}.
+ *
+ * @param element the element to search for (may be null)
+ * @return the last occurrence index, or -1 if not found
+ */
+ default int lastIndexOf(TextElement element) {
+ return findLast(e -> Objects.equals(e, element));
+ }
+
+ /**
+ * Finds the next occurrence of element starting from fromIndex (inclusive).
+ * Equivalent to {@code findNext(fromIndex, e -> Objects.equals(e, element))}.
+ *
+ * @param fromIndex the starting index (inclusive)
+ * @param element the element to search for (may be null)
+ * @return the next occurrence index, or -1 if not found
+ */
+ default int indexOf(int fromIndex, TextElement element) {
+ return findNext(fromIndex, e -> Objects.equals(e, element));
+ }
+
+ /**
+ * Finds the previous occurrence of element before fromIndex (inclusive).
+ * Equivalent to {@code findPrevious(fromIndex, e -> Objects.equals(e, element))}.
+ *
+ * @param fromIndex the starting index (inclusive)
+ * @param element the element to search for (may be null)
+ * @return the previous occurrence index, or -1 if not found
+ */
+ default int lastIndexOf(int fromIndex, TextElement element) {
+ return findPrevious(fromIndex, e -> Objects.equals(e, element));
+ }
+
+ // === FILTERING ===
+ /**
+ * Returns a new list containing elements from the start until the predicate fails.
+ * The returned list is independent of this sequence.
+ *
+ * @param predicate the condition to test
+ * @return a new list of matching elements
+ * @throws NullPointerException if predicate is null
+ */
+ List takeWhile(Predicate predicate);
+
+ /**
+ * Returns a sublist view [fromIndex, toIndex).
+ * The returned list is backed by this sequence, so changes affect both.
+ *
+ * @param fromIndex low endpoint (inclusive)
+ * @param toIndex high endpoint (exclusive)
+ * @return a sublist view
+ * @throws IndexOutOfBoundsException if indices are out of range
+ */
+ List subList(int fromIndex, int toIndex);
+
+ // === MUTATION ===
+ /**
+ * Inserts element at the specified index.
+ * WARNING: Caller must adjust subsequent indices manually.
+ *
+ * @param index position to insert at
+ * @param element element to insert
+ * @throws IndexOutOfBoundsException if index is out of range
+ * @throws NullPointerException if element is null
+ */
+ void insert(int index, TextElement element);
+
+ /**
+ * Inserts all elements at the specified index.
+ * WARNING: Caller must adjust subsequent indices manually.
+ *
+ * @param index position to insert at
+ * @param elements elements to insert
+ * @throws IndexOutOfBoundsException if index is out of range
+ * @throws NullPointerException if elements is null
+ */
+ void insertAll(int index, List elements);
+
+ /**
+ * Removes the element at the specified index.
+ * WARNING: Caller must adjust subsequent indices manually.
+ *
+ * @param index position to remove from
+ * @throws IndexOutOfBoundsException if index is out of range
+ */
+ void remove(int index);
+
+ /**
+ * Removes elements in range [fromIndex, toIndex] (inclusive on both ends).
+ * WARNING: Caller must adjust subsequent indices manually.
+ *
+ * @param fromIndex start of range (inclusive)
+ * @param toIndex end of range (inclusive)
+ * @throws IndexOutOfBoundsException if indices are out of range or fromIndex > toIndex
+ */
+ void removeRange(int fromIndex, int toIndex);
+
+ // === ACCESS ===
+ /**
+ * Returns the element at the specified index.
+ *
+ * @param index the index
+ * @return the element at that position
+ * @throws IndexOutOfBoundsException if index is out of range
+ */
+ TextElement get(int index);
+
+ /**
+ * Checks if the index is valid (0 <= index < size).
+ *
+ * @param index the index to check
+ * @return true if index is valid
+ */
+ boolean isValidIndex(int index);
+
+ /**
+ * Returns the number of elements in this sequence.
+ *
+ * @return the size
+ */
+ int size();
+
+ /**
+ * Checks if this sequence is empty.
+ *
+ * @return true if size is 0
+ */
+ boolean isEmpty();
+
+ /**
+ * Returns an unmodifiable view of the underlying list.
+ * Changes to the original list are visible in the returned view.
+ *
+ * @return an unmodifiable list view
+ */
+ List toList();
+
+ /**
+ * Returns the underlying mutable list.
+ *
+ * WARNING: This exposes the internal list directly.
+ * Modifications will affect this sequence.
+ *
+ * @return the mutable list
+ */
+ List toMutableList();
+
+ // === ITERATION ===
+ /**
+ * Returns an iterator starting at the specified index.
+ *
+ * @param fromIndex the starting position
+ * @return an iterator with position tracking
+ * @throws IndexOutOfBoundsException if fromIndex is out of range
+ */
+ TextElementIterator iterator(int fromIndex);
+
+ /**
+ * Returns an iterator starting at index 0.
+ *
+ * @return an iterator from the beginning
+ */
+ default TextElementIterator iterator() {
+ return iterator(0);
+ }
+
+ /**
+ * Returns a stream of elements for functional operations.
+ *
+ * @return a stream over the elements
+ */
+ default Stream stream() {
+ return toList().stream();
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TokenOwnerDetector.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TokenOwnerDetector.java
new file mode 100755
index 0000000000..69d3265f48
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TokenOwnerDetector.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ */
+package com.github.javaparser.printer.lexicalpreservation;
+
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.body.FieldDeclaration;
+import com.github.javaparser.ast.type.Type;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Detects which node actually owns the tokens for a given node in the AST.
+ *
+ * This utility is essential for the LexicalPreservingPrinter because the token
+ * assignment algorithm assigns tokens based on position in the source code, not
+ * necessarily to the logical AST node.
+ *
+ *
Core Problem: In JavaParser's AST, child nodes may appear in the source
+ * code before their parent node's position. The LPP assigns tokens to the
+ * nearest enclosing node whose range includes the token's position.
+ *
+ *
Example: In {@code Set> x;}, the tokens for
+ * {@code Pair} are assigned to {@code VariableDeclarationExpr},
+ * not to the {@code Pair} type node. When replacing {@code Pair}, the LPP needs
+ * to know to regenerate {@code VariableDeclarationExpr}.
+ *
+ * Implementation: Uses the Strategy pattern with multiple detection
+ * strategies, each handling a specific category of nodes (Types, Annotations,
+ * Modifiers, etc.). Strategies are tried in priority order.
+ *
+ * @since 3.28.0
+ */
+class TokenOwnerDetector {
+
+ /**
+ * Strategy interface for detecting token owners.
+ *
+ *
Each strategy implementation checks if it applies to the given node,
+ * then searches for the token owner by walking up the AST.
+ */
+ @FunctionalInterface
+ interface DetectionStrategy {
+
+ /**
+ * Attempts to find the token owner for the given node.
+ *
+ * @param node the node to analyze
+ * @return the token owner if this strategy applies, empty otherwise
+ */
+ Optional detect(Node node);
+ }
+
+ /**
+ * Detection strategies in priority order.
+ *
+ * Order matters: strategies are tried sequentially, first match wins.
+ * Most frequent cases are placed first for performance (early exit).
+ *
+ *
Priority rationale:
+ *
+ * - TypeOwnerStrategy - Most common, critical for Issue #3365
+ * - AnnotationOwnerStrategy - Common in modern Java code
+ * - ModifierOwnerStrategy - Moderately common
+ * - TypeParameterOwnerStrategy - Less common (generics only)
+ * - NameInExpressionStrategy - Rare edge cases
+ *
+ */
+ private static final List STRATEGIES = Arrays.asList(new TypeOwnerStrategy());
+
+ /**
+ * Finds the node that owns the tokens for the given node.
+ *
+ * Algorithm:
+ *
+ * - Try each detection strategy in priority order
+ * - Return the first non-null owner found
+ * - If no strategy applies, the node owns its own tokens
+ *
+ *
+ * @param node the node to find the token owner for
+ * @return the node that owns the tokens, never null
+ * @throws IllegalArgumentException if node is null
+ */
+ static Node findTokenOwner(Node node) {
+ if (node == null) {
+ throw new IllegalArgumentException("node cannot be null");
+ }
+ // Try each strategy in order
+ for (DetectionStrategy strategy : STRATEGIES) {
+ Optional owner = strategy.detect(node);
+ if (owner.isPresent() && owner.get() != node) {
+ return owner.get();
+ }
+ }
+ // Default: node owns its own tokens
+ return node;
+ }
+
+ /**
+ * Determines if token owner regeneration is needed after a node replacement.
+ *
+ * Context: When a node is replaced in the AST (e.g., replacing {@code Pair}
+ * with {@code SimpleImmutableEntry} in Issue #3365), the LexicalPreservingPrinter's
+ * Observer notifies the change. However, the LPP only regenerates the NodeText for
+ * the immediate parent of the replaced node by default.
+ *
+ *
Problem: If the tokens for the replaced node are actually owned by an
+ * ancestor further up the tree (as detected by {@link #findTokenOwner(Node)}), the
+ * LPP won't regenerate the correct NodeText, resulting in the change not appearing
+ * in the output.
+ *
+ *
Example where regeneration is needed:
+ *
{@code
+ * Set> x;
+ *
+ * // When replacing Pair type:
+ * // - parent: TypeArguments (immediate parent of Pair)
+ * // - tokenOwner: VariableDeclarationExpr (owns the tokens)
+ * // - replacedNode: ClassOrInterfaceType (Pair)
+ *
+ * // Result: needsRegeneration = true (tokenOwner != parent)
+ * }
+ *
+ * Example where regeneration is NOT needed:
+ *
{@code
+ * x = 5;
+ *
+ * // When replacing the literal 5:
+ * // - parent: AssignExpr (immediate parent of literal)
+ * // - tokenOwner: AssignExpr (same as parent)
+ * // - replacedNode: IntegerLiteralExpr (5)
+ *
+ * // Result: needsRegeneration = false (normal LPP handling works)
+ * }
+ *
+ * Decision criteria:
+ *
+ * - If tokenOwner == parent: No regeneration needed (normal path)
+ * - If replacedNode is a Type: Regeneration needed (Issue #3365 case)
+ * - If replacedNode is inside a Type: Regeneration needed (nested case)
+ * - Otherwise: No regeneration needed
+ *
+ *
+ * @param parent the immediate parent of the replaced node (where LPP would normally regenerate)
+ * @param tokenOwner the actual owner of the tokens (as detected by findTokenOwner)
+ * @param replacedNode the node being replaced in the AST
+ * @return true if the tokenOwner's NodeText should be regenerated, false if normal LPP handling is sufficient
+ */
+ static boolean needsRegeneration(Node parent, Node tokenOwner, Node replacedNode) {
+ // Case 1: Token owner is the same as the immediate parent
+ // This is the normal case where LPP's default behavior (regenerating the parent) works correctly.
+ // Example: x = 5; → replacing 5 in AssignExpr
+ if (tokenOwner.equals(parent)) {
+ return false;
+ }
+ // WORKAROUND: Multiple variable declarations share same type
+ if (tokenOwner instanceof FieldDeclaration) {
+ FieldDeclaration field = (FieldDeclaration) tokenOwner;
+ if (field.getVariables().size() > 1) {
+ // Let LPP handle it normally
+ return false;
+ }
+ }
+ // Case 2: Replaced node is directly a Type
+ // This is the most common case requiring special handling (Issue #3365).
+ // Types in declarations have their tokens owned by the declaration, not by the Type node itself.
+ // Example: Set> x; → replacing Pair type
+ if (replacedNode instanceof Type) {
+ return true;
+ }
+ // Case 3: Replaced node is contained within a Type
+ // This handles nested cases where a node inside a type (e.g., type arguments) is replaced.
+ // We walk up from the replaced node to the parent, checking if we pass through a Type node.
+ // Example: Set> → replacing "String" inside Pair's type arguments
+ Node current = replacedNode.getParentNode().orElse(null);
+ while (current != null && current != parent) {
+ if (current instanceof Type) {
+ // Found a Type node in the ancestry chain → regeneration needed
+ return true;
+ }
+ current = current.getParentNode().orElse(null);
+ }
+ // Case 4: None of the above
+ // The replaced node is not type-related and tokenOwner != parent.
+ // This is rare but possible (e.g., annotations, modifiers in some cases).
+ // Conservative approach: don't regenerate unless we're sure we need to.
+ // If this causes issues, we can add more cases (annotations, modifiers, etc.)
+ return false;
+ }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TokenTextElement.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TokenTextElement.java
index d488b53392..2fb0fb4945 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TokenTextElement.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TokenTextElement.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
- * Copyright (C) 2011, 2013-2024 The JavaParser Team.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
*
* This file is part of JavaParser.
*
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TypeOwnerStrategy.java b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TypeOwnerStrategy.java
new file mode 100755
index 0000000000..a2217fc13b
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/lexicalpreservation/TypeOwnerStrategy.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2026 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ */
+package com.github.javaparser.printer.lexicalpreservation;
+
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.body.*;
+import com.github.javaparser.ast.expr.*;
+import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt;
+import com.github.javaparser.ast.stmt.ExpressionStmt;
+import com.github.javaparser.ast.type.Type;
+import com.github.javaparser.utils.Optionals;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Strategy for detecting token owners of Type nodes and their children.
+ *
+ * This is the most complex strategy because types appear in many contexts:
+ *
+ * - Variable declarations (local, field)
+ * - Method return types
+ * - Parameters
+ * - Extends/implements clauses
+ * - Cast expressions
+ * - Instanceof expressions
+ * - Type parameters and bounds
+ *
+ *
+ * This strategy is critical for fixing Issue #3365 where nested generic types
+ * (e.g., {@code Set>}) were not properly updated by the LPP.
+ */
+class TypeOwnerStrategy implements TokenOwnerDetector.DetectionStrategy {
+
+ @Override
+ public Optional detect(Node node) {
+ // Check if this strategy applies
+ Type type = findTypeNode(node);
+ if (type == null) {
+ return Optional.empty();
+ }
+ // Find the owner
+ Node owner = findTypeOwner(type);
+ return Optional.ofNullable(owner);
+ }
+
+ /**
+ * Finds the Type node for the given node.
+ * Returns the node itself if it's a Type, or searches ancestors.
+ *
+ * @param node the node to analyze
+ * @return the Type node, or null if not found or not in a type context
+ */
+ private Type findTypeNode(Node node) {
+ if (node instanceof Type) {
+ return (Type) node;
+ }
+ // Search ancestors, but stop at boundaries
+ Node current = node;
+ while (current != null) {
+ if (current instanceof Type) {
+ return (Type) current;
+ }
+ // Stop at boundaries that indicate we're not in a type context
+ if (current instanceof BodyDeclaration || current instanceof ExpressionStmt) {
+ return null;
+ }
+ current = current.getParentNode().orElse(null);
+ }
+ return null;
+ }
+
+ /**
+ * Finds the declaration that owns the tokens for the given type.
+ *
+ * Algorithm: Walk up the AST from the type, checking each parent
+ * against known contexts where types appear. Return the first matching
+ * declaration.
+ *
+ * @param type the type node (never null)
+ * @return the token owner, or the type itself if no owner found
+ */
+ private Node findTypeOwner(Type type) {
+ Node current = type;
+ while (current != null) {
+ Node parent = current.getParentNode().orElse(null);
+ if (parent == null) {
+ break;
+ }
+ final Node currentNode = current;
+ // Check each context using Optional chaining for clean code
+ Optional owner = Optionals.or(
+ () -> checkVariableContext(parent, currentNode),
+ () -> checkParameterContext(parent, currentNode),
+ () -> checkMethodContext(parent, currentNode),
+ () -> checkClassContext(parent, currentNode),
+ () -> checkExpressionContext(parent, currentNode),
+ () -> checkStatementContext(parent, currentNode));
+ if (owner.isPresent()) {
+ return owner.get();
+ }
+ current = parent;
+ }
+ // Fallback: type owns its own tokens
+ return type;
+ }
+
+ // ========================================================================
+ // CONTEXT CHECKERS
+ // Each method checks if the parent is a specific context that owns tokens
+ // ========================================================================
+ /**
+ * Checks variable declaration contexts: local variables, fields, enum constants.
+ *
+ * @param parent the potential owner
+ * @param current the current node in the walk
+ * @return the owner if this context applies
+ */
+ private Optional checkVariableContext(Node parent, Node current) {
+ // Local variable declaration
+ if (parent instanceof VariableDeclarationExpr) {
+ return Optional.of(parent);
+ }
+ // Field declaration
+ if (parent instanceof FieldDeclaration) {
+ return Optional.of(parent);
+ }
+ // Enum constant declaration
+ if (parent instanceof EnumConstantDeclaration) {
+ return Optional.of(parent);
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Checks parameter contexts: method parameters, receiver parameters.
+ *
+ * @param parent the potential owner
+ * @param current the current node in the walk
+ * @return the owner if this context applies
+ */
+ private Optional checkParameterContext(Node parent, Node current) {
+ // Method/constructor parameter
+ if (parent instanceof Parameter) {
+ return Optional.of(parent);
+ }
+ // Receiver parameter (e.g., void method(MyClass MyClass.this))
+ if (parent instanceof ReceiverParameter) {
+ return Optional.of(parent);
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Checks method/constructor contexts: return types, constructor declarations.
+ *
+ * @param parent the potential owner
+ * @param current the current node in the walk
+ * @return the owner if this context applies
+ */
+ private Optional