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.expectations.mocking;
7   
8   import static java.lang.reflect.Modifier.ABSTRACT;
9   import static java.lang.reflect.Modifier.FINAL;
10  import static java.lang.reflect.Modifier.PRIVATE;
11  import static java.lang.reflect.Modifier.PROTECTED;
12  import static java.lang.reflect.Modifier.PUBLIC;
13  import static java.lang.reflect.Modifier.STATIC;
14  import static java.lang.reflect.Modifier.isNative;
15  
16  import static mockit.asm.jvmConstants.Access.ENUM;
17  import static mockit.asm.jvmConstants.Access.SYNTHETIC;
18  import static mockit.asm.jvmConstants.Opcodes.ACONST_NULL;
19  import static mockit.asm.jvmConstants.Opcodes.DUP;
20  import static mockit.asm.jvmConstants.Opcodes.IF_ACMPEQ;
21  import static mockit.asm.jvmConstants.Opcodes.POP;
22  import static mockit.internal.expectations.MockingFilters.validateAsMockable;
23  import static mockit.internal.util.ObjectMethods.isMethodFromObject;
24  import static mockit.internal.util.Utilities.HOTSPOT_VM;
25  
26  import edu.umd.cs.findbugs.annotations.NonNull;
27  import edu.umd.cs.findbugs.annotations.Nullable;
28  
29  import java.util.ArrayList;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  
34  import mockit.asm.classes.ClassInfo;
35  import mockit.asm.classes.ClassReader;
36  import mockit.asm.controlFlow.Label;
37  import mockit.asm.methods.MethodVisitor;
38  import mockit.asm.types.JavaType;
39  import mockit.internal.BaseClassModifier;
40  import mockit.internal.expectations.ExecutionMode;
41  
42  final class MockedClassModifier extends BaseClassModifier {
43      private static final int METHOD_ACCESS_MASK = PRIVATE + SYNTHETIC + ABSTRACT;
44      private static final int PUBLIC_OR_PROTECTED = PUBLIC + PROTECTED;
45      private static final boolean NATIVE_UNSUPPORTED = !HOTSPOT_VM;
46  
47      private static final Map<String, String> FILTERS = new HashMap<>(4);
48      static {
49          FILTERS.put("java/lang/Object", "<init> clone getClass hashCode wait notify notifyAll ");
50          FILTERS.put("java/io/File", "compareTo equals getName getPath hashCode toString ");
51          FILTERS.put("java/util/logging/Logger", "<init> getName ");
52          FILTERS.put("java/util/jar/JarEntry", "<init> ");
53      }
54  
55      @Nullable
56      private final MockedType mockedType;
57      private String className;
58      private String methodSignature;
59      @Nullable
60      private String baseClassNameForCapturedInstanceMethods;
61      @NonNull
62      private ExecutionMode executionMode;
63      private boolean isProxy;
64      @Nullable
65      private String defaultFilters;
66      @Nullable
67      List<String> enumSubclasses;
68  
69      MockedClassModifier(@Nullable ClassLoader classLoader, @NonNull ClassReader classReader,
70              @Nullable MockedType typeMetadata) {
71          super(classReader);
72          mockedType = typeMetadata;
73          setUseClassLoadingBridge(classLoader);
74          executionMode = typeMetadata != null && typeMetadata.injectable ? ExecutionMode.PerInstance
75                  : ExecutionMode.Regular;
76      }
77  
78      void useDynamicMocking() {
79          executionMode = ExecutionMode.Partial;
80      }
81  
82      void setClassNameForCapturedInstanceMethods(@NonNull String internalClassName) {
83          baseClassNameForCapturedInstanceMethods = internalClassName;
84      }
85  
86      @Override
87      public void visit(int version, int access, @NonNull String name, @NonNull ClassInfo additionalInfo) {
88          validateMockingOfJREClass(name);
89  
90          super.visit(version, access, name, additionalInfo);
91          isProxy = "java/lang/reflect/Proxy".equals(additionalInfo.superName);
92  
93          if (isProxy) {
94              className = additionalInfo.interfaces[0];
95          } else {
96              className = name;
97              defaultFilters = FILTERS.get(name);
98          }
99  
100         if (baseClassNameForCapturedInstanceMethods != null) {
101             className = baseClassNameForCapturedInstanceMethods;
102         }
103     }
104 
105     private void validateMockingOfJREClass(@NonNull String internalName) {
106         if (internalName.startsWith("java/")) {
107             validateAsMockable(internalName);
108 
109             if (executionMode == ExecutionMode.Regular && mockedType != null && isFullMockingDisallowed(internalName)) {
110                 String modifyingClassName = internalName.replace('/', '.');
111 
112                 if (modifyingClassName.equals(mockedType.getClassType().getName())) {
113                     throw new IllegalArgumentException("Class " + modifyingClassName
114                             + " cannot be @Mocked fully; instead, use @Injectable or partial mocking");
115                 }
116             }
117         }
118     }
119 
120     private static boolean isFullMockingDisallowed(@NonNull String classDesc) {
121         return classDesc.startsWith("java/io/") && ("java/io/FileOutputStream".equals(classDesc)
122                 || "java/io/FileInputStream".equals(classDesc) || "java/io/FileWriter".equals(classDesc)
123                 || "java/io/PrintWriter java/io/Writer java/io/DataInputStream".contains(classDesc));
124     }
125 
126     @Override
127     public void visitInnerClass(@NonNull String name, @Nullable String outerName, @Nullable String innerName,
128             int access) {
129         cw.visitInnerClass(name, outerName, innerName, access);
130 
131         // The second condition is for classes compiled with Java 8 or older, which had a bug (as an anonymous class can
132         // never be static).
133         if (access == ENUM + FINAL || access == ENUM + STATIC) {
134             if (enumSubclasses == null) {
135                 enumSubclasses = new ArrayList<>();
136             }
137 
138             enumSubclasses.add(name);
139         }
140     }
141 
142     @Nullable
143     @Override
144     public MethodVisitor visitMethod(final int access, @NonNull final String name, @NonNull final String desc,
145             @Nullable final String signature, @Nullable String[] exceptions) {
146         if ((access & METHOD_ACCESS_MASK) != 0) {
147             return unmodifiedBytecode(access, name, desc, signature, exceptions);
148         }
149 
150         methodSignature = signature;
151 
152         if ("<init>".equals(name)) {
153             if (isConstructorNotAllowedByMockingFilters(name)) {
154                 return unmodifiedBytecode(access, name, desc, signature, exceptions);
155             }
156         } else {
157             if (stubOutFinalizeMethod(access, name, desc)) {
158                 return null;
159             }
160 
161             if ("<clinit>".equals(name)) {
162                 return stubOutClassInitializationIfApplicable(access);
163             }
164 
165             if (isMethodNotToBeMocked(access, name, desc) || isMethodNotAllowedByMockingFilters(access, name)) {
166                 return unmodifiedBytecode(access, name, desc, signature, exceptions);
167             }
168         }
169 
170         startModifiedMethodVersion(access, name, desc, signature, exceptions);
171 
172         if (isNative(methodAccess)) {
173             generateEmptyImplementation(methodDesc);
174             return methodAnnotationsVisitor;
175         }
176 
177         return copyOriginalImplementationWithInjectedInterceptionCode();
178     }
179 
180     @NonNull
181     private MethodVisitor unmodifiedBytecode(int access, @NonNull String name, @NonNull String desc,
182             @Nullable String signature, @Nullable String[] exceptions) {
183         return cw.visitMethod(access, name, desc, signature, exceptions);
184     }
185 
186     private boolean isConstructorNotAllowedByMockingFilters(@NonNull String name) {
187         return isProxy || executionMode != ExecutionMode.Regular || isUnmockableInvocation(name);
188     }
189 
190     private boolean isUnmockableInvocation(@NonNull String name) {
191         if (defaultFilters == null) {
192             return false;
193         }
194 
195         int i = defaultFilters.indexOf(name);
196         return i > -1 && defaultFilters.charAt(i + name.length()) == ' ';
197     }
198 
199     private boolean isMethodNotToBeMocked(int access, @NonNull String name, @NonNull String desc) {
200         return isNative(access) && (NATIVE_UNSUPPORTED || (access & PUBLIC_OR_PROTECTED) == 0)
201                 || (isProxy || executionMode == ExecutionMode.Partial) && (isMethodFromObject(name, desc)
202                         || "annotationType".equals(name) && "()Ljava/lang/Class;".equals(desc));
203     }
204 
205     @Nullable
206     private MethodVisitor stubOutClassInitializationIfApplicable(int access) {
207         startModifiedMethodVersion(access, "<clinit>", "()V", null, null);
208 
209         if (mockedType != null && mockedType.isClassInitializationToBeStubbedOut()) {
210             generateEmptyImplementation();
211             return null;
212         }
213 
214         return mw;
215     }
216 
217     private boolean stubOutFinalizeMethod(int access, @NonNull String name, @NonNull String desc) {
218         if ("finalize".equals(name) && "()V".equals(desc)) {
219             startModifiedMethodVersion(access, name, desc, null, null);
220             generateEmptyImplementation();
221             return true;
222         }
223 
224         return false;
225     }
226 
227     private boolean isMethodNotAllowedByMockingFilters(int access, @NonNull String name) {
228         return baseClassNameForCapturedInstanceMethods != null && (access & STATIC) != 0
229                 || executionMode.isMethodToBeIgnored(access) || isUnmockableInvocation(name);
230     }
231 
232     @Override
233     protected void generateInterceptionCode() {
234         if (useClassLoadingBridge) {
235             generateCallToHandlerThroughMockingBridge();
236         } else {
237             generateDirectCallToHandler(className, methodAccess, methodName, methodDesc, methodSignature,
238                     executionMode);
239         }
240 
241         generateDecisionBetweenReturningOrContinuingToRealImplementation();
242     }
243 
244     private void generateCallToHandlerThroughMockingBridge() {
245         generateCodeToObtainInstanceOfClassLoadingBridge(MockedBridge.MB);
246 
247         // First and second "invoke" arguments:
248         boolean isStatic = generateCodeToPassThisOrNullIfStaticMethod();
249         mw.visitInsn(ACONST_NULL);
250 
251         // Create array for call arguments (third "invoke" argument):
252         JavaType[] argTypes = JavaType.getArgumentTypes(methodDesc);
253         generateCodeToCreateArrayOfObject(6 + argTypes.length);
254 
255         int i = 0;
256         generateCodeToFillArrayElement(i, methodAccess);
257         i++;
258         generateCodeToFillArrayElement(i, className);
259         i++;
260         generateCodeToFillArrayElement(i, methodName);
261         i++;
262         generateCodeToFillArrayElement(i, methodDesc);
263         i++;
264         generateCodeToFillArrayElement(i, methodSignature);
265         i++;
266         generateCodeToFillArrayElement(i, executionMode.ordinal());
267         i++;
268 
269         generateCodeToFillArrayWithParameterValues(argTypes, i, isStatic ? 0 : 1);
270         generateCallToInvocationHandler();
271     }
272 
273     private void generateDecisionBetweenReturningOrContinuingToRealImplementation() {
274         Label startOfRealImplementation = new Label();
275         mw.visitInsn(DUP);
276         mw.visitLdcInsn(VOID_TYPE);
277         mw.visitJumpInsn(IF_ACMPEQ, startOfRealImplementation);
278         generateReturnWithObjectAtTopOfTheStack(methodDesc);
279         mw.visitLabel(startOfRealImplementation);
280         mw.visitInsn(POP);
281     }
282 }