From 7496a8f17047a1e1dd4a968632b1e4d2cbb5fda1 Mon Sep 17 00:00:00 2001 From: Reinier Zwitserloot Date: Tue, 12 Jun 2012 18:53:47 +0200 Subject: Port of lombok-pg's @ExtensionMethod support for eclipse. Doesn't work yet. --- .../lombok/eclipse/agent/EclipsePatcher.java | 46 +- .../agent/ExtensionMethodCompletionProposal.java | 99 +++++ .../lombok/eclipse/agent/PatchDelegatePortal.java | 67 +++ .../lombok/eclipse/agent/PatchExtensionMethod.java | 471 +++++++++++++++++++++ .../eclipse/agent/PatchExtensionMethodPortal.java | 133 ++++++ 5 files changed, 815 insertions(+), 1 deletion(-) create mode 100644 src/eclipseAgent/lombok/eclipse/agent/ExtensionMethodCompletionProposal.java create mode 100644 src/eclipseAgent/lombok/eclipse/agent/PatchDelegatePortal.java create mode 100644 src/eclipseAgent/lombok/eclipse/agent/PatchExtensionMethod.java create mode 100644 src/eclipseAgent/lombok/eclipse/agent/PatchExtensionMethodPortal.java (limited to 'src/eclipseAgent') diff --git a/src/eclipseAgent/lombok/eclipse/agent/EclipsePatcher.java b/src/eclipseAgent/lombok/eclipse/agent/EclipsePatcher.java index c14710c8..380b961e 100644 --- a/src/eclipseAgent/lombok/eclipse/agent/EclipsePatcher.java +++ b/src/eclipseAgent/lombok/eclipse/agent/EclipsePatcher.java @@ -21,6 +21,8 @@ */ package lombok.eclipse.agent; +import static lombok.patcher.scripts.ScriptBuilder.*; + import java.lang.instrument.Instrumentation; import java.util.Collection; import java.util.Collections; @@ -101,6 +103,7 @@ public class EclipsePatcher extends Agent { patchAvoidReparsingGeneratedCode(sm); patchLombokizeAST(sm); patchEcjTransformers(sm, ecjOnly); + patchExtensionMethod(sm, ecjOnly); if (reloadExistingClasses) sm.reloadClasses(instrumentation); } @@ -546,7 +549,7 @@ public class EclipsePatcher extends Agent { sm.addScript(ScriptBuilder.exitEarly() .target(new MethodTarget(CLASSSCOPE_SIG, "buildFieldsAndMethods", "void")) .request(StackRequest.THIS) - .decisionMethod(new Hook(PatchDelegate.class.getName(), "handleDelegateForType", "boolean", CLASSSCOPE_SIG)) + .decisionMethod(new Hook("lombok.eclipse.agent.PatchDelegatePortal", "handleDelegateForType", "boolean", "java.lang.Object")) .build()); } @@ -641,4 +644,45 @@ public class EclipsePatcher extends Agent { .wrapMethod(new Hook("lombok.eclipse.agent.PatchFixes", "convertAnnotations", ANNOTATION_SIG + "[]", ANNOTATION_SIG + "[]", I_ANNOTATABLE_SIG)) .request(StackRequest.PARAM1, StackRequest.RETURN_VALUE).build()); } + + private static void patchExtensionMethod(ScriptManager sm, boolean ecj) { + final String PATCH_EXTENSIONMETHOD_PORTAL = "lombok.eclipse.agent.PatchExtensionMethodPortal"; + final String MESSAGE_SEND_SIG = "org.eclipse.jdt.internal.compiler.ast.MessageSend"; + final String TYPE_BINDING_SIG = "org.eclipse.jdt.internal.compiler.lookup.TypeBinding"; + final String BLOCK_SCOPE_SIG = "org.eclipse.jdt.internal.compiler.lookup.BlockScope"; + final String TYPE_BINDINGS_SIG = "org.eclipse.jdt.internal.compiler.lookup.TypeBinding[]"; + final String PROBLEM_REPORTER_SIG = "org.eclipse.jdt.internal.compiler.problem.ProblemReporter"; + final String METHOD_BINDING_SIG = "org.eclipse.jdt.internal.compiler.lookup.MethodBinding"; + final String COMPLETION_PROPOSAL_COLLECTOR_SIG = "org.eclipse.jdt.ui.text.java.CompletionProposalCollector"; + final String I_JAVA_COMPLETION_PROPOSAL_SIG = "org.eclipse.jdt.ui.text.java.IJavaCompletionProposal[]"; + + sm.addScript(wrapReturnValue() + .target(new MethodTarget(MESSAGE_SEND_SIG, "resolveType", TYPE_BINDING_SIG, BLOCK_SCOPE_SIG)) + .request(StackRequest.RETURN_VALUE) + .request(StackRequest.THIS) + .request(StackRequest.PARAM1) + .wrapMethod(new Hook(PATCH_EXTENSIONMETHOD_PORTAL, "resolveType", TYPE_BINDING_SIG, "java.lang.Object", "java.lang.Object", "java.lang.Object")) + .build()); + + sm.addScript(replaceMethodCall() + .target(new MethodTarget(MESSAGE_SEND_SIG, "resolveType", TYPE_BINDING_SIG, BLOCK_SCOPE_SIG)) + .methodToReplace(new Hook(PROBLEM_REPORTER_SIG, "errorNoMethodFor", "void", MESSAGE_SEND_SIG, TYPE_BINDING_SIG, TYPE_BINDINGS_SIG)) + .replacementMethod(new Hook(PATCH_EXTENSIONMETHOD_PORTAL, "errorNoMethodFor", "void", "java.lang.Object", "java.lang.Object", "java.lang.Object", "java.lang.Object")) + .build()); + sm.addScript(replaceMethodCall() + .target(new MethodTarget(MESSAGE_SEND_SIG, "resolveType", TYPE_BINDING_SIG, BLOCK_SCOPE_SIG)) + .methodToReplace(new Hook(PROBLEM_REPORTER_SIG, "invalidMethod", "void", MESSAGE_SEND_SIG, METHOD_BINDING_SIG)) + .replacementMethod(new Hook(PATCH_EXTENSIONMETHOD_PORTAL, "invalidMethod", "void", "java.lang.Object", "java.lang.Object", "java.lang.Object")) + .build()); + + if (!ecj) { + sm.addScript(wrapReturnValue() + .target(new MethodTarget(COMPLETION_PROPOSAL_COLLECTOR_SIG, "getJavaCompletionProposals", I_JAVA_COMPLETION_PROPOSAL_SIG)) + .request(StackRequest.RETURN_VALUE) + .request(StackRequest.THIS) +// .wrapMethod(new Hook(PATCH_EXTENSIONMETHOD_PORTAL, "getJavaCompletionProposals", I_JAVA_COMPLETION_PROPOSAL_SIG, I_JAVA_COMPLETION_PROPOSAL_SIG, COMPLETION_PROPOSAL_COLLECTOR_SIG)) + .wrapMethod(new Hook(PATCH_EXTENSIONMETHOD_PORTAL, "getJavaCompletionProposals", "java.lang.Object", "java.lang.Object", "java.lang.Object")) + .build()); + } + } } diff --git a/src/eclipseAgent/lombok/eclipse/agent/ExtensionMethodCompletionProposal.java b/src/eclipseAgent/lombok/eclipse/agent/ExtensionMethodCompletionProposal.java new file mode 100644 index 00000000..46ce63f9 --- /dev/null +++ b/src/eclipseAgent/lombok/eclipse/agent/ExtensionMethodCompletionProposal.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2012 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.agent; + +import static org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants.AccStatic; + +import java.util.Arrays; + +import org.eclipse.jdt.core.CompletionProposal; +import org.eclipse.jdt.core.compiler.CharOperation; +import org.eclipse.jdt.internal.codeassist.CompletionEngine; +import org.eclipse.jdt.internal.codeassist.InternalCompletionProposal; +import org.eclipse.jdt.internal.codeassist.complete.CompletionOnMemberAccess; +import org.eclipse.jdt.internal.codeassist.complete.CompletionOnQualifiedNameReference; +import org.eclipse.jdt.internal.codeassist.complete.CompletionOnSingleNameReference; +import org.eclipse.jdt.internal.compiler.ast.ASTNode; +import org.eclipse.jdt.internal.compiler.lookup.MethodBinding; +import org.eclipse.jdt.internal.compiler.lookup.TypeBinding; + +public class ExtensionMethodCompletionProposal extends InternalCompletionProposal { + public ExtensionMethodCompletionProposal(final int replacementOffset) { + super(CompletionProposal.METHOD_REF, replacementOffset - 1); + } + + public void setMethodBinding(final MethodBinding method, final ASTNode node) { + MethodBinding original = method.original(); + TypeBinding[] parameters = Arrays.copyOf(method.parameters, method.parameters.length); + method.parameters = Arrays.copyOfRange(method.parameters, 1, method.parameters.length); + TypeBinding[] originalParameters = null; + if (original != method) { + originalParameters = Arrays.copyOf(method.original().parameters, method.original().parameters.length); + method.original().parameters = Arrays.copyOfRange(method.original().parameters, 1, method.original().parameters.length); + } + + int length = method.parameters == null ? 0 : method.parameters.length; + char[][] parameterPackageNames = new char[length][]; + char[][] parameterTypeNames = new char[length][]; + + for (int i = 0; i < length; i++) { + TypeBinding type = method.original().parameters[i]; + parameterPackageNames[i] = type.qualifiedPackageName(); + parameterTypeNames[i] = type.qualifiedSourceName(); + } + char[] completion = CharOperation.concat(method.selector, new char[] { '(', ')' }); + setDeclarationSignature(CompletionEngine.getSignature(method.declaringClass)); + setSignature(CompletionEngine.getSignature(method)); + + if (original != method) { + setOriginalSignature(CompletionEngine.getSignature(original)); + } + setDeclarationPackageName(method.declaringClass.qualifiedPackageName()); + setDeclarationTypeName(method.declaringClass.qualifiedSourceName()); + setParameterPackageNames(parameterPackageNames); + setParameterTypeNames(parameterTypeNames); + setPackageName(method.returnType.qualifiedPackageName()); + setTypeName(method.returnType.qualifiedSourceName()); + setName(method.selector); + setCompletion(completion); + setFlags(method.modifiers & (~AccStatic)); + int index = node.sourceEnd + 1; + if (node instanceof CompletionOnQualifiedNameReference) { + index -= ((CompletionOnQualifiedNameReference) node).completionIdentifier.length; + } + if (node instanceof CompletionOnMemberAccess) { + index -= ((CompletionOnMemberAccess) node).token.length; + } + if (node instanceof CompletionOnSingleNameReference) { + index -= ((CompletionOnSingleNameReference) node).token.length; + } + setReplaceRange(index, index); + setTokenRange(index, index); + + setRelevance(100); + + method.parameters = parameters; + if (original != method) { + method.original().parameters = originalParameters; + } + } +} diff --git a/src/eclipseAgent/lombok/eclipse/agent/PatchDelegatePortal.java b/src/eclipseAgent/lombok/eclipse/agent/PatchDelegatePortal.java new file mode 100644 index 00000000..1620014c --- /dev/null +++ b/src/eclipseAgent/lombok/eclipse/agent/PatchDelegatePortal.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2012 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.agent; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import lombok.Lombok; + +public class PatchDelegatePortal { + static final String CLASS_SCOPE = "org.eclipse.jdt.internal.compiler.lookup.ClassScope"; + + public static boolean handleDelegateForType(Object classScope) { + try { + return (Boolean) Reflection.handleDelegateForType.invoke(null, classScope); + } catch (NoClassDefFoundError e) { + //ignore, we don't have access to the correct ECJ classes, so lombok can't possibly + //do anything useful here. + return false; + } catch (IllegalAccessException e) { + throw Lombok.sneakyThrow(e); + } catch (InvocationTargetException e) { + throw Lombok.sneakyThrow(e); + } catch (NullPointerException e) { + e.initCause(Reflection.problem); + throw e; + } + } + + private static final class Reflection { + public static final Method handleDelegateForType; + public static final Throwable problem; + + static { + Method m = null; + Throwable problem_ = null; + try { + m = PatchDelegate.class.getMethod("handleDelegateForType", Class.forName(CLASS_SCOPE)); + } 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; + } + handleDelegateForType = m; + problem = problem_; + } + } +} diff --git a/src/eclipseAgent/lombok/eclipse/agent/PatchExtensionMethod.java b/src/eclipseAgent/lombok/eclipse/agent/PatchExtensionMethod.java new file mode 100644 index 00000000..589f791c --- /dev/null +++ b/src/eclipseAgent/lombok/eclipse/agent/PatchExtensionMethod.java @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2012 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.agent; + +import static lombok.eclipse.handlers.EclipseHandlerUtil.createAnnotation; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +import lombok.core.AST.Kind; +import lombok.core.AnnotationValues; +import lombok.core.AnnotationValues.AnnotationValueDecodeFail; +import lombok.eclipse.EclipseAST; +import lombok.eclipse.EclipseNode; +import lombok.eclipse.TransformEclipseAST; +import lombok.eclipse.handlers.EclipseHandlerUtil; +import lombok.experimental.ExtensionMethod; + +import org.eclipse.jdt.core.CompletionProposal; +import org.eclipse.jdt.internal.codeassist.InternalCompletionContext; +import org.eclipse.jdt.internal.codeassist.InternalCompletionProposal; +import org.eclipse.jdt.internal.codeassist.InternalExtendedCompletionContext; +import org.eclipse.jdt.internal.codeassist.complete.CompletionOnMemberAccess; +import org.eclipse.jdt.internal.codeassist.complete.CompletionOnQualifiedNameReference; +import org.eclipse.jdt.internal.codeassist.complete.CompletionOnSingleNameReference; +import org.eclipse.jdt.internal.compiler.ast.ASTNode; +import org.eclipse.jdt.internal.compiler.ast.Annotation; +import org.eclipse.jdt.internal.compiler.ast.ClassLiteralAccess; +import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; +import org.eclipse.jdt.internal.compiler.ast.Expression; +import org.eclipse.jdt.internal.compiler.ast.FieldReference; +import org.eclipse.jdt.internal.compiler.ast.MessageSend; +import org.eclipse.jdt.internal.compiler.ast.NameReference; +import org.eclipse.jdt.internal.compiler.ast.QualifiedNameReference; +import org.eclipse.jdt.internal.compiler.ast.SingleNameReference; +import org.eclipse.jdt.internal.compiler.ast.ThisReference; +import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration; +import org.eclipse.jdt.internal.compiler.lookup.Binding; +import org.eclipse.jdt.internal.compiler.lookup.BlockScope; +import org.eclipse.jdt.internal.compiler.lookup.ClassScope; +import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope; +import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment; +import org.eclipse.jdt.internal.compiler.lookup.MethodBinding; +import org.eclipse.jdt.internal.compiler.lookup.ProblemMethodBinding; +import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding; +import org.eclipse.jdt.internal.compiler.lookup.Scope; +import org.eclipse.jdt.internal.compiler.lookup.TypeBinding; +import org.eclipse.jdt.internal.compiler.lookup.TypeIds; +import org.eclipse.jdt.internal.compiler.lookup.VariableBinding; +import org.eclipse.jdt.internal.compiler.problem.ProblemReporter; +import org.eclipse.jdt.internal.core.SearchableEnvironment; +import org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal; +import org.eclipse.jdt.ui.text.java.CompletionProposalCollector; +import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal; + +public class PatchExtensionMethod { + private static class Extension { + List extensionMethods; + boolean suppressBaseMethods; + } + + private static class PostponedNoMethodError implements PostponedError { + private final ProblemReporter problemReporter; + private final MessageSend messageSend; + private final TypeBinding recType; + private final TypeBinding[] params; + + PostponedNoMethodError(ProblemReporter problemReporter, MessageSend messageSend, TypeBinding recType, TypeBinding[] params) { + this.problemReporter = problemReporter; + this.messageSend = messageSend; + this.recType = recType; + this.params = params; + } + + public void fire() { + problemReporter.errorNoMethodFor(messageSend, recType, params); + } + } + + private static class PostponedInvalidMethodError implements PostponedError { + private final ProblemReporter problemReporter; + private final MessageSend messageSend; + private final MethodBinding method; + + PostponedInvalidMethodError(ProblemReporter problemReporter, MessageSend messageSend, MethodBinding method) { + this.problemReporter = problemReporter; + this.messageSend = messageSend; + this.method = method; + } + + public void fire() { + problemReporter.invalidMethod(messageSend, method); + } + } + + private static interface PostponedError { + public void fire(); + } + + public static EclipseNode getTypeNode(TypeDeclaration decl) { + CompilationUnitDeclaration cud = decl.scope.compilationUnitScope().referenceContext; + EclipseAST astNode = TransformEclipseAST.getAST(cud, false); + EclipseNode node = astNode.get(decl); + if (node == null) { + astNode = TransformEclipseAST.getAST(cud, true); + node = astNode.get(decl); + } + return node; + } + + public static Annotation getAnnotation(Class expectedType, EclipseNode node) { + TypeDeclaration decl = (TypeDeclaration) node.get(); + if (decl.annotations != null) for (Annotation ann : decl.annotations) { + if (EclipseHandlerUtil.typeMatches(expectedType, node, ann.type)) return ann; + } + return null; + } + + private static EclipseNode upToType(EclipseNode typeNode) { + EclipseNode node = typeNode; + do { + node = node.up(); + } while ((node != null) && (node.getKind() != Kind.TYPE)); + return node; + } + + private static List getApplicableExtensionMethods(EclipseNode typeNode, Annotation ann, TypeBinding receiverType) { + List extensions = new ArrayList(); + if ((typeNode != null) && (ann != null) && (receiverType != null)) { + BlockScope blockScope = ((TypeDeclaration) typeNode.get()).initializerScope; + EclipseNode annotationNode = typeNode.getNodeFor(ann); + AnnotationValues annotation = createAnnotation(ExtensionMethod.class, annotationNode); + boolean suppressBaseMethods = false; + try { + suppressBaseMethods = annotation.getInstance().suppressBaseMethods(); + } catch (AnnotationValueDecodeFail fail) { + fail.owner.setError(fail.getMessage(), fail.idx); + } + for (Object extensionMethodProvider : annotation.getActualExpressions("value")) { + if (extensionMethodProvider instanceof ClassLiteralAccess) { + TypeBinding binding = ((ClassLiteralAccess) extensionMethodProvider).type.resolveType(blockScope); + if (binding == null) continue; + if (!binding.isClass() && !binding.isEnum()) continue; + Extension e = new Extension(); + e.extensionMethods = getApplicableExtensionMethodsDefinedInProvider(typeNode, (ReferenceBinding) binding, receiverType); + e.suppressBaseMethods = suppressBaseMethods; + extensions.add(e); + } + } + } + return extensions; + } + + private static List getApplicableExtensionMethodsDefinedInProvider(EclipseNode typeNode, ReferenceBinding extensionMethodProviderBinding, + TypeBinding receiverType) { + + List extensionMethods = new ArrayList(); + CompilationUnitScope cuScope = ((CompilationUnitDeclaration) typeNode.top().get()).scope; + for (MethodBinding method : extensionMethodProviderBinding.methods()) { + if (!method.isStatic()) continue; + if (!method.isPublic()) continue; + if (method.parameters == null || method.parameters.length == 0) continue; + TypeBinding firstArgType = method.parameters[0]; + if (receiverType.isProvablyDistinct(firstArgType) && !receiverType.isCompatibleWith(firstArgType.erasure())) continue; + TypeBinding[] argumentTypes = Arrays.copyOfRange(method.parameters, 1, method.parameters.length); + if ((receiverType instanceof ReferenceBinding) && ((ReferenceBinding) receiverType).getExactMethod(method.selector, argumentTypes, cuScope) != null) continue; + extensionMethods.add(method); + } + return extensionMethods; + } + + private static final Map ERRORS = new WeakHashMap(); + + public static void errorNoMethodFor(ProblemReporter problemReporter, MessageSend messageSend, TypeBinding recType, TypeBinding[] params) { + ERRORS.put(messageSend, new PostponedNoMethodError(problemReporter, messageSend, recType, params)); + } + + public static void invalidMethod(ProblemReporter problemReporter, MessageSend messageSend, MethodBinding method) { + ERRORS.put(messageSend, new PostponedInvalidMethodError(problemReporter, messageSend, method)); + } + + public static TypeBinding resolveType(TypeBinding resolvedType, MessageSend methodCall, BlockScope scope) { + List extensions = new ArrayList(); + TypeDeclaration decl = scope.classScope().referenceContext; + + EclipseNode owningType = null; + + for (EclipseNode typeNode = getTypeNode(decl); typeNode != null; typeNode = upToType(typeNode)) { + Annotation ann = getAnnotation(ExtensionMethod.class, typeNode); + if (ann != null) { + extensions.addAll(0, getApplicableExtensionMethods(typeNode, ann, methodCall.receiver.resolvedType)); + if (owningType == null) owningType = typeNode; + } + } + + if (methodCall.binding == null) return resolvedType; + + for (Extension extension : extensions) { + if (!extension.suppressBaseMethods && !(methodCall.binding instanceof ProblemMethodBinding)) continue; + for (MethodBinding extensionMethod : extension.extensionMethods) { + if (!Arrays.equals(methodCall.selector, extensionMethod.selector)) continue; + ERRORS.remove(methodCall); + if (methodCall.receiver instanceof ThisReference) { + methodCall.receiver.bits &= ~ASTNode.IsImplicitThis; + } + List arguments = new ArrayList(); + arguments.add(methodCall.receiver); + if (methodCall.arguments != null) arguments.addAll(Arrays.asList(methodCall.arguments)); + List argumentTypes = new ArrayList(); + argumentTypes.add(methodCall.receiver.resolvedType); + if (methodCall.binding.parameters != null) argumentTypes.addAll(Arrays.asList(methodCall.binding.parameters)); + MethodBinding fixedBinding = scope.getMethod(extensionMethod.declaringClass, methodCall.selector, argumentTypes.toArray(new TypeBinding[0]), methodCall); + if (fixedBinding instanceof ProblemMethodBinding) { + if (fixedBinding.declaringClass != null) { + scope.problemReporter().invalidMethod(methodCall, fixedBinding); + } + } else { + for (int i = 0, iend = arguments.size(); i < iend; i++) { + Expression arg = arguments.get(i); + if (fixedBinding.parameters[i].isArrayType() != arg.resolvedType.isArrayType()) break; + if (arg.resolvedType.isArrayType()) { + if (arg instanceof MessageSend) { + ((MessageSend) arg).valueCast = arg.resolvedType; + } + } + if (!fixedBinding.parameters[i].isBaseType() && arg.resolvedType.isBaseType()) { + int id = arg.resolvedType.id; + arg.implicitConversion = TypeIds.BOXING | (id + (id << 4)); // magic see TypeIds + } else if (fixedBinding.parameters[i].isBaseType() && !arg.resolvedType.isBaseType()) { + int id = fixedBinding.parameters[i].id; + arg.implicitConversion = TypeIds.UNBOXING | (id + (id << 4)); // magic see TypeIds + } + } + methodCall.arguments = arguments.toArray(new Expression[0]); + + methodCall.receiver = createNameRef(extensionMethod.declaringClass, methodCall); + methodCall.actualReceiverType = extensionMethod.declaringClass; + methodCall.binding = fixedBinding; + methodCall.resolvedType = methodCall.binding.returnType; + } + return methodCall.resolvedType; + } + } + + PostponedError error = ERRORS.get(methodCall); + if (error != null) error.fire(); + + ERRORS.remove(methodCall); + return resolvedType; + } + + private static NameReference createNameRef(TypeBinding typeBinding, ASTNode source) { + long p = ((long) source.sourceStart << 32) | source.sourceEnd; + char[] pkg = typeBinding.qualifiedPackageName(); + char[] basename = typeBinding.qualifiedSourceName(); + + StringBuilder sb = new StringBuilder(); + if (pkg != null) sb.append(pkg); + if (sb.length() > 0) sb.append("."); + sb.append(basename); + + String tName = sb.toString(); + + if (tName.indexOf('.') == -1) { + return new SingleNameReference(basename, p); + } else { + char[][] sources; + String[] in = tName.split("\\."); + sources = new char[in.length][]; + for (int i = 0; i < in.length; i++) sources[i] = in[i].toCharArray(); + long[] poss = new long[in.length]; + Arrays.fill(poss, p); + return new QualifiedNameReference(sources, poss, source.sourceStart, source.sourceEnd); + } + } + + public static IJavaCompletionProposal[] getJavaCompletionProposals(IJavaCompletionProposal[] javaCompletionProposals, + CompletionProposalCollector completionProposalCollector) { + + List proposals = new ArrayList(Arrays.asList(javaCompletionProposals)); + if (canExtendCodeAssist(proposals)) { + IJavaCompletionProposal firstProposal = proposals.get(0); + int replacementOffset = getReplacementOffset(firstProposal); + for (Extension extension : getExtensionMethods(completionProposalCollector)) { + for (MethodBinding method : extension.extensionMethods) { + ExtensionMethodCompletionProposal newProposal = new ExtensionMethodCompletionProposal(replacementOffset); + copyNameLookupAndCompletionEngine(completionProposalCollector, firstProposal, newProposal); + ASTNode node = getAssistNode(completionProposalCollector); + newProposal.setMethodBinding(method, node); + createAndAddJavaCompletionProposal(completionProposalCollector, newProposal, proposals); + } + } + } + return proposals.toArray(new IJavaCompletionProposal[proposals.size()]); + } + + private static void copyNameLookupAndCompletionEngine(CompletionProposalCollector completionProposalCollector, IJavaCompletionProposal proposal, + InternalCompletionProposal newProposal) { + + try { + InternalCompletionContext context = (InternalCompletionContext) Reflection.contextField.get(completionProposalCollector); + InternalExtendedCompletionContext extendedContext = (InternalExtendedCompletionContext) Reflection.extendedContextField.get(context); + LookupEnvironment lookupEnvironment = (LookupEnvironment) Reflection.lookupEnvironmentField.get(extendedContext); + Reflection.nameLookupField.set(newProposal, ((SearchableEnvironment) lookupEnvironment.nameEnvironment).nameLookup); + Reflection.completionEngineField.set(newProposal, lookupEnvironment.typeRequestor); + } catch (IllegalAccessException ignore) { + // ignore + } + } + + private static void createAndAddJavaCompletionProposal(CompletionProposalCollector completionProposalCollector, CompletionProposal newProposal, + List proposals) { + + try { + proposals.add((IJavaCompletionProposal) Reflection.createJavaCompletionProposalMethod.invoke(completionProposalCollector, newProposal)); + } catch (Exception ignore) { + // ignore + } + } + + private static boolean canExtendCodeAssist(List proposals) { + return !proposals.isEmpty() && Reflection.isComplete(); + } + + private static int getReplacementOffset(IJavaCompletionProposal proposal) { + try { + return Reflection.replacementOffsetField.getInt(proposal); + } catch (Exception ignore) { + return 0; + } + } + + private static List getExtensionMethods(CompletionProposalCollector completionProposalCollector) { + List extensions = new ArrayList(); + ClassScope classScope = getClassScope(completionProposalCollector); + if (classScope != null) { + TypeDeclaration decl = classScope.referenceContext; + TypeBinding firstParameterType = getFirstParameterType(decl, completionProposalCollector); + for (EclipseNode typeNode = getTypeNode(decl); typeNode != null; typeNode = upToType(typeNode)) { + Annotation ann = getAnnotation(ExtensionMethod.class, typeNode); + extensions.addAll(0, getApplicableExtensionMethods(typeNode, ann, firstParameterType)); + } + } + return extensions; + } + + private static ClassScope getClassScope(CompletionProposalCollector completionProposalCollector) { + ClassScope scope = null; + try { + InternalCompletionContext context = (InternalCompletionContext) Reflection.contextField.get(completionProposalCollector); + InternalExtendedCompletionContext extendedContext = (InternalExtendedCompletionContext) Reflection.extendedContextField.get(context); + if (extendedContext != null) { + Scope assistScope = ((Scope) Reflection.assistScopeField.get(extendedContext)); + if (assistScope != null) { + scope = assistScope.classScope(); + } + } + } catch (IllegalAccessException ignore) { + // ignore + } + return scope; + } + + private static TypeBinding getFirstParameterType(TypeDeclaration decl, CompletionProposalCollector completionProposalCollector) { + TypeBinding firstParameterType = null; + ASTNode node = getAssistNode(completionProposalCollector); + if (node == null) return null; + if (!(node instanceof CompletionOnQualifiedNameReference) && !(node instanceof CompletionOnSingleNameReference) && !(node instanceof CompletionOnMemberAccess)) return null; + + if (node instanceof NameReference) { + Binding binding = ((NameReference) node).binding; + if ((node instanceof SingleNameReference) && (((SingleNameReference) node).token.length == 0)) { + firstParameterType = decl.binding; + } else if (binding instanceof VariableBinding) { + firstParameterType = ((VariableBinding) binding).type; + } else if (binding instanceof TypeBinding) { + firstParameterType = (TypeBinding) binding; + } + } else if (node instanceof FieldReference) { + firstParameterType = ((FieldReference) node).actualReceiverType; + } + return firstParameterType; + } + + private static ASTNode getAssistNode(CompletionProposalCollector completionProposalCollector) { + try { + InternalCompletionContext context = (InternalCompletionContext) Reflection.contextField.get(completionProposalCollector); + InternalExtendedCompletionContext extendedContext = (InternalExtendedCompletionContext) Reflection.extendedContextField.get(context); + if (extendedContext == null) return null; + return (ASTNode) Reflection.assistNodeField.get(extendedContext); + } catch (Exception ignore) { + return null; + } + } + + private static class Reflection { + public static final Field replacementOffsetField; + public static final Field contextField; + public static final Field extendedContextField; + public static final Field assistNodeField; + public static final Field assistScopeField; + public static final Field lookupEnvironmentField; + public static final Field completionEngineField; + public static final Field nameLookupField; + public static final Method createJavaCompletionProposalMethod; + + static { + replacementOffsetField = accessField(AbstractJavaCompletionProposal.class, "fReplacementOffset"); + contextField = accessField(CompletionProposalCollector.class, "fContext"); + extendedContextField = accessField(InternalCompletionContext.class, "extendedContext"); + assistNodeField = accessField(InternalExtendedCompletionContext.class, "assistNode"); + assistScopeField = accessField(InternalExtendedCompletionContext.class, "assistScope"); + lookupEnvironmentField = accessField(InternalExtendedCompletionContext.class, "lookupEnvironment"); + completionEngineField = accessField(InternalCompletionProposal.class, "completionEngine"); + nameLookupField = accessField(InternalCompletionProposal.class, "nameLookup"); + createJavaCompletionProposalMethod = accessMethod(CompletionProposalCollector.class, "createJavaCompletionProposal", CompletionProposal.class); + } + + private static boolean isComplete() { + Object[] requiredFieldsAndMethods = { replacementOffsetField, contextField, extendedContextField, assistNodeField, assistScopeField, lookupEnvironmentField, completionEngineField, nameLookupField, createJavaCompletionProposalMethod }; + for (Object o : requiredFieldsAndMethods) if (o == null) return false; + return true; + } + + private static Field accessField(Class clazz, String fieldName) { + try { + return makeAccessible(clazz.getDeclaredField(fieldName)); + } catch (Exception e) { + return null; + } + } + + private static Method accessMethod(Class clazz, String methodName, Class parameter) { + try { + return makeAccessible(clazz.getDeclaredMethod(methodName, parameter)); + } catch (Exception e) { + return null; + } + } + + private static T makeAccessible(T object) { + object.setAccessible(true); + return object; + } + } +} diff --git a/src/eclipseAgent/lombok/eclipse/agent/PatchExtensionMethodPortal.java b/src/eclipseAgent/lombok/eclipse/agent/PatchExtensionMethodPortal.java new file mode 100644 index 00000000..2ccc14ae --- /dev/null +++ b/src/eclipseAgent/lombok/eclipse/agent/PatchExtensionMethodPortal.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2012 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.agent; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.eclipse.jdt.internal.compiler.lookup.TypeBinding; + +import lombok.Lombok; + + +public class PatchExtensionMethodPortal { + private static final String TYPE_BINDING = "org.eclipse.jdt.internal.compiler.lookup.TypeBinding"; + private static final String TYPE_BINDING_ARRAY = "[Lorg.eclipse.jdt.internal.compiler.lookup.TypeBinding;"; + private static final String MESSAGE_SEND = "org.eclipse.jdt.internal.compiler.ast.MessageSend"; + private static final String BLOCK_SCOPE = "org.eclipse.jdt.internal.compiler.lookup.BlockScope"; + private static final String METHOD_BINDING = "org.eclipse.jdt.internal.compiler.lookup.MethodBinding"; + private static final String PROBLEM_REPORTER = "org.eclipse.jdt.internal.compiler.problem.ProblemReporter"; + private static final String COMPLETION_PROPOSAL_COLLECTOR = "org.eclipse.jdt.ui.text.java.CompletionProposalCollector"; + private static final String I_JAVA_COMPLETION_PROPOSAL_ARRAY = "[Lorg.eclipse.jdt.ui.text.java.IJavaCompletionProposal;"; + + public static TypeBinding resolveType(Object resolvedType, Object methodCall, Object scope) { + try { + return (TypeBinding) Reflection.resolveType.invoke(null, resolvedType, methodCall, scope); + } catch (NoClassDefFoundError e) { + //ignore, we don't have access to the correct ECJ classes, so lombok can't possibly + //do anything useful here. + return (TypeBinding) resolvedType; + } catch (IllegalAccessException e) { + throw Lombok.sneakyThrow(e); + } catch (InvocationTargetException e) { + throw Lombok.sneakyThrow(e); + } catch (NullPointerException e) { + e.initCause(Reflection.problem); + throw e; + } + } + + public static void errorNoMethodFor(Object problemReporter, Object messageSend, Object recType, Object params) { + try { + Reflection.errorNoMethodFor.invoke(null, problemReporter, messageSend, recType, params); + } 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) { + Lombok.sneakyThrow(e); + } catch (InvocationTargetException e) { + Lombok.sneakyThrow(e); + } catch (NullPointerException e) { + e.initCause(Reflection.problem); + throw e; + } + } + + public static void invalidMethod(Object problemReporter, Object messageSend, Object method) { + try { + Reflection.invalidMethod.invoke(null, problemReporter, messageSend, method); + } 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) { + Lombok.sneakyThrow(e); + } catch (InvocationTargetException e) { + Lombok.sneakyThrow(e); + } catch (NullPointerException e) { + e.initCause(Reflection.problem); + throw e; + } + } + + public static Object getJavaCompletionProposals(Object[] javaCompletionProposals, Object completionProposalCollector) { + try { + return Reflection.getJavaCompletionProposals.invoke(null, javaCompletionProposals, completionProposalCollector); + } catch (NoClassDefFoundError e) { + //ignore, we don't have access to the correct ECJ classes, so lombok can't possibly + //do anything useful here. + return javaCompletionProposals; + } catch (IllegalAccessException e) { + throw Lombok.sneakyThrow(e); + } catch (InvocationTargetException e) { + throw Lombok.sneakyThrow(e); + } catch (NullPointerException e) { + e.initCause(Reflection.problem); + throw e; + } + } + + private static final class Reflection { + public static final Method resolveType, errorNoMethodFor, invalidMethod, getJavaCompletionProposals; + public static final Throwable problem; + + static { + Method m = null, n = null, o = null, p = null; + Throwable problem_ = null; + try { + m = PatchExtensionMethod.class.getMethod("resolveType", Class.forName(TYPE_BINDING), Class.forName(MESSAGE_SEND), Class.forName(BLOCK_SCOPE)); + n = PatchExtensionMethod.class.getMethod("errorNoMethodFor", Class.forName(PROBLEM_REPORTER), + Class.forName(MESSAGE_SEND), Class.forName(TYPE_BINDING), Class.forName(TYPE_BINDING_ARRAY)); + o = PatchExtensionMethod.class.getMethod("invalidMethod", Class.forName(PROBLEM_REPORTER), Class.forName(MESSAGE_SEND), Class.forName(METHOD_BINDING)); + p = PatchExtensionMethod.class.getMethod("getJavaCompletionProposals", Class.forName(I_JAVA_COMPLETION_PROPOSAL_ARRAY), Class.forName(COMPLETION_PROPOSAL_COLLECTOR)); + } 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; + } + resolveType = m; + errorNoMethodFor = n; + invalidMethod = o; + getJavaCompletionProposals = p; + problem = problem_; + } + } +} -- cgit