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