Home | History | Annotate | Download | only in lexicalpreservation
      1 /*
      2  * Copyright (C) 2007-2010 Jlio 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.Node;
     27 import com.github.javaparser.ast.NodeList;
     28 import com.github.javaparser.ast.body.VariableDeclarator;
     29 import com.github.javaparser.ast.comments.Comment;
     30 import com.github.javaparser.ast.comments.JavadocComment;
     31 import com.github.javaparser.ast.nodeTypes.NodeWithVariables;
     32 import com.github.javaparser.ast.observer.AstObserver;
     33 import com.github.javaparser.ast.observer.ObservableProperty;
     34 import com.github.javaparser.ast.observer.PropagatingAstObserver;
     35 import com.github.javaparser.ast.type.PrimitiveType;
     36 import com.github.javaparser.ast.visitor.TreeVisitor;
     37 import com.github.javaparser.printer.ConcreteSyntaxModel;
     38 import com.github.javaparser.printer.concretesyntaxmodel.CsmElement;
     39 import com.github.javaparser.printer.concretesyntaxmodel.CsmMix;
     40 import com.github.javaparser.printer.concretesyntaxmodel.CsmToken;
     41 import com.github.javaparser.utils.Pair;
     42 import com.github.javaparser.utils.Utils;
     43 
     44 import java.io.IOException;
     45 import java.io.StringWriter;
     46 import java.io.Writer;
     47 import java.lang.reflect.InvocationTargetException;
     48 import java.lang.reflect.Method;
     49 import java.lang.reflect.ParameterizedType;
     50 import java.util.*;
     51 import java.util.stream.Collectors;
     52 
     53 import static com.github.javaparser.GeneratedJavaParserConstants.*;
     54 import static com.github.javaparser.TokenTypes.eolTokenKind;
     55 import static com.github.javaparser.utils.Utils.assertNotNull;
     56 import static com.github.javaparser.utils.Utils.decapitalize;
     57 import static java.util.Comparator.*;
     58 
     59 /**
     60  * A Lexical Preserving Printer is used to capture all the lexical information while parsing, update them when
     61  * operating on the AST and then used them to reproduce the source code
     62  * in its original formatting including the AST changes.
     63  */
     64 public class LexicalPreservingPrinter {
     65 
     66     /**
     67      * The nodetext for a node is stored in the node's data field. This is the key to set and retrieve it.
     68      */
     69     public static final DataKey<NodeText> NODE_TEXT_DATA = new DataKey<NodeText>() {
     70     };
     71 
     72     //
     73     // Factory methods
     74     //
     75 
     76     /**
     77      * Parse the code and setup the LexicalPreservingPrinter.
     78      *
     79      * @deprecated use setup(Node) and the static methods on this class.
     80      */
     81     public static <N extends Node> Pair<ParseResult<N>, LexicalPreservingPrinter> setup(ParseStart<N> parseStart,
     82                                                                                         Provider provider) {
     83         ParseResult<N> parseResult = new JavaParser().parse(parseStart, provider);
     84         if (!parseResult.isSuccessful()) {
     85             throw new RuntimeException("Parsing failed, unable to setup the lexical preservation printer: "
     86                     + parseResult.getProblems());
     87         }
     88         LexicalPreservingPrinter lexicalPreservingPrinter = new LexicalPreservingPrinter(parseResult.getResult().get());
     89         return new Pair<>(parseResult, lexicalPreservingPrinter);
     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 Node> N setup(N node) {
    105         assertNotNull(node);
    106 
    107         node.getTokenRange().ifPresent(r -> {
    108             storeInitialText(node);
    109 
    110             // Setup observer
    111             AstObserver observer = createObserver();
    112 
    113             node.registerForSubtree(observer);
    114         });
    115         return node;
    116     }
    117 
    118     //
    119     // Constructor and setup
    120     //
    121 
    122     /**
    123      * @deprecated use setup(Node) to prepare a node for lexical preservation,
    124      * then use the static methods on this class to print it.
    125      */
    126     @Deprecated
    127     public LexicalPreservingPrinter(Node node) {
    128         setup(node);
    129     }
    130 
    131     private static AstObserver createObserver() {
    132         return new PropagatingAstObserver() {
    133             @Override
    134             public void concretePropertyChange(Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
    135                 // Not really a change, ignoring
    136                 if ((oldValue != null && oldValue.equals(newValue)) || (oldValue == null && newValue == null)) {
    137                     return;
    138                 }
    139                 if (property == ObservableProperty.RANGE || property == ObservableProperty.COMMENTED_NODE) {
    140                     return;
    141                 }
    142                 if (property == ObservableProperty.COMMENT) {
    143                     if (!observedNode.getParentNode().isPresent()) {
    144                         throw new IllegalStateException();
    145                     }
    146                     NodeText nodeText = getOrCreateNodeText(observedNode.getParentNode().get());
    147                     if (oldValue == null) {
    148                         // Find the position of the comment node and put in front of it the comment and a newline
    149                         int index = nodeText.findChild(observedNode);
    150                         nodeText.addChild(index, (Comment) newValue);
    151                         nodeText.addToken(index + 1, eolTokenKind(), Utils.EOL);
    152                     } else if (newValue == null) {
    153                         if (oldValue instanceof JavadocComment) {
    154                             JavadocComment javadocComment = (JavadocComment) oldValue;
    155                             List<TokenTextElement> matchingTokens = nodeText.getElements().stream().filter(e -> e.isToken(JAVADOC_COMMENT)
    156                                     && ((TokenTextElement) e).getText().equals("/**" + javadocComment.getContent() + "*/")).map(e -> (TokenTextElement) e).collect(Collectors.toList());
    157                             if (matchingTokens.size() != 1) {
    158                                 throw new IllegalStateException();
    159                             }
    160                             int index = nodeText.findElement(matchingTokens.get(0));
    161                             nodeText.removeElement(index);
    162                             if (nodeText.getElements().get(index).isNewline()) {
    163                                 nodeText.removeElement(index);
    164                             }
    165                         } else {
    166                             throw new UnsupportedOperationException();
    167                         }
    168                     } else {
    169                         if (oldValue instanceof JavadocComment) {
    170                             JavadocComment oldJavadocComment = (JavadocComment) oldValue;
    171                             List<TokenTextElement> matchingTokens = nodeText.getElements().stream().filter(e -> e.isToken(JAVADOC_COMMENT)
    172                                     && ((TokenTextElement) e).getText().equals("/**" + oldJavadocComment.getContent() + "*/")).map(e -> (TokenTextElement) e).collect(Collectors.toList());
    173                             if (matchingTokens.size() != 1) {
    174                                 throw new IllegalStateException();
    175                             }
    176                             JavadocComment newJavadocComment = (JavadocComment) newValue;
    177                             nodeText.replace(matchingTokens.get(0), new TokenTextElement(JAVADOC_COMMENT, "/**" + newJavadocComment.getContent() + "*/"));
    178                         } else {
    179                             throw new UnsupportedOperationException();
    180                         }
    181                     }
    182                 }
    183                 NodeText nodeText = getOrCreateNodeText(observedNode);
    184 
    185                 if (nodeText == null) {
    186                     throw new NullPointerException(observedNode.getClass().getSimpleName());
    187                 }
    188 
    189                 new LexicalDifferenceCalculator().calculatePropertyChange(nodeText, observedNode, property, oldValue, newValue);
    190             }
    191 
    192             @Override
    193             public void concreteListChange(NodeList changedList, ListChangeType type, int index, Node nodeAddedOrRemoved) {
    194                 NodeText nodeText = getOrCreateNodeText(changedList.getParentNodeForChildren());
    195                 if (type == ListChangeType.REMOVAL) {
    196                     new LexicalDifferenceCalculator().calculateListRemovalDifference(findNodeListName(changedList), changedList, index).apply(nodeText, changedList.getParentNodeForChildren());
    197                 } else if (type == ListChangeType.ADDITION) {
    198                     new LexicalDifferenceCalculator().calculateListAdditionDifference(findNodeListName(changedList), changedList, index, nodeAddedOrRemoved).apply(nodeText, changedList.getParentNodeForChildren());
    199                 } else {
    200                     throw new UnsupportedOperationException();
    201                 }
    202             }
    203 
    204             @Override
    205             public void concreteListReplacement(NodeList changedList, int index, Node oldValue, Node newValue) {
    206                 NodeText nodeText = getOrCreateNodeText(changedList.getParentNodeForChildren());
    207                 new LexicalDifferenceCalculator().calculateListReplacementDifference(findNodeListName(changedList), changedList, index, newValue).apply(nodeText, changedList.getParentNodeForChildren());
    208             }
    209         };
    210     }
    211 
    212     private static void storeInitialText(Node root) {
    213         Map<Node, List<JavaToken>> tokensByNode = new IdentityHashMap<>();
    214 
    215         // We go over tokens and find to which nodes they belong. Note that we do not traverse the tokens as they were
    216         // on a list but as they were organized in a tree. At each time we select only the branch corresponding to the
    217         // range of interest and ignore all other branches
    218         for (JavaToken token : root.getTokenRange().get()) {
    219             Range tokenRange = token.getRange().orElseThrow(() -> new RuntimeException("Token without range: " + token));
    220             Node owner = findNodeForToken(root, tokenRange);
    221             if (owner == null) {
    222                 throw new RuntimeException("Token without node owning it: " + token);
    223             }
    224             if (!tokensByNode.containsKey(owner)) {
    225                 tokensByNode.put(owner, new LinkedList<>());
    226             }
    227             tokensByNode.get(owner).add(token);
    228         }
    229 
    230         // Now that we know the tokens we use them to create the initial NodeText for each node
    231         new TreeVisitor() {
    232             @Override
    233             public void process(Node node) {
    234                 if (!PhantomNodeLogic.isPhantomNode(node)) {
    235                     LexicalPreservingPrinter.storeInitialTextForOneNode(node, tokensByNode.get(node));
    236                 }
    237             }
    238         }.visitBreadthFirst(root);
    239     }
    240 
    241     private static Node findNodeForToken(Node node, Range tokenRange) {
    242         if (PhantomNodeLogic.isPhantomNode(node)) {
    243             return null;
    244         }
    245         if (node.getRange().get().contains(tokenRange)) {
    246             for (Node child : node.getChildNodes()) {
    247                 Node found = findNodeForToken(child, tokenRange);
    248                 if (found != null) {
    249                     return found;
    250                 }
    251             }
    252             return node;
    253         } else {
    254             return null;
    255         }
    256     }
    257 
    258     private static void storeInitialTextForOneNode(Node node, List<JavaToken> nodeTokens) {
    259         if (nodeTokens == null) {
    260             nodeTokens = Collections.emptyList();
    261         }
    262         List<Pair<Range, TextElement>> elements = new LinkedList<>();
    263         for (Node child : node.getChildNodes()) {
    264             if (!PhantomNodeLogic.isPhantomNode(child)) {
    265                 if (!child.getRange().isPresent()) {
    266                     throw new RuntimeException("Range not present on node " + child);
    267                 }
    268                 elements.add(new Pair<>(child.getRange().get(), new ChildTextElement(child)));
    269             }
    270         }
    271         for (JavaToken token : nodeTokens) {
    272             elements.add(new Pair<>(token.getRange().get(), new TokenTextElement(token)));
    273         }
    274         elements.sort(comparing(e -> e.a.begin));
    275         node.setData(NODE_TEXT_DATA, new NodeText(elements.stream().map(p -> p.b).collect(Collectors.toList())));
    276     }
    277 
    278     //
    279     // Iterators
    280     //
    281 
    282     private static Iterator<TokenTextElement> tokensPreceeding(final Node node) {
    283         if (!node.getParentNode().isPresent()) {
    284             return new TextElementIteratorsFactory.EmptyIterator<>();
    285         }
    286         // There is the awfully painful case of the fake types involved in variable declarators and
    287         // fields or variable declaration that are, of course, an exception...
    288 
    289         NodeText parentNodeText = getOrCreateNodeText(node.getParentNode().get());
    290         int index = parentNodeText.tryToFindChild(node);
    291         if (index == NodeText.NOT_FOUND) {
    292             if (node.getParentNode().get() instanceof VariableDeclarator) {
    293                 return tokensPreceeding(node.getParentNode().get());
    294             } else {
    295                 throw new IllegalArgumentException(
    296                         String.format("I could not find child '%s' in parent '%s'. parentNodeText: %s",
    297                                 node, node.getParentNode().get(), parentNodeText));
    298             }
    299         }
    300 
    301         return new TextElementIteratorsFactory.CascadingIterator<>(
    302                 TextElementIteratorsFactory.partialReverseIterator(parentNodeText, index - 1),
    303                 () -> tokensPreceeding(node.getParentNode().get()));
    304     }
    305 
    306     //
    307     // Printing methods
    308     //
    309 
    310     /**
    311      * Print a Node into a String, preserving the lexical information.
    312      */
    313     public static String print(Node node) {
    314         StringWriter writer = new StringWriter();
    315         try {
    316             print(node, writer);
    317         } catch (IOException e) {
    318             throw new RuntimeException("Unexpected IOException on a StringWriter", e);
    319         }
    320         return writer.toString();
    321     }
    322 
    323     /**
    324      * Print a Node into a Writer, preserving the lexical information.
    325      */
    326     public static void print(Node node, Writer writer) throws IOException {
    327         if (!node.containsData(NODE_TEXT_DATA)) {
    328             getOrCreateNodeText(node);
    329         }
    330         final NodeText text = node.getData(NODE_TEXT_DATA);
    331         writer.append(text.expand());
    332     }
    333 
    334     //
    335     // Methods to handle transformations
    336     //
    337 
    338     private static void prettyPrintingTextNode(Node node, NodeText nodeText) {
    339         if (node instanceof PrimitiveType) {
    340             PrimitiveType primitiveType = (PrimitiveType) node;
    341             switch (primitiveType.getType()) {
    342                 case BOOLEAN:
    343                     nodeText.addToken(BOOLEAN, node.toString());
    344                     break;
    345                 case CHAR:
    346                     nodeText.addToken(CHAR, node.toString());
    347                     break;
    348                 case BYTE:
    349                     nodeText.addToken(BYTE, node.toString());
    350                     break;
    351                 case SHORT:
    352                     nodeText.addToken(SHORT, node.toString());
    353                     break;
    354                 case INT:
    355                     nodeText.addToken(INT, node.toString());
    356                     break;
    357                 case LONG:
    358                     nodeText.addToken(LONG, node.toString());
    359                     break;
    360                 case FLOAT:
    361                     nodeText.addToken(FLOAT, node.toString());
    362                     break;
    363                 case DOUBLE:
    364                     nodeText.addToken(DOUBLE, node.toString());
    365                     break;
    366                 default:
    367                     throw new IllegalArgumentException();
    368             }
    369             return;
    370         }
    371         if (node instanceof JavadocComment) {
    372             nodeText.addToken(JAVADOC_COMMENT, "/**" + ((JavadocComment) node).getContent() + "*/");
    373             return;
    374         }
    375 
    376         interpret(node, ConcreteSyntaxModel.forClass(node.getClass()), nodeText);
    377     }
    378 
    379     private static NodeText interpret(Node node, CsmElement csm, NodeText nodeText) {
    380         LexicalDifferenceCalculator.CalculatedSyntaxModel calculatedSyntaxModel = new LexicalDifferenceCalculator().calculatedSyntaxModelForNode(csm, node);
    381 
    382         List<TokenTextElement> indentation = findIndentation(node);
    383 
    384         boolean pendingIndentation = false;
    385         for (CsmElement element : calculatedSyntaxModel.elements) {
    386             if (pendingIndentation && !(element instanceof CsmToken && ((CsmToken) element).isNewLine())) {
    387                 indentation.forEach(nodeText::addElement);
    388             }
    389             pendingIndentation = false;
    390             if (element instanceof LexicalDifferenceCalculator.CsmChild) {
    391                 nodeText.addChild(((LexicalDifferenceCalculator.CsmChild) element).getChild());
    392             } else if (element instanceof CsmToken) {
    393                 CsmToken csmToken = (CsmToken) element;
    394                 nodeText.addToken(csmToken.getTokenType(), csmToken.getContent(node));
    395                 if (csmToken.isNewLine()) {
    396                     pendingIndentation = true;
    397                 }
    398             } else if (element instanceof CsmMix) {
    399                 CsmMix csmMix = (CsmMix) element;
    400                 csmMix.getElements().forEach(e -> interpret(node, e, nodeText));
    401             } else {
    402                 throw new UnsupportedOperationException(element.getClass().getSimpleName());
    403             }
    404         }
    405         // Array brackets are a pain... we do not have a way to represent them explicitly in the AST
    406         // so they have to be handled in a special way
    407         if (node instanceof VariableDeclarator) {
    408             VariableDeclarator variableDeclarator = (VariableDeclarator) node;
    409             variableDeclarator.getParentNode().ifPresent(parent ->
    410                     ((NodeWithVariables<?>) parent).getMaximumCommonType().ifPresent(mct -> {
    411                         int extraArrayLevels = variableDeclarator.getType().getArrayLevel() - mct.getArrayLevel();
    412                         for (int i = 0; i < extraArrayLevels; i++) {
    413                             nodeText.addElement(new TokenTextElement(LBRACKET));
    414                             nodeText.addElement(new TokenTextElement(RBRACKET));
    415                         }
    416                     })
    417             );
    418         }
    419         return nodeText;
    420     }
    421 
    422     // Visible for testing
    423     static NodeText getOrCreateNodeText(Node node) {
    424         if (!node.containsData(NODE_TEXT_DATA)) {
    425             NodeText nodeText = new NodeText();
    426             node.setData(NODE_TEXT_DATA, nodeText);
    427             prettyPrintingTextNode(node, nodeText);
    428         }
    429         return node.getData(NODE_TEXT_DATA);
    430     }
    431 
    432     // Visible for testing
    433     static List<TokenTextElement> findIndentation(Node node) {
    434         List<TokenTextElement> followingNewlines = new LinkedList<>();
    435         Iterator<TokenTextElement> it = tokensPreceeding(node);
    436         while (it.hasNext()) {
    437             TokenTextElement tte = it.next();
    438             if (tte.getTokenKind() == SINGLE_LINE_COMMENT
    439                     || tte.isNewline()) {
    440                 break;
    441             } else {
    442                 followingNewlines.add(tte);
    443             }
    444         }
    445         Collections.reverse(followingNewlines);
    446         for (int i = 0; i < followingNewlines.size(); i++) {
    447             if (!followingNewlines.get(i).isSpaceOrTab()) {
    448                 return followingNewlines.subList(0, i);
    449             }
    450         }
    451         return followingNewlines;
    452     }
    453 
    454     //
    455     // Helper methods
    456     //
    457 
    458     private static boolean isReturningOptionalNodeList(Method m) {
    459         if (!m.getReturnType().getCanonicalName().equals(Optional.class.getCanonicalName())) {
    460             return false;
    461         }
    462         if (!(m.getGenericReturnType() instanceof ParameterizedType)) {
    463             return false;
    464         }
    465         ParameterizedType parameterizedType = (ParameterizedType) m.getGenericReturnType();
    466         java.lang.reflect.Type optionalArgument = parameterizedType.getActualTypeArguments()[0];
    467         return (optionalArgument.getTypeName().startsWith(NodeList.class.getCanonicalName()));
    468     }
    469 
    470     private static ObservableProperty findNodeListName(NodeList nodeList) {
    471         Node parent = nodeList.getParentNodeForChildren();
    472         for (Method m : parent.getClass().getMethods()) {
    473             if (m.getParameterCount() == 0 && m.getReturnType().getCanonicalName().equals(NodeList.class.getCanonicalName())) {
    474                 try {
    475                     Object raw = m.invoke(parent);
    476                     if (!(raw instanceof NodeList)) {
    477                         throw new IllegalStateException("Expected NodeList, found " + raw.getClass().getCanonicalName());
    478                     }
    479                     NodeList result = (NodeList) raw;
    480                     if (result == nodeList) {
    481                         String name = m.getName();
    482                         if (name.startsWith("get")) {
    483                             name = name.substring("get".length());
    484                         }
    485                         return ObservableProperty.fromCamelCaseName(decapitalize(name));
    486                     }
    487                 } catch (IllegalAccessException | InvocationTargetException e) {
    488                     throw new RuntimeException(e);
    489                 }
    490             } else if (m.getParameterCount() == 0 && isReturningOptionalNodeList(m)) {
    491                 try {
    492                     Optional<NodeList<?>> raw = (Optional<NodeList<?>>) m.invoke(parent);
    493                     if (raw.isPresent() && raw.get() == nodeList) {
    494                         String name = m.getName();
    495                         if (name.startsWith("get")) {
    496                             name = name.substring("get".length());
    497                         }
    498                         return ObservableProperty.fromCamelCaseName(decapitalize(name));
    499                     }
    500                 } catch (IllegalAccessException | InvocationTargetException e) {
    501                     throw new RuntimeException(e);
    502                 }
    503             }
    504         }
    505         throw new IllegalArgumentException("Cannot find list name of NodeList of size " + nodeList.size());
    506     }
    507 }
    508