JavaParser Source Viewer

Home|JavaParser/com/github/javaparser/printer/lexicalpreservation/LexicalPreservingPrinter.java
1/*
2 * Copyright (C) 2007-2010 JĂșlio Vilmar Gesser.
3 * Copyright (C) 2011, 2013-2020 The JavaParser Team.
4 *
5 * This file is part of JavaParser.
6 *
7 * JavaParser can be used either under the terms of
8 * a) the GNU Lesser General Public License as published by
9 *     the Free Software Foundation, either version 3 of the License, or
10 *     (at your option) any later version.
11 * b) the terms of the Apache License
12 *
13 * You should have received a copy of both licenses in LICENCE.LGPL and
14 * LICENCE.APACHE. Please refer to those files for details.
15 *
16 * JavaParser is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 * GNU Lesser General Public License for more details.
20 */
21
22package com.github.javaparser.printer.lexicalpreservation;
23
24import com.github.javaparser.JavaToken;
25import com.github.javaparser.Range;
26import com.github.javaparser.ast.DataKey;
27import com.github.javaparser.ast.Modifier;
28import com.github.javaparser.ast.Node;
29import com.github.javaparser.ast.NodeList;
30import com.github.javaparser.ast.body.VariableDeclarator;
31import com.github.javaparser.ast.comments.BlockComment;
32import com.github.javaparser.ast.comments.Comment;
33import com.github.javaparser.ast.comments.JavadocComment;
34import com.github.javaparser.ast.comments.LineComment;
35import com.github.javaparser.ast.nodeTypes.NodeWithVariables;
36import com.github.javaparser.ast.observer.AstObserver;
37import com.github.javaparser.ast.observer.ObservableProperty;
38import com.github.javaparser.ast.observer.PropagatingAstObserver;
39import com.github.javaparser.ast.type.PrimitiveType;
40import com.github.javaparser.ast.visitor.TreeVisitor;
41import com.github.javaparser.printer.ConcreteSyntaxModel;
42import com.github.javaparser.printer.concretesyntaxmodel.CsmElement;
43import com.github.javaparser.printer.concretesyntaxmodel.CsmIndent;
44import com.github.javaparser.printer.concretesyntaxmodel.CsmMix;
45import com.github.javaparser.printer.concretesyntaxmodel.CsmToken;
46import com.github.javaparser.printer.concretesyntaxmodel.CsmUnindent;
47import com.github.javaparser.utils.LineSeparator;
48import com.github.javaparser.utils.Pair;
49
50import java.io.IOException;
51import java.io.StringWriter;
52import java.io.Writer;
53import java.lang.reflect.InvocationTargetException;
54import java.lang.reflect.Method;
55import java.lang.reflect.ParameterizedType;
56import java.util.Collections;
57import java.util.IdentityHashMap;
58import java.util.Iterator;
59import java.util.LinkedList;
60import java.util.List;
61import java.util.Map;
62import java.util.Optional;
63
64import static com.github.javaparser.GeneratedJavaParserConstants.*;
65import static com.github.javaparser.TokenTypes.eolTokenKind;
66import static com.github.javaparser.utils.Utils.assertNotNull;
67import static com.github.javaparser.utils.Utils.decapitalize;
68import static java.util.Comparator.comparing;
69import static java.util.stream.Collectors.toList;
70
71/**
72 * A Lexical Preserving Printer is used to capture all the lexical information while parsing, update them when
73 * operating on the AST and then used them to reproduce the source code
74 * in its original formatting including the AST changes.
75 */
76public class LexicalPreservingPrinter {
77
78    private static AstObserver observer;
79
80    /**
81     * The nodetext for a node is stored in the node's data field. This is the key to set and retrieve it.
82     */
83    public static final DataKey<NodeTextNODE_TEXT_DATA = new DataKey<NodeText>() {
84    };
85
86    private static final LexicalDifferenceCalculator LEXICAL_DIFFERENCE_CALCULATOR = new LexicalDifferenceCalculator();
87
88    //
89    // Factory methods
90    //
91
92    /**
93     * Prepares the node so it can be used in the print methods.
94     * The correct order is:
95     * <ol>
96     * <li>Parse some code</li>
97     * <li>Call this setup method on the result</li>
98     * <li>Make changes to the AST as desired</li>
99     * <li>Use one of the print methods on this class to print out the original source code with your changes added</li>
100     * </ol>
101     *
102     * @return the node passed as a parameter for your convenience.
103     */
104    public static <N extends NodeN setup(N node) {
105        assertNotNull(node);
106
107        if (observer == null) {
108            observer = createObserver();
109        }
110
111        node.getTokenRange().ifPresent(r -> {
112            storeInitialText(node);
113            // Setup observer
114            if (!node.isRegistered(observer)) {
115                node.registerForSubtree(observer);
116            }
117        });
118        return node;
119    }
120
121    //
122    // Constructor and setup
123    //
124
125    private static AstObserver createObserver() {
126        return new LexicalPreservingPrinter.Observer();
127    }
128
129    private static class Observer extends PropagatingAstObserver {
130
131        @Override
132        public void concretePropertyChange(Node observedNodeObservableProperty propertyObject oldValueObject newValue) {
133            if (oldValue == newValue) {
134                // Not really a change, ignore
135                return;
136            }
137            if (property == ObservableProperty.RANGE || property == ObservableProperty.COMMENTED_NODE) {
138                return;
139            }
140            if (property == ObservableProperty.COMMENT) {
141                Optional<NodeparentNode = observedNode.getParentNode();
142                NodeText nodeText = parentNode
143                        .map(parent -> getOrCreateNodeText(parentNode.get()))
144                        // We're at the root node. 
145                        .orElse(getOrCreateNodeText(observedNode));
146
147                if (oldValue == null) {
148                    int index = parentNode.isPresent() ?
149                            // Find the position of the comment node and put in front of it the [...]
150                            nodeText.findChild(observedNode) :
151                            // 
152                            0;
153                    // Add the same indent depth of the comment to the following node
154                    fixIndentOfMovedNode(nodeTextindex);
155
156                    LineSeparator lineSeparator = observedNode.getLineEndingStyleOrDefault(LineSeparator.SYSTEM);
157                    nodeText.addElement(indexmakeCommentToken((CommentnewValue));
158                    nodeText.addToken(index + 1eolTokenKind(lineSeparator), lineSeparator.asRawString());
159                } else if (newValue == null) {
160                    if (oldValue instanceof Comment) {
161                        if (((CommentoldValue).isOrphan()) {
162                            nodeText = getOrCreateNodeText(observedNode);
163                        }
164                        int index = getIndexOfComment((CommentoldValuenodeText);
165                        nodeText.removeElement(index);
166                        if (nodeText.getElements().get(index).isNewline()) {
167                            nodeText.removeElement(index);
168                        }
169                    } else {
170                        throw new UnsupportedOperationException();
171                    }
172                } else {
173                    List<TokenTextElementmatchingTokens = findTokenTextElementForComment((CommentoldValuenodeText);
174
175                    if (matchingTokens.size() != 1) {
176                        throw new IllegalStateException("The matching comment to be replaced could not be found");
177                    }
178
179                    Comment newComment = (CommentnewValue;
180                    TokenTextElement matchingElement = matchingTokens.get(0);
181                    nodeText.replace(matchingElement.and(matchingElement.matchByRange()), makeCommentToken(newComment));
182                }
183            }
184            NodeText nodeText = getOrCreateNodeText(observedNode);
185
186            if (nodeText == null) {
187                throw new NullPointerException(observedNode.getClass().getSimpleName());
188            }
189
190            LEXICAL_DIFFERENCE_CALCULATOR.calculatePropertyChange(nodeTextobservedNodepropertyoldValuenewValue);
191        }
192
193        private TokenTextElement makeCommentToken(Comment newComment) {
194            if (newComment.isJavadocComment()) {
195                return new TokenTextElement(JAVADOC_COMMENT"/**" + newComment.getContent() + "*/");
196            }
197            if (newComment.isLineComment()) {
198                return new TokenTextElement(SINGLE_LINE_COMMENT"//" + newComment.getContent());
199            }
200            if (newComment.isBlockComment()) {
201                return new TokenTextElement(MULTI_LINE_COMMENT"/*" + newComment.getContent() + "*/");
202            }
203            throw new UnsupportedOperationException("Unknown type of comment: " + newComment.getClass().getSimpleName());
204
205        }
206
207        private int getIndexOfComment(Comment oldValueNodeText nodeText) {
208            List<TokenTextElementmatchingTokens = findTokenTextElementForComment(oldValuenodeText);
209
210            if (!matchingTokens.isEmpty()) {
211                TextElement matchingElement = matchingTokens.get(0);
212                return nodeText.findElement(matchingElement.and(matchingElement.matchByRange()));
213            }
214            // If no matching TokenTextElements were found, we try searching through ChildTextElements as well
215            List<ChildTextElementmatchingChilds = findChildTextElementForComment(oldValuenodeText);
216            ChildTextElement matchingChild = matchingChilds.get(0);
217            return nodeText.findElement(matchingChild.and(matchingChild.matchByRange()));
218        }
219
220        private List<ChildTextElementfindChildTextElementForComment(Comment oldValueNodeText nodeText) {
221            List<ChildTextElementmatchingChildElements;
222
223            matchingChildElements = nodeText.getElements().stream()
224                    .filter(e -> e.isChild())
225                    .map(c -> (ChildTextElementc)
226                    .filter(c -> c.isComment())
227                    .filter(c -> ((Commentc.getChild()).getContent().equals(oldValue.getContent()))
228                    .collect(toList());
229
230            if (matchingChildElements.size() > 1) {
231                // Duplicate child nodes found, refine the result
232                matchingChildElements = matchingChildElements.stream()
233                        .filter(t -> isEqualRange(t.getChild().getRange(), oldValue.getRange()))
234                        .collect(toList());
235            }
236
237            if (matchingChildElements.size() != 1) {
238                throw new IllegalStateException("The matching child text element for the comment to be removed could not be found.");
239            }
240
241            return matchingChildElements;
242        }
243
244        private List<TokenTextElementfindTokenTextElementForComment(Comment oldValueNodeText nodeText) {
245            List<TokenTextElementmatchingTokens;
246
247            if (oldValue instanceof JavadocComment) {
248                matchingTokens = nodeText.getElements().stream()
249                        .filter(e -> e.isToken(JAVADOC_COMMENT))
250                        .map(e -> (TokenTextElemente)
251                        .filter(t -> t.getText().equals("/**" + oldValue.getContent() + "*/"))
252                        .collect(toList());
253            } else if (oldValue instanceof BlockComment) {
254                matchingTokens = nodeText.getElements().stream()
255                        .filter(e -> e.isToken(MULTI_LINE_COMMENT))
256                        .map(e -> (TokenTextElemente)
257                        .filter(t -> t.getText().equals("/*" + oldValue.getContent() + "*/"))
258                        .collect(toList());
259            } else {
260                matchingTokens = nodeText.getElements().stream()
261                        .filter(e -> e.isToken(SINGLE_LINE_COMMENT))
262                        .map(e -> (TokenTextElemente)
263                        .filter(t -> t.getText().trim().equals(("//" + oldValue.getContent()).trim()))
264                        .collect(toList());
265            }
266
267            if (matchingTokens.size() > 1) {
268                // Duplicate comments found, refine the result
269                matchingTokens = matchingTokens.stream()
270                        .filter(t -> isEqualRange(t.getToken().getRange(), oldValue.getRange()))
271                        .collect(toList());
272            }
273
274            return matchingTokens;
275        }
276
277        private boolean isEqualRange(Optional<Rangerange1Optional<Rangerange2) {
278            if (range1.isPresent() && range2.isPresent()) {
279                return range1.get().equals(range2.get());
280            }
281
282            return false;
283        }
284
285        /**
286         * This method inserts new space tokens at the given {@code index}. If a new comment is added to the node
287         * at the position of {@code index}, the new comment and the node will have the same indent.
288         *
289         * @param nodeText The text of the node
290         * @param index    The position where a new comment will be added to
291         */
292        private void fixIndentOfMovedNode(NodeText nodeTextint index) {
293            if (index <= 0) {
294                return;
295            }
296
297            for (int i = index - 1i >= 0i--) {
298                TextElement spaceCandidate = nodeText.getTextElement(i);
299                if (!spaceCandidate.isSpaceOrTab()) {
300                    if (spaceCandidate.isNewline() && i != index - 1) {
301                        for (int j = 0j < (index - 1) - ij++) {
302                            nodeText.addElement(index, new TokenTextElement(JavaToken.Kind.SPACE.getKind()));
303                        }
304                    }
305                    break;
306                }
307            }
308        }
309
310        @Override
311        public void concreteListChange(NodeList<?> changedListListChangeType typeint indexNode nodeAddedOrRemoved) {
312            NodeText nodeText = getOrCreateNodeText(changedList.getParentNodeForChildren());
313            final List<DifferenceElementdifferenceElements;
314            if (type == AstObserver.ListChangeType.REMOVAL) {
315                differenceElements = LEXICAL_DIFFERENCE_CALCULATOR.calculateListRemovalDifference(findNodeListName(changedList), changedListindex);
316            } else if (type == AstObserver.ListChangeType.ADDITION) {
317                differenceElements = LEXICAL_DIFFERENCE_CALCULATOR.calculateListAdditionDifference(findNodeListName(changedList), changedListindexnodeAddedOrRemoved);
318            } else {
319                throw new UnsupportedOperationException();
320            }
321
322            Difference difference = new Difference(differenceElementsnodeTextchangedList.getParentNodeForChildren());
323            difference.apply();
324        }
325
326        @Override
327        public void concreteListReplacement(NodeList<?> changedListint indexNode oldValueNode newValue) {
328            NodeText nodeText = getOrCreateNodeText(changedList.getParentNodeForChildren());
329            List<DifferenceElementdifferenceElements = LEXICAL_DIFFERENCE_CALCULATOR.calculateListReplacementDifference(findNodeListName(changedList), changedListindexnewValue);
330
331            Difference difference = new Difference(differenceElementsnodeTextchangedList.getParentNodeForChildren());
332            difference.apply();
333        }
334    }
335
336    private static void storeInitialText(Node root) {
337        Map<NodeList<JavaToken>> tokensByNode = new IdentityHashMap<>();
338
339        // We go over tokens and find to which nodes they belong. Note that we do not traverse the tokens as they were
340        // on a list but as they were organized in a tree. At each time we select only the branch corresponding to the
341        // range of interest and ignore all other branches
342        root.getTokenRange().ifPresent(rootTokenRange -> {
343            for (JavaToken token : rootTokenRange) {
344                Range tokenRange = token.getRange().orElseThrow(() -> new RuntimeException("Token without range: " + token));
345                Node owner = findNodeForToken(roottokenRange).orElseThrow(() -> new RuntimeException("Token without node owning it: " + token));
346                if (!tokensByNode.containsKey(owner)) {
347                    tokensByNode.put(owner, new LinkedList<>());
348                }
349                tokensByNode.get(owner).add(token);
350            }
351
352            // Now that we know the tokens we use them to create the initial NodeText for each node
353            new TreeVisitor() {
354                @Override
355                public void process(Node node) {
356                    if (!PhantomNodeLogic.isPhantomNode(node)) {
357                        LexicalPreservingPrinter.storeInitialTextForOneNode(nodetokensByNode.get(node));
358                    }
359                }
360            }.visitBreadthFirst(root);
361        });
362    }
363
364    private static Optional<NodefindNodeForToken(Node nodeRange tokenRange) {
365        if (PhantomNodeLogic.isPhantomNode(node)) {
366            return Optional.empty();
367        }
368        if(!node.getRange().isPresent()) {
369            return Optional.empty();
370        }
371        if (!node.getRange().get().contains(tokenRange)) {
372            return Optional.empty();
373        }
374
375        for (Node child : node.getChildNodes()) {
376            Optional<Nodefound = findNodeForToken(childtokenRange);
377            if (found.isPresent()) {
378                return found;
379            }
380        }
381        return Optional.of(node);
382    }
383
384    private static void storeInitialTextForOneNode(Node nodeList<JavaTokennodeTokens) {
385        if (nodeTokens == null) {
386            nodeTokens = Collections.emptyList();
387        }
388        List<Pair<RangeTextElement>> elements = new LinkedList<>();
389        for (Node child : node.getChildNodes()) {
390            if (!PhantomNodeLogic.isPhantomNode(child)) {
391                if (!child.getRange().isPresent()) {
392                    throw new RuntimeException("Range not present on node " + child);
393                }
394                elements.add(new Pair<>(child.getRange().get(), new ChildTextElement(child)));
395            }
396        }
397        for (JavaToken token : nodeTokens) {
398            elements.add(new Pair<>(token.getRange().get(), new TokenTextElement(token)));
399        }
400        elements.sort(comparing(e -> e.a.begin));
401        node.setData(NODE_TEXT_DATA, new NodeText(elements.stream().map(p -> p.b).collect(toList())));
402    }
403
404    //
405    // Iterators
406    //
407
408    private static Iterator<TokenTextElementtokensPreceeding(final Node node) {
409        if (!node.getParentNode().isPresent()) {
410            return new TextElementIteratorsFactory.EmptyIterator<>();
411        }
412        // There is the awfully painful case of the fake types involved in variable declarators and
413        // fields or variable declaration that are, of course, an exception...
414
415        NodeText parentNodeText = getOrCreateNodeText(node.getParentNode().get());
416        int index = parentNodeText.tryToFindChild(node);
417        if (index == NodeText.NOT_FOUND) {
418            if (node.getParentNode().get() instanceof VariableDeclarator) {
419                return tokensPreceeding(node.getParentNode().get());
420            } else {
421                throw new IllegalArgumentException(
422                        String.format("I could not find child '%s' in parent '%s'. parentNodeText: %s",
423                                nodenode.getParentNode().get(), parentNodeText));
424            }
425        }
426
427        return new TextElementIteratorsFactory.CascadingIterator<>(
428                TextElementIteratorsFactory.partialReverseIterator(parentNodeTextindex - 1),
429                () -> tokensPreceeding(node.getParentNode().get()));
430    }
431
432    //
433    // Printing methods
434    //
435
436    /**
437     * Print a Node into a String, preserving the lexical information.
438     */
439    public static String print(Node node) {
440        StringWriter writer = new StringWriter();
441        try {
442            print(nodewriter);
443        } catch (IOException e) {
444            throw new RuntimeException("Unexpected IOException on a StringWriter"e);
445        }
446        return writer.toString();
447    }
448
449    /**
450     * Print a Node into a Writer, preserving the lexical information.
451     */
452    public static void print(Node nodeWriter writer) throws IOException {
453        if (!node.containsData(NODE_TEXT_DATA)) {
454            getOrCreateNodeText(node);
455        }
456        final NodeText text = node.getData(NODE_TEXT_DATA);
457        writer.append(text.expand());
458    }
459
460    //
461    // Methods to handle transformations
462    //
463
464    private static void prettyPrintingTextNode(Node nodeNodeText nodeText) {
465        if (node instanceof PrimitiveType) {
466            PrimitiveType primitiveType = (PrimitiveTypenode;
467            switch (primitiveType.getType()) {
468                case BOOLEAN:
469                    nodeText.addToken(BOOLEANnode.toString());
470                    break;
471                case CHAR:
472                    nodeText.addToken(CHARnode.toString());
473                    break;
474                case BYTE:
475                    nodeText.addToken(BYTEnode.toString());
476                    break;
477                case SHORT:
478                    nodeText.addToken(SHORTnode.toString());
479                    break;
480                case INT:
481                    nodeText.addToken(INTnode.toString());
482                    break;
483                case LONG:
484                    nodeText.addToken(LONGnode.toString());
485                    break;
486                case FLOAT:
487                    nodeText.addToken(FLOATnode.toString());
488                    break;
489                case DOUBLE:
490                    nodeText.addToken(DOUBLEnode.toString());
491                    break;
492                default:
493                    throw new IllegalArgumentException();
494            }
495            return;
496        }
497        if (node instanceof JavadocComment) {
498            nodeText.addToken(JAVADOC_COMMENT"/**" + ((JavadocCommentnode).getContent() + "*/");
499            return;
500        }
501        if (node instanceof BlockComment) {
502            nodeText.addToken(MULTI_LINE_COMMENT"/*" + ((BlockCommentnode).getContent() + "*/");
503            return;
504        }
505        if (node instanceof LineComment) {
506            nodeText.addToken(SINGLE_LINE_COMMENT"//" + ((LineCommentnode).getContent());
507            return;
508        }
509        if (node instanceof Modifier) {
510            Modifier modifier = (Modifiernode;
511            nodeText.addToken(LexicalDifferenceCalculator.toToken(modifier), modifier.getKeyword().asString());
512            return;
513        }
514
515        interpret(nodeConcreteSyntaxModel.forClass(node.getClass()), nodeText);
516    }
517
518    /**
519     * TODO: Process CsmIndent and CsmUnindent before reaching this point
520     */
521    private static NodeText interpret(Node nodeCsmElement csmNodeText nodeText) {
522        LexicalDifferenceCalculator.CalculatedSyntaxModel calculatedSyntaxModel = new LexicalDifferenceCalculator().calculatedSyntaxModelForNode(csmnode);
523
524        List<TokenTextElementindentation = findIndentation(node);
525
526        boolean pendingIndentation = false;
527        for (CsmElement element : calculatedSyntaxModel.elements) {
528            if (element instanceof CsmIndent) {
529                int indexCurrentElement = calculatedSyntaxModel.elements.indexOf(element);
530                if (calculatedSyntaxModel.elements.size() > indexCurrentElement &&
531                        !(calculatedSyntaxModel.elements.get(indexCurrentElement + 1) instanceof CsmUnindent)) {
532                    for (int i = 0i < Difference.STANDARD_INDENTATION_SIZEi++) {
533                        indentation.add(new TokenTextElement(SPACE" "));
534                    }
535                }
536            } else if (element instanceof CsmUnindent) {
537                for (int i = 0i < Difference.STANDARD_INDENTATION_SIZE && indentation.size() > 0i++) {
538                    indentation.remove(indentation.size() - 1);
539                }
540            }
541
542            if (pendingIndentation && !(element instanceof CsmToken && ((CsmTokenelement).isNewLine())) {
543                indentation.forEach(nodeText::addElement);
544            }
545
546            pendingIndentation = false;
547            if (element instanceof LexicalDifferenceCalculator.CsmChild) {
548                nodeText.addChild(((LexicalDifferenceCalculator.CsmChildelement).getChild());
549            } else if (element instanceof CsmToken) {
550                CsmToken csmToken = (CsmTokenelement;
551                nodeText.addToken(csmToken.getTokenType(), csmToken.getContent(node));
552                if (csmToken.isNewLine()) {
553                    pendingIndentation = true;
554                }
555            } else if (element instanceof CsmMix) {
556                CsmMix csmMix = (CsmMixelement;
557                csmMix.getElements().forEach(e -> interpret(nodeenodeText));
558            } else {
559                // Indentation should probably be dealt with before because an indentation has effects also on the
560                // following lines
561                if (!(element instanceof CsmIndent) && !(element instanceof CsmUnindent)) {
562                    throw new UnsupportedOperationException(element.getClass().getSimpleName());
563                }
564            }
565        }
566        // Array brackets are a pain... we do not have a way to represent them explicitly in the AST
567        // so they have to be handled in a special way
568        if (node instanceof VariableDeclarator) {
569            VariableDeclarator variableDeclarator = (VariableDeclaratornode;
570            variableDeclarator.getParentNode().ifPresent(parent ->
571                    ((NodeWithVariables<?>) parent).getMaximumCommonType().ifPresent(mct -> {
572                        int extraArrayLevels = variableDeclarator.getType().getArrayLevel() - mct.getArrayLevel();
573                        for (int i = 0i < extraArrayLevelsi++) {
574                            nodeText.addElement(new TokenTextElement(LBRACKET));
575                            nodeText.addElement(new TokenTextElement(RBRACKET));
576                        }
577                    })
578            );
579        }
580        return nodeText;
581    }
582
583    // Visible for testing
584    static NodeText getOrCreateNodeText(Node node) {
585        if (!node.containsData(NODE_TEXT_DATA)) {
586            NodeText nodeText = new NodeText();
587            node.setData(NODE_TEXT_DATAnodeText);
588            prettyPrintingTextNode(nodenodeText);
589        }
590        return node.getData(NODE_TEXT_DATA);
591    }
592
593    // Visible for testing
594    static List<TokenTextElementfindIndentation(Node node) {
595        List<TokenTextElementfollowingNewlines = new LinkedList<>();
596        Iterator<TokenTextElementit = tokensPreceeding(node);
597        while (it.hasNext()) {
598            TokenTextElement tte = it.next();
599            if (tte.getTokenKind() == SINGLE_LINE_COMMENT
600                    || tte.isNewline()) {
601                break;
602            } else {
603                followingNewlines.add(tte);
604            }
605        }
606        Collections.reverse(followingNewlines);
607        for (int i = 0i < followingNewlines.size(); i++) {
608            if (!followingNewlines.get(i).isSpaceOrTab()) {
609                return followingNewlines.subList(0i);
610            }
611        }
612        return followingNewlines;
613    }
614
615    //
616    // Helper methods
617    //
618
619    private static boolean isReturningOptionalNodeList(Method m) {
620        if (!m.getReturnType().getCanonicalName().equals(Optional.class.getCanonicalName())) {
621            return false;
622        }
623        if (!(m.getGenericReturnType() instanceof ParameterizedType)) {
624            return false;
625        }
626        ParameterizedType parameterizedType = (ParameterizedTypem.getGenericReturnType();
627        java.lang.reflect.Type optionalArgument = parameterizedType.getActualTypeArguments()[0];
628        return (optionalArgument.getTypeName().startsWith(NodeList.class.getCanonicalName()));
629    }
630
631    private static ObservableProperty findNodeListName(NodeList<?> nodeList) {
632        Node parent = nodeList.getParentNodeForChildren();
633        for (Method m : parent.getClass().getMethods()) {
634            if (m.getParameterCount() == 0 && m.getReturnType().getCanonicalName().equals(NodeList.class.getCanonicalName())) {
635                try {
636                    Object raw = m.invoke(parent);
637                    if (!(raw instanceof NodeList)) {
638                        throw new IllegalStateException("Expected NodeList, found " + raw.getClass().getCanonicalName());
639                    }
640                    NodeList<?> result = (NodeList<?>) raw;
641                    if (result == nodeList) {
642                        String name = m.getName();
643                        if (name.startsWith("get")) {
644                            name = name.substring("get".length());
645                        }
646                        return ObservableProperty.fromCamelCaseName(decapitalize(name));
647                    }
648                } catch (IllegalAccessException | InvocationTargetException e) {
649                    throw new RuntimeException(e);
650                }
651            } else if (m.getParameterCount() == 0 && isReturningOptionalNodeList(m)) {
652                try {
653                    Optional<NodeList<?>> raw = (Optional<NodeList<?>>) m.invoke(parent);
654                    if (raw.isPresent() && raw.get() == nodeList) {
655                        String name = m.getName();
656                        if (name.startsWith("get")) {
657                            name = name.substring("get".length());
658                        }
659                        return ObservableProperty.fromCamelCaseName(decapitalize(name));
660                    }
661                } catch (IllegalAccessException | InvocationTargetException e) {
662                    throw new RuntimeException(e);
663                }
664            }
665        }
666        throw new IllegalArgumentException("Cannot find list name of NodeList of size " + nodeList.size());
667    }
668}
669
MembersX
LexicalPreservingPrinter:Observer:concretePropertyChange:Block:Block:Block:Block:index
LexicalPreservingPrinter:isReturningOptionalNodeList:Block:optionalArgument
LexicalPreservingPrinter:storeInitialText:Block:tokensByNode
LexicalPreservingPrinter:Observer:concreteListChange:Block:nodeText
LexicalPreservingPrinter:Observer:concretePropertyChange:Block:Block:Block:matchingElement
LexicalPreservingPrinter:interpret:Block:indentation
LexicalPreservingPrinter:findNodeListName:Block:Block:Block:Block:raw
LexicalPreservingPrinter:storeInitialText
LexicalPreservingPrinter:Observer:concretePropertyChange
LexicalPreservingPrinter:prettyPrintingTextNode
LexicalPreservingPrinter:findIndentation:Block:followingNewlines
LexicalPreservingPrinter:Observer:concreteListReplacement:Block:differenceElements
LexicalPreservingPrinter:interpret:Block:calculatedSyntaxModel
LexicalPreservingPrinter:tokensPreceeding:Block:index
LexicalPreservingPrinter:interpret:Block:Block:variableDeclarator
LexicalPreservingPrinter:findNodeListName:Block:parent
LexicalPreservingPrinter:getOrCreateNodeText
LexicalPreservingPrinter:Observer:concretePropertyChange:Block:Block:Block:index
LexicalPreservingPrinter:interpret:Block:Block:Block:csmMix
LexicalPreservingPrinter:createObserver
LexicalPreservingPrinter:Observer:findChildTextElementForComment
LexicalPreservingPrinter:Observer:getIndexOfComment:Block:Block:matchingElement
LexicalPreservingPrinter:Observer:getIndexOfComment:Block:matchingTokens
LexicalPreservingPrinter:tokensPreceeding
LexicalPreservingPrinter:Observer:getIndexOfComment:Block:matchingChild
LexicalPreservingPrinter:prettyPrintingTextNode:Block:Block:primitiveType
LexicalPreservingPrinter:findIndentation:Block:it
LexicalPreservingPrinter:Observer:concretePropertyChange:Block:nodeText
LexicalPreservingPrinter:Observer:concretePropertyChange:Block:Block:Block:lineSeparator
LexicalPreservingPrinter:prettyPrintingTextNode:Block:Block:modifier
LexicalPreservingPrinter:findNodeListName:Block:Block:Block:Block:result
LexicalPreservingPrinter:isReturningOptionalNodeList:Block:parameterizedType
LexicalPreservingPrinter:Observer:findChildTextElementForComment:Block:matchingChildElements
LexicalPreservingPrinter:print
LexicalPreservingPrinter:Observer:isEqualRange
LexicalPreservingPrinter:setup
LexicalPreservingPrinter:tokensPreceeding:Block:parentNodeText
LexicalPreservingPrinter:interpret
LexicalPreservingPrinter:interpret:Block:Block:Block:extraArrayLevels
LexicalPreservingPrinter:Observer:findTokenTextElementForComment:Block:matchingTokens
LexicalPreservingPrinter:interpret:Block:Block:Block:csmToken
LexicalPreservingPrinter:storeInitialText:Block:Block:Block:tokenRange
LexicalPreservingPrinter:Observer:concretePropertyChange:Block:Block:nodeText
LexicalPreservingPrinter:storeInitialTextForOneNode:Block:elements
LexicalPreservingPrinter:findIndentation:Block:Block:tte
LexicalPreservingPrinter:print:Block:text
LexicalPreservingPrinter:Observer:concreteListReplacement:Block:difference
LexicalPreservingPrinter:interpret:Block:pendingIndentation
LexicalPreservingPrinter:Observer:concretePropertyChange:Block:Block:parentNode
LexicalPreservingPrinter:observer
LexicalPreservingPrinter:Observer:concretePropertyChange:Block:Block:Block:matchingTokens
LexicalPreservingPrinter:Observer:concretePropertyChange:Block:Block:Block:newComment
LexicalPreservingPrinter:print:Block:writer
LexicalPreservingPrinter:Observer:concreteListReplacement:Block:nodeText
LexicalPreservingPrinter:Observer:concreteListChange
LexicalPreservingPrinter:findNodeForToken
LexicalPreservingPrinter:Observer:concreteListReplacement
LexicalPreservingPrinter:Observer:fixIndentOfMovedNode
LexicalPreservingPrinter:storeInitialTextForOneNode
LexicalPreservingPrinter:Observer:concreteListChange:Block:difference
LexicalPreservingPrinter:LEXICAL_DIFFERENCE_CALCULATOR
LexicalPreservingPrinter:findNodeListName
LexicalPreservingPrinter:Observer:makeCommentToken
LexicalPreservingPrinter:findIndentation
LexicalPreservingPrinter:findNodeListName:Block:Block:Block:Block:Block:name
LexicalPreservingPrinter:Observer:getIndexOfComment
LexicalPreservingPrinter:getOrCreateNodeText:Block:Block:nodeText
LexicalPreservingPrinter:NODE_TEXT_DATA
LexicalPreservingPrinter:storeInitialText:Block:Block:process
LexicalPreservingPrinter:Observer:concreteListChange:Block:differenceElements
LexicalPreservingPrinter:interpret:Block:Block:Block:indexCurrentElement
LexicalPreservingPrinter:isReturningOptionalNodeList
LexicalPreservingPrinter:findNodeForToken:Block:Block:found
LexicalPreservingPrinter:Observer:fixIndentOfMovedNode:Block:Block:spaceCandidate
LexicalPreservingPrinter:storeInitialText:Block:Block:Block:owner
LexicalPreservingPrinter:Observer:findTokenTextElementForComment
LexicalPreservingPrinter:Observer:getIndexOfComment:Block:matchingChilds
Members
X