| /******************************************************************************* |
| * Copyright (c) 2000, 2016 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| |
| package org.eclipse.jdt.internal.corext.util; |
| |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import org.eclipse.core.runtime.Assert; |
| |
| import org.eclipse.jdt.core.Flags; |
| import org.eclipse.jdt.core.IJavaElement; |
| import org.eclipse.jdt.core.IMember; |
| import org.eclipse.jdt.core.IMethod; |
| import org.eclipse.jdt.core.IType; |
| import org.eclipse.jdt.core.ITypeHierarchy; |
| import org.eclipse.jdt.core.ITypeParameter; |
| import org.eclipse.jdt.core.JavaModelException; |
| import org.eclipse.jdt.core.Signature; |
| |
| |
| /** |
| * Finds overriding and overridden methods based on the Java model. |
| */ |
| // @see JDTUIHelperClasses |
| public class MethodOverrideTester { |
| private static class Substitutions { |
| |
| public static final Substitutions EMPTY_SUBST= new Substitutions(); |
| |
| private HashMap<String, String[]> fMap; |
| |
| public Substitutions() { |
| fMap= null; |
| } |
| |
| public void addSubstitution(String typeVariable, String substitution, String erasure) { |
| if (fMap == null) { |
| fMap= new HashMap<>(3); |
| } |
| fMap.put(typeVariable, new String[] { substitution, erasure }); |
| } |
| |
| private String[] getSubstArray(String typeVariable) { |
| if (fMap != null) { |
| return fMap.get(typeVariable); |
| } |
| return null; |
| } |
| |
| public String getSubstitution(String typeVariable) { |
| String[] subst= getSubstArray(typeVariable); |
| if (subst != null) { |
| return subst[0]; |
| } |
| return null; |
| } |
| |
| public String getErasure(String typeVariable) { |
| String[] subst= getSubstArray(typeVariable); |
| if (subst != null) { |
| return subst[1]; |
| } |
| return null; |
| } |
| } |
| |
| private final IType fFocusType; |
| private final ITypeHierarchy fHierarchy; |
| |
| private Map <IMethod, Substitutions> fMethodSubstitutions; |
| private Map<IType, Substitutions> fTypeVariableSubstitutions; |
| |
| public MethodOverrideTester(IType focusType, ITypeHierarchy hierarchy) { |
| if (focusType == null || hierarchy == null) { |
| throw new IllegalArgumentException(); |
| } |
| fFocusType= focusType; |
| fHierarchy= hierarchy; |
| fTypeVariableSubstitutions= null; |
| fMethodSubstitutions= null; |
| } |
| |
| public IType getFocusType() { |
| return fFocusType; |
| } |
| |
| public ITypeHierarchy getTypeHierarchy() { |
| return fHierarchy; |
| } |
| |
| /** |
| * Finds the method that declares the given method. A declaring method is the 'original' method declaration that does |
| * not override nor implement a method. <code>null</code> is returned it the given method does not override |
| * a method. When searching, super class are examined before implemented interfaces. |
| * @param overriding the overriding method |
| * @param testVisibility If true the result is tested on visibility. Null is returned if the method is not visible. |
| * @return the declaring method, or <code>null</code> |
| * @throws JavaModelException if a problem occurs |
| */ |
| public IMethod findDeclaringMethod(IMethod overriding, boolean testVisibility) throws JavaModelException { |
| IMethod result= null; |
| IMethod overridden= findOverriddenMethod(overriding, testVisibility); |
| while (overridden != null) { |
| result= overridden; |
| overridden= findOverriddenMethod(result, testVisibility); |
| } |
| return result; |
| } |
| |
| /** |
| * Finds the method that is overridden by the given method. |
| * First the super class is examined and then the implemented interfaces. |
| * @param overriding the overriding method |
| * @param testVisibility If true the result is tested on visibility. Null is returned if the method is not visible. |
| * @return a method that is directly overridden by the given method, or <code>null</code> |
| * @throws JavaModelException if a problem occurs |
| */ |
| public IMethod findOverriddenMethod(IMethod overriding, boolean testVisibility) throws JavaModelException { |
| int flags= overriding.getFlags(); |
| if (Flags.isPrivate(flags) || Flags.isStatic(flags) || overriding.isConstructor()) { |
| return null; |
| } |
| |
| IType type= overriding.getDeclaringType(); |
| IType superClass= fHierarchy.getSuperclass(type); |
| if (superClass != null) { |
| IMethod res= findOverriddenMethodInHierarchy(superClass, overriding); |
| if (res != null) { |
| if (!testVisibility || JavaModelUtil.isVisibleInHierarchy(res, type.getPackageFragment())) { |
| return res; |
| } |
| } |
| } |
| for (IType intf : fHierarchy.getSuperInterfaces(type)) { |
| IMethod res= findOverriddenMethodInHierarchy(intf, overriding); |
| if (res != null) { |
| return res; // methods from interfaces are always public and therefore visible |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Finds the directly overridden method in a type and its super types. First the super class is examined and then the implemented interfaces. |
| * With generics it is possible that 2 methods in the same type are overidden at the same time. In that case, the first overridden method found is returned. |
| * @param type The type to find methods in |
| * @param overriding The overriding method |
| * @return The first overridden method or <code>null</code> if no method is overridden |
| * @throws JavaModelException if a problem occurs |
| */ |
| public IMethod findOverriddenMethodInHierarchy(IType type, IMethod overriding) throws JavaModelException { |
| IMethod method= findOverriddenMethodInType(type, overriding); |
| if (method != null) { |
| return method; |
| } |
| IType superClass= fHierarchy.getSuperclass(type); |
| if (superClass != null) { |
| IMethod res= findOverriddenMethodInHierarchy(superClass, overriding); |
| if (res != null) { |
| return res; |
| } |
| } |
| for (IType superInterface : fHierarchy.getSuperInterfaces(type)) { |
| IMethod res= findOverriddenMethodInHierarchy(superInterface, overriding); |
| if (res != null) { |
| return res; |
| } |
| } |
| return method; |
| } |
| |
| /** |
| * Finds an overridden method in a type. With generics it is possible that 2 methods in the same type are overridden at the same time. |
| * In that case the first overridden method found is returned. |
| * @param overriddenType The type to find methods in |
| * @param overriding The overriding method |
| * @return The first overridden method or <code>null</code> if no method is overridden |
| * @throws JavaModelException if a problem occurs |
| */ |
| public IMethod findOverriddenMethodInType(IType overriddenType, IMethod overriding) throws JavaModelException { |
| int flags= overriding.getFlags(); |
| if (Flags.isPrivate(flags) || Flags.isStatic(flags) || overriding.isConstructor()) |
| return null; |
| for (IMethod overridden : overriddenType.getMethods()) { |
| flags= overridden.getFlags(); |
| if (Flags.isPrivate(flags) || Flags.isStatic(flags) || overridden.isConstructor()) |
| continue; |
| if (isSubsignature(overriding, overridden)) { |
| return overridden; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Finds an overriding method in a type. |
| * @param overridingType The type to find methods in |
| * @param overridden The overridden method |
| * @return The overriding method or <code>null</code> if no method is overriding. |
| * @throws JavaModelException if a problem occurs |
| */ |
| public IMethod findOverridingMethodInType(IType overridingType, IMethod overridden) throws JavaModelException { |
| int flags= overridden.getFlags(); |
| if (Flags.isPrivate(flags) || Flags.isStatic(flags) || overridden.isConstructor()) |
| return null; |
| for (IMethod overriding : overridingType.getMethods()) { |
| flags= overriding.getFlags(); |
| if (Flags.isPrivate(flags) || Flags.isStatic(flags) || overriding.isConstructor()) |
| continue; |
| if (isSubsignature(overriding, overridden)) { |
| return overriding; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Tests if a method is a subsignature of another method. |
| * @param overriding overriding method (m1) |
| * @param overridden overridden method (m2) |
| * @return <code>true</code> iff the method <code>m1</code> is a subsignature of the method <code>m2</code>. |
| * This is one of the requirements for m1 to override m2. |
| * Accessibility and return types are not taken into account. |
| * Note that subsignature is <em>not</em> symmetric! |
| * @throws JavaModelException if a problem occurs |
| */ |
| public boolean isSubsignature(IMethod overriding, IMethod overridden) throws JavaModelException { |
| if (!overridden.getElementName().equals(overriding.getElementName())) { |
| return false; |
| } |
| int nParameters= overridden.getNumberOfParameters(); |
| if (nParameters != overriding.getNumberOfParameters()) { |
| return false; |
| } |
| |
| if (!hasCompatibleTypeParameters(overriding, overridden)) { |
| return false; |
| } |
| |
| return nParameters == 0 || hasCompatibleParameterTypes(overriding, overridden); |
| } |
| |
| private boolean hasCompatibleTypeParameters(IMethod overriding, IMethod overridden) throws JavaModelException { |
| ITypeParameter[] overriddenTypeParameters= overridden.getTypeParameters(); |
| ITypeParameter[] overridingTypeParameters= overriding.getTypeParameters(); |
| int nOverridingTypeParameters= overridingTypeParameters.length; |
| if (overriddenTypeParameters.length != nOverridingTypeParameters) { |
| return nOverridingTypeParameters == 0; |
| } |
| Substitutions overriddenSubst= getMethodSubstitions(overridden); |
| Substitutions overridingSubst= getMethodSubstitions(overriding); |
| for (int i= 0; i < nOverridingTypeParameters; i++) { |
| String erasure1= overriddenSubst.getErasure(overriddenTypeParameters[i].getElementName()); |
| String erasure2= overridingSubst.getErasure(overridingTypeParameters[i].getElementName()); |
| if (erasure1 == null || !erasure1.equals(erasure2)) { |
| return false; |
| } |
| // comparing only the erasure is not really correct: Need to compare all bounds, that can be in different order |
| int nBounds= overriddenTypeParameters[i].getBounds().length; |
| if (nBounds > 1 && nBounds != overridingTypeParameters[i].getBounds().length) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private boolean hasCompatibleParameterTypes(IMethod overriding, IMethod overridden) throws JavaModelException { |
| String[] overriddenParamTypes= overridden.getParameterTypes(); |
| String[] overridingParamTypes= overriding.getParameterTypes(); |
| |
| String[] substitutedOverriding= new String[overridingParamTypes.length]; |
| boolean testErasure= false; |
| |
| for (int i= 0; i < overridingParamTypes.length; i++) { |
| String overriddenParamSig= overriddenParamTypes[i]; |
| String overriddenParamName= getSubstitutedTypeName(overriddenParamSig, overridden); |
| String overridingParamName= getSubstitutedTypeName(overridingParamTypes[i], overriding); |
| substitutedOverriding[i]= overridingParamName; |
| if (!overriddenParamName.equals(overridingParamName)) { |
| testErasure= true; |
| break; |
| } |
| } |
| if (testErasure) { |
| for (int i= 0; i < overridingParamTypes.length; i++) { |
| String overriddenParamSig= overriddenParamTypes[i]; |
| String overriddenParamName= getErasedTypeName(overriddenParamSig, overridden); |
| String overridingParamName= substitutedOverriding[i]; |
| if (overridingParamName == null) |
| overridingParamName= getSubstitutedTypeName(overridingParamTypes[i], overriding); |
| if (!overriddenParamName.equals(overridingParamName)) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| private String getVariableSubstitution(IMember context, String variableName) throws JavaModelException { |
| IType type; |
| if (context instanceof IMethod) { |
| String subst= getMethodSubstitions((IMethod) context).getSubstitution(variableName); |
| if (subst != null) { |
| return subst; |
| } |
| type= context.getDeclaringType(); |
| } else { |
| type= (IType) context; |
| } |
| String subst= getTypeSubstitions(type).getSubstitution(variableName); |
| if (subst != null) { |
| return subst; |
| } |
| IJavaElement parent= type.getParent(); |
| if (parent instanceof IMethod) { |
| return getVariableSubstitution((IMethod) parent, variableName); |
| } else if (type.getDeclaringType() != null) { |
| return getVariableSubstitution(type.getDeclaringType(), variableName); |
| } |
| return variableName; // not a type variable |
| } |
| |
| private String getVariableErasure(IMember context, String variableName) throws JavaModelException { |
| IType type; |
| if (context instanceof IMethod) { |
| String subst= getMethodSubstitions((IMethod) context).getErasure(variableName); |
| if (subst != null) { |
| return subst; |
| } |
| type= context.getDeclaringType(); |
| } else { |
| type= (IType) context; |
| } |
| String subst= getTypeSubstitions(type).getErasure(variableName); |
| if (subst != null) { |
| return subst; |
| } |
| IJavaElement parent= type.getParent(); |
| if (parent instanceof IMethod) { |
| return getVariableErasure((IMethod) parent, variableName); |
| } else if (type.getDeclaringType() != null) { |
| return getVariableErasure(type.getDeclaringType(), variableName); |
| } |
| return variableName; // not a type variable |
| } |
| |
| /* |
| * Returns the substitutions for a method's type parameters |
| */ |
| private Substitutions getMethodSubstitions(IMethod method) throws JavaModelException { |
| if (fMethodSubstitutions == null) { |
| fMethodSubstitutions= new LRUMap<>(3); |
| } |
| |
| Substitutions s= fMethodSubstitutions.get(method); |
| if (s == null) { |
| ITypeParameter[] typeParameters= method.getTypeParameters(); |
| if (typeParameters.length == 0) { |
| s= Substitutions.EMPTY_SUBST; |
| } else { |
| IType instantiatedType= method.getDeclaringType(); |
| s= new Substitutions(); |
| for (int i= 0; i < typeParameters.length; i++) { |
| ITypeParameter curr= typeParameters[i]; |
| s.addSubstitution(curr.getElementName(), '+' + String.valueOf(i), getTypeParameterErasure(curr, instantiatedType)); |
| } |
| } |
| fMethodSubstitutions.put(method, s); |
| } |
| return s; |
| } |
| |
| /* |
| * Returns the substitutions for a type's type parameters |
| */ |
| private Substitutions getTypeSubstitions(IType type) throws JavaModelException { |
| if (fTypeVariableSubstitutions == null) { |
| fTypeVariableSubstitutions= new HashMap<>(); |
| computeSubstitutions(fFocusType, null, null); |
| } |
| Substitutions subst= fTypeVariableSubstitutions.get(type); |
| if (subst == null) { |
| return Substitutions.EMPTY_SUBST; |
| } |
| return subst; |
| } |
| |
| private void computeSubstitutions(IType instantiatedType, IType instantiatingType, String[] typeArguments) throws JavaModelException { |
| Substitutions s= new Substitutions(); |
| fTypeVariableSubstitutions.put(instantiatedType, s); |
| |
| ITypeParameter[] typeParameters= instantiatedType.getTypeParameters(); |
| |
| if (instantiatingType == null) { // the focus type |
| for (ITypeParameter curr : typeParameters) { |
| // use star to make type variables different from type refs |
| s.addSubstitution(curr.getElementName(), '*' + curr.getElementName(), getTypeParameterErasure(curr, instantiatedType)); |
| } |
| } else { |
| if (typeParameters.length == typeArguments.length) { |
| for (int i= 0; i < typeParameters.length; i++) { |
| ITypeParameter curr= typeParameters[i]; |
| String substString= getSubstitutedTypeName(typeArguments[i], instantiatingType); // substitute in the context of the instantiatingType |
| String erasure= getErasedTypeName(typeArguments[i], instantiatingType); // get the erasure from the type argument |
| s.addSubstitution(curr.getElementName(), substString, erasure); |
| } |
| } else if (typeArguments.length == 0) { // raw type reference |
| for (ITypeParameter curr : typeParameters) { |
| String erasure= getTypeParameterErasure(curr, instantiatedType); |
| s.addSubstitution(curr.getElementName(), erasure, erasure); |
| } |
| } else { |
| // code with errors |
| } |
| } |
| String superclassTypeSignature= instantiatedType.getSuperclassTypeSignature(); |
| if (superclassTypeSignature != null) { |
| String[] superTypeArguments= Signature.getTypeArguments(superclassTypeSignature); |
| IType superclass= fHierarchy.getSuperclass(instantiatedType); |
| if (superclass != null && !fTypeVariableSubstitutions.containsKey(superclass)) { |
| computeSubstitutions(superclass, instantiatedType, superTypeArguments); |
| } |
| } |
| String[] superInterfacesTypeSignature; |
| if (instantiatedType.isAnonymous()) { |
| // special case: superinterface is also returned by IType#getSuperclassTypeSignature() |
| superInterfacesTypeSignature= new String[] { superclassTypeSignature }; |
| } else { |
| superInterfacesTypeSignature= instantiatedType.getSuperInterfaceTypeSignatures(); |
| } |
| int nInterfaces= superInterfacesTypeSignature.length; |
| if (nInterfaces > 0) { |
| IType[] superInterfaces= fHierarchy.getSuperInterfaces(instantiatedType); |
| if (superInterfaces.length == nInterfaces) { |
| for (int i= 0; i < nInterfaces; i++) { |
| String[] superTypeArguments= Signature.getTypeArguments(superInterfacesTypeSignature[i]); |
| IType superInterface= superInterfaces[i]; |
| if (!fTypeVariableSubstitutions.containsKey(superInterface)) { |
| computeSubstitutions(superInterface, instantiatedType, superTypeArguments); |
| } |
| } |
| } |
| } |
| } |
| |
| private String getTypeParameterErasure(ITypeParameter typeParameter, IType context) throws JavaModelException { |
| String[] bounds= typeParameter.getBounds(); |
| if (bounds.length > 0) { |
| return getErasedTypeName(Signature.createTypeSignature(bounds[0], false), context); |
| } |
| return "Object"; //$NON-NLS-1$ |
| } |
| |
| |
| /** |
| * Translates the type signature to a 'normalized' type name where all variables are substituted for the given type or method context. |
| * The returned name contains only simple names and can be used to compare against other substituted type names |
| * @param typeSig The type signature to translate |
| * @param context The context for the substitution |
| * @return a type name |
| * @throws JavaModelException if a problem occurs |
| */ |
| private String getSubstitutedTypeName(String typeSig, IMember context) throws JavaModelException { |
| return internalGetSubstitutedTypeName(typeSig, context, false, new StringBuffer()).toString(); |
| } |
| |
| private String getErasedTypeName(String typeSig, IMember context) throws JavaModelException { |
| return internalGetSubstitutedTypeName(typeSig, context, true, new StringBuffer()).toString(); |
| } |
| |
| private StringBuffer internalGetSubstitutedTypeName(String typeSig, IMember context, boolean erasure, StringBuffer buf) throws JavaModelException { |
| int sigKind= Signature.getTypeSignatureKind(typeSig); |
| switch (sigKind) { |
| case Signature.BASE_TYPE_SIGNATURE: |
| return buf.append(Signature.toString(typeSig)); |
| case Signature.ARRAY_TYPE_SIGNATURE: |
| internalGetSubstitutedTypeName(Signature.getElementType(typeSig), context, erasure, buf); |
| for (int i= Signature.getArrayCount(typeSig); i > 0; i--) { |
| buf.append('[').append(']'); |
| } |
| return buf; |
| case Signature.CLASS_TYPE_SIGNATURE: { |
| String erasureSig= Signature.getTypeErasure(typeSig); |
| String erasureName= Signature.getSimpleName(Signature.toString(erasureSig)); |
| |
| char ch= erasureSig.charAt(0); |
| if (ch == Signature.C_RESOLVED) { |
| buf.append(erasureName); |
| } else if (ch == Signature.C_UNRESOLVED) { // could be a type variable |
| if (erasure) { |
| buf.append(getVariableErasure(context, erasureName)); |
| } else { |
| buf.append(getVariableSubstitution(context, erasureName)); |
| } |
| } else { |
| Assert.isTrue(false, "Unknown class type signature"); //$NON-NLS-1$ |
| } |
| if (!erasure) { |
| String[] typeArguments= Signature.getTypeArguments(typeSig); |
| if (typeArguments.length > 0) { |
| buf.append('<'); |
| for (int i= 0; i < typeArguments.length; i++) { |
| if (i > 0) { |
| buf.append(','); |
| } |
| internalGetSubstitutedTypeName(typeArguments[i], context, erasure, buf); |
| } |
| buf.append('>'); |
| } |
| } |
| return buf; |
| } |
| case Signature.TYPE_VARIABLE_SIGNATURE: |
| String varName= Signature.toString(typeSig); |
| if (erasure) { |
| return buf.append(getVariableErasure(context, varName)); |
| } else { |
| return buf.append(getVariableSubstitution(context, varName)); |
| } |
| case Signature.WILDCARD_TYPE_SIGNATURE: { |
| buf.append('?'); |
| char ch= typeSig.charAt(0); |
| if (ch == Signature.C_STAR) { |
| return buf; |
| } else if (ch == Signature.C_EXTENDS) { |
| buf.append(" extends "); //$NON-NLS-1$ |
| } else { |
| buf.append(" super "); //$NON-NLS-1$ |
| } |
| return internalGetSubstitutedTypeName(typeSig.substring(1), context, erasure, buf); |
| } |
| case Signature.CAPTURE_TYPE_SIGNATURE: |
| return internalGetSubstitutedTypeName(typeSig.substring(1), context, erasure, buf); |
| default: |
| Assert.isTrue(false, "Unhandled type signature kind"); //$NON-NLS-1$ |
| return buf; |
| } |
| } |
| |
| } |