View Javadoc
1   /*
2    * Copyright (c) 2006 JMockit developers
3    * This file is subject to the terms of the MIT license (see LICENSE.txt).
4    */
5   package mockit.internal.faking;
6   
7   import static java.lang.reflect.Modifier.isNative;
8   
9   import static mockit.internal.util.ObjectMethods.isMethodFromObject;
10  
11  import edu.umd.cs.findbugs.annotations.NonNull;
12  import edu.umd.cs.findbugs.annotations.Nullable;
13  
14  import java.lang.annotation.Annotation;
15  import java.lang.reflect.Method;
16  import java.lang.reflect.Modifier;
17  import java.lang.reflect.Type;
18  import java.util.ArrayList;
19  import java.util.List;
20  
21  import mockit.internal.ClassLoadingBridge;
22  import mockit.internal.reflection.GenericTypeReflection;
23  import mockit.internal.reflection.GenericTypeReflection.GenericSignature;
24  import mockit.internal.state.TestRun;
25  import mockit.internal.util.TypeDescriptor;
26  import mockit.internal.util.Utilities;
27  
28  /**
29   * A container for the fake methods "collected" from a fake class.
30   */
31  final class FakeMethods {
32      @NonNull
33      private final Class<?> realClass;
34      private final boolean targetTypeIsAClass;
35      private final boolean reentrantRealClass;
36      @NonNull
37      private final List<FakeMethod> methods;
38      @Nullable
39      private FakeMethod adviceMethod;
40      @NonNull
41      private final GenericTypeReflection typeParametersToTypeArguments;
42      @NonNull
43      private String fakeClassInternalName;
44      @Nullable
45      private List<FakeState> fakeStates;
46  
47      final class FakeMethod {
48          private final int access;
49          @NonNull
50          final String name;
51          @NonNull
52          final String desc;
53          private final boolean isByNameOnly;
54          final boolean isAdvice;
55          @NonNull
56          final String fakeDescWithoutInvocationParameter;
57          private boolean hasMatchingRealMethod;
58          @Nullable
59          private GenericSignature fakeSignature;
60          private int indexForFakeState;
61          private boolean nativeRealMethod;
62  
63          private FakeMethod(int access, @NonNull String name, @NonNull String desc) {
64              this.access = access;
65              this.name = name;
66              this.desc = desc;
67  
68              if (desc.contains("Lmockit/Invocation;")) {
69                  fakeDescWithoutInvocationParameter = '(' + desc.substring(20);
70                  isByNameOnly = name.charAt(0) != '$' && fakeDescWithoutInvocationParameter.startsWith("()");
71                  isAdvice = "$advice".equals(name) && "()Ljava/lang/Object;".equals(fakeDescWithoutInvocationParameter);
72              } else {
73                  fakeDescWithoutInvocationParameter = desc;
74                  isByNameOnly = false;
75                  isAdvice = false;
76              }
77  
78              hasMatchingRealMethod = false;
79              indexForFakeState = -1;
80          }
81  
82          @SuppressWarnings("StringEquality")
83          boolean hasInvocationParameter() {
84              return desc != fakeDescWithoutInvocationParameter;
85          }
86  
87          boolean hasInvocationParameterOnly() {
88              return isByNameOnly || isAdvice;
89          }
90  
91          boolean isMatch(int realAccess, @NonNull String realName, @NonNull String realDesc,
92                  @Nullable String signature) {
93              if (name.equals(realName) && hasMatchingParameters(realDesc, signature)) {
94                  hasMatchingRealMethod = true;
95                  nativeRealMethod = isNative(realAccess);
96                  return true;
97              }
98  
99              return false;
100         }
101 
102         private boolean hasMatchingParameters(@NonNull String methodDesc, @Nullable String signature) {
103             boolean sameParametersIgnoringGenerics = fakeDescWithoutInvocationParameter.equals(methodDesc);
104 
105             if (sameParametersIgnoringGenerics || signature == null) {
106                 return sameParametersIgnoringGenerics;
107             }
108 
109             if (fakeSignature == null) {
110                 fakeSignature = typeParametersToTypeArguments.parseSignature(fakeDescWithoutInvocationParameter);
111             }
112 
113             return fakeSignature.satisfiesGenericSignature(signature);
114         }
115 
116         boolean isMatchByName(@NonNull String realName) {
117             return isByNameOnly && name.equals(realName);
118         }
119 
120         @NonNull
121         Class<?> getRealClass() {
122             return realClass;
123         }
124 
125         int getIndexForFakeState() {
126             return indexForFakeState;
127         }
128 
129         boolean isStatic() {
130             return Modifier.isStatic(access);
131         }
132 
133         boolean isPublic() {
134             return Modifier.isPublic(access);
135         }
136 
137         boolean isForGenericMethod() {
138             return fakeSignature != null;
139         }
140 
141         boolean isForNativeMethod() {
142             return nativeRealMethod;
143         }
144 
145         boolean requiresFakeState() {
146             return hasInvocationParameter() || reentrantRealClass;
147         }
148 
149         boolean canBeReentered() {
150             return targetTypeIsAClass && !nativeRealMethod;
151         }
152     }
153 
154     FakeMethods(@NonNull Class<?> realClass, @Nullable Type targetType) {
155         this.realClass = realClass;
156 
157         if (targetType == null || realClass == targetType) {
158             targetTypeIsAClass = true;
159         } else {
160             Class<?> targetClass = Utilities.getClassType(targetType);
161             targetTypeIsAClass = !targetClass.isInterface();
162         }
163 
164         reentrantRealClass = targetTypeIsAClass
165                 && ClassLoadingBridge.instanceOfClassThatParticipatesInClassLoading(realClass);
166         methods = new ArrayList<>();
167         typeParametersToTypeArguments = new GenericTypeReflection(realClass, targetType);
168         fakeClassInternalName = "";
169     }
170 
171     @NonNull
172     Class<?> getRealClass() {
173         return realClass;
174     }
175 
176     @Nullable
177     FakeMethod addMethod(boolean fromSuperClass, int access, @NonNull String name, @NonNull String desc) {
178         if (fromSuperClass && isMethodAlreadyAdded(name, desc)) {
179             return null;
180         }
181 
182         FakeMethod fakeMethod = new FakeMethod(access, name, desc);
183 
184         if (fakeMethod.isAdvice) {
185             adviceMethod = fakeMethod;
186         } else {
187             methods.add(fakeMethod);
188         }
189 
190         return fakeMethod;
191     }
192 
193     private boolean isMethodAlreadyAdded(@NonNull String name, @NonNull String desc) {
194         int p = desc.lastIndexOf(')');
195         String params = desc.substring(0, p + 1);
196 
197         for (FakeMethod fakeMethod : methods) {
198             if (fakeMethod.name.equals(name) && fakeMethod.desc.startsWith(params)) {
199                 return true;
200             }
201         }
202 
203         return false;
204     }
205 
206     void addFakeState(@NonNull FakeState fakeState) {
207         if (fakeStates == null) {
208             fakeStates = new ArrayList<>(4);
209         }
210 
211         fakeState.fakeMethod.indexForFakeState = fakeStates.size();
212         fakeStates.add(fakeState);
213     }
214 
215     /**
216      * Finds a fake method with the same signature of a given real method, if previously collected from the fake class.
217      * Also handles "match-by-name-only" fake methods with only the <code>Invocation</code> parameter, and the
218      * <code>$advice</code> fake method.
219      * <p>
220      * This operation can be performed only once for any given fake method in this container, so that after the last
221      * real method is processed there should be no fake methods left unused in the container.
222      */
223     @Nullable
224     FakeMethod findMethod(int access, @NonNull String name, @NonNull String desc, @Nullable String signature) {
225         FakeMethod fakeMethodMatchingByNameOnly = null;
226 
227         for (FakeMethod fakeMethod : methods) {
228             if (fakeMethod.isMatch(access, name, desc, signature)) {
229                 // Mocking native methods with IntrinsicCandidate annotations will cause the VM to terminate illegally.
230                 if (isNative(access) && hasIntrinsicCandidateAnnotation(getRealClass(), name, desc)) {
231                     throw new UnsupportedOperationException(
232                             "Native methods annotated with IntrinsicCandidate cannot be mocked: "
233                                     + getRealClass().getSimpleName() + "#" + name);
234                 }
235                 return fakeMethod;
236             }
237 
238             if (fakeMethod.isMatchByName(name)) {
239                 fakeMethodMatchingByNameOnly = fakeMethod;
240             }
241         }
242 
243         if (fakeMethodMatchingByNameOnly != null) {
244             return fakeMethodMatchingByNameOnly;
245         }
246 
247         if (adviceMethod != null && !isNative(access) && !isConstructorOrClassInitialization(name)
248                 && !isMethodFromObject(name, desc)) {
249             return adviceMethod;
250         }
251 
252         return null;
253     }
254 
255     private boolean hasIntrinsicCandidateAnnotation(Class<?> clazz, String methodName, String methodDescriptor) {
256         Class<?>[] parameterTypes = TypeDescriptor.getParameterTypes(methodDescriptor);
257 
258         try {
259             // All access modifiers
260             Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
261             Annotation[] annotations = method.getAnnotations();
262 
263             for (Annotation annotation : annotations) {
264                 String annotationName = annotation.annotationType().getSimpleName();
265                 // JDK11: jdk.internal.HotSpotIntrinsicCandidate
266                 // JDK17, 21: jdk.internal.vm.annotation.IntrinsicCandidate
267                 if (annotationName.contains("IntrinsicCandidate")) {
268                     return true;
269                 }
270             }
271         } catch (NoSuchMethodException e) {
272             return false;
273         }
274         return false;
275     }
276 
277     private static boolean isConstructorOrClassInitialization(@NonNull String memberName) {
278         return "$init".equals(memberName) || "$clinit".equals(memberName);
279     }
280 
281     @NonNull
282     String getFakeClassInternalName() {
283         return fakeClassInternalName;
284     }
285 
286     void setFakeClassInternalName(@NonNull String fakeClassInternalName) {
287         this.fakeClassInternalName = fakeClassInternalName.intern();
288     }
289 
290     boolean hasUnusedFakes() {
291         if (adviceMethod != null) {
292             return true;
293         }
294 
295         for (FakeMethod method : methods) {
296             if (!method.hasMatchingRealMethod) {
297                 return true;
298             }
299         }
300 
301         return false;
302     }
303 
304     void registerFakeStates(@NonNull Object fake, boolean forStartupFake) {
305         if (fakeStates != null) {
306             FakeStates allFakeStates = TestRun.getFakeStates();
307 
308             if (forStartupFake) {
309                 allFakeStates.addStartupFakeAndItsFakeStates(fake, fakeStates);
310             } else {
311                 allFakeStates.addFakeAndItsFakeStates(fake, fakeStates);
312             }
313         }
314     }
315 }