/** * Copyright (c) 2010, 2014 Darmstadt University of Technology. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Marcel Bruch - initial API and implementation. * Andreas Sewe - better handling of generics. * Johannes Dorn - refactoring. */ package org.eclipse.recommenders.completion.rcp.utils; import static com.google.common.base.Objects.firstNonNull; import static com.google.common.base.Optional.absent; import static org.eclipse.jdt.core.compiler.CharOperation.NO_CHAR; import static org.eclipse.recommenders.utils.LogMessages.LOG_WARNING_FAILED_TO_ACCESS_FIELD_REFLECTIVELY; import static org.eclipse.recommenders.utils.Logs.log; import static org.eclipse.recommenders.utils.Reflections.getDeclaredField; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import org.eclipse.jdt.core.CompletionProposal; import org.eclipse.jdt.core.Signature; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.internal.codeassist.InternalCompletionProposal; import org.eclipse.jdt.internal.compiler.lookup.MethodBinding; import org.eclipse.jdt.internal.compiler.lookup.TypeConstants; import org.eclipse.recommenders.internal.completion.rcp.Constants; import org.eclipse.recommenders.internal.completion.rcp.l10n.LogMessages; import org.eclipse.recommenders.rcp.utils.CompilerBindings; import org.eclipse.recommenders.utils.Reflections; import org.eclipse.recommenders.utils.names.IMethodName; import org.eclipse.recommenders.utils.names.VmMethodName; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Preconditions; @SuppressWarnings("restriction") public final class ProposalUtils { private ProposalUtils() { } private static final IMethodName OBJECT_CLONE = VmMethodName.get("Ljava/lang/Object.clone()Ljava/lang/Object;"); //$NON-NLS-1$ private static final char[] INIT = "<init>".toCharArray(); //$NON-NLS-1$ private static final char[] JAVA_LANG_OBJECT = "Ljava.lang.Object;".toCharArray(); //$NON-NLS-1$ private static final Method INTERNAL_COMPLETION_PROPOSAL_GET_BINDING = Reflections .getDeclaredMethod(InternalCompletionProposal.class, "getBinding").orNull(); //$NON-NLS-1$ @VisibleForTesting public static boolean isGetBindingSupported() { return INTERNAL_COMPLETION_PROPOSAL_GET_BINDING != null; } /** * Workaround needed to handle proposals with generic signatures properly. * * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=380203">Bug 380203</a>. */ private static final Field ORIGINAL_SIGNATURE = getDeclaredField(true, InternalCompletionProposal.class, "originalSignature") //$NON-NLS-1$ .orNull(); public static Optional<IMethodName> toMethodName(CompletionProposal proposal) { Preconditions.checkArgument(isKindSupported(proposal)); if (INTERNAL_COMPLETION_PROPOSAL_GET_BINDING != null && proposal instanceof InternalCompletionProposal) { try { MethodBinding binding = (MethodBinding) INTERNAL_COMPLETION_PROPOSAL_GET_BINDING.invoke(proposal); IMethodName methodName = CompilerBindings.toMethodName(binding).orNull(); if (methodName == null) { return toMethodNameFallback(proposal); } return Optional.of(methodName); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { return toMethodNameFallback(proposal); } } return toMethodNameFallback(proposal); } private static Optional<IMethodName> toMethodNameFallback(CompletionProposal proposal) { if (isArrayCloneMethod(proposal)) { return Optional.of(OBJECT_CLONE); } if (Constants.DEBUG) { log(LogMessages.INFO_FALLBACK_METHOD_NAME_CREATION, toLogString(proposal)); } StringBuilder builder = new StringBuilder(); // Declaring type char[] declarationSignature = proposal.getDeclarationSignature(); char[] binaryTypeSignature = getBinaryTypeSignatureCopy(declarationSignature); char[] erasedBinaryTypeSignature = Signature.getTypeErasure(binaryTypeSignature); CharOperation.replace(erasedBinaryTypeSignature, '.', '/'); builder.append(erasedBinaryTypeSignature, 0, erasedBinaryTypeSignature.length - 1); builder.append('.'); // Method name builder.append(proposal.isConstructor() ? INIT : proposal.getName()); builder.append('('); // Parameter types char[] signature = getSignature(proposal); char[][] typeParameters = Signature.getTypeParameters(declarationSignature); char[][] parameterTypes = Signature.getParameterTypes(signature); for (char[] parameterType : parameterTypes) { appendType(builder, parameterType, typeParameters); } builder.append(')'); // Return type appendType(builder, Signature.getReturnType(signature), typeParameters); String methodName = builder.toString(); try { return Optional.<IMethodName>of(VmMethodName.get(methodName)); } catch (Exception e) { log(LogMessages.ERROR_SYNTATICALLY_INCORRECT_METHOD_NAME, e, methodName, toLogString(proposal)); return absent(); } } /** * Ensures that the separator of inner and outer types is always a dollar sign. * * <p> * This is necessary, as JDT uses a dot rather than dollar sign to separate inner and outer type <em>if</em> the * outer type is parameterized. * </p> * * <p> * Examples: * </p> * * <ul> * <li><code>org.example.Outer$Inner<:java.lang.String></code> -> * <code>org.example.Outer$Inner<:java.lang.String></code></li> * <li><code>org.example.Outer<:java.lang.String>.Inner</code> -> * <code>org.example.Outer<:java.lang.String>$Inner</code></li> * <ul> */ private static char[] getBinaryTypeSignatureCopy(char[] parameterizedTypeSignature) { char[] binaryTypeSignature = Arrays.copyOf(parameterizedTypeSignature, parameterizedTypeSignature.length); int nextDot = -1; while ((nextDot = CharOperation.indexOf(Signature.C_DOT, binaryTypeSignature, nextDot + 1)) > 0) { if (binaryTypeSignature[nextDot - 1] == Signature.C_GENERIC_END) { binaryTypeSignature[nextDot] = Signature.C_DOLLAR; } } return binaryTypeSignature; } private static void appendType(StringBuilder builder, char[] type, char[][] typeParameters) { switch (Signature.getTypeSignatureKind(type)) { case Signature.TYPE_VARIABLE_SIGNATURE: char[] typeVariableName = CharOperation.subarray(type, 1, type.length - 1); char[] resolvedTypeVariable = resolveTypeVariable(typeVariableName, typeParameters); builder.append(CharOperation.replaceOnCopy(resolvedTypeVariable, '.', '/')); break; case Signature.ARRAY_TYPE_SIGNATURE: int dimensions = Signature.getArrayCount(type); builder.append(type, 0, dimensions); appendType(builder, Signature.getElementType(type), typeParameters); break; default: char[] erasedParameterType = Signature.getTypeErasure(type); builder.append(CharOperation.replaceOnCopy(erasedParameterType, '.', '/')); break; } } private static char[] resolveTypeVariable(char[] typeVariableName, char[][] typeParameters) { for (char[] typeParameter : typeParameters) { if (CharOperation.equals(typeVariableName, Signature.getTypeVariable(typeParameter))) { char[][] typeParameterBounds = Signature.getTypeParameterBounds(typeParameter); if (typeParameterBounds.length > 0) { return typeParameterBounds[0]; } else { return JAVA_LANG_OBJECT; } } } // No match found. Assume Object. return JAVA_LANG_OBJECT; } private static String toLogString(CompletionProposal proposal) { if (proposal == null) { return "null proposal"; //$NON-NLS-1$ } return new StringBuilder().append(firstNonNull(proposal.getDeclarationSignature(), NO_CHAR)).append('#') .append(firstNonNull(proposal.getName(), NO_CHAR)).append('#') .append(firstNonNull(proposal.getSignature(), NO_CHAR)).toString(); } private static boolean isKindSupported(CompletionProposal proposal) { switch (proposal.getKind()) { case CompletionProposal.METHOD_REF: return true; case CompletionProposal.METHOD_REF_WITH_CASTED_RECEIVER: return true; case CompletionProposal.METHOD_DECLARATION: return true; case CompletionProposal.CONSTRUCTOR_INVOCATION: return true; default: return false; } } private static boolean isArrayCloneMethod(CompletionProposal proposal) { if (proposal.isConstructor()) { // Not a method proposal return false; } char[] declarationSignature = proposal.getDeclarationSignature(); if (declarationSignature[0] != '[') { // Not an array return false; } if (!CharOperation.equals(TypeConstants.CLONE, proposal.getName())) { // Not named clone return false; } char[] signature = proposal.getSignature(); if (signature.length != declarationSignature.length + 2 || signature[0] != '(' || signature[1] != ')') { // Overload of real (no-args) clone method return false; } if (!CharOperation.endsWith(signature, declarationSignature)) { // Wrong return type return false; } return true; } private static char[] getSignature(CompletionProposal proposal) { char[] signature = null; if (canUseReflection(proposal)) { try { signature = (char[]) ORIGINAL_SIGNATURE.get(proposal); } catch (IllegalArgumentException | IllegalAccessException e) { log(LOG_WARNING_FAILED_TO_ACCESS_FIELD_REFLECTIVELY, e, ORIGINAL_SIGNATURE, ORIGINAL_SIGNATURE.getDeclaringClass()); } } return signature != null ? signature : proposal.getSignature(); } private static boolean canUseReflection(CompletionProposal proposal) { return proposal instanceof InternalCompletionProposal && ORIGINAL_SIGNATURE != null && ORIGINAL_SIGNATURE.isAccessible(); } }