/*
* Copyright (c) 2006 Rogério Liesenfeld
* This file is subject to the terms of the MIT license (see LICENSE.txt).
*/
package mockit.internal.expectations.transformation;
import javax.annotation.*;
import mockit.external.asm.*;
import static mockit.external.asm.Opcodes.*;
import static mockit.internal.util.TypeConversion.*;
final class InvocationBlockModifier extends MethodVisitor
{
private static final String CLASS_DESC = Type.getInternalName(ActiveInvocations.class);
private static final Type[] NO_PARAMETERS = new Type[0];
private static final String ANY_FIELDS =
"any anyString anyInt anyBoolean anyLong anyDouble anyFloat anyChar anyShort anyByte";
private static final String WITH_METHODS =
"withArgThat(Lorg/hamcrest/Matcher;)Ljava/lang/Object; " +
"with(Lmockit/Delegate;)Ljava/lang/Object; " +
"withAny(Ljava/lang/Object;)Ljava/lang/Object; " +
"withCapture()Ljava/lang/Object; withCapture(Ljava/util/List;)Ljava/lang/Object; " +
"withCapture(Ljava/lang/Object;)Ljava/util/List; " +
"withEqual(Ljava/lang/Object;)Ljava/lang/Object; withEqual(DD)D withEqual(FD)F " +
"withInstanceLike(Ljava/lang/Object;)Ljava/lang/Object; " +
"withInstanceOf(Ljava/lang/Class;)Ljava/lang/Object; " +
"withNotEqual(Ljava/lang/Object;)Ljava/lang/Object; " +
"withNull()Ljava/lang/Object; withNotNull()Ljava/lang/Object; " +
"withSameInstance(Ljava/lang/Object;)Ljava/lang/Object; " +
"withSubstring(Ljava/lang/CharSequence;)Ljava/lang/CharSequence; " +
"withPrefix(Ljava/lang/CharSequence;)Ljava/lang/CharSequence; " +
"withSuffix(Ljava/lang/CharSequence;)Ljava/lang/CharSequence; " +
"withMatch(Ljava/lang/CharSequence;)Ljava/lang/CharSequence;";
@Nonnull private final MethodWriter mw;
// Input data:
@Nonnull private final String blockOwner;
private final boolean callEndInvocations;
// Takes care of withCapture() matchers, if any:
private boolean justAfterWithCaptureInvocation;
@Nonnull private final ArgumentCapturing argumentCapturing;
// Stores the index of the local variable holding a list passed in a withCapture(List) call, if any:
private int lastLoadedVarIndex;
// Helper fields that allow argument matchers to be moved to the correct positions of their
// corresponding parameters:
@Nonnull private final int[] matcherStacks;
private int matcherCount;
private int stackSize;
@Nonnull private Type[] parameterTypes;
final class Capture
{
final int opcode;
final int varIndex;
@Nullable String typeToCapture;
private int parameterIndex;
private boolean parameterIndexFixed;
Capture(int opcode, int varIndex, @Nullable String typeToCapture)
{
this.opcode = opcode;
this.varIndex = varIndex;
this.typeToCapture = typeToCapture;
parameterIndex = matcherCount - 1;
}
Capture(int varIndex)
{
opcode = ALOAD;
this.varIndex = varIndex;
parameterIndex = matcherCount;
}
/**
* Generates bytecode that will be responsible for performing the following steps:
* 1. Get the argument value (an Object) for the last matched invocation.
* 2. Cast to a reference type or unbox to a primitive type, as needed.
* 3. Store the converted value in its local variable.
*/
void generateCodeToStoreCapturedValue()
{
if (opcode != ALOAD) {
mw.visitIntInsn(SIPUSH, parameterIndex);
generateCallToActiveInvocationsMethod("matchedArgument", "(I)Ljava/lang/Object;");
Type argType = getArgumentType();
generateCastOrUnboxing(mw, argType, opcode);
mw.visitVarInsn(opcode, varIndex);
}
}
@Nonnull
private Type getArgumentType()
{
if (typeToCapture == null) {
return parameterTypes[parameterIndex];
}
else if (typeToCapture.charAt(0) == '[') {
return Type.getType(typeToCapture);
}
else {
return Type.getType('L' + typeToCapture + ';');
}
}
boolean fixParameterIndex(int originalIndex, int newIndex)
{
if (!parameterIndexFixed && parameterIndex == originalIndex) {
parameterIndex = newIndex;
parameterIndexFixed = true;
return true;
}
return false;
}
void generateCallToSetArgumentTypeIfNeeded()
{
if (opcode == ALOAD) {
mw.visitIntInsn(SIPUSH, parameterIndex);
mw.visitLdcInsn(varIndex);
generateCallToActiveInvocationsMethod("setExpectedArgumentType", "(II)V");
}
else if (typeToCapture != null && !isTypeToCaptureSameAsParameterType(typeToCapture)) {
mw.visitIntInsn(SIPUSH, parameterIndex);
mw.visitLdcInsn(typeToCapture);
generateCallToActiveInvocationsMethod("setExpectedArgumentType", "(ILjava/lang/String;)V");
}
}
private boolean isTypeToCaptureSameAsParameterType(@Nonnull String typeDesc)
{
Type parameterType = parameterTypes[parameterIndex];
int sort = parameterType.getSort();
if (sort == Type.OBJECT || sort == Type.ARRAY) {
return typeDesc.equals(parameterType.getInternalName());
}
return isPrimitiveWrapper(typeDesc);
}
}
InvocationBlockModifier(@Nonnull MethodWriter mw, @Nonnull String blockOwner, boolean callEndInvocations)
{
super(mw);
this.mw = mw;
this.blockOwner = blockOwner;
this.callEndInvocations = callEndInvocations;
matcherStacks = new int[40];
argumentCapturing = new ArgumentCapturing(this);
parameterTypes = NO_PARAMETERS;
}
private void generateCallToActiveInvocationsMethod(@Nonnull String name, @Nonnull String desc)
{
visitMethodInstruction(INVOKESTATIC, CLASS_DESC, name, desc, false);
}
@Override
public void visitFieldInsn(int opcode, String owner, String name, String desc)
{
boolean getField = opcode == GETFIELD;
if ((getField || opcode == PUTFIELD) && blockOwner.equals(owner)) {
if (name.indexOf('$') > 0) {
// Nothing to do.
}
else if (getField && name.startsWith("any") && ANY_FIELDS.contains(name)) {
generateCodeToAddArgumentMatcherForAnyField(owner, name, desc);
return;
}
else if (!getField && generateCodeThatReplacesAssignmentToSpecialField(name)) {
visitInsn(POP);
return;
}
}
stackSize += stackSizeVariationForFieldAccess(opcode, desc);
mw.visitFieldInsn(opcode, owner, name, desc);
}
private boolean generateCodeThatReplacesAssignmentToSpecialField(@Nonnull String fieldName)
{
if ("result".equals(fieldName)) {
generateCallToActiveInvocationsMethod("addResult", "(Ljava/lang/Object;)V");
return true;
}
if ("times".equals(fieldName) || "minTimes".equals(fieldName) || "maxTimes".equals(fieldName)) {
generateCallToActiveInvocationsMethod(fieldName, "(I)V");
return true;
}
return false;
}
private void generateCodeToAddArgumentMatcherForAnyField(
@Nonnull String fieldOwner, @Nonnull String name, @Nonnull String desc)
{
mw.visitFieldInsn(GETFIELD, fieldOwner, name, desc);
generateCallToActiveInvocationsMethod(name, "()V");
matcherStacks[matcherCount++] = stackSize;
}
private static int stackSizeVariationForFieldAccess(int opcode, @Nonnull String fieldType)
{
char c = fieldType.charAt(0);
boolean twoByteType = c == 'D' || c == 'J';
switch (opcode) {
case GETSTATIC: return twoByteType ? 2 : 1;
case PUTSTATIC: return twoByteType ? -2 : -1;
case GETFIELD: return twoByteType ? 1 : 0;
default: return twoByteType ? -3 : -2;
}
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf)
{
if (opcode == INVOKESTATIC && (isBoxing(owner, name, desc) || isAccessMethod(owner, name))) {
// It's an invocation to a primitive boxing method or to a synthetic method for private access, just ignore it.
visitMethodInstruction(INVOKESTATIC, owner, name, desc, itf);
}
else if (isCallToArgumentMatcher(opcode, owner, name, desc)) {
visitMethodInstruction(INVOKEVIRTUAL, owner, name, desc, itf);
boolean withCaptureMethod = "withCapture".equals(name);
if (argumentCapturing.registerMatcher(withCaptureMethod, desc, lastLoadedVarIndex)) {
justAfterWithCaptureInvocation = withCaptureMethod;
matcherStacks[matcherCount++] = stackSize;
}
}
else if (isUnboxing(opcode, owner, desc)) {
if (justAfterWithCaptureInvocation) {
generateCodeToReplaceNullWithZeroOnTopOfStack(desc);
justAfterWithCaptureInvocation = false;
}
else {
visitMethodInstruction(opcode, owner, name, desc, itf);
}
}
else {
handleMockedOrNonMockedInvocation(opcode, owner, name, desc, itf);
}
}
private boolean isAccessMethod(@Nonnull String methodOwner, @Nonnull String name)
{
return !methodOwner.equals(blockOwner) && name.startsWith("access$");
}
private void visitMethodInstruction(
@Nonnegative int opcode, @Nonnull String owner, @Nonnull String name, @Nonnull String desc, boolean itf)
{
int argSize = Type.getArgumentsAndReturnSizes(desc);
int sizeVariation = (argSize & 0x03) - (argSize >> 2);
if (opcode == INVOKESTATIC) {
sizeVariation++;
}
stackSize += sizeVariation;
mw.visitMethodInsn(opcode, owner, name, desc, itf);
}
private boolean isCallToArgumentMatcher(
@Nonnegative int opcode, @Nonnull String owner, @Nonnull String name, @Nonnull String desc)
{
return
opcode == INVOKEVIRTUAL && owner.equals(blockOwner) &&
name.startsWith("with") && WITH_METHODS.contains(name + desc);
}
private void generateCodeToReplaceNullWithZeroOnTopOfStack(@Nonnull String unboxingMethodDesc)
{
char primitiveTypeCode = unboxingMethodDesc.charAt(2);
visitInsn(POP);
int zeroOpcode;
switch (primitiveTypeCode) {
case 'J': zeroOpcode = LCONST_0; break;
case 'F': zeroOpcode = FCONST_0; break;
case 'D': zeroOpcode = DCONST_0; break;
default: zeroOpcode = ICONST_0;
}
visitInsn(zeroOpcode);
}
private void handleMockedOrNonMockedInvocation(
@Nonnegative int opcode, @Nonnull String owner, @Nonnull String name, @Nonnull String desc, boolean itf)
{
if (matcherCount == 0) {
visitMethodInstruction(opcode, owner, name, desc, itf);
}
else {
boolean mockedInvocationUsingTheMatchers = handleInvocationParameters(desc);
visitMethodInstruction(opcode, owner, name, desc, itf);
handleArgumentCapturingIfNeeded(mockedInvocationUsingTheMatchers);
}
}
private boolean handleInvocationParameters(@Nonnull String desc)
{
parameterTypes = Type.getArgumentTypes(desc);
int stackAfter = stackSize - sumOfParameterSizes();
boolean mockedInvocationUsingTheMatchers = stackAfter < matcherStacks[0];
if (mockedInvocationUsingTheMatchers) {
generateCallsToMoveArgMatchers(stackAfter);
argumentCapturing.generateCallsToSetArgumentTypesToCaptureIfAny();
matcherCount = 0;
}
return mockedInvocationUsingTheMatchers;
}
@Nonnegative
private int sumOfParameterSizes()
{
int sum = 0;
for (Type argType : parameterTypes) {
sum += argType.getSize();
}
return sum;
}
private void generateCallsToMoveArgMatchers(@Nonnegative int initialStack)
{
int stack = initialStack;
int nextMatcher = 0;
int matcherStack = matcherStacks[0];
for (int i = 0; i < parameterTypes.length && nextMatcher < matcherCount; i++) {
stack += parameterTypes[i].getSize();
if (stack == matcherStack || stack == matcherStack + 1) {
if (nextMatcher < i) {
generateCallToMoveArgMatcher(nextMatcher, i);
argumentCapturing.updateCaptureIfAny(nextMatcher, i);
}
matcherStack = matcherStacks[++nextMatcher];
}
}
}
private void generateCallToMoveArgMatcher(@Nonnegative int originalMatcherIndex, @Nonnegative int toIndex)
{
mw.visitIntInsn(SIPUSH, originalMatcherIndex);
mw.visitIntInsn(SIPUSH, toIndex);
generateCallToActiveInvocationsMethod("moveArgMatcher", "(II)V");
}
private void handleArgumentCapturingIfNeeded(boolean mockedInvocationUsingTheMatchers)
{
if (mockedInvocationUsingTheMatchers) {
argumentCapturing.generateCallsToCaptureMatchedArgumentsIfPending();
}
justAfterWithCaptureInvocation = false;
}
@Override
public void visitLabel(Label label)
{
mw.visitLabel(label);
if (!label.isDebug()) {
stackSize = 0;
}
}
@Override
public void visitTypeInsn(int opcode, @Nonnull String type)
{
argumentCapturing.registerTypeToCaptureIfApplicable(opcode, type);
if (opcode == NEW) {
stackSize++;
}
mw.visitTypeInsn(opcode, type);
}
@Override
public void visitIntInsn(int opcode, int operand)
{
if (opcode != NEWARRAY) {
stackSize++;
}
mw.visitIntInsn(opcode, operand);
}
@Override
public void visitVarInsn(int opcode, @SuppressWarnings("ParameterNameDiffersFromOverriddenParameter") int varIndex)
{
if (opcode == ALOAD) {
lastLoadedVarIndex = varIndex;
}
argumentCapturing.registerAssignmentToCaptureVariableIfApplicable(opcode, varIndex);
if (opcode != RET) {
stackSize += Frame.SIZE[opcode];
}
mw.visitVarInsn(opcode, varIndex);
}
@Override
public void visitLdcInsn(Object cst)
{
stackSize++;
if (cst instanceof Long || cst instanceof Double) {
stackSize++;
}
mw.visitLdcInsn(cst);
}
@Override
public void visitJumpInsn(int opcode, Label label)
{
if (opcode != JSR) {
stackSize += Frame.SIZE[opcode];
}
mw.visitJumpInsn(opcode, label);
}
@Override
public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels)
{
stackSize--;
mw.visitTableSwitchInsn(min, max, dflt, labels);
}
@Override
public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels)
{
stackSize--;
mw.visitLookupSwitchInsn(dflt, keys, labels);
}
@Override
public void visitMultiANewArrayInsn(String desc, int dims)
{
stackSize += 1 - dims;
mw.visitMultiANewArrayInsn(desc, dims);
}
@Override
public void visitInsn(int opcode)
{
if (opcode == RETURN && callEndInvocations) {
generateCallToActiveInvocationsMethod("endInvocations", "()V");
}
else {
stackSize += Frame.SIZE[opcode];
}
mw.visitInsn(opcode);
}
@Override
public void visitLocalVariable(
@Nonnull String name, @Nonnull String desc, @Nullable String signature, @Nonnull Label start, @Nonnull Label end,
int index)
{
if (signature != null) {
argumentCapturing.registerTypeToCaptureIntoListIfApplicable(index, signature);
}
// In classes instrumented with EMMA some local variable information can be lost, so we discard it entirely to
// avoid a ClassFormatError.
if (end.position > 0) {
mw.visitLocalVariable(name, desc, signature, start, end, index);
}
}
}