All Downloads are FREE. Search and download functionalities are using the official Maven repository.

mockit.internal.expectations.transformation.InvocationBlockModifier Maven / Gradle / Ivy

/*
 * Copyright (c) 2006 JMockit developers
 * This file is subject to the terms of the MIT license (see LICENSE.txt).
 */
package mockit.internal.expectations.transformation;

import static mockit.asm.jvmConstants.Opcodes.ALOAD;
import static mockit.asm.jvmConstants.Opcodes.DCONST_0;
import static mockit.asm.jvmConstants.Opcodes.FCONST_0;
import static mockit.asm.jvmConstants.Opcodes.GETFIELD;
import static mockit.asm.jvmConstants.Opcodes.GETSTATIC;
import static mockit.asm.jvmConstants.Opcodes.ICONST_0;
import static mockit.asm.jvmConstants.Opcodes.INVOKESTATIC;
import static mockit.asm.jvmConstants.Opcodes.INVOKEVIRTUAL;
import static mockit.asm.jvmConstants.Opcodes.LCONST_0;
import static mockit.asm.jvmConstants.Opcodes.NEW;
import static mockit.asm.jvmConstants.Opcodes.NEWARRAY;
import static mockit.asm.jvmConstants.Opcodes.POP;
import static mockit.asm.jvmConstants.Opcodes.PUTFIELD;
import static mockit.asm.jvmConstants.Opcodes.PUTSTATIC;
import static mockit.asm.jvmConstants.Opcodes.RETURN;
import static mockit.internal.util.TypeConversionBytecode.isBoxing;
import static mockit.internal.util.TypeConversionBytecode.isUnboxing;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;

import mockit.asm.controlFlow.Label;
import mockit.asm.jvmConstants.JVMInstruction;
import mockit.asm.methods.MethodWriter;
import mockit.asm.methods.WrappingMethodVisitor;
import mockit.asm.types.JavaType;

import org.checkerframework.checker.index.qual.NonNegative;

public final class InvocationBlockModifier extends WrappingMethodVisitor {
    private static final String CLASS_DESC = "mockit/internal/expectations/ActiveInvocations";

    // Input data:
    @NonNull
    private final String blockOwner;

    // Keeps track of the current stack size (after each bytecode instruction) within the invocation block:
    @NonNegative
    private int stackSize;

    // Handle withCapture()/anyXyz/withXyz matchers, if any:
    @NonNull
    final ArgumentMatching argumentMatching;
    @NonNull
    final ArgumentCapturing argumentCapturing;
    private boolean justAfterWithCaptureInvocation;

    // Stores the index of the local variable holding a list passed in a withCapture(List) call, if any:
    @NonNegative
    private int lastLoadedVarIndex;

    InvocationBlockModifier(@NonNull MethodWriter mw, @NonNull String blockOwner) {
        super(mw);
        this.blockOwner = blockOwner;
        argumentMatching = new ArgumentMatching(this);
        argumentCapturing = new ArgumentCapturing(this);
    }

    void generateCallToActiveInvocationsMethod(@NonNull String name) {
        mw.visitMethodInsn(INVOKESTATIC, CLASS_DESC, name, "()V", false);
    }

    void generateCallToActiveInvocationsMethod(@NonNull String name, @NonNull String desc) {
        visitMethodInstruction(INVOKESTATIC, CLASS_DESC, name, desc, false);
    }

    @Override
    public void visitFieldInsn(@NonNegative int opcode, @NonNull String owner, @NonNull String name,
            @NonNull String desc) {
        boolean getField = opcode == GETFIELD;

        if ((getField || opcode == PUTFIELD) && blockOwner.equals(owner)) {
            if (name.indexOf('$') > 0) {
                // Nothing to do.
            } else if (getField && ArgumentMatching.isAnyField(name)) {
                argumentMatching.generateCodeToAddArgumentMatcherForAnyField(owner, name, desc);
                argumentMatching.addMatcher(stackSize);
                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 static int stackSizeVariationForFieldAccess(@NonNegative 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;
            case PUTFIELD:
                return twoByteType ? -3 : -2;
            default:
                throw new IllegalArgumentException("Invalid field access opcode: " + opcode);
        }
    }

    @Override
    public void visitMethodInsn(@NonNegative int opcode, @NonNull String owner, @NonNull String name,
            @NonNull 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;
                argumentMatching.addMatcher(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) {
        if (!"()V".equals(desc)) {
            int argAndRetSize = JavaType.getArgumentsAndReturnSizes(desc);
            int argSize = argAndRetSize >> 2;

            if (opcode == INVOKESTATIC) {
                argSize--;
            }

            stackSize -= argSize;

            int retSize = argAndRetSize & 0x03;
            stackSize += retSize;
        } else if (opcode != INVOKESTATIC) {
            stackSize--;
        }

        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)
                && ArgumentMatching.isCallToArgumentMatcher(name, desc);
    }

    private void generateCodeToReplaceNullWithZeroOnTopOfStack(@NonNull String unboxingMethodDesc) {
        visitInsn(POP);

        char primitiveTypeCode = unboxingMethodDesc.charAt(2);
        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 (argumentMatching.getMatcherCount() == 0) {
            visitMethodInstruction(opcode, owner, name, desc, itf);
        } else {
            boolean mockedInvocationUsingTheMatchers = argumentMatching.handleInvocationParameters(stackSize, desc);
            visitMethodInstruction(opcode, owner, name, desc, itf);
            handleArgumentCapturingIfNeeded(mockedInvocationUsingTheMatchers);
        }
    }

    private void handleArgumentCapturingIfNeeded(boolean mockedInvocationUsingTheMatchers) {
        if (mockedInvocationUsingTheMatchers) {
            argumentCapturing.generateCallsToCaptureMatchedArgumentsIfPending();
        }

        justAfterWithCaptureInvocation = false;
    }

    @Override
    public void visitLabel(@NonNull Label label) {
        mw.visitLabel(label);

        if (!label.isDebug()) {
            stackSize = 0;
        }
    }

    @Override
    public void visitTypeInsn(@NonNegative int opcode, @NonNull String typeDesc) {
        argumentCapturing.registerTypeToCaptureIfApplicable(opcode, typeDesc);

        if (opcode == NEW) {
            stackSize++;
        }

        mw.visitTypeInsn(opcode, typeDesc);
    }

    @Override
    public void visitIntInsn(@NonNegative int opcode, int operand) {
        if (opcode != NEWARRAY) {
            stackSize++;
        }

        mw.visitIntInsn(opcode, operand);
    }

    @Override
    public void visitVarInsn(@NonNegative int opcode, @NonNegative int varIndex) {
        if (opcode == ALOAD) {
            lastLoadedVarIndex = varIndex;
        }

        argumentCapturing.registerAssignmentToCaptureVariableIfApplicable(opcode, varIndex);
        stackSize += JVMInstruction.SIZE[opcode];
        mw.visitVarInsn(opcode, varIndex);
    }

    @Override
    public void visitLdcInsn(@NonNull Object cst) {
        stackSize++;

        if (cst instanceof Long || cst instanceof Double) {
            stackSize++;
        }

        mw.visitLdcInsn(cst);
    }

    @Override
    public void visitJumpInsn(@NonNegative int opcode, @NonNull Label label) {
        stackSize += JVMInstruction.SIZE[opcode];
        mw.visitJumpInsn(opcode, label);
    }

    @Override
    public void visitTableSwitchInsn(int min, int max, @NonNull Label dflt, @NonNull Label... labels) {
        stackSize--;
        mw.visitTableSwitchInsn(min, max, dflt, labels);
    }

    @Override
    public void visitLookupSwitchInsn(@NonNull Label dflt, @NonNull int[] keys, @NonNull Label[] labels) {
        stackSize--;
        mw.visitLookupSwitchInsn(dflt, keys, labels);
    }

    @Override
    public void visitMultiANewArrayInsn(@NonNull String desc, @NonNegative int dims) {
        stackSize += 1 - dims;
        mw.visitMultiANewArrayInsn(desc, dims);
    }

    @Override
    public void visitInsn(@NonNegative int opcode) {
        if (opcode == RETURN) {
            generateCallToActiveInvocationsMethod("endInvocations");
        } else {
            stackSize += JVMInstruction.SIZE[opcode];
        }

        mw.visitInsn(opcode);
    }

    @Override
    public void visitLocalVariable(@NonNull String name, @NonNull String desc, @Nullable String signature,
            @NonNull Label start, @NonNull Label end, @NonNegative 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);
        }
    }

    @NonNull
    MethodWriter getMethodWriter() {
        return mw;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy