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 mockit.asm.jvmConstants.Access.ABSTRACT;
9   import static mockit.asm.jvmConstants.Access.BRIDGE;
10  import static mockit.asm.jvmConstants.Access.NATIVE;
11  import static mockit.asm.jvmConstants.Access.SYNTHETIC;
12  
13  import edu.umd.cs.findbugs.annotations.NonNull;
14  import edu.umd.cs.findbugs.annotations.Nullable;
15  
16  import java.lang.reflect.Method;
17  import java.util.EnumSet;
18  import java.util.List;
19  
20  import mockit.Mock;
21  import mockit.MockUp;
22  import mockit.asm.metadata.ClassMetadataReader;
23  import mockit.asm.metadata.ClassMetadataReader.Attribute;
24  import mockit.asm.metadata.ClassMetadataReader.MethodInfo;
25  import mockit.asm.types.JavaType;
26  import mockit.internal.ClassFile;
27  import mockit.internal.faking.FakeMethods.FakeMethod;
28  import mockit.internal.util.ClassLoad;
29  import mockit.internal.util.TypeDescriptor;
30  
31  /**
32   * Responsible for collecting the signatures of all methods defined in a given fake class which are explicitly annotated
33   * as {@link Mock fakes}.
34   */
35  final class FakeMethodCollector {
36      private static final int INVALID_METHOD_ACCESSES = BRIDGE + SYNTHETIC + ABSTRACT + NATIVE;
37      private static final EnumSet<Attribute> ANNOTATIONS = EnumSet.of(Attribute.Annotations);
38  
39      @NonNull
40      private final FakeMethods fakeMethods;
41      private boolean collectingFromSuperClass;
42  
43      FakeMethodCollector(@NonNull FakeMethods fakeMethods) {
44          this.fakeMethods = fakeMethods;
45      }
46  
47      void collectFakeMethods(@NonNull Class<?> fakeClass) {
48          ClassLoad.registerLoadedClass(fakeClass);
49          fakeMethods.setFakeClassInternalName(JavaType.getInternalName(fakeClass));
50  
51          Class<?> classToCollectFakesFrom = fakeClass;
52  
53          do {
54              byte[] classfileBytes = ClassFile.readBytesFromClassFile(classToCollectFakesFrom);
55              ClassMetadataReader cmr = new ClassMetadataReader(classfileBytes, ANNOTATIONS);
56              List<MethodInfo> methods = cmr.getMethods();
57              addFakeMethods(classToCollectFakesFrom, methods);
58  
59              classToCollectFakesFrom = classToCollectFakesFrom.getSuperclass();
60              collectingFromSuperClass = true;
61          } while (classToCollectFakesFrom != MockUp.class);
62      }
63  
64      private void addFakeMethods(@NonNull Class<?> fakeClass, @NonNull List<MethodInfo> methods) {
65          for (MethodInfo method : methods) {
66              int access = method.accessFlags;
67  
68              if ((access & INVALID_METHOD_ACCESSES) == 0 && method.isMethod() && method.hasAnnotation("Lmockit/Mock;")) {
69                  FakeMethod fakeMethod = fakeMethods.addMethod(collectingFromSuperClass, access, method.name,
70                          method.desc);
71  
72                  if (fakeMethod != null) {
73                      FakeState fakeState = createFakeStateIfRequired(fakeMethod);
74                      applyInvocationConstraintsIfAny(fakeClass, method, fakeMethod, fakeState);
75                  }
76              }
77          }
78      }
79  
80      @Nullable
81      private FakeState createFakeStateIfRequired(@NonNull FakeMethod fakeMethod) {
82          if (!fakeMethod.requiresFakeState()) {
83              return null;
84          }
85  
86          FakeState fakeState = new FakeState(fakeMethod);
87          fakeMethods.addFakeState(fakeState);
88          return fakeState;
89      }
90  
91      private void applyInvocationConstraintsIfAny(@NonNull Class<?> fakeClass, @NonNull MethodInfo methodInfo,
92              @NonNull FakeMethod fakeMethod, @Nullable FakeState existingFakeState) {
93          Method javaMethod = findJavaMethod(fakeClass, methodInfo);
94          Mock annotation = javaMethod.getAnnotation(Mock.class);
95  
96          if (annotation == null) {
97              return;
98          }
99  
100         int expectedInvocations = annotation.invocations();
101         int minInvocations = annotation.minInvocations();
102         int maxInvocations = annotation.maxInvocations();
103 
104         boolean hasConstraints = expectedInvocations >= 0 || minInvocations > 0 || maxInvocations >= 0;
105 
106         if (!hasConstraints) {
107             return;
108         }
109 
110         FakeState fakeState = existingFakeState;
111 
112         if (fakeState == null) {
113             fakeState = new FakeState(fakeMethod);
114         }
115 
116         if (expectedInvocations >= 0) {
117             fakeState.setExpectedInvocations(expectedInvocations);
118         }
119 
120         if (minInvocations > 0) {
121             fakeState.setMinExpectedInvocations(minInvocations);
122         }
123 
124         if (maxInvocations >= 0) {
125             fakeState.setMaxExpectedInvocations(maxInvocations);
126         }
127 
128         if (existingFakeState == null) {
129             fakeMethods.addFakeState(fakeState);
130         }
131     }
132 
133     @NonNull
134     private Method findJavaMethod(@NonNull Class<?> fakeClass, @NonNull MethodInfo methodInfo) {
135         Class<?>[] parameterTypes = TypeDescriptor.getParameterTypes(methodInfo.desc);
136 
137         try {
138             Method method = fakeClass.getDeclaredMethod(methodInfo.name, parameterTypes);
139 
140             try {
141                 method.setAccessible(true);
142             } catch (SecurityException ignore) {
143                 // Best-effort only; access will still work for public/protected members.
144             }
145 
146             return method;
147         } catch (NoSuchMethodException e) {
148             throw new IllegalStateException(
149                     "Unable to resolve @Mock method " + fakeClass.getName() + '#' + methodInfo.name, e);
150         }
151     }
152 }