/* * Copyright (C) 2009-2013 The Project Lombok Authors. * * 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.eclipse; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import org.eclipse.jdt.internal.compiler.CompilationResult; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import lombok.Lombok; import lombok.core.AST; import lombok.core.LombokImmutableList; 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.CompilationUnitDeclaration; import org.eclipse.jdt.internal.compiler.ast.ConstructorDeclaration; import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration; import org.eclipse.jdt.internal.compiler.ast.ImportReference; 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.core.BasicCompilationUnit; import org.eclipse.jdt.internal.core.JavaElement; import org.eclipse.jdt.internal.core.JavaProject; /** * Wraps around Eclipse's internal AST view to add useful features as well as the ability to visit parents from children, * something Eclipse own AST system does not offer. */ public class EclipseAST extends AST { /** * Creates a new EclipseAST of the provided Compilation Unit. * * @param ast The compilation unit, which serves as the top level node in the tree to be built. */ public EclipseAST(CompilationUnitDeclaration ast) { super(toFileName(ast), packageDeclaration(ast), new EclipseImportList(ast)); this.compilationUnitDeclaration = ast; setTop(buildCompilationUnit(ast)); this.completeParse = isComplete(ast); clearChanged(); } private static String packageDeclaration(CompilationUnitDeclaration cud) { ImportReference pkg = cud.currentPackage; return pkg == null ? null : Eclipse.toQualifiedName(pkg.getImportName()); } @Override public int getSourceVersion() { long sl = compilationUnitDeclaration.problemReporter.options.sourceLevel; long cl = compilationUnitDeclaration.problemReporter.options.complianceLevel; sl >>= 16; cl >>= 16; if (sl == 0) sl = cl; if (cl == 0) cl = sl; return Math.min((int)(sl - 44), (int)(cl - 44)); } @Override public int getLatestJavaSpecSupported() { return Eclipse.getEcjCompilerVersion(); } /** * Runs through the entire AST, starting at the compilation unit, calling the provided visitor's visit methods * for each node, depth first. */ public void traverse(EclipseASTVisitor visitor) { top().traverse(visitor); } void traverseChildren(EclipseASTVisitor visitor, EclipseNode node) { LombokImmutableList children = node.down(); int len = children.size(); for (int i = 0; i < len; i++) { children.get(i).traverse(visitor); } } /** * Eclipse starts off with a 'diet' parse which leaves method bodies blank, amongst other shortcuts. * * For such diet parses, this method returns false, otherwise it returns true. Any lombok processor * that needs the contents of methods should just do nothing (and return false so it gets another shot later!) * when this is false. */ public boolean isCompleteParse() { return completeParse; } class ParseProblem { final boolean isWarning; final String message; final int sourceStart; final int sourceEnd; ParseProblem(boolean isWarning, String message, int sourceStart, int sourceEnd) { this.isWarning = isWarning; this.message = message; this.sourceStart = sourceStart; this.sourceEnd = sourceEnd; } void addToCompilationResult() { CompilationUnitDeclaration cud = (CompilationUnitDeclaration) top().get(); addProblemToCompilationResult(cud.getFileName(), cud.compilationResult, isWarning, message, sourceStart, sourceEnd); } } private void propagateProblems() { if (queuedProblems.isEmpty()) return; CompilationUnitDeclaration cud = (CompilationUnitDeclaration) top().get(); if (cud.compilationResult == null) return; for (ParseProblem problem : queuedProblems) problem.addToCompilationResult(); queuedProblems.clear(); } private final List queuedProblems = new ArrayList(); void addProblem(ParseProblem problem) { queuedProblems.add(problem); propagateProblems(); } /** * Adds a problem to the provided CompilationResult object so that it will show up * in the Problems/Warnings view. */ public static void addProblemToCompilationResult(char[] fileNameArray, CompilationResult result, boolean isWarning, String message, int sourceStart, int sourceEnd) { try { EcjReflectionCheck.addProblemToCompilationResult.invoke(null, fileNameArray, result, isWarning, message, sourceStart, sourceEnd); } catch (NoClassDefFoundError e) { //ignore, we don't have access to the correct ECJ classes, so lombok can't possibly //do anything useful here. } catch (IllegalAccessException e) { throw Lombok.sneakyThrow(e); } catch (InvocationTargetException e) { throw Lombok.sneakyThrow(e); } catch (NullPointerException e) { if (!"false".equals(System.getProperty("lombok.debug.reflection", "false"))) { e.initCause(EcjReflectionCheck.problem); throw e; } //ignore, we don't have access to the correct ECJ classes, so lombok can't possibly //do anything useful here. } } private final CompilationUnitDeclaration compilationUnitDeclaration; private boolean completeParse; private static String toFileName(CompilationUnitDeclaration ast) { return ast.compilationResult.fileName == null ? null : new String(ast.compilationResult.fileName); } private static final Pattern PROJECT_NAME_FROM_FILEPATH = Pattern.compile("^/([^/]+)/(.*)$"); /** * Returns the JavaProject object (eclipse's abstraction of the project) associated with the source file that is represented by this AST. */ public JavaProject getProject() { CompilationUnitDeclaration cud = (CompilationUnitDeclaration) top().get(); if (cud.compilationResult().getCompilationUnit() instanceof JavaElement) { JavaElement icu = (JavaElement) cud.compilationResult.compilationUnit; return (JavaProject) icu.getJavaProject(); } char[] fn = cud.compilationResult().getFileName(); Matcher m = PROJECT_NAME_FROM_FILEPATH.matcher(new String(fn)); if (m.matches()) { String projName = m.group(1); String path = m.group(2); return EclipseProjectSearcher.getProject(projName); } return null; } /** * Call this method to move an EclipseAST generated for a diet parse to rebuild itself for the full parse - * with filled in method bodies and such. Also propagates problems and errors, which in diet parse * mode can't be reliably added to the problems/warnings view. */ public void rebuild(boolean force) { propagateProblems(); if (completeParse && !force) return; boolean changed = isChanged(); boolean newCompleteParse = isComplete(compilationUnitDeclaration); if (!newCompleteParse && !force) return; top().rebuild(); this.completeParse = newCompleteParse; if (!changed) clearChanged(); } private static boolean isComplete(CompilationUnitDeclaration unit) { return (unit.bits & ASTNode.HasAllMethodBodies) != 0; } /** {@inheritDoc} */ @Override protected EclipseNode buildTree(ASTNode node, Kind kind) { switch (kind) { case COMPILATION_UNIT: return buildCompilationUnit((CompilationUnitDeclaration) node); case TYPE: return buildType((TypeDeclaration) node); case FIELD: return buildField((FieldDeclaration) node); case INITIALIZER: return buildInitializer((Initializer) node); case METHOD: return buildMethod((AbstractMethodDeclaration) node); case ARGUMENT: return buildLocal((Argument) node, kind); case LOCAL: return buildLocal((LocalDeclaration) node, kind); case STATEMENT: return buildStatement((Statement) node); case ANNOTATION: return buildAnnotation((Annotation) node, false); default: throw new AssertionError("Did not expect to arrive here: " + kind); } } private EclipseNode buildCompilationUnit(CompilationUnitDeclaration top) { if (setAndGetAsHandled(top)) return null; List children = buildTypes(top.types); return putInMap(new EclipseNode(this, top, children, Kind.COMPILATION_UNIT)); } private void addIfNotNull(Collection collection, EclipseNode n) { if (n != null) collection.add(n); } private List buildTypes(TypeDeclaration[] children) { List childNodes = new ArrayList(); if (children != null) for (TypeDeclaration type : children) addIfNotNull(childNodes, buildType(type)); return childNodes; } private EclipseNode buildType(TypeDeclaration type) { if (setAndGetAsHandled(type)) return null; List childNodes = new ArrayList(); childNodes.addAll(buildFields(type.fields)); childNodes.addAll(buildTypes(type.memberTypes)); childNodes.addAll(buildMethods(type.methods)); childNodes.addAll(buildAnnotations(type.annotations, false)); return putInMap(new EclipseNode(this, type, childNodes, Kind.TYPE)); } private Collection buildFields(FieldDeclaration[] children) { List childNodes = new ArrayList(); if (children != null) for (FieldDeclaration child : children) addIfNotNull(childNodes, buildField(child)); return childNodes; } private static List singleton(T item) { List list = new ArrayList(); if (item != null) list.add(item); return list; } private EclipseNode buildField(FieldDeclaration field) { if (field instanceof Initializer) return buildInitializer((Initializer)field); if (setAndGetAsHandled(field)) return null; List childNodes = new ArrayList(); addIfNotNull(childNodes, buildStatement(field.initialization)); childNodes.addAll(buildAnnotations(field.annotations, true)); return putInMap(new EclipseNode(this, field, childNodes, Kind.FIELD)); } private EclipseNode buildInitializer(Initializer initializer) { if (setAndGetAsHandled(initializer)) return null; return putInMap(new EclipseNode(this, initializer, singleton(buildStatement(initializer.block)), Kind.INITIALIZER)); } private Collection buildMethods(AbstractMethodDeclaration[] children) { List childNodes = new ArrayList(); if (children != null) for (AbstractMethodDeclaration method : children) addIfNotNull(childNodes, buildMethod(method)); return childNodes; } private EclipseNode buildMethod(AbstractMethodDeclaration method) { if (setAndGetAsHandled(method)) return null; List childNodes = new ArrayList(); childNodes.addAll(buildArguments(method.arguments)); if (method instanceof ConstructorDeclaration) { ConstructorDeclaration constructor = (ConstructorDeclaration) method; addIfNotNull(childNodes, buildStatement(constructor.constructorCall)); } childNodes.addAll(buildStatements(method.statements)); childNodes.addAll(buildAnnotations(method.annotations, false)); return putInMap(new EclipseNode(this, method, childNodes, Kind.METHOD)); } //Arguments are a kind of LocalDeclaration. They can definitely contain lombok annotations, so we care about them. private Collection buildArguments(Argument[] children) { List childNodes = new ArrayList(); if (children != null) for (LocalDeclaration local : children) { addIfNotNull(childNodes, buildLocal(local, Kind.ARGUMENT)); } return childNodes; } private EclipseNode buildLocal(LocalDeclaration local, Kind kind) { if (setAndGetAsHandled(local)) return null; List childNodes = new ArrayList(); addIfNotNull(childNodes, buildStatement(local.initialization)); childNodes.addAll(buildAnnotations(local.annotations, true)); return putInMap(new EclipseNode(this, local, childNodes, kind)); } private Collection buildAnnotations(Annotation[] annotations, boolean varDecl) { List elements = new ArrayList(); if (annotations != null) for (Annotation an : annotations) addIfNotNull(elements, buildAnnotation(an, varDecl)); return elements; } private EclipseNode buildAnnotation(Annotation annotation, boolean field) { if (annotation == null) return null; boolean handled = setAndGetAsHandled(annotation); if (!field && handled) { // @Foo int x, y; is handled in eclipse by putting the same annotation node on 2 FieldDeclarations. return null; } return putInMap(new EclipseNode(this, annotation, null, Kind.ANNOTATION)); } private Collection buildStatements(Statement[] children) { List childNodes = new ArrayList(); if (children != null) for (Statement child : children) addIfNotNull(childNodes, buildStatement(child)); return childNodes; } private EclipseNode buildStatement(Statement child) { if (child == null) return null; if (child instanceof TypeDeclaration) return buildType((TypeDeclaration)child); if (child instanceof LocalDeclaration) return buildLocal((LocalDeclaration)child, Kind.LOCAL); if (setAndGetAsHandled(child)) return null; return drill(child); } private EclipseNode drill(Statement statement) { List childNodes = new ArrayList(); for (FieldAccess fa : fieldsOf(statement.getClass())) childNodes.addAll(buildWithField(EclipseNode.class, statement, fa)); return putInMap(new EclipseNode(this, statement, childNodes, Kind.STATEMENT)); } /** For Eclipse, only Statement counts, as Expression is a subclass of it, even though this isn't * entirely correct according to the JLS spec (only some expressions can be used as statements, not all of them). */ @Override protected Collection> getStatementTypes() { return Collections.>singleton(Statement.class); } private static class EcjReflectionCheck { private static final String COMPILATIONRESULT_TYPE = "org.eclipse.jdt.internal.compiler.CompilationResult"; public static Method addProblemToCompilationResult; public static final Throwable problem; static { Throwable problem_ = null; Method m = null; try { m = EclipseAstProblemView.class.getMethod("addProblemToCompilationResult", char[].class, Class.forName(COMPILATIONRESULT_TYPE), boolean.class, String.class, int.class, int.class); } catch (Throwable t) { // That's problematic, but as long as no local classes are used we don't actually need it. // Better fail on local classes than crash altogether. problem_ = t; } addProblemToCompilationResult = m; problem = problem_; } } }