package lombok.eclipse; import static lombok.Lombok.sneakyThrow; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import org.eclipse.jdt.core.compiler.CategorizedProblem; import org.eclipse.jdt.internal.compiler.CompilationResult; import org.eclipse.jdt.internal.compiler.apt.dispatch.AptProblem; import org.eclipse.jdt.internal.compiler.ast.ASTNode; import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration; import org.eclipse.jdt.internal.compiler.ast.Annotation; import org.eclipse.jdt.internal.compiler.ast.Argument; import org.eclipse.jdt.internal.compiler.ast.Clinit; import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration; import org.eclipse.jdt.internal.compiler.ast.Initializer; import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration; import org.eclipse.jdt.internal.compiler.ast.Statement; import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration; import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities; import org.eclipse.jdt.internal.compiler.util.Util; public class EclipseAST { public void traverse(EclipseASTVisitor visitor) { Node current = top(); visitor.visitCompilationUnit(current, (CompilationUnitDeclaration)current.node); traverseChildren(visitor, current); visitor.endVisitCompilationUnit(current, (CompilationUnitDeclaration)current.node); } private void traverseChildren(EclipseASTVisitor visitor, Node node) { for ( Node child : node.children ) { ASTNode n = child.node; if ( n instanceof TypeDeclaration ) { visitor.visitType(child, (TypeDeclaration)n); traverseChildren(visitor, child); visitor.endVisitType(child, (TypeDeclaration)n); } else if ( n instanceof Initializer ) { visitor.visitInitializer(child, (Initializer)n); traverseChildren(visitor, child); visitor.endVisitInitializer(child, (Initializer)n); } else if ( n instanceof FieldDeclaration ) { visitor.visitField(child, (FieldDeclaration)n); traverseChildren(visitor, child); visitor.endVisitField(child, (FieldDeclaration)n); } else if ( n instanceof AbstractMethodDeclaration ) { if ( n instanceof Clinit ) continue; visitor.visitMethod(child, (AbstractMethodDeclaration)n); traverseChildren(visitor, child); visitor.endVisitMethod(child, (AbstractMethodDeclaration)n); } else if ( n instanceof LocalDeclaration ) { visitor.visitLocal(child, (LocalDeclaration)n); traverseChildren(visitor, child); visitor.endVisitLocal(child, (LocalDeclaration)n); } else if ( n instanceof Annotation ) { Node parent = child.up(); if ( parent.node instanceof TypeDeclaration ) visitor.visitAnnotationOnType(parent, (TypeDeclaration)parent.node, (Annotation)n); else if ( parent.node instanceof AbstractMethodDeclaration ) visitor.visitAnnotationOnMethod(parent, (AbstractMethodDeclaration)parent.node, (Annotation)n); else if ( parent.node instanceof FieldDeclaration ) visitor.visitAnnotationOnField(parent, (FieldDeclaration)parent.node, (Annotation)n); else if ( parent.node instanceof LocalDeclaration ) visitor.visitAnnotationOnLocal(parent, (LocalDeclaration)parent.node, (Annotation)n); } else if ( n instanceof Statement ) { visitor.visitStatement(child, (Statement)n); traverseChildren(visitor, child); visitor.endVisitStatement(node, (Statement)n); } else throw new AssertionError("Can't be reached"); } } public boolean isCompleteParse() { return completeParse; } public String getFileName() { return fileName; } public Node top() { return top; } public Node get(ASTNode node) { return nodeMap.get(node); } private class ParseProblem { final boolean isWarning; final String message; final Node node; final int sourceStart; final int sourceEnd; public ParseProblem(boolean isWarning, String message, Node node, int sourceStart, int sourceEnd) { this.isWarning = isWarning; this.message = message; this.node = node; this.sourceStart = sourceStart; this.sourceEnd = sourceEnd; } void addToCompilationResult() { addProblemToCompilationResult(getFileName(), (CompilationUnitDeclaration) top().getEclipseNode(), isWarning, message, node.getEclipseNode(), sourceStart, sourceEnd); } } public void propagateProblems() { if ( queuedProblems.isEmpty() ) return; CompilationUnitDeclaration cud = (CompilationUnitDeclaration) top().getEclipseNode(); if ( cud.compilationResult == null ) return; for ( ParseProblem problem : queuedProblems ) problem.addToCompilationResult(); queuedProblems.clear(); } private final List queuedProblems = new ArrayList(); private void addProblem(ParseProblem problem) { queuedProblems.add(problem); propagateProblems(); } static void addProblemToCompilationResult(String fileName, CompilationUnitDeclaration ast, boolean isWarning, String message, ASTNode node, int sourceStart, int sourceEnd) { char[] fileNameArray = fileName.toCharArray(); int lineNumber = 0; int columnNumber = 1; CompilationResult result = ast.compilationResult; int[] lineEnds = null; lineNumber = sourceStart >= 0 ? Util.getLineNumber(sourceStart, lineEnds = result.getLineSeparatorPositions(), 0, lineEnds.length-1) : 0; columnNumber = sourceStart >= 0 ? Util.searchColumnNumber(result.getLineSeparatorPositions(), lineNumber,sourceStart) : 0; CategorizedProblem ecProblem = new AptProblem(null, fileNameArray, message, 0, new String[0], isWarning ? ProblemSeverities.Warning : ProblemSeverities.Error, sourceStart, sourceEnd, lineNumber, columnNumber); ast.compilationResult.record(ecProblem, null); } public final class Node { final ASTNode node; Node parent; final Collection children; boolean handled; private final boolean isStructurallySignificant; Node(ASTNode node, Collection children) { this.node = node; this.children = children == null ? Collections.emptyList() : children; this.isStructurallySignificant = calculateIsStructurallySignificant(); } public void addError(String message) { this.addError(message, this.getEclipseNode().sourceStart, this.getEclipseNode().sourceEnd); } public void addError(String message, int sourceStart, int sourceEnd) { addProblem(new ParseProblem(false, message, this, sourceStart, sourceEnd)); } public void addWarning(String message) { this.addWarning(message, this.getEclipseNode().sourceStart, this.getEclipseNode().sourceEnd); } public void addWarning(String message, int sourceStart, int sourceEnd) { addProblem(new ParseProblem(true, message, this, sourceStart, sourceEnd)); } public ASTNode getEclipseNode() { return node; } /** 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; } /** * 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; } private boolean calculateIsStructurallySignificant() { if ( node instanceof TypeDeclaration ) return true; if ( node instanceof AbstractMethodDeclaration ) return true; if ( node instanceof FieldDeclaration ) return true; if ( node instanceof LocalDeclaration ) return true; if ( node instanceof CompilationUnitDeclaration ) return true; return false; } /** * 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; } public Collection down() { return children; } public boolean isHandled() { return handled; } public Node setHandled() { this.handled = true; return this; } public Node top() { return top; } public String getFileName() { return fileName; } public boolean isCompleteParse() { return completeParse; } } private final Map identityDetector = new IdentityHashMap(); private Map nodeMap = new HashMap(); private final CompilationUnitDeclaration compilationUnitDeclaration; private final String fileName; private Node top; private boolean completeParse; public EclipseAST(CompilationUnitDeclaration ast) { this.compilationUnitDeclaration = ast; this.fileName = ast.compilationResult.fileName == null ? "(unknown).java" : new String(ast.compilationResult.fileName); this.top = buildTree(ast); this.completeParse = isComplete(ast); } public void reparse() { propagateProblems(); if ( completeParse ) return; boolean newCompleteParse = isComplete(compilationUnitDeclaration); if ( !newCompleteParse ) return; Map oldMap = nodeMap; nodeMap = new HashMap(); this.top = buildTree(compilationUnitDeclaration); //Retain 'handled' flags. for ( Map.Entry e : nodeMap.entrySet() ) { Node oldEntry = oldMap.get(e.getKey()); if ( oldEntry != null && oldEntry.handled ) e.getValue().handled = true; } this.completeParse = true; } private static boolean isComplete(CompilationUnitDeclaration unit) { return (unit.bits & ASTNode.HasAllMethodBodies) > 0; } private Node putInMap(Node parent) { for ( Node child : parent.children ) child.parent = parent; nodeMap.put(parent.node, parent); identityDetector.put(parent.node, null); return parent; } private Node buildTree(CompilationUnitDeclaration top) { identityDetector.clear(); Collection children = buildTree(top.types); return putInMap(new Node(top, children)); } private void addIfNotNull(Collection collection, Node n) { if ( n != null ) collection.add(n); } private Collection buildTree(TypeDeclaration[] children) { if ( children == null ) return Collections.emptyList(); List childNodes = new ArrayList(); for ( TypeDeclaration type : children ) addIfNotNull(childNodes, buildTree(type)); return childNodes; } private Node buildTree(TypeDeclaration type) { if ( identityDetector.containsKey(type) ) return null; List childNodes = new ArrayList(); childNodes.addAll(buildTree(type.fields)); childNodes.addAll(buildTree(type.memberTypes)); childNodes.addAll(buildTree(type.methods)); childNodes.addAll(buildTree(type.annotations)); return putInMap(new Node(type, childNodes)); } private Collection buildTree(FieldDeclaration[] children) { if ( children == null ) return Collections.emptyList(); List childNodes = new ArrayList(); for ( FieldDeclaration child : children ) addIfNotNull(childNodes, buildTree(child)); return childNodes; } private static Collection singleton(T item) { if ( item == null ) return Collections.emptyList(); else return Collections.singleton(item); } private Node buildTree(FieldDeclaration field) { if ( field instanceof Initializer ) return buildTree((Initializer)field); if ( identityDetector.containsKey(field) ) return null; List childNodes = new ArrayList(); addIfNotNull(childNodes, buildWithStatement(field.initialization)); childNodes.addAll(buildTree(field.annotations)); return putInMap(new Node(field, childNodes)); } private Node buildTree(Initializer initializer) { if ( identityDetector.containsKey(initializer) ) return null; return putInMap(new Node(initializer, singleton(buildWithStatement(initializer.block)))); } private Collection buildTree(AbstractMethodDeclaration[] children) { if ( children == null ) return Collections.emptyList(); List childNodes = new ArrayList(); for (AbstractMethodDeclaration method : children ) addIfNotNull(childNodes, buildTree(method)); return childNodes; } private Node buildTree(AbstractMethodDeclaration method) { if ( identityDetector.containsKey(method) ) return null; List childNodes = new ArrayList(); childNodes.addAll(buildTree(method.arguments)); childNodes.addAll(buildTree(method.statements)); childNodes.addAll(buildTree(method.annotations)); return putInMap(new Node(method, childNodes)); } //Arguments are a kind of LocalDeclaration. They can definitely contain lombok annotations, so we care about them. private Collection buildTree(Argument[] children) { if ( children == null ) return Collections.emptyList(); List childNodes = new ArrayList(); for ( LocalDeclaration local : children ) { addIfNotNull(childNodes, buildTree(local)); } return childNodes; } private Node buildTree(LocalDeclaration local) { if ( identityDetector.containsKey(local) ) return null; List childNodes = new ArrayList(); addIfNotNull(childNodes, buildWithStatement(local.initialization)); childNodes.addAll(buildTree(local.annotations)); return putInMap(new Node(local, childNodes)); } private Collection buildTree(Annotation[] annotations) { if ( annotations == null ) return Collections.emptyList(); List elements = new ArrayList(); for ( Annotation an : annotations ) { if ( an == null ) continue; elements.add(putInMap(new Node(an, null))); } return elements; } private Collection buildTree(Statement[] children) { if ( children == null ) return Collections.emptyList(); List childNodes = new ArrayList(); for ( Statement child : children ) addIfNotNull(childNodes, buildWithStatement(child)); return childNodes; } //Almost anything is a statement, so this method has a different name to avoid overloading confusion private Node buildWithStatement(Statement child) { if ( child == null || identityDetector.containsKey(child) ) return null; if ( child instanceof TypeDeclaration ) return buildTree((TypeDeclaration)child); if ( child instanceof LocalDeclaration ) return buildTree((LocalDeclaration)child); //We drill down because LocalDeclarations and TypeDeclarations can occur anywhere, even in, say, //an if block, or even the expression on an assert statement! identityDetector.put(child, null); return drill(child); } private Node drill(Statement statement) { List childNodes = new ArrayList(); for ( FieldAccess fa : fieldsOf(statement.getClass()) ) childNodes.addAll(buildWithField(statement, fa)); return putInMap(new Node(statement, childNodes)); } private static class FieldAccess { final Field field; final int dim; FieldAccess(Field field, int dim) { this.field = field; this.dim = dim; } } private static Map, Collection> fieldsOfASTClasses = new HashMap, Collection>(); private 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 == ASTNode.class || c == null ) return; for ( Field f : c.getDeclaredFields() ) { if ( Modifier.isStatic(f.getModifiers()) ) continue; Class t = f.getType(); int dim = 0; while ( t.isArray() ) { dim++; t = t.getComponentType(); } if ( Statement.class.isAssignableFrom(t) ) { f.setAccessible(true); fields.add(new FieldAccess(f, dim)); } } getFields(c.getSuperclass(), fields); } private Collection buildWithField(Statement statement, FieldAccess fa) { List list = new ArrayList(); buildWithField(statement, fa, list); return list; } private void buildWithField(Statement child, FieldAccess fa, Collection list) { try { Object o = fa.field.get(child); if ( fa.dim == 0 ) addIfNotNull(list, buildWithStatement((Statement)o)); else buildWithArray(o, list, fa.dim); } catch ( IllegalAccessException e ) { sneakyThrow(e); } } private void buildWithArray(Object array, Collection list, int dim) { if ( array == null ) return; if ( dim == 1 ) for ( Object v : (Object[])array ) { addIfNotNull(list, buildWithStatement((Statement)v)); } else for ( Object v : (Object[])array ) { buildWithArray(v, list, dim-1); } } }