com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of javaparser-core Show documentation
Show all versions of javaparser-core Show documentation
The core parser functionality. This may be all you need.
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.*;
import static com.github.javaparser.TokenTypes.eolTokenKind;
import static com.github.javaparser.utils.Utils.assertNotNull;
import static com.github.javaparser.utils.Utils.decapitalize;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
import com.github.javaparser.JavaToken;
import com.github.javaparser.Range;
import com.github.javaparser.ast.DataKey;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.comments.BlockComment;
import com.github.javaparser.ast.comments.Comment;
import com.github.javaparser.ast.comments.JavadocComment;
import com.github.javaparser.ast.comments.LineComment;
import com.github.javaparser.ast.nodeTypes.NodeWithVariables;
import com.github.javaparser.ast.observer.AstObserver;
import com.github.javaparser.ast.observer.ObservableProperty;
import com.github.javaparser.ast.observer.PropagatingAstObserver;
import com.github.javaparser.ast.type.PrimitiveType;
import com.github.javaparser.ast.visitor.TreeVisitor;
import com.github.javaparser.printer.ConcreteSyntaxModel;
import com.github.javaparser.printer.concretesyntaxmodel.*;
import com.github.javaparser.printer.lexicalpreservation.LexicalDifferenceCalculator.CsmChild;
import com.github.javaparser.utils.LineSeparator;
import com.github.javaparser.utils.Pair;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.*;
/**
* A Lexical Preserving Printer is used to capture all the lexical information while parsing, update them when
* operating on the AST and then used them to reproduce the source code
* in its original formatting including the AST changes.
*/
public class LexicalPreservingPrinter {
private static String JAVA_UTIL_OPTIONAL = Optional.class.getCanonicalName();
private static String JAVAPARSER_AST_NODELIST = NodeList.class.getCanonicalName();
private static AstObserver observer;
/**
* The nodetext for a node is stored in the node's data field. This is the key to set and retrieve it.
*/
public static final DataKey NODE_TEXT_DATA = new DataKey() {};
private static final LexicalDifferenceCalculator LEXICAL_DIFFERENCE_CALCULATOR = new LexicalDifferenceCalculator();
//
// Factory methods
//
/**
* Prepares the node so it can be used in the print methods.
* The correct order is:
*
* - Parse some code
* - Call this setup method on the result
* - Make changes to the AST as desired
* - Use one of the print methods on this class to print out the original source code with your changes added
*
*
* @return the node passed as a parameter for your convenience.
*/
public static N setup(N node) {
assertNotNull(node);
if (observer == null) {
observer = createObserver();
}
node.getTokenRange().ifPresent(r -> {
storeInitialText(node);
// Setup observer
if (!node.isRegistered(observer)) {
node.registerForSubtree(observer);
}
});
return node;
}
/*
* Returns true if the lexical preserving printer is initialized on the node
*/
public static boolean isAvailableOn(Node node) {
return node.containsData(NODE_TEXT_DATA);
}
//
// Constructor and setup
//
private static AstObserver createObserver() {
return new LexicalPreservingPrinter.Observer();
}
private static class Observer extends PropagatingAstObserver {
@Override
public void concretePropertyChange(
Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
if (oldValue == newValue) {
// Not really a change, ignore
return;
}
if (property == ObservableProperty.RANGE || property == ObservableProperty.COMMENTED_NODE) {
return;
}
if (property == ObservableProperty.COMMENT) {
Optional parentNode = observedNode.getParentNode();
NodeText nodeText = parentNode
.map(parent -> getOrCreateNodeText(parentNode.get()))
.orElseGet(() -> getOrCreateNodeText(observedNode));
if (oldValue == null) {
// this case corresponds to the addition of a comment
int index = parentNode.isPresent()
? // Find the position of the comment node and put in front of it the [...]
nodeText.findChild(observedNode)
: //
0;
/* Add the same indentation to the comment as the previous node
* for example if we want to add a comment on the body of the method declaration :
* Actual code
* {@code
* public class Foo {
* void visit(final UnknownType n, final Void arg)
* {
* }
* }
* }
* Expected result
* {@code
* public class Foo {
* void visit(final UnknownType n, final Void arg)
* //added comment <-- we should insert indentation before the comment
* {
* }
* }
* }
*/
fixIndentOfAddedNode(nodeText, index - 1);
LineSeparator lineSeparator = observedNode.getLineEndingStyleOrDefault(LineSeparator.SYSTEM);
nodeText.addElement(index++, makeCommentToken((Comment) newValue));
nodeText.addToken(index, eolTokenKind(lineSeparator), lineSeparator.asRawString());
// code indentation after inserting an eol token may be wrong
} else if (newValue == null) {
// this case corresponds to a deletion of a comment
if (oldValue instanceof Comment) {
if (((Comment) oldValue).isOrphan()) {
nodeText = getOrCreateNodeText(observedNode);
}
int index = getIndexOfComment((Comment) oldValue, nodeText);
nodeText.removeElement(index);
if (isCompleteLine(nodeText.getElements(), index)) {
removeAllExtraCharacters(nodeText.getElements(), index);
} else {
removeAllExtraCharactersStartingFrom(
nodeText.getElements().listIterator(index));
}
} else {
throw new UnsupportedOperationException("Trying to remove something that is not a comment!");
}
} else {
// this is a replacement of a comment
List matchingTokens =
findTokenTextElementForComment((Comment) oldValue, nodeText);
if (matchingTokens.size() != 1) {
throw new IllegalStateException("The matching comment to be replaced could not be found");
}
Comment newComment = (Comment) newValue;
TokenTextElement matchingElement = matchingTokens.get(0);
nodeText.replace(matchingElement.and(matchingElement.matchByRange()), makeCommentToken(newComment));
}
}
NodeText nodeText = getOrCreateNodeText(observedNode);
if (nodeText == null) {
throw new NullPointerException(observedNode.getClass().getSimpleName());
}
LEXICAL_DIFFERENCE_CALCULATOR.calculatePropertyChange(nodeText, observedNode, property, oldValue, newValue);
}
private boolean isCompleteLine(List elements, int index) {
if (index <= 0 || index >= elements.size()) return false;
boolean isCompleteLine = true;
ListIterator iterator = elements.listIterator(index);
// verify if elements after the index are only spaces or tabs
while (iterator.hasNext()) {
TextElement textElement = iterator.next();
if (textElement.isNewline()) break;
if (textElement.isSpaceOrTab()) continue;
isCompleteLine = false;
break;
}
// verify if elements before the index are only spaces or tabs
iterator = elements.listIterator(index);
while (iterator.hasPrevious() && isCompleteLine) {
TextElement textElement = iterator.previous();
if (textElement.isNewline()) break;
if (textElement.isSpaceOrTab()) continue;
isCompleteLine = false;
}
return isCompleteLine;
}
private void removeAllExtraCharacters(List elements, int index) {
if (index < 0 || index >= elements.size()) return;
removeAllExtraCharactersStartingFrom(elements.listIterator(index));
removeAllExtraCharactersBeforePosition(elements.listIterator(index));
}
/*
* Removes all spaces,tabs characters before this position
*/
private void removeAllExtraCharactersBeforePosition(ListIterator iterator) {
while (iterator.hasPrevious()) {
TextElement textElement = iterator.previous();
if (textElement.isSpaceOrTab()) {
iterator.remove();
continue;
}
break;
}
}
/*
* Removes all spaces,tabs or new line characters starting from this position
*/
private void removeAllExtraCharactersStartingFrom(ListIterator iterator) {
while (iterator.hasNext()) {
TextElement textElement = iterator.next();
if (textElement.isSpaceOrTab()) {
iterator.remove();
continue;
}
if (textElement.isNewline()) {
iterator.remove();
}
break;
}
}
private TokenTextElement makeCommentToken(Comment newComment) {
if (newComment.isJavadocComment()) {
return new TokenTextElement(
JAVADOC_COMMENT, newComment.getHeader() + newComment.getContent() + newComment.getFooter());
}
if (newComment.isLineComment()) {
return new TokenTextElement(SINGLE_LINE_COMMENT, newComment.getHeader() + newComment.getContent());
}
if (newComment.isBlockComment()) {
return new TokenTextElement(
MULTI_LINE_COMMENT, newComment.getHeader() + newComment.getContent() + newComment.getFooter());
}
throw new UnsupportedOperationException(
"Unknown type of comment: " + newComment.getClass().getSimpleName());
}
private int getIndexOfComment(Comment oldValue, NodeText nodeText) {
List matchingTokens = findTokenTextElementForComment(oldValue, nodeText);
if (!matchingTokens.isEmpty()) {
TextElement matchingElement = matchingTokens.get(0);
return nodeText.findElement(matchingElement.and(matchingElement.matchByRange()));
}
// If no matching TokenTextElements were found, we try searching through ChildTextElements as well
List matchingChilds = findChildTextElementForComment(oldValue, nodeText);
ChildTextElement matchingChild = matchingChilds.get(0);
return nodeText.findElement(matchingChild.and(matchingChild.matchByRange()));
}
/*
* Comment
*/
private List findChildTextElementForComment(Comment oldValue, NodeText nodeText) {
List matchingChildElements;
matchingChildElements = selectMatchingChildElements(oldValue, nodeText);
if (matchingChildElements.size() > 1) {
// Duplicate child nodes found, refine the result
matchingChildElements = matchingChildElements.stream()
.filter(t -> t.getChild().hasRange() && oldValue.hasRange())
.filter(t -> t.getChild()
.getRange()
.get()
.equals(oldValue.getRange().get())
|| (t.getChild().getComment().isPresent()
&& t.getChild().getComment().get().hasRange()
&& t.getChild()
.getComment()
.get()
.getRange()
.get()
.equals(oldValue.getRange().get())))
.collect(toList());
}
if (matchingChildElements.size() != 1) {
throw new IllegalStateException(
"The matching child text element for the comment to be removed could not be found.");
}
return matchingChildElements;
}
private List selectMatchingChildElements(Comment oldValue, NodeText nodeText) {
List result = new ArrayList<>();
List childTextElements = nodeText.getElements().stream()
.filter(e -> e.isChild())
.map(c -> (ChildTextElement) c)
.collect(toList());
ListIterator iterator = childTextElements.listIterator();
while (iterator.hasNext()) {
ChildTextElement textElement = iterator.next();
if (textElement.isComment() && isSameComment(((Comment) textElement.getChild()), oldValue)) {
result.add(textElement);
continue;
}
Node node = textElement.getChild();
if (node.getComment().isPresent()
&& isSameComment(node.getComment().get(), oldValue)) {
result.add(textElement);
continue;
}
}
return result;
}
private boolean isSameComment(Comment childValue, Comment oldValue) {
return childValue.getContent().equals(oldValue.getContent());
}
private List findTokenTextElementForComment(Comment oldValue, NodeText nodeText) {
List matchingTokens;
if (oldValue instanceof JavadocComment) {
matchingTokens = nodeText.getElements().stream()
.filter(e -> e.isToken(JAVADOC_COMMENT))
.map(e -> (TokenTextElement) e)
.filter(t -> t.getText().equals(oldValue.asString()))
.collect(toList());
} else if (oldValue instanceof BlockComment) {
matchingTokens = nodeText.getElements().stream()
.filter(e -> e.isToken(MULTI_LINE_COMMENT))
.map(e -> (TokenTextElement) e)
.filter(t -> t.getText().equals(oldValue.asString()))
.collect(toList());
} else {
matchingTokens = nodeText.getElements().stream()
.filter(e -> e.isToken(SINGLE_LINE_COMMENT))
.map(e -> (TokenTextElement) e)
.filter(t -> t.getText().trim().equals((oldValue.asString()).trim()))
.collect(toList());
}
// To check that a comment matches in the list of tokens, if exists the range must be always checked,
// as comments with the same content may exist on different lines.
return matchingTokens.stream()
.filter(t -> (!t.getToken().hasRange() && !oldValue.hasRange())
|| (t.getToken().hasRange()
&& oldValue.hasRange()
&& t.getToken()
.getRange()
.get()
.equals(oldValue.getRange().get())))
.collect(toList());
}
/**
* This method inserts new space tokens at the given {@code index}. If a new
* comment is added to the token list at the position following {@code index},
* the new comment and the node will have the same indent.
*
* @param nodeText The text of the node
* @param index The position at which the analysis should start
*/
private void fixIndentOfAddedNode(NodeText nodeText, int index) {
if (index <= 0) {
return;
}
TextElement currentSpaceCandidate = null;
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()));
}
}
}
break;
}
}
}
@Override
public void concreteListChange(
NodeList> changedList, ListChangeType type, int index, Node nodeAddedOrRemoved) {
NodeText nodeText = getOrCreateNodeText(changedList.getParentNodeForChildren());
final List differenceElements;
if (type == AstObserver.ListChangeType.REMOVAL) {
differenceElements = LEXICAL_DIFFERENCE_CALCULATOR.calculateListRemovalDifference(
findNodeListName(changedList), changedList, index);
} else if (type == AstObserver.ListChangeType.ADDITION) {
differenceElements = LEXICAL_DIFFERENCE_CALCULATOR.calculateListAdditionDifference(
findNodeListName(changedList), changedList, index, nodeAddedOrRemoved);
} else {
throw new UnsupportedOperationException("Unknown change type: " + type);
}
Difference difference =
new Difference(differenceElements, nodeText, changedList.getParentNodeForChildren());
difference.apply();
}
@Override
public void concreteListReplacement(NodeList> changedList, int index, Node oldValue, Node newValue) {
NodeText nodeText = getOrCreateNodeText(changedList.getParentNodeForChildren());
List differenceElements =
LEXICAL_DIFFERENCE_CALCULATOR.calculateListReplacementDifference(
findNodeListName(changedList), changedList, index, newValue);
Difference difference =
new Difference(differenceElements, nodeText, changedList.getParentNodeForChildren());
difference.apply();
}
}
private static void storeInitialText(Node root) {
Map> tokensByNode = new IdentityHashMap<>();
// We go over tokens and find to which nodes they belong. Note that we do not traverse the tokens as they were
// on a list but as they were organized in a tree. At each time we select only the branch corresponding to the
// range of interest and ignore all other branches
root.getTokenRange().ifPresent(rootTokenRange -> {
for (JavaToken token : rootTokenRange) {
Range tokenRange =
token.getRange().orElseThrow(() -> new RuntimeException("Token without range: " + token));
Node owner = root.findByRange(tokenRange)
.orElseThrow(() -> new RuntimeException("Token without node owning it: " + token));
if (!tokensByNode.containsKey(owner)) {
tokensByNode.put(owner, new LinkedList<>());
}
tokensByNode.get(owner).add(token);
}
// Now that we know the tokens we use them to create the initial NodeText for each node
new TreeVisitor() {
@Override
public void process(Node node) {
if (!node.isPhantom()) {
storeInitialTextForOneNode(node, tokensByNode.get(node));
}
}
}.visitBreadthFirst(root);
});
}
private static void storeInitialTextForOneNode(Node node, List nodeTokens) {
if (nodeTokens == null) {
nodeTokens = Collections.emptyList();
}
List> elements = new LinkedList<>();
for (Node child : node.getChildNodes()) {
if (!child.isPhantom()) {
if (!child.hasRange()) {
throw new RuntimeException("Range not present on node " + child);
}
elements.add(new Pair<>(child.getRange().get(), new ChildTextElement(child)));
}
}
for (JavaToken token : nodeTokens) {
elements.add(new Pair<>(token.getRange().get(), new TokenTextElement(token)));
}
elements.sort(comparing(e -> e.a.begin));
node.setData(
NODE_TEXT_DATA, new NodeText(elements.stream().map(p -> p.b).collect(toList())));
}
//
// Iterators
//
private static Iterator tokensPreceeding(final Node node) {
if (!node.getParentNode().isPresent()) {
return new TextElementIteratorsFactory.EmptyIterator<>();
}
// There is the awfully painful case of the fake types involved in variable declarators and
// fields or variable declaration that are, of course, an exception...
NodeText parentNodeText = getOrCreateNodeText(node.getParentNode().get());
int index = parentNodeText.tryToFindChild(node);
if (index == NodeText.NOT_FOUND) {
if (node.getParentNode().get() instanceof VariableDeclarator) {
return tokensPreceeding(node.getParentNode().get());
}
return new TextElementIteratorsFactory.EmptyIterator();
}
return new TextElementIteratorsFactory.CascadingIterator<>(
TextElementIteratorsFactory.partialReverseIterator(parentNodeText, index - 1),
() -> tokensPreceeding(node.getParentNode().get()));
}
//
// Printing methods
//
/**
* Print a Node into a String, preserving the lexical information.
*/
public static String print(Node node) {
LexicalPreservingVisitor visitor = new LexicalPreservingVisitor();
final NodeText nodeText = getOrCreateNodeText(node);
nodeText.getElements().forEach(element -> element.accept(visitor));
return visitor.toString();
}
//
// Methods to handle transformations
//
private static void prettyPrintingTextNode(Node node, NodeText nodeText) {
if (node instanceof PrimitiveType) {
PrimitiveType primitiveType = (PrimitiveType) node;
switch (primitiveType.getType()) {
case BOOLEAN:
nodeText.addToken(BOOLEAN, node.toString());
break;
case CHAR:
nodeText.addToken(CHAR, node.toString());
break;
case BYTE:
nodeText.addToken(BYTE, node.toString());
break;
case SHORT:
nodeText.addToken(SHORT, node.toString());
break;
case INT:
nodeText.addToken(INT, node.toString());
break;
case LONG:
nodeText.addToken(LONG, node.toString());
break;
case FLOAT:
nodeText.addToken(FLOAT, node.toString());
break;
case DOUBLE:
nodeText.addToken(DOUBLE, node.toString());
break;
default:
throw new IllegalArgumentException();
}
return;
}
if (node instanceof JavadocComment) {
Comment comment = (JavadocComment) node;
nodeText.addToken(
JAVADOC_COMMENT, comment.getHeader() + ((JavadocComment) node).getContent() + comment.getFooter());
return;
}
if (node instanceof BlockComment) {
Comment comment = (BlockComment) node;
nodeText.addToken(
MULTI_LINE_COMMENT, comment.getHeader() + ((BlockComment) node).getContent() + comment.getFooter());
return;
}
if (node instanceof LineComment) {
Comment comment = (LineComment) node;
nodeText.addToken(SINGLE_LINE_COMMENT, comment.getHeader() + comment.getContent());
return;
}
if (node instanceof Modifier) {
Modifier modifier = (Modifier) node;
nodeText.addToken(
LexicalDifferenceCalculator.toToken(modifier),
modifier.getKeyword().asString());
return;
}
interpret(node, ConcreteSyntaxModel.forClass(node.getClass()), nodeText);
}
/**
* TODO: Process CsmIndent and CsmUnindent before reaching this point
*/
private static NodeText interpret(Node node, CsmElement csm, NodeText nodeText) {
LexicalDifferenceCalculator.CalculatedSyntaxModel calculatedSyntaxModel =
new LexicalDifferenceCalculator().calculatedSyntaxModelForNode(csm, node);
List indentation = findIndentation(node);
boolean pendingIndentation = false;
// Add a comment and line separator if necessary
node.getComment().ifPresent(comment -> {
// new comment has no range so in this case we want to force the comment before the node
if (!comment.hasRange()) {
LineSeparator lineSeparator = node.getLineEndingStyleOrDefault(LineSeparator.SYSTEM);
calculatedSyntaxModel.elements.add(
0, new CsmToken(eolTokenKind(lineSeparator), lineSeparator.asRawString()));
calculatedSyntaxModel.elements.add(0, new CsmChild(comment));
}
});
for (CsmElement element : calculatedSyntaxModel.elements) {
if (element instanceof CsmIndent) {
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, " "));
}
}
} else if (element instanceof CsmUnindent) {
for (int i = 0; i < Difference.STANDARD_INDENTATION_SIZE && indentation.size() > 0; i++) {
indentation.remove(indentation.size() - 1);
}
}
if (pendingIndentation && !(element instanceof CsmToken && ((CsmToken) element).isNewLine())) {
indentation.forEach(nodeText::addElement);
}
pendingIndentation = false;
if (element instanceof LexicalDifferenceCalculator.CsmChild) {
nodeText.addChild(((LexicalDifferenceCalculator.CsmChild) element).getChild());
} else if (element instanceof CsmToken) {
CsmToken csmToken = (CsmToken) element;
nodeText.addToken(csmToken.getTokenType(), csmToken.getContent());
if (csmToken.isNewLine()) {
pendingIndentation = true;
}
} else if (element instanceof CsmMix) {
CsmMix csmMix = (CsmMix) element;
csmMix.getElements().forEach(e -> interpret(node, e, nodeText));
} else {
// Indentation should probably be dealt with before because an indentation has effects also on the
// following lines
if (!(element instanceof CsmIndent) && !(element instanceof CsmUnindent)) {
throw new UnsupportedOperationException(
"Unknown element type: " + element.getClass().getSimpleName());
}
}
}
// Array brackets are a pain... we do not have a way to represent them explicitly in the AST
// so they have to be handled in a special way
if (node instanceof VariableDeclarator) {
VariableDeclarator variableDeclarator = (VariableDeclarator) node;
variableDeclarator.getParentNode().ifPresent(parent -> ((NodeWithVariables>) parent)
.getMaximumCommonType()
.ifPresent(mct -> {
int extraArrayLevels = variableDeclarator.getType().getArrayLevel() - mct.getArrayLevel();
for (int i = 0; i < extraArrayLevels; i++) {
nodeText.addElement(new TokenTextElement(LBRACKET));
nodeText.addElement(new TokenTextElement(RBRACKET));
}
}));
}
return nodeText;
}
// Visible for testing
static NodeText getOrCreateNodeText(Node node) {
if (!node.containsData(NODE_TEXT_DATA)) {
NodeText nodeText = new NodeText();
node.setData(NODE_TEXT_DATA, nodeText);
prettyPrintingTextNode(node, nodeText);
}
return node.getData(NODE_TEXT_DATA);
}
// Visible for testing
static List findIndentation(Node node) {
List followingNewlines = new LinkedList<>();
Iterator it = tokensPreceeding(node);
while (it.hasNext()) {
TokenTextElement tte = it.next();
if (tte.getTokenKind() == SINGLE_LINE_COMMENT || tte.isNewline()) {
break;
}
followingNewlines.add(tte);
}
Collections.reverse(followingNewlines);
for (int i = 0; i < followingNewlines.size(); i++) {
if (!followingNewlines.get(i).isSpaceOrTab()) {
return followingNewlines.subList(0, i);
}
}
return followingNewlines;
}
//
// Helper methods
//
private static boolean isReturningOptionalNodeList(Method m) {
if (!m.getReturnType().getCanonicalName().equals(JAVA_UTIL_OPTIONAL)) {
return false;
}
if (!(m.getGenericReturnType() instanceof ParameterizedType)) {
return false;
}
ParameterizedType parameterizedType = (ParameterizedType) m.getGenericReturnType();
java.lang.reflect.Type optionalArgument = parameterizedType.getActualTypeArguments()[0];
return (optionalArgument.getTypeName().startsWith(JAVAPARSER_AST_NODELIST));
}
private static ObservableProperty findNodeListName(NodeList> nodeList) {
Node parent = nodeList.getParentNodeForChildren();
for (Method m : parent.getClass().getMethods()) {
if (m.getParameterCount() == 0
&& m.getReturnType().getCanonicalName().equals(JAVAPARSER_AST_NODELIST)) {
try {
Object raw = m.invoke(parent);
if (!(raw instanceof NodeList)) {
throw new IllegalStateException(
"Expected NodeList, found " + raw.getClass().getCanonicalName());
}
NodeList> result = (NodeList>) raw;
if (result == nodeList) {
String name = m.getName();
if (name.startsWith("get")) {
name = name.substring("get".length());
}
return ObservableProperty.fromCamelCaseName(decapitalize(name));
}
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
} else if (m.getParameterCount() == 0 && isReturningOptionalNodeList(m)) {
try {
Optional> raw = (Optional>) m.invoke(parent);
if (raw.isPresent() && raw.get() == nodeList) {
String name = m.getName();
if (name.startsWith("get")) {
name = name.substring("get".length());
}
return ObservableProperty.fromCamelCaseName(decapitalize(name));
}
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
throw new IllegalArgumentException("Cannot find list name of NodeList of size " + nodeList.size());
}
}