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.coverage.modification;
7   
8   import static mockit.asm.jvmConstants.Access.ABSTRACT;
9   import static mockit.asm.jvmConstants.Access.ANNOTATION;
10  import static mockit.asm.jvmConstants.Access.ENUM;
11  import static mockit.asm.jvmConstants.Access.FINAL;
12  import static mockit.asm.jvmConstants.Access.INTERFACE;
13  import static mockit.asm.jvmConstants.Access.STATIC;
14  import static mockit.asm.jvmConstants.Access.SUPER;
15  import static mockit.asm.jvmConstants.Access.SYNTHETIC;
16  
17  import edu.umd.cs.findbugs.annotations.NonNull;
18  import edu.umd.cs.findbugs.annotations.Nullable;
19  
20  import java.util.HashMap;
21  import java.util.Map;
22  
23  import mockit.asm.classes.ClassInfo;
24  import mockit.asm.classes.ClassReader;
25  import mockit.asm.classes.ClassWriter;
26  import mockit.asm.classes.WrappingClassVisitor;
27  import mockit.asm.fields.FieldVisitor;
28  import mockit.asm.methods.MethodVisitor;
29  import mockit.asm.methods.MethodWriter;
30  import mockit.coverage.data.CoverageData;
31  import mockit.coverage.data.FileCoverageData;
32  import mockit.internal.ClassFile;
33  
34  final class CoverageModifier extends WrappingClassVisitor {
35      private static final Map<String, CoverageModifier> INNER_CLASS_MODIFIERS = new HashMap<>();
36      private static final int FIELD_MODIFIERS_TO_IGNORE = FINAL + SYNTHETIC;
37  
38      @Nullable
39      static byte[] recoverModifiedByteCodeIfAvailable(@NonNull String innerClassName) {
40          CoverageModifier modifier = INNER_CLASS_MODIFIERS.remove(innerClassName);
41          return modifier == null ? null : modifier.toByteArray();
42      }
43  
44      @Nullable
45      private String internalClassName;
46      @Nullable
47      private String simpleClassName;
48      @NonNull
49      private String sourceFileName;
50      @Nullable
51      private FileCoverageData fileData;
52      private final boolean forInnerClass;
53      private boolean forEnumClass;
54      @Nullable
55      private String kindOfTopLevelType;
56  
57      CoverageModifier(@NonNull ClassReader cr) {
58          this(cr, false);
59      }
60  
61      private CoverageModifier(@NonNull ClassReader cr, boolean forInnerClass) {
62          super(new ClassWriter(cr));
63          sourceFileName = "";
64          this.forInnerClass = forInnerClass;
65      }
66  
67      private CoverageModifier(@NonNull ClassReader cr, @NonNull CoverageModifier other,
68              @Nullable String simpleClassName) {
69          this(cr, true);
70          sourceFileName = other.sourceFileName;
71          fileData = other.fileData;
72          internalClassName = other.internalClassName;
73          this.simpleClassName = simpleClassName;
74      }
75  
76      @Override
77      public void visit(int version, int access, @NonNull String name, @NonNull ClassInfo additionalInfo) {
78          if ((access & SYNTHETIC) != 0) {
79              throw new VisitInterruptedException();
80          }
81  
82          boolean nestedType = name.indexOf('$') > 0;
83  
84          if (!nestedType && kindOfTopLevelType == null) {
85              // noinspection ConstantConditions
86              kindOfTopLevelType = getKindOfJavaType(access, additionalInfo.superName);
87          }
88  
89          forEnumClass = (access & ENUM) != 0;
90  
91          String sourceFileDebugName = getSourceFileDebugName(additionalInfo);
92  
93          if (!forInnerClass) {
94              extractClassAndSourceFileName(name);
95  
96              boolean cannotModify = (access & ANNOTATION) != 0;
97  
98              if (cannotModify) {
99                  throw VisitInterruptedException.INSTANCE;
100             }
101 
102             registerAsInnerClassModifierIfApplicable(access, name, nestedType);
103             createFileData(sourceFileDebugName);
104         }
105 
106         cw.visit(version, access, name, additionalInfo);
107     }
108 
109     @NonNull
110     private static String getKindOfJavaType(int typeModifiers, @NonNull String superName) {
111         if ((typeModifiers & ANNOTATION) != 0) {
112             return "ant";
113         }
114         if ((typeModifiers & INTERFACE) != 0) {
115             return "itf";
116         }
117         if ((typeModifiers & ENUM) != 0) {
118             return "enm";
119         }
120         if ((typeModifiers & ABSTRACT) != 0) {
121             return "absCls";
122         }
123         if (superName.endsWith("Exception") || superName.endsWith("Error")) {
124             return "exc";
125         }
126         return "cls";
127     }
128 
129     @NonNull
130     private static String getSourceFileDebugName(@NonNull ClassInfo additionalInfo) {
131         String sourceFileDebugName = additionalInfo.sourceFileName;
132 
133         if (sourceFileDebugName == null || !sourceFileDebugName.endsWith(".java")) {
134             throw VisitInterruptedException.INSTANCE;
135         }
136 
137         return sourceFileDebugName;
138     }
139 
140     private void extractClassAndSourceFileName(@NonNull String className) {
141         internalClassName = className;
142         int p = className.lastIndexOf('/');
143 
144         if (p < 0) {
145             simpleClassName = className;
146             sourceFileName = "";
147         } else {
148             simpleClassName = className.substring(p + 1);
149             sourceFileName = className.substring(0, p + 1);
150         }
151     }
152 
153     private void registerAsInnerClassModifierIfApplicable(int access, @NonNull String name, boolean nestedType) {
154         if (!forEnumClass && (access & SUPER) != 0 && nestedType) {
155             INNER_CLASS_MODIFIERS.put(name.replace('/', '.'), this);
156         }
157     }
158 
159     private void createFileData(@NonNull String sourceFileDebugName) {
160         sourceFileName += sourceFileDebugName;
161         fileData = CoverageData.instance().getOrAddFile(sourceFileName, kindOfTopLevelType);
162     }
163 
164     @Override
165     public void visitInnerClass(@NonNull String name, @Nullable String outerName, @Nullable String innerName,
166             int access) {
167         cw.visitInnerClass(name, outerName, innerName, access);
168 
169         if (forInnerClass || isSyntheticOrEnumClass(access) || !isNestedInsideClassBeingModified(name, outerName)) {
170             return;
171         }
172 
173         String innerClassName = name.replace('/', '.');
174 
175         if (INNER_CLASS_MODIFIERS.containsKey(innerClassName)) {
176             return;
177         }
178 
179         ClassReader innerCR = ClassFile.createClassReader(CoverageModifier.class.getClassLoader(), name);
180 
181         if (innerCR != null) {
182             CoverageModifier innerClassModifier = new CoverageModifier(innerCR, this, innerName);
183             innerCR.accept(innerClassModifier);
184             INNER_CLASS_MODIFIERS.put(innerClassName, innerClassModifier);
185         }
186     }
187 
188     private static boolean isSyntheticOrEnumClass(int access) {
189         return (access & SYNTHETIC) != 0 || access == STATIC + ENUM;
190     }
191 
192     private boolean isNestedInsideClassBeingModified(@NonNull String internalName, @Nullable String outerName) {
193         String className = outerName == null ? internalName : outerName;
194         int p = className.indexOf('$');
195         String outerClassName = p < 0 ? className : className.substring(0, p);
196 
197         return outerClassName.equals(internalClassName);
198     }
199 
200     @Override
201     public FieldVisitor visitField(int access, @NonNull String name, @NonNull String desc, @Nullable String signature,
202             @Nullable Object value) {
203         if (fileData != null && simpleClassName != null && (access & FIELD_MODIFIERS_TO_IGNORE) == 0) {
204             fileData.dataCoverageInfo.addField(simpleClassName, name, (access & STATIC) != 0);
205         }
206 
207         return cw.visitField(access, name, desc, signature, value);
208     }
209 
210     @Override
211     public MethodVisitor visitMethod(int access, @NonNull String name, @NonNull String desc, @Nullable String signature,
212             @Nullable String[] exceptions) {
213         MethodWriter mw = cw.visitMethod(access, name, desc, signature, exceptions);
214 
215         if ((access & SYNTHETIC) != 0 || fileData == null || "<clinit>".equals(name) && forEnumClass) {
216             return mw;
217         }
218 
219         return new MethodModifier(mw, sourceFileName, fileData);
220     }
221 }