diff options
-rw-r--r-- | src/core/lombok/eclipse/Eclipse.java | 12 | ||||
-rw-r--r-- | src/core/lombok/eclipse/TransformEclipseAST.java | 2 | ||||
-rw-r--r-- | src/core/lombok/eclipse/handlers/HandleVal.java | 38 | ||||
-rw-r--r-- | src/core/lombok/val.java | 4 | ||||
-rw-r--r-- | src/eclipseAgent/lombok/eclipse/agent/PatchVal.java | 223 |
5 files changed, 233 insertions, 46 deletions
diff --git a/src/core/lombok/eclipse/Eclipse.java b/src/core/lombok/eclipse/Eclipse.java index 5dcd9de8..ddba726a 100644 --- a/src/core/lombok/eclipse/Eclipse.java +++ b/src/core/lombok/eclipse/Eclipse.java @@ -533,7 +533,17 @@ public class Eclipse { */ public static boolean annotationTypeMatches(Class<? extends java.lang.annotation.Annotation> type, EclipseNode node) { if (node.getKind() != Kind.ANNOTATION) return false; - TypeReference typeRef = ((Annotation)node.get()).type; + return typeMatches(type, node, ((Annotation)node.get()).type); + } + + /** + * Checks if the given TypeReference node is likely to be a reference to the provided class. + * + * @param type An actual type. This method checks if {@code typeNode} is likely to be a reference to this type. + * @param node A Lombok AST node. Any node in the appropriate compilation unit will do (used to get access to import statements). + * @param typeNode A type reference to check. + */ + public static boolean typeMatches(Class<?> type, EclipseNode node, TypeReference typeRef) { if (typeRef == null || typeRef.getTypeName() == null) return false; String typeName = toQualifiedName(typeRef.getTypeName()); diff --git a/src/core/lombok/eclipse/TransformEclipseAST.java b/src/core/lombok/eclipse/TransformEclipseAST.java index bfc35244..1f31bd59 100644 --- a/src/core/lombok/eclipse/TransformEclipseAST.java +++ b/src/core/lombok/eclipse/TransformEclipseAST.java @@ -123,6 +123,8 @@ public class TransformEclipseAST { if (Symbols.hasSymbol("lombok.disable")) return; + // Do NOT abort if (ast.bits & ASTNode.HasAllMethodBodies) != 0 - that doesn't work. + try { EclipseAST existing = getAST(ast); new TransformEclipseAST(existing).go(); diff --git a/src/core/lombok/eclipse/handlers/HandleVal.java b/src/core/lombok/eclipse/handlers/HandleVal.java index 7f4f36fb..8ab43131 100644 --- a/src/core/lombok/eclipse/handlers/HandleVal.java +++ b/src/core/lombok/eclipse/handlers/HandleVal.java @@ -21,6 +21,8 @@ */ package lombok.eclipse.handlers; +import lombok.val; +import lombok.eclipse.Eclipse; import lombok.eclipse.EclipseASTAdapter; import lombok.eclipse.EclipseASTVisitor; import lombok.eclipse.EclipseNode; @@ -28,7 +30,6 @@ import lombok.eclipse.EclipseNode; import org.eclipse.jdt.internal.compiler.ast.ArrayInitializer; import org.eclipse.jdt.internal.compiler.ast.ForeachStatement; import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration; -import org.eclipse.jdt.internal.compiler.ast.SingleTypeReference; import org.mangosdk.spi.ProviderFor; /* @@ -37,25 +38,22 @@ import org.mangosdk.spi.ProviderFor; @ProviderFor(EclipseASTVisitor.class) public class HandleVal extends EclipseASTAdapter { @Override public void visitLocal(EclipseNode localNode, LocalDeclaration local) { - if (local.type instanceof SingleTypeReference) { - char[] token = ((SingleTypeReference)local.type).token; - if (token == null || token.length != 3) return; - else if (token[0] != 'v' || token[1] != 'a' || token[2] != 'l') return; - - boolean variableOfForEach = false; - - if (localNode.directUp().get() instanceof ForeachStatement) { - ForeachStatement fs = (ForeachStatement) localNode.directUp().get(); - variableOfForEach = fs.elementVariable == local; - } - - if (local.initialization == null && !variableOfForEach) { - localNode.addError("'val' on a local variable requires an initializer expression"); - } - - if (local.initialization instanceof ArrayInitializer) { - localNode.addError("'val' is not compatible with array initializer expressions. Use the full form (new int[] { ... } instead of just { ... })"); - } + if (!Eclipse.typeMatches(val.class, localNode, local.type)) return; + boolean variableOfForEach = false; + + if (localNode.directUp().get() instanceof ForeachStatement) { + ForeachStatement fs = (ForeachStatement) localNode.directUp().get(); + variableOfForEach = fs.elementVariable == local; + } + + if (local.initialization == null && !variableOfForEach) { + localNode.addError("'val' on a local variable requires an initializer expression"); + return; + } + + if (local.initialization instanceof ArrayInitializer) { + localNode.addError("'val' is not compatible with array initializer expressions. Use the full form (new int[] { ... } instead of just { ... })"); + return; } } } diff --git a/src/core/lombok/val.java b/src/core/lombok/val.java index baab0f90..33afd3ba 100644 --- a/src/core/lombok/val.java +++ b/src/core/lombok/val.java @@ -25,5 +25,7 @@ package lombok; * Use {@code val} as the type of any local variable declaration (even in a for-each statement), and the type will be inferred from the initializing expression. * For example: {@code val x = 10.0;} will infer {@code double}, and {@code val y = new ArrayList<String>();} will infer {@code ArrayList<String>}. The local variable * will also be made final. + * + * Note that this is an interface because {@code val x = 10;} will be desugared to <code>@val int x = 10;</code> */ -public class val {} +public @interface val {} diff --git a/src/eclipseAgent/lombok/eclipse/agent/PatchVal.java b/src/eclipseAgent/lombok/eclipse/agent/PatchVal.java index b6523c3c..68b90286 100644 --- a/src/eclipseAgent/lombok/eclipse/agent/PatchVal.java +++ b/src/eclipseAgent/lombok/eclipse/agent/PatchVal.java @@ -23,8 +23,13 @@ */ package lombok.eclipse.agent; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import lombok.Lombok; import lombok.eclipse.Eclipse; import lombok.patcher.Hook; import lombok.patcher.MethodTarget; @@ -32,8 +37,16 @@ import lombok.patcher.ScriptManager; import lombok.patcher.StackRequest; import lombok.patcher.scripts.ScriptBuilder; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.IExtendedModifier; +import org.eclipse.jdt.core.dom.MarkerAnnotation; +import org.eclipse.jdt.core.dom.Modifier; +import org.eclipse.jdt.core.dom.Modifier.ModifierKeyword; +import org.eclipse.jdt.core.dom.Name; +import org.eclipse.jdt.core.dom.VariableDeclarationStatement; import org.eclipse.jdt.internal.compiler.ast.ASTNode; import org.eclipse.jdt.internal.compiler.ast.AbstractVariableDeclaration; +import org.eclipse.jdt.internal.compiler.ast.Annotation; import org.eclipse.jdt.internal.compiler.ast.Expression; import org.eclipse.jdt.internal.compiler.ast.ForeachStatement; import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration; @@ -65,6 +78,8 @@ public class PatchVal { final String BLOCKSCOPE_SIG = "org.eclipse.jdt.internal.compiler.lookup.BlockScope"; final String PARSER_SIG = "org.eclipse.jdt.internal.compiler.parser.Parser"; final String TYPEBINDING_SIG = "org.eclipse.jdt.internal.compiler.lookup.TypeBinding"; + final String VARIABLEDECLARATIONSTATEMENT_SIG = "org.eclipse.jdt.core.dom.VariableDeclarationStatement"; + final String ASTCONVERTER_SIG = "org.eclipse.jdt.core.dom.ASTConverter"; sm.addScript(ScriptBuilder.exitEarly() .target(new MethodTarget(LOCALDECLARATION_SIG, "resolve", "void", BLOCKSCOPE_SIG)) @@ -119,15 +134,28 @@ public class PatchVal { .request(StackRequest.THIS) .wrapMethod(new Hook("lombok.eclipse.agent.PatchVal", "copyInitializationOfForEachIterable", "void", PARSER_SIG)) .build()); + + + sm.addScript(ScriptBuilder.wrapReturnValue() + .target(new MethodTarget(ASTCONVERTER_SIG, "setModifiers", "void", VARIABLEDECLARATIONSTATEMENT_SIG, LOCALDECLARATION_SIG)) + .wrapMethod(new Hook("lombok.eclipse.agent.PatchVal", "addFinalAndValAnnotationToVariableDeclarationStatement", + "void", "java.lang.Object", VARIABLEDECLARATIONSTATEMENT_SIG, LOCALDECLARATION_SIG)) + .transplant().request(StackRequest.THIS, StackRequest.PARAM1, StackRequest.PARAM2).build()); } } - private static final class Reflection { - private static final Field initCopyField, iterableCopyField; - private static final Field astStackField, astPtrField; + public static final class Reflection { + public static final Field initCopyField, iterableCopyField; + public static final Field astStackField, astPtrField; + public static final Constructor<Modifier> modifierConstructor; + public static final Constructor<MarkerAnnotation> markerAnnotationConstructor; + public static final Method astConverterRecordNodes; static { Field a = null, b = null, c = null, d = null; + Constructor<Modifier> f = null; + Constructor<MarkerAnnotation> g = null; + Method h = null; try { a = LocalDeclaration.class.getDeclaredField("$initCopy"); @@ -141,6 +169,13 @@ public class PatchVal { c.setAccessible(true); d = Parser.class.getDeclaredField("astPtr"); d.setAccessible(true); + f = Modifier.class.getDeclaredConstructor(AST.class); + f.setAccessible(true); + g = MarkerAnnotation.class.getDeclaredConstructor(AST.class); + g.setAccessible(true); + Class<?> z = Class.forName("org.eclipse.jdt.core.dom.ASTConverter"); + h = z.getDeclaredMethod("recordNodes", org.eclipse.jdt.core.dom.ASTNode.class, org.eclipse.jdt.internal.compiler.ast.ASTNode.class); + h.setAccessible(true); } catch (Exception e) { // Most likely we're in ecj or some other plugin usage of the eclipse compiler. No need for this. } @@ -149,6 +184,9 @@ public class PatchVal { iterableCopyField = b; astStackField = c; astPtrField = d; + modifierConstructor = f; + markerAnnotationConstructor = g; + astConverterRecordNodes = h; } } @@ -178,6 +216,121 @@ public class PatchVal { } } + public static void addFinalAndValAnnotationToVariableDeclarationStatement(Object converter, VariableDeclarationStatement out, LocalDeclaration in) { + // First check that 'in' has the final flag on, and a @val / @lombok.val annotation. + if ((in.modifiers & ClassFileConstants.AccFinal) == 0) return; + if (in.annotations == null) return; + boolean found = false; + Annotation valAnnotation = null; + + for (Annotation ann : in.annotations) { + if (ann.type instanceof SingleTypeReference) { + if (matches("val", ((SingleTypeReference)ann.type).token)) { + found = true; + valAnnotation = ann; + break; + } + } + if (ann.type instanceof QualifiedTypeReference) { + char[][] tokens = ((QualifiedTypeReference)ann.type).tokens; + if (tokens != null && tokens.length == 2 && matches("lombok", tokens[0]) && matches("val", tokens[1])) { + found = true; + valAnnotation = ann; + break; + } + } + } + + if (!found) return; + + // Now check that 'out' is missing either of these. + + @SuppressWarnings("unchecked") List<IExtendedModifier> modifiers = out.modifiers(); + + if (modifiers == null) return; // This is null only if the project is 1.4 or less. Lombok doesn't work in that. + boolean finalIsPresent = false; + boolean valIsPresent = false; + + for (Object present : modifiers) { + if (present instanceof Modifier) { + ModifierKeyword keyword = ((Modifier)present).getKeyword(); + if (keyword == null) continue; + if (keyword.toFlagValue() == Modifier.FINAL) finalIsPresent = true; + } + + if (present instanceof org.eclipse.jdt.core.dom.Annotation) { + Name typeName = ((org.eclipse.jdt.core.dom.Annotation) present).getTypeName(); + if (typeName != null) { + String fullyQualifiedName = typeName.getFullyQualifiedName(); + if ("val".equals(fullyQualifiedName) || "lombok.val".equals(fullyQualifiedName)) { + valIsPresent = true; + } + } + } + } + + if (!finalIsPresent) { + modifiers.add( + createModifier(out.getAST(), ModifierKeyword.FINAL_KEYWORD, valAnnotation.sourceStart, valAnnotation.sourceEnd)); + } + + if (!valIsPresent) { + MarkerAnnotation newAnnotation = createValAnnotation(out.getAST(), valAnnotation, valAnnotation.sourceStart, valAnnotation.sourceEnd); + try { + Reflection.astConverterRecordNodes.invoke(converter, newAnnotation, valAnnotation); + Reflection.astConverterRecordNodes.invoke(converter, newAnnotation.getTypeName(), valAnnotation.type); + } catch (IllegalAccessException e) { + Lombok.sneakyThrow(e); + } catch (InvocationTargetException e) { + Lombok.sneakyThrow(e.getCause()); + } + modifiers.add(newAnnotation); + } + } + + public static MarkerAnnotation createValAnnotation(AST ast, Annotation original, int start, int end) { + MarkerAnnotation out = null; + try { + out = Reflection.markerAnnotationConstructor.newInstance(ast); + } catch (InstantiationException e) { + Lombok.sneakyThrow(e); + } catch (IllegalAccessException e) { + Lombok.sneakyThrow(e); + } catch (InvocationTargetException e) { + Lombok.sneakyThrow(e); + } + + if (out != null) { + if (original.type instanceof SingleTypeReference) { + out.setTypeName(ast.newSimpleName("val")); + } else { + out.setTypeName(ast.newQualifiedName(ast.newSimpleName("lombok"), ast.newSimpleName("val"))); + } + out.setSourceRange(start, end - start + 1); + } + + return out; + } + + public static Modifier createModifier(AST ast, ModifierKeyword keyword, int start, int end) { + Modifier modifier = null; + try { + modifier = Reflection.modifierConstructor.newInstance(ast); + } catch (InstantiationException e) { + Lombok.sneakyThrow(e); + } catch (IllegalAccessException e) { + Lombok.sneakyThrow(e); + } catch (InvocationTargetException e) { + Lombok.sneakyThrow(e); + } + + if (modifier != null) { + modifier.setKeyword(keyword); + modifier.setSourceRange(start, end - start + 1); + } + return modifier; + } + public static TypeBinding skipResolveInitializerIfAlreadyCalled(Expression expr, BlockScope scope) { if (expr.resolvedType != null) return expr.resolvedType; return expr.resolveType(scope); @@ -197,31 +350,38 @@ public class PatchVal { return true; } - public static boolean handleValForLocalDeclaration(LocalDeclaration local, BlockScope scope) { - if (local == null || !LocalDeclaration.class.equals(local.getClass())) return false; - boolean decomponent = false; - - boolean isVal = false; - - if (local.type instanceof SingleTypeReference) { - char[] token = ((SingleTypeReference)local.type).token; - if (matches("val", token)) isVal = true; + private static boolean couldBeVal(TypeReference ref) { + if (ref instanceof SingleTypeReference) { + char[] token = ((SingleTypeReference)ref).token; + return matches("val", token); } - if (local.type instanceof QualifiedTypeReference) { - char[][] tokens = ((QualifiedTypeReference)local.type).tokens; - if (tokens != null && tokens.length == 2 && matches("lombok", tokens[0]) && matches("val", tokens[1])) isVal = true; + if (ref instanceof QualifiedTypeReference) { + char[][] tokens = ((QualifiedTypeReference)ref).tokens; + if (tokens == null || tokens.length != 2) return false; + return matches("lombok", tokens[0]) && matches("val", tokens[1]); } - if (!isVal) return false; + return false; + } + + private static boolean isVal(TypeReference ref, BlockScope scope) { + if (!couldBeVal(ref)) return false; - TypeBinding resolvedType = local.type.resolvedType; - if (resolvedType == null) resolvedType = local.type.resolveType(scope, false); + TypeBinding resolvedType = ref.resolvedType; + if (resolvedType == null) resolvedType = ref.resolveType(scope, false); if (resolvedType == null) return false; char[] pkg = resolvedType.qualifiedPackageName(); char[] nm = resolvedType.qualifiedSourceName(); - if (!matches("lombok", pkg) || !matches("val", nm)) return false; + return matches("lombok", pkg) && matches("val", nm); + } + + public static boolean handleValForLocalDeclaration(LocalDeclaration local, BlockScope scope) { + if (local == null || !LocalDeclaration.class.equals(local.getClass())) return false; + boolean decomponent = false; + + if (!isVal(local.type, scope)) return false; Expression init = local.initialization; if (init == null && Reflection.initCopyField != null) { @@ -251,10 +411,12 @@ public class PatchVal { } local.modifiers |= ClassFileConstants.AccFinal; + local.annotations = addValAnnotation(local.annotations, local.type, scope); local.type = replacement != null ? replacement : new QualifiedTypeReference(TypeConstants.JAVA_LANG_OBJECT, Eclipse.poss(local.type, 3)); return false; } + public static void copyInitializationOfForEachIterable(Parser parser) { ASTNode[] astStack; int astPtr; @@ -282,23 +444,36 @@ public class PatchVal { } public static boolean handleValForForEach(ForeachStatement forEach, BlockScope scope) { - if (forEach.elementVariable != null && forEach.elementVariable.type instanceof SingleTypeReference) { - char[] token = ((SingleTypeReference)forEach.elementVariable.type).token; - if (token == null || token.length != 3) return false; - else if (token[0] != 'v' || token[1] != 'a' || token[2] != 'l') return false; - } else return false; + if (forEach.elementVariable == null) return false; + + if (!isVal(forEach.elementVariable.type, scope)) return false; TypeBinding component = getForEachComponentType(forEach.collection, scope); if (component == null) return false; TypeReference replacement = Eclipse.makeType(component, forEach.elementVariable.type, false); forEach.elementVariable.modifiers |= ClassFileConstants.AccFinal; + forEach.elementVariable.annotations = addValAnnotation(forEach.elementVariable.annotations, forEach.elementVariable.type, scope); forEach.elementVariable.type = replacement != null ? replacement : new QualifiedTypeReference(TypeConstants.JAVA_LANG_OBJECT, Eclipse.poss(forEach.elementVariable.type, 3)); return false; } + private static Annotation[] addValAnnotation(Annotation[] originals, TypeReference originalRef, BlockScope scope) { + Annotation[] newAnn; + if (originals != null) { + newAnn = new Annotation[1 + originals.length]; + System.arraycopy(originals, 0, newAnn, 0, originals.length); + } else { + newAnn = new Annotation[1]; + } + + newAnn[newAnn.length - 1] = new org.eclipse.jdt.internal.compiler.ast.MarkerAnnotation(originalRef, originalRef.sourceStart); + + return newAnn; + } + private static TypeBinding getForEachComponentType(Expression collection, BlockScope scope) { if (collection != null) { TypeBinding resolved = collection.resolveType(scope); |