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