/* * Copyright © 2009 Reinier Zwitserloot and Roel Spilker. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package lombok.core; import static lombok.Lombok.sneakyThrow; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; /** * Lombok wraps the AST produced by a target platform into its own AST system, mostly because both eclipse and javac * do not allow upward traversal (from a method to its owning type, for example). * * @param N The common type of all AST nodes in the internal representation of the target platform. * For example, JCTree for javac, and ASTNode for eclipse. */ public abstract class AST { /** The kind of node represented by a given AST.Node object. */ public enum Kind { COMPILATION_UNIT, TYPE, FIELD, INITIALIZER, METHOD, ANNOTATION, ARGUMENT, LOCAL, STATEMENT; } private Node top; private final String fileName; private Map identityDetector = new IdentityHashMap(); private Map nodeMap = new IdentityHashMap(); protected AST(String fileName) { this.fileName = fileName == null ? "(unknown).java" : fileName; } /** Set the node object that wraps the internal Compilation Unit node. */ protected void setTop(Node top) { this.top = top; } /** * Return the content of the package declaration on this AST's top (Compilation Unit) node. * * Example: "java.util". */ public abstract String getPackageDeclaration(); /** * Return the contents of each non-static import statement on this AST's top (Compilation Unit) node. * * Example: "java.util.IOException". */ public abstract Collection getImportStatements(); /** * Puts the given node in the map so that javac/eclipse's own internal AST object can be translated to * an AST.Node object. Also registers the object as visited to avoid endless loops. */ protected T putInMap(T node) { nodeMap.put(node.get(), node); identityDetector.put(node.get(), null); return node; } /** Returns the node map, that can map javac/eclipse internal AST objects to AST.Node objects. */ protected Map getNodeMap() { return nodeMap; } /** Clears the registry that avoids endless loops, and empties the node map. The existing node map * object is left untouched, and instead a new map is created. */ protected void clearState() { identityDetector = new IdentityHashMap(); nodeMap = new IdentityHashMap(); } /** * Marks the stated node as handled (to avoid endless loops if 2 nodes refer to each other, or a node * refers to itself). Will then return true if it was already set as handled before this call - in which * case you should do nothing lest the AST build process loops endlessly. */ protected boolean setAndGetAsHandled(N node) { if ( identityDetector.containsKey(node) ) return true; identityDetector.put(node, null); return false; } public String getFileName() { return fileName; } /** The AST.Node object representing the Compilation Unit. */ public Node top() { return top; } /** Maps a javac/eclipse internal AST Node to the appropriate AST.Node object. */ public Node get(N node) { return nodeMap.get(node); } @SuppressWarnings("unchecked") private Node replaceNewWithExistingOld(Map oldNodes, Node newNode) { Node oldNode = oldNodes.get(newNode.get()); Node targetNode = oldNode == null ? newNode : oldNode; List children = new ArrayList(); for ( Node child : newNode.children ) { Node oldChild = replaceNewWithExistingOld(oldNodes, child); children.add(oldChild); oldChild.parent = targetNode; } targetNode.children.clear(); ((List)targetNode.children).addAll(children); return targetNode; } /** An instance of this class wraps an eclipse/javac internal node object. */ public abstract class Node { protected final Kind kind; protected final N node; protected final List children; protected Node parent; /** This flag has no specified meaning; you can set and retrieve it. * * In practice, for annotation nodes it means: Some AnnotationHandler finished whatever changes were required, * and for all other nodes it means: This node was made by a lombok operation. */ protected boolean handled; /** structurally significant are those nodes that can be annotated in java 1.6 or are method-like toplevels, * so fields, local declarations, method arguments, methods, types, the Compilation Unit itself, and initializers. */ protected boolean isStructurallySignificant; /** * Creates a new Node object that represents the provided node. * * Make sure you manually set the parent correctly. * * @param node The AST object in the target parser's own internal AST tree that this node object will represent. * @param children A list of child nodes. Passing in null results in the children list being empty, not null. * @param kind The kind of node represented by this object. */ protected Node(N node, List children, Kind kind) { this.kind = kind; this.node = node; this.children = children == null ? new ArrayList() : children; for ( Node child : this.children ) child.parent = this; this.isStructurallySignificant = calculateIsStructurallySignificant(); } /** {@inheritDoc} */ @Override public String toString() { return String.format("NODE %s (%s) %s%s", kind, node == null ? "(NULL)" : node.getClass(), handled ? "[HANDLED]" : "", node == null ? "" : node); } /** * Convenient shortcut to the owning JavacAST object's getPackageDeclaration method. * * @see AST#getPackageDeclaration() */ public String getPackageDeclaration() { return AST.this.getPackageDeclaration(); } /** * Convenient shortcut to the owning JavacAST object's getImportStatements method. * * @see AST#getImportStatements() */ public Collection getImportStatements() { return AST.this.getImportStatements(); } /** * See {@link #isStructurallySignificant}. */ protected abstract boolean calculateIsStructurallySignificant(); /** * Convenient shortcut to the owning JavacAST object's getNodeFor method. * * @see AST#getNodeFor() */ public Node getNodeFor(N obj) { return AST.this.get(obj); } /** * @return The javac/eclipse internal AST object wrapped by this AST.Node object. */ public N get() { return node; } /** * Replaces the AST node represented by this node object with the provided node. This node must * have a parent, obviously, for this to work. * * Also affects the underlying (eclipse/javac) AST. */ @SuppressWarnings("unchecked") public Node replaceWith(N newN, Kind kind) { Node newNode = buildTree(newN, kind); newNode.parent = parent; for ( int i = 0 ; i < parent.children.size() ; i++ ) { if ( parent.children.get(i) == this ) ((List)parent.children).set(i, newNode); } parent.replaceChildNode(get(), newN); return newNode; } /** * Replaces the stated node with a new one. The old node must be a direct child of this node. * * Also affects the underlying (eclipse/javac) AST. */ public void replaceChildNode(N oldN, N newN) { replaceStatementInNode(get(), oldN, newN); } public Kind getKind() { return kind; } /** * Return the name of your type (simple name), method, field, or local variable. Return null if this * node doesn't really have a name, such as initializers, while statements, etc. */ public abstract String getName(); /** Returns the structurally significant node that encloses this one. * * @see #isStructurallySignificant() */ public Node up() { Node result = parent; while ( result != null && !result.isStructurallySignificant ) result = result.parent; return result; } /** * Returns the direct parent node in the AST tree of this node. For example, a local variable declaration's * direct parent can be e.g. an If block, but its up() Node is the Method that contains it. */ public Node directUp() { return parent; } /** * Returns all children nodes. * * A copy is created, so changing the list has no effect. Also, while iterating through this list, * you may add, remove, or replace children without causing ConcurrentModificationExceptions. */ public Collection down() { return new ArrayList(children); } /** * returns the value of the 'handled' flag. * * @see #handled */ public boolean isHandled() { return handled; } /** * Sets the handled flag, then returns 'this'. * * @see #handled */ public Node setHandled() { this.handled = true; return this; } /** * Convenient shortcut to the owning JavacAST object's top method. * * @see AST#top() */ public Node top() { return top; } /** * Convenient shortcut to the owning JavacAST object's getFileName method. * * @see AST#getFileName() */ public String getFileName() { return fileName; } /** * Adds the stated node as a direct child of this node. * * Does not change the underlying (javac/eclipse) AST, only the wrapped view. */ @SuppressWarnings("unchecked") public Node add(N newChild, Kind kind) { Node n = buildTree(newChild, kind); if ( n == null ) return null; n.parent = this; ((List)children).add(n); return n; } /** * Reparses the AST node represented by this node. Any existing nodes that occupy a different space in the AST are rehomed, any * nodes that no longer exist are removed, and new nodes are created. * * Careful - the node you call this on must not itself have been removed or rehomed - it rebuilds all children. */ public void rebuild() { Map oldNodes = new IdentityHashMap(); gatherAndRemoveChildren(oldNodes); Node newNode = buildTree(get(), kind); replaceNewWithExistingOld(oldNodes, newNode); } private void gatherAndRemoveChildren(Map map) { for ( Node child : children ) child.gatherAndRemoveChildren(map); identityDetector.remove(get()); map.put(get(), this); children.clear(); nodeMap.remove(get()); } /** * Removes the stated node, which must be a direct child of this node, from the AST. * * Does not change the underlying (javac/eclipse) AST, only the wrapped view. */ public void removeChild(Node child) { children.remove(child); } /** * Sets the handled flag on this node, and all child nodes, then returns this. * * @see #handled */ public Node recursiveSetHandled() { this.handled = true; for ( Node child : children ) child.recursiveSetHandled(); return this; } /** Generate a compiler error on this node. */ public abstract void addError(String message); /** Generate a compiler warning on this node. */ public abstract void addWarning(String message); /** * Structurally significant means: LocalDeclaration, TypeDeclaration, MethodDeclaration, ConstructorDeclaration, * FieldDeclaration, Initializer, and CompilationUnitDeclaration. * The rest is e.g. if statements, while loops, etc. */ public boolean isStructurallySignificant() { return isStructurallySignificant; } } /** Build an AST.Node object for the stated internal (javac/eclipse) AST Node object. */ protected abstract Node buildTree(N item, Kind kind); /** * Represents a field that contains AST children. */ protected static class FieldAccess { /** The actual field. */ public final Field field; /** Dimensions of the field. Works for arrays, or for java.util.collections. */ public final int dim; FieldAccess(Field field, int dim) { this.field = field; this.dim = dim; } } private static Map, Collection> fieldsOfASTClasses = new HashMap, Collection>(); /** Returns FieldAccess objects for the stated class. Each field that contains objects of the kind returned by * {@link #getStatementTypes()}, either directly or inside of an array or java.util.collection (or array-of-arrays, * or collection-of-collections, etcetera), is returned. */ protected Collection fieldsOf(Class c) { Collection fields = fieldsOfASTClasses.get(c); if ( fields != null ) return fields; fields = new ArrayList(); getFields(c, fields); fieldsOfASTClasses.put(c, fields); return fields; } private void getFields(Class c, Collection fields) { if ( c == Object.class || c == null ) return; for ( Field f : c.getDeclaredFields() ) { if ( Modifier.isStatic(f.getModifiers()) ) continue; Class t = f.getType(); int dim = 0; if ( t.isArray() ) { while ( t.isArray() ) { dim++; t = t.getComponentType(); } } else if ( Collection.class.isAssignableFrom(t) ) { while ( Collection.class.isAssignableFrom(t) ) { dim++; t = getComponentType(f.getGenericType()); } } for ( Class statementType : getStatementTypes() ) { if ( statementType.isAssignableFrom(t) ) { f.setAccessible(true); fields.add(new FieldAccess(f, dim)); break; } } } getFields(c.getSuperclass(), fields); } private Class getComponentType(Type type) { if ( type instanceof ParameterizedType ) { Type component = ((ParameterizedType)type).getActualTypeArguments()[0]; return component instanceof Class ? (Class)component : Object.class; } else return Object.class; } /** * The supertypes which are considered AST Node children. Usually, the Statement, and the Expression, * though some platforms (such as eclipse) group these under one common supertype. */ protected abstract Collection> getStatementTypes(); /** * buildTree implementation that uses reflection to find all child nodes by way of inspecting * the fields. */ protected Collection buildWithField(Class nodeType, N statement, FieldAccess fa) { List list = new ArrayList(); buildWithField0(nodeType, statement, fa, list); return list; } /** * Uses reflection to find the given direct child on the given statement, and replace it with a new child. */ protected boolean replaceStatementInNode(N statement, N oldN, N newN) { for ( FieldAccess fa : fieldsOf(statement.getClass()) ) { if ( replaceStatementInField(fa, statement, oldN, newN) ) return true; } return false; } private boolean replaceStatementInField(FieldAccess fa, N statement, N oldN, N newN) { try { Object o = fa.field.get(statement); if ( o == null ) return false; if ( o == oldN ) { fa.field.set(statement, newN); return true; } if ( fa.dim > 0 ) { if ( o.getClass().isArray() ) { return replaceStatementInArray(o, oldN, newN); } else if ( Collection.class.isInstance(o) ) { return replaceStatementInCollection(fa.field, statement, new ArrayList>(), (Collection)o, oldN, newN); } } return false; } catch ( IllegalAccessException e ) { throw sneakyThrow(e); } } private boolean replaceStatementInCollection(Field field, Object fieldRef, List> chain, Collection collection, N oldN, N newN) throws IllegalAccessException { if ( collection == null ) return false; int idx = -1; for ( Object o : collection ) { idx++; if ( o == null ) continue; if ( Collection.class.isInstance(o) ) { Collection newC = (Collection)o; List> newChain = new ArrayList>(chain); newChain.add(newC); if ( replaceStatementInCollection(field, fieldRef, newChain, newC, oldN, newN) ) return true; } if ( o == oldN ) { setElementInASTCollection(field, fieldRef, chain, collection, idx, newN); return true; } } return false; } /** Override if your AST collection does not support the set method. Javac's for example, does not. */ @SuppressWarnings("unchecked") protected void setElementInASTCollection(Field field, Object fieldRef, List> chain, Collection collection, int idx, N newN) throws IllegalAccessException { if ( collection instanceof List ) { ((List)collection).set(idx, newN); } } private boolean replaceStatementInArray(Object array, N oldN, N newN) { if ( array == null ) return false; int len = Array.getLength(array); for ( int i = 0 ; i < len ; i++ ) { Object o = Array.get(array, i); if ( o == null ) continue; if ( o.getClass().isArray() ) { if ( replaceStatementInArray(o, oldN, newN) ) return true; } else if ( o == oldN ) { Array.set(array, i, newN); return true; } } return false; } @SuppressWarnings("unchecked") private void buildWithField0(Class nodeType, N child, FieldAccess fa, Collection list) { try { Object o = fa.field.get(child); if ( o == null ) return; if ( fa.dim == 0 ) { Node node = buildTree((N)o, Kind.STATEMENT); if ( node != null ) list.add(nodeType.cast(node)); } else if ( o.getClass().isArray() ) buildWithArray(nodeType, o, list, fa.dim); else if ( Collection.class.isInstance(o) ) buildWithCollection(nodeType, o, list, fa.dim); } catch ( IllegalAccessException e ) { sneakyThrow(e); } } @SuppressWarnings("unchecked") private void buildWithArray(Class nodeType, Object array, Collection list, int dim) { if ( dim == 1 ) for ( Object v : (Object[])array ) { if ( v == null ) continue; Node node = buildTree((N)v, Kind.STATEMENT); if ( node != null ) list.add(nodeType.cast(node)); } else for ( Object v : (Object[])array ) { if ( v == null ) return; buildWithArray(nodeType, v, list, dim-1); } } @SuppressWarnings("unchecked") private void buildWithCollection(Class nodeType, Object collection, Collection list, int dim) { if ( dim == 1 ) for ( Object v : (Collection)collection ) { if ( v == null ) continue; Node node = buildTree((N)v, Kind.STATEMENT); if ( node != null ) list.add(nodeType.cast(node)); } else for ( Object v : (Collection)collection ) { buildWithCollection(nodeType, v, list, dim-1); } } }