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.Opcodes.ACONST_NULL;
9   import static mockit.asm.jvmConstants.Opcodes.DCONST_0;
10  import static mockit.asm.jvmConstants.Opcodes.DUP;
11  import static mockit.asm.jvmConstants.Opcodes.DUP2_X1;
12  import static mockit.asm.jvmConstants.Opcodes.DUP_X1;
13  import static mockit.asm.jvmConstants.Opcodes.DUP_X2;
14  import static mockit.asm.jvmConstants.Opcodes.FCONST_0;
15  import static mockit.asm.jvmConstants.Opcodes.GETFIELD;
16  import static mockit.asm.jvmConstants.Opcodes.GETSTATIC;
17  import static mockit.asm.jvmConstants.Opcodes.GOTO;
18  import static mockit.asm.jvmConstants.Opcodes.ICONST_0;
19  import static mockit.asm.jvmConstants.Opcodes.INVOKESPECIAL;
20  import static mockit.asm.jvmConstants.Opcodes.INVOKESTATIC;
21  import static mockit.asm.jvmConstants.Opcodes.INVOKEVIRTUAL;
22  import static mockit.asm.jvmConstants.Opcodes.IRETURN;
23  import static mockit.asm.jvmConstants.Opcodes.LCONST_0;
24  import static mockit.asm.jvmConstants.Opcodes.POP;
25  import static mockit.asm.jvmConstants.Opcodes.POP2;
26  import static mockit.asm.jvmConstants.Opcodes.PUTSTATIC;
27  import static mockit.asm.jvmConstants.Opcodes.RETURN;
28  import static mockit.asm.jvmConstants.Opcodes.SIPUSH;
29  
30  import edu.umd.cs.findbugs.annotations.NonNull;
31  import edu.umd.cs.findbugs.annotations.Nullable;
32  
33  import mockit.asm.annotations.AnnotationVisitor;
34  import mockit.asm.controlFlow.Label;
35  import mockit.asm.methods.MethodWriter;
36  import mockit.asm.methods.WrappingMethodVisitor;
37  import mockit.coverage.data.FileCoverageData;
38  import mockit.coverage.lines.PerFileLineCoverage;
39  
40  import org.checkerframework.checker.index.qual.NonNegative;
41  
42  final class MethodModifier extends WrappingMethodVisitor {
43      private static final String DATA_RECORDING_CLASS = "mockit/coverage/TestRun";
44  
45      @NonNull
46      private final String sourceFileName;
47      @NonNull
48      private final FileCoverageData fileData;
49      @NonNull
50      private final PerFileLineCoverage lineCoverageInfo;
51      @NonNull
52      private final CFGTracking cfgTracking;
53      private boolean foundInterestingInstruction;
54      @NonNegative
55      int currentLine;
56  
57      MethodModifier(@NonNull MethodWriter mw, @NonNull String sourceFileName, @NonNull FileCoverageData fileData) {
58          super(mw);
59          this.sourceFileName = sourceFileName;
60          this.fileData = fileData;
61          lineCoverageInfo = fileData.getLineCoverageData();
62          cfgTracking = new CFGTracking(lineCoverageInfo);
63      }
64  
65      @Override
66      public AnnotationVisitor visitAnnotation(@NonNull String desc) {
67          boolean isTestMethod = desc.startsWith("Lorg/junit/") || desc.startsWith("Lorg/testng/");
68  
69          if (isTestMethod) {
70              throw VisitInterruptedException.INSTANCE;
71          }
72  
73          return mw.visitAnnotation(desc);
74      }
75  
76      @Override
77      public void visitLineNumber(@NonNegative int line, @NonNull Label start) {
78          lineCoverageInfo.addLine(line);
79          currentLine = line;
80          cfgTracking.startNewLine();
81          generateCallToRegisterLineExecution();
82          mw.visitLineNumber(line, start);
83      }
84  
85      private void generateCallToRegisterLineExecution() {
86          mw.visitIntInsn(SIPUSH, fileData.index);
87          pushCurrentLineOnTheStack();
88          mw.visitMethodInsn(INVOKESTATIC, DATA_RECORDING_CLASS, "lineExecuted", "(II)V", false);
89      }
90  
91      private void pushCurrentLineOnTheStack() {
92          if (currentLine <= Short.MAX_VALUE) {
93              mw.visitIntInsn(SIPUSH, currentLine);
94          } else {
95              mw.visitLdcInsn(currentLine);
96          }
97      }
98  
99      @Override
100     public void visitLabel(@NonNull Label label) {
101         mw.visitLabel(label);
102         cfgTracking.afterNewLabel(currentLine, label);
103     }
104 
105     @Override
106     public void visitJumpInsn(@NonNegative int opcode, @NonNull Label label) {
107         Label jumpSource = mw.getCurrentBlock();
108         assert jumpSource != null;
109 
110         mw.visitJumpInsn(opcode, label);
111 
112         if (opcode == GOTO) {
113             cfgTracking.afterGoto();
114         } else {
115             cfgTracking.afterConditionalJump(this, jumpSource, label);
116         }
117     }
118 
119     private void generateCallToRegisterBranchTargetExecutionIfPending() {
120         cfgTracking.generateCallToRegisterBranchTargetExecutionIfPending(this);
121     }
122 
123     void generateCallToRegisterBranchTargetExecution(@NonNegative int branchIndex) {
124         mw.visitIntInsn(SIPUSH, fileData.index);
125         pushCurrentLineOnTheStack();
126         mw.visitIntInsn(SIPUSH, branchIndex);
127         mw.visitMethodInsn(INVOKESTATIC, DATA_RECORDING_CLASS, "branchExecuted", "(III)V", false);
128     }
129 
130     @Override
131     public void visitInsn(@NonNegative int opcode) {
132         boolean isReturn = opcode >= IRETURN && opcode <= RETURN;
133 
134         if (!isReturn && !isDefaultReturnValue(opcode)) {
135             foundInterestingInstruction = true;
136         }
137 
138         if (isReturn && !foundInterestingInstruction && cfgTracking.hasOnlyOneLabelBeingVisited()) {
139             lineCoverageInfo.getOrCreateLineData(currentLine).markAsUnreachable();
140         } else {
141             cfgTracking.beforeNoOperandInstruction(this, opcode);
142         }
143 
144         mw.visitInsn(opcode);
145     }
146 
147     private static boolean isDefaultReturnValue(@NonNegative int opcode) {
148         return opcode == ACONST_NULL || opcode == ICONST_0 || opcode == LCONST_0 || opcode == FCONST_0
149                 || opcode == DCONST_0;
150     }
151 
152     @Override
153     public void visitIntInsn(@NonNegative int opcode, int operand) {
154         foundInterestingInstruction = true;
155         generateCallToRegisterBranchTargetExecutionIfPending();
156         mw.visitIntInsn(opcode, operand);
157     }
158 
159     @Override
160     public void visitVarInsn(@NonNegative int opcode, @NonNegative int varIndex) {
161         generateCallToRegisterBranchTargetExecutionIfPending();
162         mw.visitVarInsn(opcode, varIndex);
163     }
164 
165     @Override
166     public void visitTypeInsn(@NonNegative int opcode, @NonNull String typeDesc) {
167         generateCallToRegisterBranchTargetExecutionIfPending();
168         mw.visitTypeInsn(opcode, typeDesc);
169     }
170 
171     @Override
172     public void visitFieldInsn(@NonNegative int opcode, @NonNull String owner, @NonNull String name,
173             @NonNull String desc) {
174         // TODO: need to also process field instructions inside accessor methods (STATIC + SYNTHETIC, "access$nnn")
175         boolean getField = opcode == GETSTATIC || opcode == GETFIELD;
176         boolean isStatic = opcode == PUTSTATIC || opcode == GETSTATIC;
177         char fieldType = desc.charAt(0);
178         boolean size2 = fieldType == 'J' || fieldType == 'D';
179         String classAndFieldNames = null;
180         boolean fieldHasData = false;
181 
182         if (!owner.startsWith("java/")) {
183             classAndFieldNames = owner.substring(owner.lastIndexOf('/') + 1) + '.' + name;
184             fieldHasData = fileData.dataCoverageInfo.isFieldWithCoverageData(classAndFieldNames);
185 
186             if (fieldHasData && !isStatic) {
187                 generateCodeToSaveInstanceReferenceOnTheStack(getField, size2);
188             }
189         }
190 
191         generateCallToRegisterBranchTargetExecutionIfPending();
192         mw.visitFieldInsn(opcode, owner, name, desc);
193 
194         if (opcode == GETSTATIC && "$assertionsDisabled".equals(name)) {
195             cfgTracking.registerAssertFoundInCurrentLine();
196         }
197 
198         if (fieldHasData) {
199             generateCallToRegisterFieldCoverage(getField, isStatic, size2, classAndFieldNames);
200         }
201     }
202 
203     private void generateCodeToSaveInstanceReferenceOnTheStack(boolean getField, boolean size2) {
204         if (getField) {
205             mw.visitInsn(DUP);
206         } else {
207             if (size2) {
208                 mw.visitInsn(DUP2_X1);
209                 mw.visitInsn(POP2);
210                 mw.visitInsn(DUP_X2);
211                 mw.visitInsn(DUP_X2);
212             } else {
213                 mw.visitInsn(DUP_X1);
214                 mw.visitInsn(POP);
215                 mw.visitInsn(DUP_X1);
216                 mw.visitInsn(DUP_X1);
217             }
218             mw.visitInsn(POP);
219         }
220     }
221 
222     private void generateCallToRegisterFieldCoverage(boolean getField, boolean isStatic, boolean size2,
223             @NonNull String classAndFieldNames) {
224         if (!isStatic && getField) {
225             if (size2) {
226                 mw.visitInsn(DUP2_X1);
227                 mw.visitInsn(POP2);
228             } else {
229                 mw.visitInsn(DUP_X1);
230                 mw.visitInsn(POP);
231             }
232         }
233 
234         mw.visitLdcInsn(sourceFileName);
235         mw.visitLdcInsn(classAndFieldNames);
236 
237         String methodToCall = getField ? "fieldRead" : "fieldAssigned";
238         String methodDesc = isStatic ? "(Ljava/lang/String;Ljava/lang/String;)V"
239                 : "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V";
240 
241         mw.visitMethodInsn(INVOKESTATIC, DATA_RECORDING_CLASS, methodToCall, methodDesc, false);
242     }
243 
244     @Override
245     public void visitMethodInsn(@NonNegative int opcode, @NonNull String owner, @NonNull String name,
246             @NonNull String desc, boolean itf) {
247         // This is to ignore bytecode belonging to a static initialization block inserted in a regular line of code by
248         // the Java
249         // compiler when the class contains at least one "assert" statement.
250         // Otherwise, that line of code would always appear as partially covered when running with assertions enabled.
251         if (opcode == INVOKEVIRTUAL && "java/lang/Class".equals(owner) && "desiredAssertionStatus".equals(name)) {
252             cfgTracking.registerAssertFoundInCurrentLine();
253         }
254 
255         if (opcode != INVOKESPECIAL || !"()V".equals(desc)) {
256             foundInterestingInstruction = true;
257         }
258 
259         generateCallToRegisterBranchTargetExecutionIfPending();
260         mw.visitMethodInsn(opcode, owner, name, desc, itf);
261         cfgTracking.afterMethodInstruction(opcode, owner, name);
262     }
263 
264     @Override
265     public void visitLdcInsn(@NonNull Object cst) {
266         foundInterestingInstruction = true;
267         generateCallToRegisterBranchTargetExecutionIfPending();
268         mw.visitLdcInsn(cst);
269     }
270 
271     @Override
272     public void visitIincInsn(@NonNegative int varIndex, int increment) {
273         generateCallToRegisterBranchTargetExecutionIfPending();
274         mw.visitIincInsn(varIndex, increment);
275     }
276 
277     @Override
278     public void visitTryCatchBlock(@NonNull Label start, @NonNull Label end, @NonNull Label handler,
279             @Nullable String type) {
280         generateCallToRegisterBranchTargetExecutionIfPending();
281         mw.visitTryCatchBlock(start, end, handler, type);
282     }
283 
284     @Override
285     public void visitLookupSwitchInsn(@NonNull Label dflt, @NonNull int[] keys, @NonNull Label[] labels) {
286         cfgTracking.beforeLookupSwitchInstruction();
287         generateCallToRegisterBranchTargetExecutionIfPending();
288         mw.visitLookupSwitchInsn(dflt, keys, labels);
289     }
290 
291     @Override
292     public void visitTableSwitchInsn(@NonNegative int min, @NonNegative int max, @NonNull Label dflt,
293             @NonNull Label... labels) {
294         generateCallToRegisterBranchTargetExecutionIfPending();
295         mw.visitTableSwitchInsn(min, max, dflt, labels);
296     }
297 
298     @Override
299     public void visitMultiANewArrayInsn(@NonNull String desc, @NonNegative int dims) {
300         generateCallToRegisterBranchTargetExecutionIfPending();
301         mw.visitMultiANewArrayInsn(desc, dims);
302     }
303 
304     @Override
305     public void visitMaxStack(@NonNegative int maxStack) {
306         if (maxStack > 1) {
307             lineCoverageInfo.markLineAsReachable(currentLine);
308         }
309 
310         mw.visitMaxStack(maxStack);
311     }
312 }