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