com.github.javaparser.printer.lexicalpreservation.Difference Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of stubparser Show documentation
Show all versions of stubparser Show documentation
This project contains a parser for the Checker Framework's stub files: https://checkerframework.org/manual/#stub . It is a fork of the JavaParser project.
The newest version!
/*
* Copyright (C) 2007-2010 Júlio Vilmar Gesser.
* Copyright (C) 2011, 2013-2024 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 static com.github.javaparser.GeneratedJavaParserConstants.LBRACE;
import static com.github.javaparser.GeneratedJavaParserConstants.RBRACE;
import static com.github.javaparser.GeneratedJavaParserConstants.SPACE;
import com.github.javaparser.GeneratedJavaParserConstants;
import com.github.javaparser.JavaToken;
import com.github.javaparser.JavaToken.Kind;
import com.github.javaparser.TokenTypes;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.comments.Comment;
import com.github.javaparser.ast.nodeTypes.NodeWithTypeArguments;
import com.github.javaparser.ast.type.ArrayType;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.printer.concretesyntaxmodel.CsmElement;
import com.github.javaparser.printer.concretesyntaxmodel.CsmIndent;
import com.github.javaparser.printer.concretesyntaxmodel.CsmUnindent;
import com.github.javaparser.printer.lexicalpreservation.LexicalDifferenceCalculator.CsmChild;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.IntStream;
/**
* A Difference should give me a sequence of elements I should find (to indicate the context) followed by a list of elements
* to remove or to add and follow by another sequence of elements.
*
* I should later be able to apply such difference to a nodeText.
*/
public class Difference {
public static final int STANDARD_INDENTATION_SIZE = 4;
private final NodeText nodeText;
private final Node node;
private final List diffElements;
private final List originalElements;
private int originalIndex = 0;
private int diffIndex = 0;
private final List indentation;
private boolean addedIndentation = false;
Difference(List diffElements, NodeText nodeText, Node node) {
if (nodeText == null) {
throw new NullPointerException("nodeText can not be null");
}
this.nodeText = nodeText;
this.node = node;
this.diffElements = diffElements;
this.originalElements = nodeText.getElements();
this.indentation = LexicalPreservingPrinter.findIndentation(node);
}
/*
* Returns the indentation used after the last line break
*/
List processIndentation(List indentation, List prevElements) {
int eolIndex = lastIndexOfEol(prevElements);
// Return "indentation" as is if no EOL element was found
if (eolIndex < 0) return indentation;
// Find consecutive space characters after the EOL element
indentation =
takeWhile(prevElements.subList(eolIndex + 1, prevElements.size()), element -> element.isWhiteSpace());
return indentation;
}
/*
* returns only the elements that match the given predicate.
* takeWhile takes elements from the initial stream while the predicate holds true.
* Meaning that when an element is encountered that does not match the predicate, the rest of the list is discarded.
*/
List takeWhile(List prevElements, Predicate predicate) {
List spaces = new ArrayList<>();
for (TextElement element : prevElements) {
if (predicate.test(element)) {
spaces.add(element);
continue;
}
break;
}
return spaces;
}
int lastIndexOfEol(List source) {
return IntStream.range(0, source.size())
.map(i -> source.size() - i - 1)
.filter(i -> source.get(i).isNewline())
.findFirst()
.orElse(-1);
}
/*
* Returns the position of the next element in the list starting from @{code fromIndex} which is a comment (Ignoring spaces)
* or -1 if it's not a comment.
*/
private int posOfNextComment(int fromIndex, List elements) {
if (!isValidIndex(fromIndex, elements)) return -1;
ArrayIterator iterator = new ArrayIterator<>(elements, fromIndex);
// search for the next consecutive space characters
while (iterator.hasNext()) {
TextElement element = iterator.next();
if (element.isSpaceOrTab()) {
continue;
}
if (element.isComment()) {
return iterator.index();
}
break;
}
return -1;
}
/*
* Returns true if the next element in the list (starting from @{code fromIndex}) is a comment
*/
private boolean isFollowedByComment(int fromIndex, List elements) {
return posOfNextComment(fromIndex, elements) != -1;
}
/*
* Removes all elements in the list starting from @{code fromIndex}) ending to @{code toIndex})
*/
private void removeElements(int fromIndex, int toIndex, List elements) {
if (!(isValidIndex(fromIndex, elements) && isValidIndex(toIndex, elements) && fromIndex <= toIndex)) return;
ListIterator iterator = elements.listIterator(fromIndex);
// removing elements
int count = fromIndex;
while (iterator.hasNext() && count <= toIndex) {
iterator.next();
iterator.remove();
count++;
}
}
private boolean isValidIndex(int index, List> elements) {
return index >= 0 && index <= elements.size();
}
/*
* Returns the position of the last new line character or -1 if there is no eol in the specified list of TextElement
*/
int lastIndexOfEolWithoutGPT(List source) {
ListIterator listIterator = source.listIterator(source.size());
int lastIndex = source.size() - 1;
while (listIterator.hasPrevious()) {
TextElement elem = listIterator.previous();
if (elem.isNewline()) {
return lastIndex;
}
lastIndex--;
}
return -1;
}
private List indentationBlock() {
List res = new LinkedList<>();
res.add(new TokenTextElement(SPACE));
res.add(new TokenTextElement(SPACE));
res.add(new TokenTextElement(SPACE));
res.add(new TokenTextElement(SPACE));
return res;
}
private boolean isAfterLBrace(NodeText nodeText, int nodeTextIndex) {
if (nodeTextIndex > 0 && nodeText.getTextElement(nodeTextIndex - 1).isToken(LBRACE)) {
return true;
}
if (nodeTextIndex > 0 && nodeText.getTextElement(nodeTextIndex - 1).isSpaceOrTab()) {
return isAfterLBrace(nodeText, nodeTextIndex - 1);
}
return false;
}
/**
* If we are at the beginning of a line, with just spaces or tabs before/after the position of the deleted element
* we should force the space to be the same as the current indentation.
* This method handles the following case if we remove the modifier {@code public} ([ ] is an indent character)
* {@code
* [ ][ ]public[ ][ ][ ]void[ ]m{}
* <-1--> <---2--->
* 1/ current indentation
* 2/ these whitespaces must be removed
* }
* should produce
* {@code
* [ ][ ]void[ ]m{}
* }
*/
int considerEnforcingIndentation(NodeText nodeText, int nodeTextIndex) {
return considerIndentation(nodeText, nodeTextIndex, indentation.size());
}
private int considerRemovingIndentation(NodeText nodeText, int nodeTextIndex) {
return considerIndentation(nodeText, nodeTextIndex, 0);
}
private int considerIndentation(NodeText nodeText, int nodeTextIndex, int numberOfCharactersToPreserve) {
EnforcingIndentationContext enforcingIndentationContext =
defineEnforcingIndentationContext(nodeText, nodeTextIndex);
// the next position in the list (by default the current position)
int res = nodeTextIndex;
if (enforcingIndentationContext.extraCharacters > 0) {
int extraCharacters = enforcingIndentationContext.extraCharacters > numberOfCharactersToPreserve
? enforcingIndentationContext.extraCharacters - numberOfCharactersToPreserve
: 0;
res = removeExtraCharacters(nodeText, enforcingIndentationContext.start, extraCharacters);
// The next position must take into account the indentation
res = extraCharacters > 0 ? res + numberOfCharactersToPreserve : res;
}
if (res < 0) {
throw new IllegalStateException();
}
return res;
}
private boolean isEnforcingIndentationActivable(RemovedGroup removedGroup) {
return (isLastElement(diffElements, diffIndex)
|| !(nextDiffElement(diffElements, diffIndex).isAdded()))
&& originalIndex < originalElements.size()
&& !removedGroup.isACompleteLine();
}
private boolean isRemovingIndentationActivable(RemovedGroup removedGroup) {
return (isLastElement(diffElements, diffIndex)
|| !(nextDiffElement(diffElements, diffIndex).isAdded()))
&& originalIndex < originalElements.size()
&& removedGroup.isACompleteLine();
}
private boolean isLastElement(List> list, int index) {
return index + 1 >= list.size();
}
private DifferenceElement nextDiffElement(List list, int index) {
return list.get(index + 1);
}
/*
* This data structure class hold the starting position of the first whitespace char
* and the number of consecutive whitespace (or tab) characters
*/
private class EnforcingIndentationContext {
int start;
int extraCharacters;
public EnforcingIndentationContext(int start) {
this(start, 0);
}
public EnforcingIndentationContext(int start, int extraCharacters) {
this.start = start;
this.extraCharacters = extraCharacters;
}
}
/**
* Remove excess white space after deleting element.
* @param nodeText Contains a list of elements to analyze
* @param nodeTextIndex Starting position in the input list
* @return The current position in the list of the elements
*/
private int removeExtraCharacters(NodeText nodeText, int nodeTextIndex, int extraCharacters) {
int count = 0;
while (nodeTextIndex >= 0 && nodeTextIndex < nodeText.numberOfElements() && count < extraCharacters) {
nodeText.removeElement(nodeTextIndex);
count++;
}
return nodeTextIndex;
}
/**
* Starting at {@code nodeTextIndex} this method tries to determine how many contiguous spaces there are between
* the previous end of line and the next non whitespace (or tab) character
* @param nodeText List of elements to analyze
* @param nodeTextIndex Starting position in the input list
* @return EnforcingIndentationContext Data structure that hold the starting position of the first whitespace char and
* The number of consecutive whitespace (or tab) characters
*/
private EnforcingIndentationContext defineEnforcingIndentationContext(NodeText nodeText, int startIndex) {
EnforcingIndentationContext ctx = new EnforcingIndentationContext(startIndex);
// compute space before startIndex value
if (startIndex < nodeText.numberOfElements() && startIndex > 0) {
// at this stage startIndex points to the first element before the deleted one
for (int i = startIndex - 1; i >= 0 && i < nodeText.numberOfElements(); i--) {
if (nodeText.getTextElement(i).isNewline()) {
break;
}
if (!isSpaceOrTabElement(nodeText, i)) {
ctx = new EnforcingIndentationContext(startIndex);
break;
}
ctx.start = i;
ctx.extraCharacters++;
}
}
// compute space after the deleted element
if (startIndex < nodeText.numberOfElements() && isSpaceOrTabElement(nodeText, startIndex)) {
// int startingFromIndex = startIndex == 0 ? startIndex : startIndex + 1;
for (int i = startIndex; i >= 0 && i < nodeText.numberOfElements(); i++) {
if (nodeText.getTextElement(i).isNewline()) {
break;
}
if (!isSpaceOrTabElement(nodeText, i)) {
break;
}
ctx.extraCharacters++;
}
}
return ctx;
}
/*
* An element is considered inlined if, before the line break, there are nodes in the list of elements
*/
private boolean isInlined(NodeText nodeText, int startIndex) {
boolean inlined = false;
if (startIndex < nodeText.numberOfElements() && startIndex >= 0) {
// at this stage startIndex points to the first element before the deleted one
for (int i = startIndex; i < nodeText.numberOfElements(); i++) {
if (nodeText.getTextElement(i).isNewline()) {
break;
}
if (nodeText.getTextElement(i).isChild()) {
inlined = true;
break;
}
}
}
return inlined;
}
/*
* Returns true if the indexed element is a space or a tab
*/
private boolean isSpaceOrTabElement(NodeText nodeText, int i) {
return nodeText.getTextElement(i).isSpaceOrTab();
}
/**
* Node that we have calculate the Difference we can apply to a concrete NodeText, modifying it according
* to the difference (adding and removing the elements provided).
*/
void apply() {
ReshuffledDiffElementExtractor.of(nodeText).extract(diffElements);
Map removedGroups = combineRemovedElementsToRemovedGroups();
do {
boolean isLeftOverDiffElement = applyLeftOverDiffElements();
boolean isLeftOverOriginalElement = applyLeftOverOriginalElements();
if (!isLeftOverDiffElement && !isLeftOverOriginalElement) {
DifferenceElement diffElement = diffElements.get(diffIndex);
if (diffElement.isAdded()) {
applyAddedDiffElement((Added) diffElement);
} else {
TextElement originalElement = originalElements.get(originalIndex);
boolean originalElementIsChild = originalElement instanceof ChildTextElement;
boolean originalElementIsToken = originalElement instanceof TokenTextElement;
if (diffElement.isKept()) {
applyKeptDiffElement(
(Kept) diffElement, originalElement, originalElementIsChild, originalElementIsToken);
} else if (diffElement.isRemoved()) {
Removed removed = (Removed) diffElement;
applyRemovedDiffElement(
removedGroups.get(removed),
removed,
originalElement,
originalElementIsChild,
originalElementIsToken);
} else {
throw new UnsupportedOperationException("Unable to apply operations from "
+ diffElement.getClass().getSimpleName() + " to "
+ originalElement.getClass().getSimpleName());
}
}
}
} while (diffIndex < diffElements.size() || originalIndex < originalElements.size());
}
private boolean applyLeftOverOriginalElements() {
boolean isLeftOverElement = false;
if (diffIndex >= diffElements.size() && originalIndex < originalElements.size()) {
TextElement originalElement = originalElements.get(originalIndex);
if (originalElement.isWhiteSpaceOrComment()) {
originalIndex++;
} else {
throw new UnsupportedOperationException(
"NodeText: " + nodeText + ". Difference: " + this + " " + originalElement);
}
isLeftOverElement = true;
}
return isLeftOverElement;
}
private boolean applyLeftOverDiffElements() {
boolean isLeftOverElement = false;
if (diffIndex < diffElements.size() && originalIndex >= originalElements.size()) {
DifferenceElement diffElement = diffElements.get(diffIndex);
if (diffElement.isKept()) {
diffIndex++;
} else if (diffElement.isAdded()) {
Added addedElement = (Added) diffElement;
nodeText.addElement(originalIndex, addedElement.toTextElement());
originalIndex++;
diffIndex++;
} else {
// let's forget this element
diffIndex++;
}
isLeftOverElement = true;
}
return isLeftOverElement;
}
/**
* Maps all Removed elements as keys to their corresponding RemovedGroup.
* A RemovedGroup contains all consecutive Removed elements.
*
* Example:
*
* Elements: Kept|Removed1|Removed2|Kept|Removed3|Added|Removed4
* Groups: <----Group1----> Group2 Group3
* Keys: Removed1+Removed2 Removed3 Removed4
*
*
* @return Map with all Removed elements as keys to their corresponding RemovedGroup
*/
private Map combineRemovedElementsToRemovedGroups() {
Map> removedElementsMap = groupConsecutiveRemovedElements();
List removedGroups = new ArrayList<>();
for (Map.Entry> entry : removedElementsMap.entrySet()) {
removedGroups.add(RemovedGroup.of(entry.getKey(), entry.getValue()));
}
Map map = new HashMap<>();
for (RemovedGroup removedGroup : removedGroups) {
for (Removed index : removedGroup) {
map.put(index, removedGroup);
}
}
return map;
}
private Map> groupConsecutiveRemovedElements() {
Map> removedElementsMap = new HashMap<>();
Integer firstElement = null;
for (int i = 0; i < diffElements.size(); i++) {
DifferenceElement diffElement = diffElements.get(i);
if (diffElement.isRemoved()) {
if (firstElement == null) {
firstElement = i;
}
removedElementsMap
.computeIfAbsent(firstElement, key -> new ArrayList<>())
.add((Removed) diffElement);
} else {
firstElement = null;
}
}
return removedElementsMap;
}
private void applyRemovedDiffElement(
RemovedGroup removedGroup,
Removed removed,
TextElement originalElement,
boolean originalElementIsChild,
boolean originalElementIsToken) {
if (removed.isChild() && originalElementIsChild) {
ChildTextElement originalElementChild = (ChildTextElement) originalElement;
if (originalElementChild.isComment()) {
// We expected to remove a proper node but we found a comment in between.
// If the comment is associated to the node we want to remove we remove it as well, otherwise we keep it
Comment comment = (Comment) originalElementChild.getChild();
if (!comment.isOrphan()
&& comment.getCommentedNode().isPresent()
&& comment.getCommentedNode().get().equals(removed.getChild())) {
nodeText.removeElement(originalIndex);
} else {
originalIndex++;
}
} else {
// If we delete the first element, it is possible that there is an indentation to be deleted which is
// stored in the parent node.
NodeText parentNodeText = new NodeText();
List indentationTokens = new ArrayList<>();
if (originalIndex == 0 && removed.getChild().getParentNode().isPresent()) {
Node startingNodeForFindingIndentation = removed.getChild();
Node parentNode = removed.getChild().getParentNode().get();
parentNodeText = LexicalPreservingPrinter.getOrCreateNodeText(parentNode);
// If we are trying to delete the first element of a node and that node is also the first element of
// the parent node, we need to look for the grandfather node which logically contains the
// indentation characters.
// This is the case, for example, when trying to delete an annotation positioned on a method
// declaration.
// The token corresponding to the annotation is the first element of the annotation node
// and it is also the first element of the parent node (MethodDeclaration),
// so the previous indentation is defined in the parent node of the method declaration.
if (!parentNodeText.getElements().isEmpty()
&& parentNode.getParentNode().isPresent()
&& parentNodeText.getTextElement(0).equals(nodeText.getTextElement(originalIndex))) {
startingNodeForFindingIndentation = parentNode;
parentNodeText = LexicalPreservingPrinter.getOrCreateNodeText(
parentNode.getParentNode().get());
}
indentationTokens = LexicalPreservingPrinter.findIndentation(startingNodeForFindingIndentation);
}
nodeText.removeElement(originalIndex);
// When we don't try to remove a complete line
// and removing the element is not the first action of a replacement (removal followed by addition)
// (in the latter case we keep the indentation)
// then we want to enforce the indentation.
if (isEnforcingIndentationActivable(removedGroup)) {
// Since the element has been deleted we try to start the analysis from the element following the
// one that was deleted
originalIndex = considerEnforcingIndentation(nodeText, originalIndex);
}
// If in front we have one space and before also we had space let's drop one space
if (originalElements.size() > originalIndex && originalIndex > 0) {
if (originalElements.get(originalIndex).isWhiteSpace()
&& originalElements.get(originalIndex - 1).isWhiteSpace()) {
// However we do not want to do that when we are about to adding or removing elements
// The intention is not very clear maybe it should clarify this with examples!
// Are we to understand that we can only do this if there is a single modification to process
// OR or if the next change is to keep the element
if ((diffIndex + 1) == diffElements.size()
|| (diffElements.get(diffIndex + 1).isKept())) {
originalElements.remove(originalIndex--);
}
}
}
// We need to know if, in the original list of elements, the deleted child node is immediately followed
// by the same comment.
// If so, it should also be deleted.
if (isFollowedByComment(originalIndex, originalElements)) {
int indexOfNextComment = posOfNextComment(originalIndex, originalElements);
removeElements(originalIndex, indexOfNextComment, originalElements);
}
if (isRemovingIndentationActivable(removedGroup)) {
// Since the element has been deleted we try to start the analysis from the previous element
originalIndex = considerRemovingIndentation(nodeText, originalIndex);
// If we delete the first element, it is possible that there is an indentation
// to be deleted which is stored in the parent node.
// We don't want to remove indentation when the node to remove is not the only
// node in the line (if there are other nodes before the next character
// indicating the end of line).
// This is for example the case when we want to delete an annotation declared on
// the same line as a method declaration.
if (originalIndex == 0 && !indentationTokens.isEmpty() && !isInlined(nodeText, originalIndex)) {
for (TextElement indentationToken : indentationTokens) {
parentNodeText.removeElement(
parentNodeText.findElement(indentationToken.and(indentationToken.matchByRange())));
}
}
}
diffIndex++;
}
} else if (removed.isChild() && originalElement.isComment()) {
// removing the comment first
nodeText.removeElement(originalIndex);
if (isRemovingIndentationActivable(removedGroup)) {
originalIndex = considerRemovingIndentation(nodeText, originalIndex);
}
} else if (removed.isToken()
&& originalElementIsToken
&& (removed.getTokenType() == ((TokenTextElement) originalElement).getTokenKind()
|| // handle EOLs separately as their token kind might not be equal. This is because the
// 'removed'
// element always has the current operating system's EOL as type
(((TokenTextElement) originalElement)
.getToken()
.getCategory()
.isEndOfLine()
&& removed.isNewLine()))) {
nodeText.removeElement(originalIndex);
diffIndex++;
} else if ((removed.isWhiteSpaceNotEol()
|| removed.getElement() instanceof CsmIndent
|| removed.getElement() instanceof CsmUnindent)
&& originalElement.isSpaceOrTab()) {
// remove the current space
nodeText.removeElement(originalIndex);
} else if (originalElementIsToken && originalElement.isWhiteSpaceOrComment()) {
originalIndex++;
// skip the newline token which may be generated unnecessarily by the concrete syntax pattern
if (removed.isNewLine()) {
diffIndex++;
}
} else if (originalElement.isLiteral()) {
nodeText.removeElement(originalIndex);
diffIndex++;
} else if (removed.isPrimitiveType()) {
if (originalElement.isPrimitive()) {
nodeText.removeElement(originalIndex);
diffIndex++;
} else {
throw new UnsupportedOperationException("removed " + removed.getElement() + " vs " + originalElement);
}
} else if (removed.isWhiteSpace()
|| removed.getElement() instanceof CsmIndent
|| removed.getElement() instanceof CsmUnindent) {
diffIndex++;
} else if (originalElement.isWhiteSpace()) {
originalIndex++;
} else if (removed.isChild()) {
// see issue #3721 this case is linked for example to a change of type of variable declarator
nodeText.removeElement(originalIndex);
diffIndex++;
} else {
throw new UnsupportedOperationException("removed " + removed.getElement() + " vs " + originalElement);
}
cleanTheLineOfLeftOverSpace(removedGroup, removed);
}
/**
* Cleans the line of left over space if there is unnecessary indentation and the element will not be replaced
*/
private void cleanTheLineOfLeftOverSpace(RemovedGroup removedGroup, Removed removed) {
if (originalIndex >= originalElements.size()) {
// if all elements were already processed there is nothing to do
return;
}
// we dont want to remove the indentation if the last removed element is a newline
// because in this case we are trying to remove the indentation of the next child element
if (!removedGroup.isProcessed()
&& removedGroup.isLastElement(removed)
&& removedGroup.isACompleteLine()
&& !removed.isNewLine()) {
Integer lastElementIndex = removedGroup.getLastElementIndex();
Optional indentation = removedGroup.getIndentation();
if (indentation.isPresent() && !isReplaced(lastElementIndex)) {
for (int i = 0; i < indentation.get(); i++) {
if (originalElements.get(originalIndex).isSpaceOrTab()) {
// If the current element is a space, remove it
nodeText.removeElement(originalIndex);
} else if (originalIndex >= 1
&& originalElements.get(originalIndex - 1).isSpaceOrTab()) {
// If the current element is not a space itself we remove the space in front of (before) it
nodeText.removeElement(originalIndex - 1);
originalIndex--;
}
// Remove remaining newline character if needed
if (nodeText.getTextElement(originalIndex).isNewline()) {
nodeText.removeElement(originalIndex);
originalIndex = originalIndex > 0 ? originalIndex-- : 0;
}
}
}
// Mark RemovedGroup as processed
removedGroup.processed();
}
}
// note:
// increment originalIndex if we want to keep the original element
// increment diffIndex if we want to skip the diff element
private void applyKeptDiffElement(
Kept kept, TextElement originalElement, boolean originalElementIsChild, boolean originalElementIsToken) {
if (originalElement.isComment()) {
originalIndex++;
} else if (kept.isChild() && ((CsmChild) kept.getElement()).getChild() instanceof Comment) {
diffIndex++;
} else if (kept.isChild() && originalElementIsChild) {
diffIndex++;
originalIndex++;
} else if (kept.isChild() && originalElementIsToken) {
if (originalElement.isWhiteSpaceOrComment()) {
originalIndex++;
} else if (originalElement.isIdentifier() && isNodeWithTypeArguments(kept)) {
diffIndex++;
// skip all token related to node with type argument declaration
// for example:
// List i : in this case originalElement is "List" and the next token is space. There is nothing to
// skip. in the originalElements list.
// List i : in this case originalElement is "List" and the next token is
// "<" so we have to skip all the tokens which are used in the typed argument declaration
// [<][String][>](3 tokens) in the originalElements list.
// List> i : in this case originalElement is "List" and the next
// token is "<" so we have to skip all the tokens which are used in the typed arguments declaration
// [<][List][<][String][>][>](6 tokens) in the originalElements list.
int step = getIndexToNextTokenElement((TokenTextElement) originalElement, 0);
originalIndex += step;
originalIndex++;
} else if (originalElement.isIdentifier() && isTypeWithFullyQualifiedName(kept)) {
diffIndex++;
// skip all token related to node with the fully qualified name
// for example:
// java.lang.Object is represented in originalElement as a list of tokens "java", ".", "lang", ".",
// "Object".
// So we have to skip 5 tokens.
int step = getIndexToNextTokenElement((TokenTextElement) originalElement, kept);
originalIndex += step;
// positioning on the next token
originalIndex++;
} else if ((originalElement.isIdentifier() || originalElement.isKeyword()) && isArrayType(kept)) {
int tokenToSkip =
getIndexToNextTokenElementInArrayType((TokenTextElement) originalElement, getArrayLevel(kept));
diffIndex++;
originalIndex += tokenToSkip;
originalIndex++;
} else if (originalElement.isIdentifier()) {
originalIndex++;
diffIndex++;
} else if (kept.isPrimitiveType()) {
originalIndex++;
diffIndex++;
} else {
// original is a token so we keep it (for example an unexpected semicolon)
originalIndex++;
}
} else if (kept.isToken() && originalElementIsToken) {
TokenTextElement originalTextToken = (TokenTextElement) originalElement;
if (kept.getTokenType() == originalTextToken.getTokenKind()) {
originalIndex++;
diffIndex++;
} else if (kept.isNewLine() && originalTextToken.isNewline()) {
originalIndex++;
diffIndex++;
} else if (kept.isNewLine() && originalTextToken.isSpaceOrTab()) {
originalIndex++;
} else if (kept.isWhiteSpaceOrComment()) {
diffIndex++;
} else if (originalTextToken.isWhiteSpaceOrComment()) {
originalIndex++;
} else if (!kept.isNewLine() && originalTextToken.isSeparator()) {
// case where originalTextToken is a separator like ";" and
// kept is not a new line or whitespace for example "}"
// see issue 2351
originalIndex++;
} else {
throw new UnsupportedOperationException(
"Csm token " + kept.getElement() + " NodeText TOKEN " + originalTextToken);
}
} else if (kept.isToken() && originalElementIsChild) {
diffIndex++;
} else if (kept.isWhiteSpace()) {
diffIndex++;
} else if (kept.isIndent()) {
diffIndex++;
} else if (kept.isUnindent()) {
// Nothing to do
diffIndex++;
} else {
throw new UnsupportedOperationException("kept " + kept.getElement() + " vs " + originalElement);
}
}
/*
* Returns the array level if the DifferenceElement is a CsmChild representing an ArrayType else 0
*/
private int getArrayLevel(DifferenceElement element) {
CsmElement csmElem = element.getElement();
if (isArrayType(element)) {
Node child = ((LexicalDifferenceCalculator.CsmChild) csmElem).getChild();
return ((ArrayType) child).getArrayLevel();
}
return 0;
}
/*
* Returns true if the DifferenceElement is a CsmChild representing an ArrayType
*/
private boolean isArrayType(DifferenceElement element) {
CsmElement csmElem = element.getElement();
return csmElem instanceof LexicalDifferenceCalculator.CsmChild
&& ((LexicalDifferenceCalculator.CsmChild) csmElem).getChild() instanceof ArrayType;
}
/*
* Returns true if the DifferenceElement is a CsmChild which represents a type with fully qualified name
*/
private boolean isTypeWithFullyQualifiedName(DifferenceElement element) {
if (!element.isChild()) return false;
CsmChild child = (CsmChild) element.getElement();
if (!ClassOrInterfaceType.class.isAssignableFrom(child.getChild().getClass())) return false;
return ((ClassOrInterfaceType) child.getChild()).getScope().isPresent();
}
/*
* Returns true if the DifferenceElement is a CsmChild with type arguments
*/
private boolean isNodeWithTypeArguments(DifferenceElement element) {
if (!element.isChild()) return false;
CsmChild child = (CsmChild) element.getElement();
if (!NodeWithTypeArguments.class.isAssignableFrom(child.getChild().getClass())) return false;
Optional> typeArgs = ((NodeWithTypeArguments) child.getChild()).getTypeArguments();
return typeArgs.isPresent() && typeArgs.get().size() > 0;
}
/*
* Try to resolve the number of token to skip in the original list to match
* a ClassOrInterfaceType with a list of tokens like "java", ".", "lang", ".", "Object"
*/
private int getIndexToNextTokenElement(TokenTextElement element, DifferenceElement kept) {
// number of token to skip
int step = 0;
// verify if the DifferenceElement is a ClassOrInterfaceType with a fully qualified name
if (!isTypeWithFullyQualifiedName(kept)) return 0;
CsmChild child = (CsmChild) kept.getElement();
// split the type fully qualified node name to an array of tokens
String[] parts =
((ClassOrInterfaceType) child.getChild()).getNameWithScope().split("\\.");
JavaToken token = element.getToken();
for (String part : parts) {
if (part.equals(token.asString())) {
// get 'dot' token
token = token.getNextToken().get();
if (!".".equals(token.asString())) break;
// get the next part
token = token.getNextToken().get();
step += 2;
continue;
}
// there is no match so we don't have token to skip
step = 0;
break;
}
return step;
}
/*
* Returns the number of tokens to skip in originalElements list to synchronize it with the DiffElements list
* This is due to the fact that types are considered as token in the originalElements list.
* For example,
* List is represented by 4 tokens ([List][<][String][>]) while it's a CsmChild element in the DiffElements list
* So in this case, getIndexToNextTokenElement(..) on the [List] token returns 3 because we have to skip 3 tokens ([<][String][>]) to synchronize
* DiffElements list and originalElements list
* The end of recursivity is reached when there is no next token or if the nested diamond operators are totally managed, to take into account this type of declaration
* List > l
* Be careful, this method must be call only if diamond operator could be found in the sequence
*
* @Param TokenTextElement the token currently analyzed
* @Param int the number of nested diamond operators
* @return the number of token to skip in originalElements list
*/
private int getIndexToNextTokenElement(TokenTextElement element, int nestedDiamondOperator) {
// number of token to skip
int step = 0;
Optional next = element.getToken().getNextToken();
if (!next.isPresent()) return step;
// because there is a token, first we need to increment the number of token to skip
step++;
// manage nested diamond operators by incrementing the level on LT token and decrementing on GT
JavaToken nextToken = next.get();
Kind kind = Kind.valueOf(nextToken.getKind());
if (isDiamondOperator(kind)) {
if (Kind.GT.equals(kind)) nestedDiamondOperator--;
else nestedDiamondOperator++;
}
// manage the fact where the first token is not a diamond operator but a whitespace
// and the end of the token sequence to skip
// for example in this declaration List a;
if (nestedDiamondOperator == 0 && !nextToken.getCategory().isWhitespace()) return step;
// recursively analyze token to skip
return step += getIndexToNextTokenElement(new TokenTextElement(nextToken), nestedDiamondOperator);
}
/*
* Returns the number of tokens to skip in originalElements list to synchronize it with the DiffElements list
*/
private int getIndexToNextTokenElementInArrayType(TokenTextElement element, int arrayLevel) {
// number of token to skip
int step = 0;
Optional next = element.getToken().getNextToken();
if (!next.isPresent()) return step;
// because there is a token, first we need to increment the number of token to skip
step++;
// manage array Level by decrementing the level on right bracket token
JavaToken nextToken = next.get();
Kind kind = Kind.valueOf(nextToken.getKind());
if (isBracket(kind)) {
if (Kind.RBRACKET.equals(kind)) arrayLevel--;
}
// manage the fact where the first token is not a diamond operator but a whitespace
// and the end of the token sequence to skip
// for example in this declaration int [] a;
if (arrayLevel == 0 && !nextToken.getCategory().isWhitespace()) return step;
// recursively analyze token to skip
return step += getIndexToNextTokenElementInArrayType(new TokenTextElement(nextToken), arrayLevel);
}
/*
* Returns true if the token is possibly a diamond operator
*/
private boolean isDiamondOperator(Kind kind) {
return Kind.GT.equals(kind) || Kind.LT.equals(kind);
}
/*
* Returns true if the token is a bracket
*/
private boolean isBracket(Kind kind) {
return Kind.LBRACKET.equals(kind) || Kind.RBRACKET.equals(kind);
}
private boolean nextIsRightBrace(int index) {
List elements = originalElements.subList(index, originalElements.size());
for (TextElement element : elements) {
if (!element.isSpaceOrTab()) {
return element.isToken(RBRACE);
}
}
return false;
}
private void applyAddedDiffElement(Added added) {
if (added.isIndent()) {
for (int i = 0; i < STANDARD_INDENTATION_SIZE; i++) {
indentation.add(new TokenTextElement(GeneratedJavaParserConstants.SPACE));
}
addedIndentation = true;
diffIndex++;
return;
}
if (added.isUnindent()) {
for (int i = 0; i < STANDARD_INDENTATION_SIZE && !indentation.isEmpty(); i++) {
indentation.remove(indentation.size() - 1);
}
addedIndentation = false;
diffIndex++;
return;
}
TextElement addedTextElement = added.toTextElement();
boolean used = false;
boolean isPreviousElementNewline =
(originalIndex > 0) && originalElements.get(originalIndex - 1).isNewline();
if (isPreviousElementNewline) {
List elements =
processIndentation(indentation, originalElements.subList(0, originalIndex - 1));
boolean nextIsRightBrace = nextIsRightBrace(originalIndex);
for (TextElement e : elements) {
if (!nextIsRightBrace
&& e instanceof TokenTextElement
&& originalElements.get(originalIndex).isToken(((TokenTextElement) e).getTokenKind())) {
originalIndex++;
} else {
nodeText.addElement(originalIndex++, e);
}
}
} else if (isAfterLBrace(nodeText, originalIndex) && !isAReplacement(diffIndex)) {
if (addedTextElement.isNewline()) {
used = true;
}
nodeText.addElement(originalIndex++, new TokenTextElement(TokenTypes.eolTokenKind()));
// This remove the space in "{ }" when adding a new line
while (originalIndex >= 2 && originalElements.get(originalIndex - 2).isSpaceOrTab()) {
originalElements.remove(originalIndex - 2);
originalIndex--;
}
for (TextElement e : processIndentation(indentation, originalElements.subList(0, originalIndex - 1))) {
nodeText.addElement(originalIndex++, e);
}
// Indentation is painful...
// Sometimes we want to force indentation: this is the case when indentation was expected but
// was actually not there. For example if we have "{ }" we would expect indentation but it is
// not there, so when adding new elements we force it. However if the indentation has been
// inserted by us in this transformation we do not want to insert it again
if (!addedIndentation) {
for (TextElement e : indentationBlock()) {
nodeText.addElement(originalIndex++, e);
}
}
}
if (!used) {
// Handling trailing comments
boolean sufficientTokensRemainToSkip = nodeText.numberOfElements() > originalIndex + 2;
boolean currentIsAComment = nodeText.getTextElement(originalIndex).isComment();
boolean previousIsAComment = originalIndex > 0
&& nodeText.getTextElement(originalIndex - 1).isComment();
boolean currentIsNewline = nodeText.getTextElement(originalIndex).isNewline();
boolean isFirstElement = originalIndex == 0;
boolean previousIsWhiteSpace = originalIndex > 0
&& nodeText.getTextElement(originalIndex - 1).isWhiteSpace();
boolean commentIsBeforeAddedElement = currentIsAComment
&& addedTextElement.getRange().isPresent()
&& nodeText.getTextElement(originalIndex)
.getRange()
.map(range ->
range.isBefore(addedTextElement.getRange().get()))
.orElse(false);
if (sufficientTokensRemainToSkip && currentIsAComment && commentIsBeforeAddedElement) {
// Need to get behind the comment:
// FIXME: Why 2? This comment and the next newline?
originalIndex += 2;
// Defer originalIndex increment
nodeText.addElement(originalIndex, addedTextElement);
// We want to adjust the indentation while considering the new element that we added
originalIndex = adjustIndentation(indentation, nodeText, originalIndex, false);
// Now we can increment
originalIndex++;
} else if (currentIsNewline && previousIsAComment) {
/*
* Manage the case where we want to add an element, after an expression which is followed by a comment on the same line.
* This is not the same case as the one who handles the trailing comments, because in this case the node text element is a new line (not a comment)
* For example : {@code private String a; // this is a }
*/
// Insert after the new line which follows this comment.
originalIndex++;
// We want to adjust the indentation while considering the new element that we added
originalIndex = adjustIndentation(indentation, nodeText, originalIndex, false);
// Defer originalIndex increment
nodeText.addElement(originalIndex, addedTextElement);
// Now we can increment.
originalIndex++;
} else if (currentIsNewline && addedTextElement.isChild()) {
// here we want to place the new child element after the current new line character.
// Except if indentation has been inserted just before this step (in the case where
// isPreviousElementNewline is true)
// or if the previous character is a space (it could be the case if we want to replace a statement)
// Example 1 : if we insert a statement (a duplicated method call expression ) after this one
// value();\n\n
// we want to have this result value();\n value();\n
not value();\n
// \nvalue();
// Example 2 : if we want to insert a statement after this one \n
we want to have
// value();\n
// not \nvalue();
--> this case appears on member replacement for example
if (!isPreviousElementNewline && !isFirstElement && !previousIsWhiteSpace) {
// Insert after the new line
originalIndex++;
// We want to adjust the indentation while considering the new element that we
// added
originalIndex = adjustIndentation(indentation, nodeText, originalIndex, false);
}
nodeText.addElement(originalIndex, addedTextElement);
originalIndex++;
} else {
nodeText.addElement(originalIndex, addedTextElement);
originalIndex++;
}
}
if (addedTextElement.isNewline()) {
boolean followedByUnindent = isFollowedByUnindent(diffElements, diffIndex);
boolean nextIsRightBrace = nextIsRightBrace(originalIndex);
boolean nextIsNewLine = originalElements.get(originalIndex).isNewline();
if ((!nextIsNewLine && !nextIsRightBrace) || followedByUnindent) {
originalIndex = adjustIndentation(indentation, nodeText, originalIndex, followedByUnindent);
}
}
diffIndex++;
}
/*
* A list iterator which provides a method to know the current positioning
*/
public static class ArrayIterator implements ListIterator {
ListIterator iterator;
public ArrayIterator(List elements) {
this(elements, 0);
}
public ArrayIterator(List elements, int index) {
this.iterator = elements.listIterator(index);
}
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public T next() {
return iterator.next();
}
@Override
public boolean hasPrevious() {
return iterator.hasPrevious();
}
@Override
public T previous() {
return iterator.previous();
}
@Override
public int nextIndex() {
return iterator.nextIndex();
}
@Override
public int previousIndex() {
return iterator.previousIndex();
}
/*
* Returns the current index in the underlying list
*/
public int index() {
return iterator.nextIndex() - 1;
}
@Override
public void remove() {
iterator.remove();
;
}
@Override
public void set(T e) {
iterator.set(e);
}
@Override
public void add(T e) {
iterator.add(e);
;
}
}
/*
* Returns true if the next element in the list is an added element of type CsmUnindent
*/
private boolean isFollowedByUnindent(List diffElements, int diffIndex) {
int nextIndexValue = diffIndex + 1;
return (nextIndexValue) < diffElements.size()
&& diffElements.get(nextIndexValue).isAdded()
&& diffElements.get(nextIndexValue).getElement() instanceof CsmUnindent;
}
private int adjustIndentation(
List indentation, NodeText nodeText, int nodeTextIndex, boolean followedByUnindent) {
List indentationAdj =
processIndentation(indentation, nodeText.getElements().subList(0, nodeTextIndex - 1));
if (nodeTextIndex < nodeText.numberOfElements()
&& nodeText.getTextElement(nodeTextIndex).isToken(RBRACE)) {
indentationAdj = indentationAdj.subList(
0, indentationAdj.size() - Math.min(STANDARD_INDENTATION_SIZE, indentationAdj.size()));
} else if (followedByUnindent) {
indentationAdj = indentationAdj.subList(0, Math.max(0, indentationAdj.size() - STANDARD_INDENTATION_SIZE));
}
for (TextElement e : indentationAdj) {
if ((nodeTextIndex < nodeText.numberOfElements())
&& nodeText.getTextElement(nodeTextIndex).isSpaceOrTab()) {
nodeTextIndex++;
} else {
nodeText.getElements().add(nodeTextIndex++, e);
}
}
if (nodeTextIndex < 0) {
throw new IllegalStateException();
}
return nodeTextIndex;
}
/*
* Returns true if the current Added
element is preceded by a Removed
element.
*/
private boolean isAReplacement(int diffIndex) {
return (diffIndex > 0)
&& diffElements.get(diffIndex).isAdded()
&& diffElements.get(diffIndex - 1).isRemoved();
}
/*
* Returns true if the current Removed
element is followed by a Added
element.
*/
private boolean isReplaced(int diffIndex) {
return (diffIndex < diffElements.size() - 1)
&& diffElements.get(diffIndex + 1).isAdded()
&& diffElements.get(diffIndex).isRemoved();
}
@Override
public String toString() {
return "Difference{" + diffElements + '}';
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy