summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Bruneton <ebruneton@free.fr>2022-05-22 14:35:18 +0000
committerEric Bruneton <ebruneton@free.fr>2022-05-22 14:35:18 +0000
commite313deded6d55c4b018d6377952a4aff728acb69 (patch)
tree5cbc31f200cb86571e0d921ceda2c614d55a59b5
parent6e0c8058574e6b80555ebd485e8c20cf5bfa1b27 (diff)
downloadow2-asm-e313deded6d55c4b018d6377952a4aff728acb69.tar.gz
Add a CheckFrameAnalyzer class in asm-util. This will be used in...
-rw-r--r--asm-analysis/src/main/java/org/objectweb/asm/tree/analysis/Analyzer.java2
-rw-r--r--asm-util/src/main/java/org/objectweb/asm/util/CheckFrameAnalyzer.java478
-rw-r--r--asm-util/src/test/java/org/objectweb/asm/util/CheckAnnotationAdapterTest.java3
-rw-r--r--asm-util/src/test/java/org/objectweb/asm/util/CheckFieldAdapterTest.java3
-rw-r--r--asm-util/src/test/java/org/objectweb/asm/util/CheckFrameAnalyzerTest.java362
-rw-r--r--asm-util/src/test/java/org/objectweb/asm/util/CheckRecordComponentAdapterTest.java3
-rw-r--r--asm-util/src/test/java/org/objectweb/asm/util/MethodNodeBuilder.java109
7 files changed, 953 insertions, 7 deletions
diff --git a/asm-analysis/src/main/java/org/objectweb/asm/tree/analysis/Analyzer.java b/asm-analysis/src/main/java/org/objectweb/asm/tree/analysis/Analyzer.java
index 60c443fc..b622f4ac 100644
--- a/asm-analysis/src/main/java/org/objectweb/asm/tree/analysis/Analyzer.java
+++ b/asm-analysis/src/main/java/org/objectweb/asm/tree/analysis/Analyzer.java
@@ -48,7 +48,7 @@ import org.objectweb.asm.tree.VarInsnNode;
* A semantic bytecode analyzer. <i>This class does not fully check that JSR and RET instructions
* are valid.</i>
*
- * @param <V> type of the Value used for the analysis.
+ * @param <V> type of the {@link Value} used for the analysis.
* @author Eric Bruneton
*/
public class Analyzer<V extends Value> implements Opcodes {
diff --git a/asm-util/src/main/java/org/objectweb/asm/util/CheckFrameAnalyzer.java b/asm-util/src/main/java/org/objectweb/asm/util/CheckFrameAnalyzer.java
new file mode 100644
index 00000000..c2eaf8ea
--- /dev/null
+++ b/asm-util/src/main/java/org/objectweb/asm/util/CheckFrameAnalyzer.java
@@ -0,0 +1,478 @@
+// ASM: a very small and fast Java bytecode manipulation framework
+// Copyright (c) 2000-2011 INRIA, France Telecom
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// 3. Neither the name of the copyright holders nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+// THE POSSIBILITY OF SUCH DAMAGE.
+package org.objectweb.asm.util;
+
+import java.util.Collections;
+import java.util.List;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.FrameNode;
+import org.objectweb.asm.tree.InsnList;
+import org.objectweb.asm.tree.InsnNode;
+import org.objectweb.asm.tree.JumpInsnNode;
+import org.objectweb.asm.tree.LabelNode;
+import org.objectweb.asm.tree.LookupSwitchInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+import org.objectweb.asm.tree.TableSwitchInsnNode;
+import org.objectweb.asm.tree.TryCatchBlockNode;
+import org.objectweb.asm.tree.TypeInsnNode;
+import org.objectweb.asm.tree.analysis.Analyzer;
+import org.objectweb.asm.tree.analysis.AnalyzerException;
+import org.objectweb.asm.tree.analysis.Frame;
+import org.objectweb.asm.tree.analysis.Interpreter;
+import org.objectweb.asm.tree.analysis.Value;
+
+/**
+ * An {@link Analyzer} subclass which checks that methods provide stack map frames where expected
+ * (i.e. at jump target and after instructions without immediate successor), and that these stack
+ * map frames are valid (for the provided interpreter; they may still be invalid for the JVM, if the
+ * {@link Interpreter} uses a simplified type system compared to the JVM verifier). This is done in
+ * two steps:
+ *
+ * <ul>
+ * <li>First, the stack map frames in {@link FrameNode}s are expanded, and stored at their
+ * respective instruction offsets. The expansion process uncompresses the APPEND, CHOP and
+ * SAME frames to FULL frames. It also converts the stack map frame verification types to
+ * {@link Value}s, via the provided {@link Interpreter}. The expansion is done in {@link
+ * #expandFrames}, by looking at each {@link FrameNode} in sequence (compressed frames are
+ * defined relatively to the previous {@link FrameNode}, or the implicit first frame). The
+ * actual decompression is done in {@link #expandFrame}, and the type conversion in {@link
+ * #newFrameValue}.
+ * <li>Next, the method instructions are checked in sequence. Starting from the implicit initial
+ * frame, the execution of each instruction <em>i</em> is simulated on the current stack map
+ * frame, with the {@link Frame#execute} method. This gives a new stack map frame <em>f</em>,
+ * representing the stack map frame state after the execution of <em>i</em>. Then:
+ * <ul>
+ * <li>If there is a next instruction and if the control flow cannot continue to it (e.g. if
+ * <em>i</em> is a RETURN or an ATHROW, for instance): an existing stack map frame
+ * <em>f0</em> (coming from the first step) is expected after <em>i</em>.
+ * <li>If there is a next instruction and if the control flow can continue to it (e.g. if
+ * <em>i</em> is a ALOAD, for instance): either there an existing stack map frame
+ * <em>f0</em> (coming from the first step) after <em>i</em>, or there is none. In the
+ * first case <em>f</em> and <em>f0</em> must be <em>compatible</em>: the types in
+ * <em>f</em> must be sub types of the corresponding types in the existing frame
+ * <em>f0</em> (otherwise an exception is thrown). In the second case, <em>f0</em> is
+ * simply set to the value of <em>f</em>.
+ * <li>If the control flow can continue to some instruction <em>j</em> (e.g. if <em>i</em>
+ * is an IF_EQ, for instance): an existing stack map frame <em>f0</em> (coming from the
+ * first step) is expected at <em>j</em>, which must be compatible with <em>f</em> (as
+ * defined previously).
+ * </ul>
+ * The sequential loop over the instructions is done in {@link #init}, which is called from
+ * the {@link Analyzer#analyze} method. Cases where the control flow cannot continue to the
+ * next instruction are handled in {@link #endControlFlow}. Cases where the control flow can
+ * continue to the next instruction, or jump to another instruction, are handled in {@link
+ * #checkFrame}. This method checks that an existing stack map frame is present when required,
+ * and checks the stack map frames compatibility with {@link #checkMerge}.
+ * </ul>
+ *
+ * @author Eric Bruneton
+ * @param <V> type of the {@link Value} used for the analysis.
+ */
+class CheckFrameAnalyzer<V extends Value> extends Analyzer<V> {
+
+ /** The interpreter to use to symbolically interpret the bytecode instructions. */
+ private final Interpreter<V> interpreter;
+
+ /** The instructions of the currently analyzed method. */
+ private InsnList insnList;
+
+ /**
+ * The number of locals in the last stack map frame processed by {@link expandFrame}. Long and
+ * double values are represented with two elements.
+ */
+ private int currentLocals;
+
+ CheckFrameAnalyzer(final Interpreter<V> interpreter) {
+ super(interpreter);
+ this.interpreter = interpreter;
+ }
+
+ @Override
+ protected void init(final String owner, final MethodNode method) throws AnalyzerException {
+ insnList = method.instructions;
+ currentLocals = Type.getArgumentsAndReturnSizes(method.desc) >> 2;
+
+ Frame<V>[] frames = getFrames();
+ Frame<V> currentFrame = frames[0];
+ expandFrames(owner, method, currentFrame);
+ for (int insnIndex = 0; insnIndex < insnList.size(); ++insnIndex) {
+ Frame<V> oldFrame = frames[insnIndex];
+
+ // Simulate the execution of this instruction.
+ AbstractInsnNode insnNode = null;
+ try {
+ insnNode = method.instructions.get(insnIndex);
+ int insnOpcode = insnNode.getOpcode();
+ int insnType = insnNode.getType();
+
+ if (insnType == AbstractInsnNode.LABEL
+ || insnType == AbstractInsnNode.LINE
+ || insnType == AbstractInsnNode.FRAME) {
+ checkFrame(insnIndex + 1, oldFrame, /* requireFrame = */ false);
+ } else {
+ currentFrame.init(oldFrame).execute(insnNode, interpreter);
+
+ if (insnNode instanceof JumpInsnNode) {
+ JumpInsnNode jumpInsn = (JumpInsnNode) insnNode;
+ if (insnOpcode != GOTO && insnOpcode != JSR) {
+ checkFrame(insnIndex + 1, currentFrame, /* requireFrame = */ false);
+ } else if (insnOpcode == GOTO) {
+ endControlFlow(insnIndex);
+ }
+ int jumpInsnIndex = insnList.indexOf(jumpInsn.label);
+ if (insnOpcode == JSR) {
+ throw new AnalyzerException(insnNode, "JSR instructions are unsupported");
+ } else {
+ checkFrame(jumpInsnIndex, currentFrame, /* requireFrame = */ true);
+ }
+ } else if (insnNode instanceof LookupSwitchInsnNode) {
+ LookupSwitchInsnNode lookupSwitchInsn = (LookupSwitchInsnNode) insnNode;
+ int targetInsnIndex = insnList.indexOf(lookupSwitchInsn.dflt);
+ checkFrame(targetInsnIndex, currentFrame, /* requireFrame = */ true);
+ for (int i = 0; i < lookupSwitchInsn.labels.size(); ++i) {
+ LabelNode label = lookupSwitchInsn.labels.get(i);
+ targetInsnIndex = insnList.indexOf(label);
+ currentFrame.initJumpTarget(insnOpcode, label);
+ checkFrame(targetInsnIndex, currentFrame, /* requireFrame = */ true);
+ }
+ endControlFlow(insnIndex);
+ } else if (insnNode instanceof TableSwitchInsnNode) {
+ TableSwitchInsnNode tableSwitchInsn = (TableSwitchInsnNode) insnNode;
+ int targetInsnIndex = insnList.indexOf(tableSwitchInsn.dflt);
+ currentFrame.initJumpTarget(insnOpcode, tableSwitchInsn.dflt);
+ checkFrame(targetInsnIndex, currentFrame, /* requireFrame = */ true);
+ newControlFlowEdge(insnIndex, targetInsnIndex);
+ for (int i = 0; i < tableSwitchInsn.labels.size(); ++i) {
+ LabelNode label = tableSwitchInsn.labels.get(i);
+ currentFrame.initJumpTarget(insnOpcode, label);
+ targetInsnIndex = insnList.indexOf(label);
+ checkFrame(targetInsnIndex, currentFrame, /* requireFrame = */ true);
+ }
+ endControlFlow(insnIndex);
+ } else if (insnOpcode == RET) {
+ throw new AnalyzerException(insnNode, "RET instructions are unsupported");
+ } else if (insnOpcode != ATHROW && (insnOpcode < IRETURN || insnOpcode > RETURN)) {
+ checkFrame(insnIndex + 1, currentFrame, /* requireFrame = */ false);
+ } else {
+ endControlFlow(insnIndex);
+ }
+ }
+
+ List<TryCatchBlockNode> insnHandlers = getHandlers(insnIndex);
+ if (insnHandlers != null) {
+ for (TryCatchBlockNode tryCatchBlock : insnHandlers) {
+ Type catchType;
+ if (tryCatchBlock.type == null) {
+ catchType = Type.getObjectType("java/lang/Throwable");
+ } else {
+ catchType = Type.getObjectType(tryCatchBlock.type);
+ }
+ Frame<V> handler = newFrame(oldFrame);
+ handler.clearStack();
+ handler.push(interpreter.newExceptionValue(tryCatchBlock, handler, catchType));
+ checkFrame(insnList.indexOf(tryCatchBlock.handler), handler, /* requireFrame = */ true);
+ }
+ }
+
+ if (!hasNextJvmInsnOrFrame(insnIndex)) {
+ break;
+ }
+ } catch (AnalyzerException e) {
+ throw new AnalyzerException(
+ e.node, "Error at instruction " + insnIndex + ": " + e.getMessage(), e);
+ } catch (RuntimeException e) {
+ // DontCheck(IllegalCatch): can't be fixed, for backward compatibility.
+ throw new AnalyzerException(
+ insnNode, "Error at instruction " + insnIndex + ": " + e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Expands the {@link FrameNode} "instructions" of the given method into {@link Frame} objects and
+ * stores them at the corresponding indices of the {@link #frames} array. The expanded frames are
+ * also associated with the label and line number nodes immediately preceding each frame node.
+ *
+ * @param owner the internal name of the class to which 'method' belongs.
+ * @param method the method whose frames must be expanded.
+ * @param initialFrame the implicit initial frame of 'method'.
+ * @throws AnalyzerException if the stack map frames of 'method', i.e. its FrameNode
+ * "instructions", are invalid.
+ */
+ private void expandFrames(
+ final String owner, final MethodNode method, final Frame<V> initialFrame)
+ throws AnalyzerException {
+ int lastJvmOrFrameInsnIndex = -1;
+ Frame<V> currentFrame = initialFrame;
+ int currentInsnIndex = 0;
+ for (AbstractInsnNode insnNode : method.instructions) {
+ if (insnNode instanceof FrameNode) {
+ try {
+ currentFrame = expandFrame(owner, currentFrame, (FrameNode) insnNode);
+ } catch (AnalyzerException e) {
+ throw new AnalyzerException(
+ e.node, "Error at instruction " + currentInsnIndex + ": " + e.getMessage(), e);
+ }
+ for (int index = lastJvmOrFrameInsnIndex + 1; index <= currentInsnIndex; ++index) {
+ getFrames()[index] = currentFrame;
+ }
+ }
+ if (isJvmInsnNode(insnNode) || insnNode instanceof FrameNode) {
+ lastJvmOrFrameInsnIndex = currentInsnIndex;
+ }
+ currentInsnIndex += 1;
+ }
+ }
+
+ /**
+ * Returns the expanded representation of the given {@link FrameNode}.
+ *
+ * @param owner the internal name of the class to which 'frameNode' belongs.
+ * @param previousFrame the frame before 'frameNode', in expanded form.
+ * @param frameNode a possibly compressed stack map frame.
+ * @return the expanded version of 'frameNode'.
+ * @throws AnalyzerException if 'frameNode' is invalid.
+ */
+ private Frame<V> expandFrame(
+ final String owner, final Frame<V> previousFrame, final FrameNode frameNode)
+ throws AnalyzerException {
+ Frame<V> frame = newFrame(previousFrame);
+ List<Object> locals = frameNode.local == null ? Collections.emptyList() : frameNode.local;
+ int currentLocal = currentLocals;
+ switch (frameNode.type) {
+ case Opcodes.F_NEW:
+ case Opcodes.F_FULL:
+ currentLocal = 0;
+ // fall through
+ case Opcodes.F_APPEND:
+ for (Object type : locals) {
+ V value = newFrameValue(owner, frameNode, type);
+ if (currentLocal + value.getSize() > frame.getLocals()) {
+ throw new AnalyzerException(frameNode, "Cannot append more locals than maxLocals");
+ }
+ frame.setLocal(currentLocal++, value);
+ if (value.getSize() == 2) {
+ frame.setLocal(currentLocal++, interpreter.newValue(null));
+ }
+ }
+ break;
+ case Opcodes.F_CHOP:
+ for (Object unusedType : locals) {
+ if (currentLocal <= 0) {
+ throw new AnalyzerException(frameNode, "Cannot chop more locals than defined");
+ }
+ if (currentLocal > 1 && frame.getLocal(currentLocal - 2).getSize() == 2) {
+ currentLocal -= 2;
+ } else {
+ currentLocal -= 1;
+ }
+ }
+ break;
+ case Opcodes.F_SAME:
+ case Opcodes.F_SAME1:
+ break;
+ default:
+ throw new AnalyzerException(frameNode, "Illegal frame type " + frameNode.type);
+ }
+ currentLocals = currentLocal;
+ while (currentLocal < frame.getLocals()) {
+ frame.setLocal(currentLocal++, interpreter.newValue(null));
+ }
+
+ List<Object> stack = frameNode.stack == null ? Collections.emptyList() : frameNode.stack;
+ frame.clearStack();
+ for (Object type : stack) {
+ frame.push(newFrameValue(owner, frameNode, type));
+ }
+ return frame;
+ }
+
+ /**
+ * Creates a new {@link Value} that represents the given stack map frame type.
+ *
+ * @param owner the internal name of the class to which 'frameNode' belongs.
+ * @param frameNode the stack map frame to which 'type' belongs.
+ * @param type an Integer, String or LabelNode object representing a primitive, reference or
+ * uninitialized a stack map frame type, respectively. See {@link FrameNode}.
+ * @return a value that represents the given type.
+ * @throws AnalyzerException if 'type' is an invalid stack map frame type.
+ */
+ private V newFrameValue(final String owner, final FrameNode frameNode, final Object type)
+ throws AnalyzerException {
+ if (type == Opcodes.TOP) {
+ return interpreter.newValue(null);
+ } else if (type == Opcodes.INTEGER) {
+ return interpreter.newValue(Type.INT_TYPE);
+ } else if (type == Opcodes.FLOAT) {
+ return interpreter.newValue(Type.FLOAT_TYPE);
+ } else if (type == Opcodes.LONG) {
+ return interpreter.newValue(Type.LONG_TYPE);
+ } else if (type == Opcodes.DOUBLE) {
+ return interpreter.newValue(Type.DOUBLE_TYPE);
+ } else if (type == Opcodes.NULL) {
+ return interpreter.newOperation(new InsnNode(Opcodes.ACONST_NULL));
+ } else if (type == Opcodes.UNINITIALIZED_THIS) {
+ return interpreter.newValue(Type.getObjectType(owner));
+ } else if (type instanceof String) {
+ return interpreter.newValue(Type.getObjectType((String) type));
+ } else if (type instanceof LabelNode) {
+ AbstractInsnNode referencedNode = (LabelNode) type;
+ while (referencedNode != null && !isJvmInsnNode(referencedNode)) {
+ referencedNode = referencedNode.getNext();
+ }
+ if (referencedNode == null || referencedNode.getOpcode() != Opcodes.NEW) {
+ throw new AnalyzerException(frameNode, "LabelNode does not designate a NEW instruction");
+ }
+ return interpreter.newValue(Type.getObjectType(((TypeInsnNode) referencedNode).desc));
+ }
+ throw new AnalyzerException(frameNode, "Illegal stack map frame value " + type);
+ }
+
+ /**
+ * Checks that the given frame is compatible with the frame at the given instruction index, if
+ * any. If there is no frame at this instruction index and none is required, the frame at
+ * 'insnIndex' is set to the given frame. Otherwise, if the merge of the two frames is not equal
+ * to the current frame at 'insnIndex', an exception is thrown.
+ *
+ * @param insnIndex an instruction index.
+ * @param frame a frame. This frame is left unchanged by this method.
+ * @param requireFrame whether a frame must already exist or not in {@link #frames} at
+ * 'insnIndex'.
+ * @throws AnalyzerException if the frames have incompatible sizes or if the frame at 'insnIndex'
+ * is missing (if required) or not compatible with 'frame'.
+ */
+ private void checkFrame(final int insnIndex, final Frame<V> frame, final boolean requireFrame)
+ throws AnalyzerException {
+ Frame<V> oldFrame = getFrames()[insnIndex];
+ if (oldFrame == null) {
+ if (requireFrame) {
+ throw new AnalyzerException(null, "Expected stack map frame at instruction " + insnIndex);
+ }
+ getFrames()[insnIndex] = newFrame(frame);
+ } else {
+ String error = checkMerge(frame, oldFrame);
+ if (error != null) {
+ throw new AnalyzerException(
+ null,
+ "Stack map frame incompatible with frame at instruction "
+ + insnIndex
+ + " ("
+ + error
+ + ")");
+ }
+ }
+ }
+
+ /**
+ * Checks that merging the two given frames would not produce any change, i.e. that the types in
+ * the source frame are sub types of the corresponding types in the destination frame.
+ *
+ * @param srcFrame a source frame. This frame is left unchanged by this method.
+ * @param dstFrame a destination frame. This frame is left unchanged by this method.
+ * @return an error message if the frames have incompatible sizes, or if a type in the source
+ * frame is not a sub type of the corresponding type in the destination frame. Returns
+ * {@literal null} otherwise.
+ */
+ private String checkMerge(final Frame<V> srcFrame, final Frame<V> dstFrame) {
+ int numLocals = srcFrame.getLocals();
+ if (numLocals != dstFrame.getLocals()) {
+ throw new AssertionError();
+ }
+ for (int i = 0; i < numLocals; ++i) {
+ V v = interpreter.merge(srcFrame.getLocal(i), dstFrame.getLocal(i));
+ if (!v.equals(dstFrame.getLocal(i))) {
+ return "incompatible types at local "
+ + i
+ + ": "
+ + srcFrame.getLocal(i)
+ + " and "
+ + dstFrame.getLocal(i);
+ }
+ }
+ int numStack = srcFrame.getStackSize();
+ if (numStack != dstFrame.getStackSize()) {
+ return "incompatible stack heights";
+ }
+ for (int i = 0; i < numStack; ++i) {
+ V v = interpreter.merge(srcFrame.getStack(i), dstFrame.getStack(i));
+ if (!v.equals(dstFrame.getStack(i))) {
+ return "incompatible types at stack item "
+ + i
+ + ": "
+ + srcFrame.getStack(i)
+ + " and "
+ + dstFrame.getStack(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Ends the control flow graph at the given instruction. This method checks that there is an
+ * existing frame for the next instruction, if any.
+ *
+ * @param insnIndex an instruction index.
+ * @throws AnalyzerException if 'insnIndex' is not the last instruction and there is no frame at
+ * 'insnIndex' + 1 in {@link #getFrames}.
+ */
+ private void endControlFlow(final int insnIndex) throws AnalyzerException {
+ if (hasNextJvmInsnOrFrame(insnIndex) && getFrames()[insnIndex + 1] == null) {
+ throw new AnalyzerException(
+ null, "Expected stack map frame at instruction " + (insnIndex + 1));
+ }
+ }
+
+ /**
+ * Returns true if the given instruction is followed by a JVM instruction or a by stack map frame.
+ *
+ * @param insnIndex an instruction index.
+ * @return true if 'insnIndex' is followed by a JVM instruction or a by stack map frame.
+ */
+ private boolean hasNextJvmInsnOrFrame(final int insnIndex) {
+ AbstractInsnNode insn = insnList.get(insnIndex).getNext();
+ while (insn != null) {
+ if (isJvmInsnNode(insn) || insn instanceof FrameNode) {
+ return true;
+ }
+ insn = insn.getNext();
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the given instruction node corresponds to a real JVM instruction.
+ *
+ * @param insnNode an instruction node.
+ * @return true except for label, line number and stack map frame nodes.
+ */
+ private static boolean isJvmInsnNode(final AbstractInsnNode insnNode) {
+ return insnNode.getOpcode() >= 0;
+ }
+}
diff --git a/asm-util/src/test/java/org/objectweb/asm/util/CheckAnnotationAdapterTest.java b/asm-util/src/test/java/org/objectweb/asm/util/CheckAnnotationAdapterTest.java
index 377150ae..8276a0f8 100644
--- a/asm-util/src/test/java/org/objectweb/asm/util/CheckAnnotationAdapterTest.java
+++ b/asm-util/src/test/java/org/objectweb/asm/util/CheckAnnotationAdapterTest.java
@@ -32,7 +32,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
-import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.test.AsmTest;
@@ -41,7 +40,7 @@ import org.objectweb.asm.test.AsmTest;
*
* @author Eric Bruneton
*/
-class CheckAnnotationAdapterTest extends AsmTest implements Opcodes {
+class CheckAnnotationAdapterTest extends AsmTest {
@Test
void testVisit_illegalAnnotationName() {
diff --git a/asm-util/src/test/java/org/objectweb/asm/util/CheckFieldAdapterTest.java b/asm-util/src/test/java/org/objectweb/asm/util/CheckFieldAdapterTest.java
index 5f4e525a..ae332d57 100644
--- a/asm-util/src/test/java/org/objectweb/asm/util/CheckFieldAdapterTest.java
+++ b/asm-util/src/test/java/org/objectweb/asm/util/CheckFieldAdapterTest.java
@@ -33,7 +33,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
-import org.objectweb.asm.Opcodes;
import org.objectweb.asm.TypeReference;
import org.objectweb.asm.test.AsmTest;
@@ -42,7 +41,7 @@ import org.objectweb.asm.test.AsmTest;
*
* @author Eric Bruneton
*/
-class CheckFieldAdapterTest extends AsmTest implements Opcodes {
+class CheckFieldAdapterTest extends AsmTest {
@Test
void testConstructor() {
diff --git a/asm-util/src/test/java/org/objectweb/asm/util/CheckFrameAnalyzerTest.java b/asm-util/src/test/java/org/objectweb/asm/util/CheckFrameAnalyzerTest.java
new file mode 100644
index 00000000..b6b4b553
--- /dev/null
+++ b/asm-util/src/test/java/org/objectweb/asm/util/CheckFrameAnalyzerTest.java
@@ -0,0 +1,362 @@
+// ASM: a very small and fast Java bytecode manipulation framework
+// Copyright (c) 2000-2011 INRIA, France Telecom
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// 3. Neither the name of the copyright holders nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+// THE POSSIBILITY OF SUCH DAMAGE.
+package org.objectweb.asm.util;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+
+import java.util.ArrayList;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.test.AsmTest;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.LabelNode;
+import org.objectweb.asm.tree.MethodNode;
+import org.objectweb.asm.tree.analysis.Analyzer;
+import org.objectweb.asm.tree.analysis.AnalyzerException;
+import org.objectweb.asm.tree.analysis.BasicValue;
+import org.objectweb.asm.tree.analysis.BasicVerifier;
+import org.objectweb.asm.tree.analysis.Frame;
+
+/**
+ * Unit tests for {@link CheckFrameAnalyzer}.
+ *
+ * @author Eric Bruneton
+ */
+class CheckFrameAnalyzerTest extends AsmTest {
+
+ private static final String CLASS_NAME = "C";
+
+ // Labels used to generate test cases.
+ private final Label label0 = new Label();
+
+ @Test
+ void testAnalyze_invalidJsr() {
+ MethodNode methodNode = new MethodNodeBuilder().jsr(label0).label(label0).vreturn().build();
+
+ Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
+
+ String message = assertThrows(AnalyzerException.class, analyze).getMessage();
+ assertTrue(message.contains("Error at instruction 0: JSR instructions are unsupported"));
+ }
+
+ @Test
+ void testAnalyze_invalidRet() {
+ MethodNode methodNode = new MethodNodeBuilder().ret(0).vreturn().build();
+
+ Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
+
+ String message = assertThrows(AnalyzerException.class, analyze).getMessage();
+ assertTrue(message.contains("Error at instruction 0: RET instructions are unsupported"));
+ }
+
+ @Test
+ void testAnalyze_missingFrameAtJumpTarget() {
+ MethodNode methodNode =
+ new MethodNodeBuilder().iconst_0().ifne(label0).iconst_0().label(label0).vreturn().build();
+
+ Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
+
+ String message = assertThrows(AnalyzerException.class, analyze).getMessage();
+ assertTrue(
+ message.contains("Error at instruction 1: Expected stack map frame at instruction 3"));
+ }
+
+ @Test
+ void testAnalyze_missingFrameAfterGoto() {
+ MethodNode methodNode =
+ new MethodNodeBuilder().nop().go(label0).nop().label(label0).vreturn().build();
+
+ Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
+
+ String message = assertThrows(AnalyzerException.class, analyze).getMessage();
+ assertTrue(
+ message.contains("Error at instruction 1: Expected stack map frame at instruction 2"));
+ }
+
+ @Test
+ void testAnalyze_illegalFrameType() {
+ MethodNode methodNode =
+ new MethodNodeBuilder()
+ .nop()
+ .go(label0)
+ .frame(123456, null, null)
+ .nop()
+ .label(label0)
+ .vreturn()
+ .build();
+
+ Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
+
+ String message = assertThrows(AnalyzerException.class, analyze).getMessage();
+ assertTrue(message.contains("Error at instruction 2: Illegal frame type 123456"));
+ }
+
+ @Test
+ void testAnalyze_invalidAppendFrame() {
+ MethodNode methodNode =
+ new MethodNodeBuilder(/* maxStack = */ 0, /* maxLocals = */ 1)
+ .nop()
+ .frame(Opcodes.F_APPEND, new Object[] {Opcodes.INTEGER}, null)
+ .vreturn()
+ .build();
+
+ Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
+
+ String message = assertThrows(AnalyzerException.class, analyze).getMessage();
+ assertTrue(
+ message.contains("Error at instruction 1: Cannot append more locals than maxLocals"));
+ }
+
+ @Test
+ void testAnalyze_invalidChopFrame() {
+ MethodNode methodNode =
+ new MethodNodeBuilder(/* maxStack = */ 0, /* maxLocals = */ 1)
+ .nop()
+ .frame(Opcodes.F_CHOP, new Object[] {null, null}, null)
+ .vreturn()
+ .build();
+
+ Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
+
+ String message = assertThrows(AnalyzerException.class, analyze).getMessage();
+ assertTrue(message.contains("Error at instruction 1: Cannot chop more locals than defined"));
+ }
+
+ @Test
+ void testAnalyze_illegalStackMapFrameValue() {
+ MethodNode methodNode =
+ new MethodNodeBuilder(/* maxStack = */ 0, /* maxLocals = */ 2)
+ .nop()
+ .frame(Opcodes.F_APPEND, new Object[] {new Object()}, null)
+ .vreturn()
+ .build();
+
+ Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
+
+ String message = assertThrows(AnalyzerException.class, analyze).getMessage();
+ assertTrue(
+ message.contains("Error at instruction 1: Illegal stack map frame value java.lang.Object"));
+ }
+
+ @Test
+ void testAnalyze_illegalLabelNodeStackMapFrameValue() {
+ MethodNode methodNode =
+ new MethodNodeBuilder(/* maxStack = */ 0, /* maxLocals = */ 2)
+ .nop()
+ .frame(Opcodes.F_APPEND, new Object[] {new LabelNode(label0)}, null)
+ .label(label0)
+ .vreturn()
+ .build();
+
+ Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
+
+ String message = assertThrows(AnalyzerException.class, analyze).getMessage();
+ assertTrue(
+ message.contains("Error at instruction 1: LabelNode does not designate a NEW instruction"));
+ }
+
+ @Test
+ void testAnalyze_frameAtJumpTargetHasIncompatibleStackHeight() {
+ MethodNode methodNode =
+ new MethodNodeBuilder()
+ .iconst_0()
+ .ifne(label0)
+ .iconst_0()
+ .label(label0)
+ .frame(Opcodes.F_SAME1, null, new Object[] {Opcodes.INTEGER})
+ .vreturn()
+ .build();
+
+ Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
+
+ String message = assertThrows(AnalyzerException.class, analyze).getMessage();
+ assertTrue(
+ message.contains(
+ "Error at instruction 1: Stack map frame incompatible with frame at instruction 3 "
+ + "(incompatible stack heights)"));
+ }
+
+ @Test
+ void testAnalyze_frameAtJumpTargetHasIncompatibleLocalValues() {
+ MethodNode methodNode =
+ new MethodNodeBuilder()
+ .iconst_0()
+ .ifne(label0)
+ .iconst_0()
+ .label(label0)
+ .frame(Opcodes.F_NEW, new Object[] {Opcodes.INTEGER}, null)
+ .vreturn()
+ .build();
+
+ Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
+
+ String message = assertThrows(AnalyzerException.class, analyze).getMessage();
+ assertTrue(
+ message.contains(
+ "Error at instruction 1: Stack map frame incompatible with frame at instruction 3 "
+ + "(incompatible types at local 0: R and I)"));
+ }
+
+ @Test
+ void testAnalyze_frameAtJumpTargetHasIncompatibleStackValues() {
+ MethodNode methodNode =
+ new MethodNodeBuilder()
+ .iconst_0()
+ .iconst_0()
+ .ifne(label0)
+ .iconst_0()
+ .iconst_0()
+ .label(label0)
+ .frame(Opcodes.F_NEW, new Object[] {"C"}, new Object[] {"C"})
+ .vreturn()
+ .build();
+
+ Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
+
+ String message = assertThrows(AnalyzerException.class, analyze).getMessage();
+ assertTrue(
+ message.contains(
+ "Error at instruction 2: Stack map frame incompatible with frame at instruction 5 "
+ + "(incompatible types at stack item 0: I and R)"));
+ }
+
+ /**
+ * Tests that the precompiled classes can be successfully analyzed from their existing stack map
+ * frames with a BasicVerifier.
+ *
+ * @throws AnalyzerException if the test class can't be analyzed.
+ */
+ @ParameterizedTest
+ @MethodSource(ALL_CLASSES_AND_LATEST_API)
+ void testAnalyze_basicVerifier(final PrecompiledClass classParameter, final Api apiParameter)
+ throws AnalyzerException {
+ assumeFalse(hasJsrOrRetInstructions(classParameter));
+ ClassNode classNode = computeFrames(classParameter);
+ Analyzer<BasicValue> analyzer = newAnalyzer();
+
+ ArrayList<Frame<? extends BasicValue>[]> methodFrames = new ArrayList<>();
+ for (MethodNode methodNode : classNode.methods) {
+ Frame<? extends BasicValue>[] result = analyzer.analyze(classNode.name, methodNode);
+ methodFrames.add(result);
+ }
+
+ for (int i = 0; i < classNode.methods.size(); ++i) {
+ Frame<? extends BasicValue>[] frames = methodFrames.get(i);
+ for (int j = 0; j < lastJvmInsnIndex(classNode.methods.get(i)); ++j) {
+ assertNotNull(frames[j]);
+ }
+ }
+ }
+
+ /**
+ * Tests that the precompiled classes can be successfully analyzed from their existing stack map
+ * frames with a BasicVerifier, even if the method node's max locals and max stack size are not
+ * set.
+ *
+ * @throws AnalyzerException if the test class can't be analyzed.
+ */
+ @ParameterizedTest
+ @MethodSource(ALL_CLASSES_AND_LATEST_API)
+ void testAnalyzeAndComputeMaxs_basicVerifier(
+ final PrecompiledClass classParameter, final Api apiParameter) throws AnalyzerException {
+ assumeFalse(hasJsrOrRetInstructions(classParameter));
+ ClassNode classNode = computeFrames(classParameter);
+ ArrayList<MethodMaxs> methodMaxs = MethodMaxs.getAndClear(classNode);
+ Analyzer<BasicValue> analyzer = newAnalyzer();
+
+ ArrayList<MethodMaxs> analyzedMethodMaxs = new ArrayList<>();
+ for (MethodNode methodNode : classNode.methods) {
+ analyzer.analyzeAndComputeMaxs(classNode.name, methodNode);
+ analyzedMethodMaxs.add(new MethodMaxs(methodNode.maxStack, methodNode.maxLocals));
+ }
+
+ for (int i = 0; i < analyzedMethodMaxs.size(); ++i) {
+ assertTrue(analyzedMethodMaxs.get(i).maxLocals >= methodMaxs.get(i).maxLocals);
+ assertTrue(analyzedMethodMaxs.get(i).maxStack >= methodMaxs.get(i).maxStack);
+ }
+ }
+
+ private static boolean hasJsrOrRetInstructions(final PrecompiledClass classParameter) {
+ return classParameter == PrecompiledClass.JDK3_ALL_INSTRUCTIONS
+ || classParameter == PrecompiledClass.JDK3_LARGE_METHOD;
+ }
+
+ private static ClassNode computeFrames(final PrecompiledClass classParameter) {
+ byte[] classFile = classParameter.getBytes();
+ ClassReader classReader = new ClassReader(classFile);
+ ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
+ classReader.accept(classWriter, 0);
+ classFile = classWriter.toByteArray();
+ ClassNode classNode = new ClassNode();
+ new ClassReader(classFile).accept(classNode, 0);
+ return classNode;
+ }
+
+ private static Analyzer<BasicValue> newAnalyzer() {
+ return new CheckFrameAnalyzer<>(new BasicVerifier());
+ }
+
+ private static int lastJvmInsnIndex(final MethodNode method) {
+ for (int i = method.instructions.size() - 1; i >= 0; --i) {
+ if (method.instructions.get(i).getOpcode() >= 0) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private static class MethodMaxs {
+
+ final int maxStack;
+ final int maxLocals;
+
+ MethodMaxs(final int maxStack, final int maxLocals) {
+ this.maxStack = maxStack;
+ this.maxLocals = maxLocals;
+ }
+
+ static ArrayList<MethodMaxs> getAndClear(final ClassNode classNode) {
+ ArrayList<MethodMaxs> methodMaxs = new ArrayList<>();
+ for (MethodNode methodNode : classNode.methods) {
+ methodMaxs.add(new MethodMaxs(methodNode.maxStack, methodNode.maxLocals));
+ methodNode.maxLocals = 0;
+ methodNode.maxStack = 0;
+ }
+ return methodMaxs;
+ }
+ }
+}
diff --git a/asm-util/src/test/java/org/objectweb/asm/util/CheckRecordComponentAdapterTest.java b/asm-util/src/test/java/org/objectweb/asm/util/CheckRecordComponentAdapterTest.java
index 7724a8a1..d406701b 100644
--- a/asm-util/src/test/java/org/objectweb/asm/util/CheckRecordComponentAdapterTest.java
+++ b/asm-util/src/test/java/org/objectweb/asm/util/CheckRecordComponentAdapterTest.java
@@ -33,7 +33,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
-import org.objectweb.asm.Opcodes;
import org.objectweb.asm.TypeReference;
import org.objectweb.asm.test.AsmTest;
@@ -42,7 +41,7 @@ import org.objectweb.asm.test.AsmTest;
*
* @author Eric Bruneton
*/
-class CheckRecordComponentAdapterTest extends AsmTest implements Opcodes {
+class CheckRecordComponentAdapterTest extends AsmTest {
@Test
void testConstructor() {
diff --git a/asm-util/src/test/java/org/objectweb/asm/util/MethodNodeBuilder.java b/asm-util/src/test/java/org/objectweb/asm/util/MethodNodeBuilder.java
new file mode 100644
index 00000000..936d4ce9
--- /dev/null
+++ b/asm-util/src/test/java/org/objectweb/asm/util/MethodNodeBuilder.java
@@ -0,0 +1,109 @@
+// ASM: a very sm14all and fast Java bytecode manipulation framework
+// Copyright (c) 2000-2011 INRIA, France Telecom
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// 3. Neither the name of the copyright holders nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+// THE POSSIBILITY OF SUCH DAMAGE.
+package org.objectweb.asm.util;
+
+import java.util.Arrays;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.FrameNode;
+import org.objectweb.asm.tree.MethodNode;
+
+/**
+ * A builder of {@link MethodNode}, to construct test cases for unit tests.
+ *
+ * @author Eric Bruneton
+ */
+final class MethodNodeBuilder {
+
+ private final MethodNode methodNode;
+
+ MethodNodeBuilder() {
+ this(/* maxStack = */ 10, /* maxLocals = */ 10);
+ }
+
+ MethodNodeBuilder(final int maxStack, final int maxLocals) {
+ methodNode = new MethodNode(Opcodes.ACC_PUBLIC, "m", "()V", null, null);
+ methodNode.maxStack = maxStack;
+ methodNode.maxLocals = maxLocals;
+ methodNode.visitCode();
+ }
+
+ MethodNodeBuilder nop() {
+ methodNode.visitInsn(Opcodes.NOP);
+ return this;
+ }
+
+ MethodNodeBuilder iconst_0() {
+ methodNode.visitInsn(Opcodes.ICONST_0);
+ return this;
+ }
+
+ MethodNodeBuilder vreturn() {
+ methodNode.visitInsn(Opcodes.RETURN);
+ return this;
+ }
+
+ MethodNodeBuilder label(final Label label) {
+ methodNode.visitLabel(label);
+ return this;
+ }
+
+ MethodNodeBuilder go(final Label label) {
+ methodNode.visitJumpInsn(Opcodes.GOTO, label);
+ return this;
+ }
+
+ MethodNodeBuilder jsr(final Label label) {
+ methodNode.visitJumpInsn(Opcodes.JSR, label);
+ return this;
+ }
+
+ MethodNodeBuilder ret(final int varIndex) {
+ methodNode.visitVarInsn(Opcodes.RET, varIndex);
+ return this;
+ }
+
+ MethodNodeBuilder ifne(final Label label) {
+ methodNode.visitJumpInsn(Opcodes.IFNE, label);
+ return this;
+ }
+
+ MethodNodeBuilder frame(final int type, final Object[] local, final Object[] stack) {
+ FrameNode frameNode = new FrameNode(Opcodes.F_NEW, 0, null, 0, null);
+ frameNode.type = type;
+ frameNode.local = local == null ? null : Arrays.asList(local);
+ frameNode.stack = stack == null ? null : Arrays.asList(stack);
+ methodNode.instructions.add(frameNode);
+ return this;
+ }
+
+ MethodNode build() {
+ methodNode.visitEnd();
+ return methodNode;
+ }
+}