Java tutorial
/******************************************************************************* * Copyright (c) 2015 Red Hat. All rights reserved. This program and the accompanying materials are * made available under the terms of the Eclipse Public License v1.0 which accompanies this * distribution, and is available at http://www.eclipse.org/legal/epl-v10.html * * Contributors: Red Hat - Initial Contribution *******************************************************************************/ package org.lambdamatic.analyzer.ast; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Array; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.lang3.ClassUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.lambdamatic.analyzer.LambdaExpressionAnalyzer; import org.lambdamatic.analyzer.ast.node.Node; import org.lambdamatic.analyzer.ast.node.ArrayVariable; import org.lambdamatic.analyzer.ast.node.Assignment; import org.lambdamatic.analyzer.ast.node.BooleanLiteral; import org.lambdamatic.analyzer.ast.node.CapturedArgument; import org.lambdamatic.analyzer.ast.node.CapturedArgumentRef; import org.lambdamatic.analyzer.ast.node.ClassLiteral; import org.lambdamatic.analyzer.ast.node.CompoundExpression; import org.lambdamatic.analyzer.ast.node.CompoundExpression.CompoundExpressionOperator; import org.lambdamatic.analyzer.ast.node.ControlFlowStatement; import org.lambdamatic.analyzer.ast.node.Expression; import org.lambdamatic.analyzer.ast.node.Expression.ExpressionType; import org.lambdamatic.analyzer.ast.node.ExpressionFactory; import org.lambdamatic.analyzer.ast.node.ExpressionStatement; import org.lambdamatic.analyzer.ast.node.FieldAccess; import org.lambdamatic.analyzer.ast.node.LambdaExpression; import org.lambdamatic.analyzer.ast.node.LocalVariable; import org.lambdamatic.analyzer.ast.node.MethodInvocation; import org.lambdamatic.analyzer.ast.node.NullLiteral; import org.lambdamatic.analyzer.ast.node.NumberLiteral; import org.lambdamatic.analyzer.ast.node.ObjectInstanciation; import org.lambdamatic.analyzer.ast.node.Operation; import org.lambdamatic.analyzer.ast.node.Operation.Operator; import org.lambdamatic.analyzer.ast.node.ReturnStatement; import org.lambdamatic.analyzer.ast.node.Statement; import org.lambdamatic.analyzer.exception.AnalyzeException; import org.objectweb.asm.ClassReader; import org.objectweb.asm.Handle; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.signature.SignatureReader; import org.objectweb.asm.signature.SignatureVisitor; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.IntInsnNode; import org.objectweb.asm.tree.InvokeDynamicInsnNode; import org.objectweb.asm.tree.JumpInsnNode; import org.objectweb.asm.tree.LabelNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.LocalVariableNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.TypeInsnNode; import org.objectweb.asm.tree.VarInsnNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An internal utility class that uses ASM to read the bytecode of a (desugared lambda expression) * method and converts it into an {@link Node}. * * @author Xavier Coulon * */ public class LambdaExpressionReader { /** * A {@link SignatureVisitor} that retrieves the class type. * * @author Xavier Coulon * */ private static final class ClassTypeRetriever extends SignatureVisitor { /** the type (once resolved). */ private Class<?> type = null; /** * Constructor. */ ClassTypeRetriever() { super(Opcodes.ASM5); } public Class<?> getType() { return this.type; } @Override public void visitClassType(final String className) { try { this.type = Class.forName(className.replace("/", ".")); } catch (final ClassNotFoundException e) { throw new AnalyzeException("Failed to retrieve class with name " + className, e); } } @Override public void visitBaseType(final char descriptor) { switch (Type.getType(Character.toString(descriptor)).getSort()) { case Type.VOID: this.type = void.class; break; case Type.BYTE: this.type = byte.class; break; case Type.BOOLEAN: this.type = boolean.class; break; case Type.SHORT: this.type = short.class; break; case Type.INT: this.type = int.class; break; case Type.DOUBLE: this.type = double.class; break; case Type.FLOAT: this.type = float.class; break; case Type.LONG: this.type = long.class; break; default: throw new AnalyzeException( "Failed to retrieve primitive class with descriptor '" + descriptor + "'"); } } } /** The usual Logger. */ static final Logger LOGGER = LoggerFactory.getLogger(LambdaExpressionReader.class); /** * Reads the given {@link List} of (bytecode) {@link AbstractInsnNode} located at the known * {@link SerializedLambdaInfo} and computes a simplified {@link Statement} based tree * representing the initial lambda expression. * * @param lambdaInfo the info about the Lambda expression synthetic implementation * @return the {@link List} of {@link Statement} found while reading the bytecode * @throws IOException if a problem occurred while reading the underlying {@link Class} */ public Pair<List<Statement>, List<LocalVariable>> readBytecodeStatement(final SerializedLambdaInfo lambdaInfo) throws IOException { final LambdaExpressionClassVisitor desugaredExpressionVisitor = new LambdaExpressionClassVisitor( lambdaInfo); try (final InputStream serializedLambdaStream = Thread.currentThread().getContextClassLoader() .getResourceAsStream(lambdaInfo.getImplClassName().replace('.', '/') + ".class")) { final ClassReader classReader = new ClassReader(serializedLambdaStream); classReader.accept(desugaredExpressionVisitor, 0); final InsnList instructions = desugaredExpressionVisitor.getInstructions(); final LocalVariables localVariables = new LocalVariables( desugaredExpressionVisitor.getLocalVariables()); final Map<String, AbstractInsnNode> labels = desugaredExpressionVisitor.getLabels(); final InsnCursor insnCursor = new InsnCursor(instructions, labels); // we must set the cursor on the first instruction before calling the readStatementSubTree() // method insnCursor.next(); final List<Statement> statements = readStatements(insnCursor, new Stack<>(), lambdaInfo.getCapturedArguments(), localVariables); // now, let's identify the lambda expression arguments (_excluding_ the captured arguments) final List<LocalVariable> lambdaExpressionArguments = localVariables.toLocalVariables(); return new ImmutablePair<>(statements, lambdaExpressionArguments); } } /** * Reads the bytecode from the given {@link InsnCursor}'s <strong>current position</strong>, until * there is no further instruction to proceed. It is the responsability of the caller to set the * cursor position. * * @param insnCursor the instruction cursor used to read the bytecode. * @param expressionStack the expression stack to put on or pop from. * @param localVariables the local variables * @return a {@link List} of {@link Statement} containing the {@link Statement} */ private List<Statement> readStatements(final InsnCursor insnCursor, final Stack<Expression> expressionStack, final List<CapturedArgument> capturedArguments, final LocalVariables localVariables) { final List<Statement> statements = new ArrayList<>(); while (insnCursor.hasCurrent()) { final AbstractInsnNode currentInstruction = insnCursor.getCurrent(); switch (currentInstruction.getType()) { case AbstractInsnNode.VAR_INSN: final VarInsnNode varInstruction = (VarInsnNode) currentInstruction; switch (currentInstruction.getOpcode()) { // load a reference onto the stack from a local variable case Opcodes.ALOAD: case Opcodes.ILOAD: // load an int value from a local variable // Note: The 'var' operand is the index of a local variable // all captured arguments come before the local variable in the method signature, // which means that the local variables table is empty on the first slots which are // "allocated" // for the captured arguments. if (varInstruction.var < capturedArguments.size()) { // if the variable index matches a captured argument // note: not using actual captured argument but rather, use a _reference_ to it. final Object capturedArgumentValue = capturedArguments.get(varInstruction.var).getValue(); final Class<?> capturedArgumentValueType = capturedArgumentValue != null ? capturedArgumentValue.getClass() : Object.class; final CapturedArgumentRef capturedArgumentRef = new CapturedArgumentRef(varInstruction.var, capturedArgumentValueType); expressionStack.add(capturedArgumentRef); } else { // the variable index matches a local variable final LocalVariableNode var = localVariables.load(varInstruction.var); expressionStack.add(new LocalVariable(var.index, var.name, readSignature(var.desc))); } break; case Opcodes.ASTORE: // store a reference into a local variable localVariables.store(varInstruction.var); break; default: throw new AnalyzeException( "Unexpected Variable instruction code: " + varInstruction.getOpcode()); } break; case AbstractInsnNode.LDC_INSN: // let's move this instruction on top of the stack until it // is used as an argument during a method call final LdcInsnNode ldcInsnNode = (LdcInsnNode) currentInstruction; final Expression constant = ExpressionFactory.getExpression(ldcInsnNode.cst); LOGGER.trace("Stacking constant {}", constant); expressionStack.add(constant); break; case AbstractInsnNode.FIELD_INSN: final FieldInsnNode fieldInsnNode = (FieldInsnNode) currentInstruction; switch (fieldInsnNode.getOpcode()) { case Opcodes.GETSTATIC: final Type ownerType = Type.getType(fieldInsnNode.desc); final FieldAccess staticFieldAccess = new FieldAccess(new ClassLiteral(getType(ownerType)), fieldInsnNode.name); expressionStack.add(staticFieldAccess); break; case Opcodes.GETFIELD: final Expression fieldAccessParent = expressionStack.pop(); final FieldAccess fieldAccess = new FieldAccess(fieldAccessParent, fieldInsnNode.name); expressionStack.add(fieldAccess); break; case Opcodes.PUTFIELD: final Expression fieldAssignationValue = expressionStack.pop(); final Expression parentSource = expressionStack.pop(); final FieldAccess source = new FieldAccess(parentSource, fieldInsnNode.name); final Assignment assignmentExpression = new Assignment(source, fieldAssignationValue); statements.add(new ExpressionStatement(assignmentExpression)); break; default: throw new AnalyzeException("Unexpected field instruction type: " + fieldInsnNode.getOpcode()); } break; case AbstractInsnNode.METHOD_INSN: final MethodInsnNode methodInsnNode = (MethodInsnNode) currentInstruction; final Type[] argumentTypes = Type.getArgumentTypes(methodInsnNode.desc); final List<Expression> args = new ArrayList<>(); final List<Class<?>> parameterTypes = new ArrayList<>(); Stream.of(argumentTypes).forEach(argumentType -> { final Expression arg = expressionStack.pop(); final String argumentClassName = argumentType.getClassName(); args.add(castOperand(arg, argumentClassName)); try { parameterTypes.add(ClassUtils.getClass(argumentClassName)); } catch (Exception e) { throw new AnalyzeException("Failed to find class '" + argumentClassName + "'", e); } }); // arguments appear in reverse order in the bytecode Collections.reverse(args); switch (methodInsnNode.getOpcode()) { case Opcodes.INVOKEINTERFACE: case Opcodes.INVOKEVIRTUAL: case Opcodes.INVOKESPECIAL: // object instantiation if (methodInsnNode.name.equals("<init>")) { final ObjectInstanciation objectVariable = (ObjectInstanciation) expressionStack.pop(); objectVariable.setInitArguments(args); } else { final Expression sourceExpression = expressionStack.pop(); final Method javaMethod = ReflectionUtils.findJavaMethod(sourceExpression.getJavaType(), methodInsnNode.name, parameterTypes); final Class<?> returnType = findReturnType(insnCursor, javaMethod); final MethodInvocation invokedMethod = new MethodInvocation(sourceExpression, javaMethod, returnType, args); expressionStack.add(invokedMethod); } break; case Opcodes.INVOKESTATIC: final Type type = Type.getObjectType(methodInsnNode.owner); try { final Class<?> sourceClass = Class.forName(type.getClassName()); final Method javaMethod = ReflectionUtils.findJavaMethod(sourceClass, methodInsnNode.name, parameterTypes); final Class<?> returnType = findReturnType(insnCursor, javaMethod); final MethodInvocation invokedStaticMethod = new MethodInvocation( new ClassLiteral(sourceClass), javaMethod, returnType, args); expressionStack.add(invokedStaticMethod); } catch (ClassNotFoundException e) { throw new AnalyzeException("Failed to retrieve class for " + methodInsnNode.owner, e); } break; default: throw new AnalyzeException("Unexpected method invocation type: " + methodInsnNode.getOpcode()); } break; case AbstractInsnNode.INVOKE_DYNAMIC_INSN: final InvokeDynamicInsnNode invokeDynamicInsnNode = (InvokeDynamicInsnNode) currentInstruction; final Handle handle = (Handle) invokeDynamicInsnNode.bsmArgs[1]; final int argNumber = Type.getArgumentTypes(invokeDynamicInsnNode.desc).length; final List<CapturedArgumentRef> lambdaArgs = new ArrayList<>(); for (int i = 0; i < argNumber; i++) { final Expression expr = expressionStack.pop(); if (expr.getExpressionType() != ExpressionType.CAPTURED_ARGUMENT_REF) { throw new AnalyzeException("Unexpected argument type when following InvokeDynamic call: " + expr.getExpressionType()); } lambdaArgs.add((CapturedArgumentRef) expr); // , expr.getValue() } Collections.reverse(lambdaArgs); final EmbeddedSerializedLambdaInfo lambdaInfo = new EmbeddedSerializedLambdaInfo(handle.getOwner(), handle.getName(), handle.getDesc(), lambdaArgs, capturedArguments); final LambdaExpression lambdaExpression = LambdaExpressionAnalyzer.getInstance() .analyzeExpression(lambdaInfo); expressionStack.add(lambdaExpression); break; case AbstractInsnNode.JUMP_INSN: statements.addAll( readJumpInstruction(insnCursor, expressionStack, capturedArguments, localVariables)); return statements; case AbstractInsnNode.INT_INSN: readIntInstruction((IntInsnNode) currentInstruction, expressionStack, localVariables); break; case AbstractInsnNode.INSN: final List<Statement> instructionStatement = readInstruction(insnCursor, expressionStack, capturedArguments, localVariables); statements.addAll(instructionStatement); break; case AbstractInsnNode.TYPE_INSN: readTypeInstruction((TypeInsnNode) currentInstruction, expressionStack, localVariables); break; default: throw new AnalyzeException( "This is embarrassing... We've reached an unexpected instruction operator: " + currentInstruction.getType()); } insnCursor.next(); } return statements; } /** * Checks if an operation of type OpsCode.CHECKCAST is following this operation, in which case it * provides valuable info about the actual returned type of the method * * @param insnCursor the {@link InsnCursor} * @param javaMethod the current Java {@link Method} * @return the return type of the given method. */ private static Class<?> findReturnType(final InsnCursor insnCursor, final Method javaMethod) { if (insnCursor.hasNext()) { final AbstractInsnNode nextOp = insnCursor.getNext(); if (nextOp.getOpcode() == Opcodes.CHECKCAST) { final TypeInsnNode checkCastInsnNode = (TypeInsnNode) nextOp; try { return Class.forName(Type.getObjectType(checkCastInsnNode.desc).getClassName()); } catch (ClassNotFoundException e) { throw new AnalyzeException("Failed to retrieve class for " + checkCastInsnNode.desc, e); } } else { // move cursor position backwards insnCursor.getPrevious(); } } return javaMethod.getReturnType(); } /** * @param typeName the fully qualified name of the type (primitive or Objecttype ) * @return the returned Java type. * @throws AnalyzeException if the Class could not be found */ private static Class<?> getArrayType(final Type type) { try { // TODO: can we avoid the array instantiation ? final Type elementType = Type.getType(type.getDescriptor()); return Array.newInstance(Class.forName(elementType.getClassName()), 0).getClass(); } catch (ClassNotFoundException e) { throw new AnalyzeException("Failed to retrieve type named " + type.getClassName(), e); } } /** * @param typeName the fully qualified name of the type (primitive or Objecttype ) * @return the returned Java type. * @throws AnalyzeException if the Class could not be found */ private static Class<?> getType(final Type type) { try { if (type.getSort() == Type.ARRAY) { // TODO: can we avoid the array instantiation ? final Type elementType = type.getElementType(); return Array.newInstance(Class.forName(elementType.getClassName()), 0).getClass(); } else { switch (type.getClassName()) { case "boolean": return boolean.class; case "byte": return byte.class; case "short": return short.class; case "int": return int.class; case "long": return long.class; case "float": return float.class; case "double": return double.class; case "char": return char.class; case "void": return void.class; default: return Class.forName(type.getClassName()); } } } catch (ClassNotFoundException e) { throw new AnalyzeException("Failed to retrieve type named " + type.getClassName(), e); } } /** * Reads the given type signature and returns a proper {@link Class}. * * @param desc the type signature to read * @return the associated {@link Class} * */ static Class<?> readSignature(final String desc) { final SignatureReader signatureReader = new SignatureReader(desc); final ClassTypeRetriever classTypeRetriever = new ClassTypeRetriever(); signatureReader.accept(classTypeRetriever); return classTypeRetriever.getType(); } /** * Reads the current {@link InsnNode} instruction and returns a {@link Statement} or {@code null} * if the instruction is not a full statement (in that case, the instruction is stored in the * given Expression {@link Stack}). * * @param insnNode the instruction to read * @param expressionStack the expression stack to put on or pop from. * @param localVariables the local variables * @return a {@link List} of {@link Statement} or empty list if no {@link Statement} was created * after reading the current instruction. * @see <a href="https://en.wikipedia.org/wiki/Java_bytecode_instruction_listings">Java bytcode * instruction listings on Wikipedia</a> */ private List<Statement> readInstruction(final InsnCursor insnCursor, final Stack<Expression> expressionStack, final List<CapturedArgument> capturedArguments, final LocalVariables localVariables) { final List<Statement> statements = new ArrayList<>(); final AbstractInsnNode insnNode = insnCursor.getCurrent(); switch (insnNode.getOpcode()) { // return a reference from a method case Opcodes.ARETURN: // return an integer from a method case Opcodes.IRETURN: statements.add(new ReturnStatement(expressionStack.pop())); break; // return void from method case Opcodes.RETURN: // wrap all pending expressions into ExpressionStatements while (!expressionStack.isEmpty()) { final Expression pendingExpression = expressionStack.pop(); statements.add(new ExpressionStatement(pendingExpression)); } break; // push a null reference onto the stack case Opcodes.ACONST_NULL: expressionStack.add(new NullLiteral()); break; // load the int value 0 onto the stack case Opcodes.ICONST_0: // applies for byte, short, int and boolean expressionStack.add(new NumberLiteral(0)); break; // load the int value 1 onto the stack case Opcodes.ICONST_1: // applies for byte, short, int and boolean expressionStack.add(new NumberLiteral(1)); break; // load the int value 2 onto the stack case Opcodes.ICONST_2: expressionStack.add(new NumberLiteral(2)); break; // load the int value 3 onto the stack case Opcodes.ICONST_3: expressionStack.add(new NumberLiteral(3)); break; // load the int value 4 onto the stack case Opcodes.ICONST_4: expressionStack.add(new NumberLiteral(4)); break; // load the int value 5 onto the stack case Opcodes.ICONST_5: expressionStack.add(new NumberLiteral(5)); break; // push the long 0 onto the stack case Opcodes.LCONST_0: expressionStack.add(new NumberLiteral(0L)); break; // push the long 1 onto the stack case Opcodes.LCONST_1: expressionStack.add(new NumberLiteral(1L)); break; // push the 0.0f onto the stack case Opcodes.FCONST_0: expressionStack.add(new NumberLiteral(0f)); break; // push the 1.0f onto the stack case Opcodes.FCONST_1: expressionStack.add(new NumberLiteral(1f)); break; // push the 2.0f onto the stack case Opcodes.FCONST_2: expressionStack.add(new NumberLiteral(2f)); break; // push the constant 0.0 onto the stack case Opcodes.DCONST_0: expressionStack.add(new NumberLiteral(0d)); break; // push the constant 1.0 onto the stack case Opcodes.DCONST_1: expressionStack.add(new NumberLiteral(1d)); break; // compare two longs values case Opcodes.LCMP: // compare two doubles case Opcodes.DCMPL: // compare two doubles case Opcodes.DCMPG: // compare two floats case Opcodes.FCMPL: // compare two floats case Opcodes.FCMPG: statements.addAll( readJumpInstruction(insnCursor.next(), expressionStack, capturedArguments, localVariables)); break; // add 2 ints case Opcodes.IADD: expressionStack.add(readOperation(Operator.ADD, expressionStack)); break; // int subtract case Opcodes.ISUB: expressionStack.add(readOperation(Operator.SUBTRACT, expressionStack)); break; // multiply 2 integers case Opcodes.IMUL: expressionStack.add(readOperation(Operator.MULTIPLY, expressionStack)); break; // divide 2 integers case Opcodes.IDIV: expressionStack.add(readOperation(Operator.DIVIDE, expressionStack)); break; // negate int case Opcodes.INEG: expressionStack.add(inverseInteger(expressionStack)); break; // discard the top value on the stack case Opcodes.POP: statements.add(new ExpressionStatement(expressionStack.pop())); break; // duplicate the value on top of the stack case Opcodes.DUP: expressionStack.push(expressionStack.peek()); break; // insert a copy of the top value into the stack two values from the top. case Opcodes.DUP_X1: expressionStack.add(expressionStack.size() - 2, expressionStack.peek()); break; // store into a reference in an array case Opcodes.AASTORE: readArrayStoreInstruction(insnNode, expressionStack); break; // converts Float to Double -> ignored. case Opcodes.F2D: break; default: throw new AnalyzeException( "Bytecode instruction with OpCode '" + insnNode.getOpcode() + "' is not supported."); } return statements; } /** * Reads the {@link Operation} using the given <code>operator</code> and the 2 top-most elements * in the given <code>expressionStack</code>. * * @param operator the Operation {@link Operator} * @param expressionStack the stack of {@link Expression} from which to take the operands * @return the result {@link Operation} * */ private static Operation readOperation(final Operator operator, final Stack<Expression> expressionStack) { final Expression rightOperand = expressionStack.pop(); final Expression leftOperand = expressionStack.pop(); return new Operation(operator, leftOperand, rightOperand); } /** * Takes the first {@link Expression} from the given {@link Stack}, assuming it is a * {@link NumberLiteral}, and returns a new {@link NumberLiteral} with its negated value. * * @param expressionStack the stack of {@link Expression} from which to take the operand * @return the result * @throws AnalyzeException if the operand is not {@link NumberLiteral} * */ private static NumberLiteral inverseInteger(final Stack<Expression> expressionStack) { final Expression operand = expressionStack.pop(); try { final Number value = ((NumberLiteral) operand).getValue(); return new NumberLiteral(-value.intValue()); } catch (ClassCastException e) { throw new AnalyzeException( "Cannot perform the inversion of operand of type " + operand.getExpressionType()); } } /** * Extracts the comparison {@link CompoundExpressionOperator} from the given {@link JumpInsnNode}. * * @param currentInstruction the comparison instruction * @return the corresponding {@link CompoundExpressionOperator} */ private static CompoundExpressionOperator extractComparisonOperator(final AbstractInsnNode currentInstruction) { switch (currentInstruction.getOpcode()) { case Opcodes.IF_ACMPNE: case Opcodes.IF_ICMPNE: case Opcodes.IFNE: return CompoundExpressionOperator.NOT_EQUALS; case Opcodes.IF_ACMPEQ: case Opcodes.IF_ICMPEQ: case Opcodes.IFEQ: return CompoundExpressionOperator.EQUALS; case Opcodes.IF_ICMPLE: case Opcodes.IFLE: return CompoundExpressionOperator.LESS_EQUALS; case Opcodes.IF_ICMPLT: case Opcodes.IFLT: return CompoundExpressionOperator.LESS; case Opcodes.IF_ICMPGE: case Opcodes.IFGE: return CompoundExpressionOperator.GREATER_EQUALS; case Opcodes.IF_ICMPGT: case Opcodes.IFGT: return CompoundExpressionOperator.GREATER; default: throw new AnalyzeException( "Failed to retrieve the operator for the current comparison instruction (opcode: " + currentInstruction.getOpcode() + ")"); } } /** * Reads the given {@link IntInsnNode} instruction and adds the associated {@link Expression} to * the given {@link Stack}. * * @param intInsnNode the instruction to read * @param expressionStack the expression stack to put on or pop from. * @param localVariables the local variables */ private static void readIntInstruction(final IntInsnNode intInsnNode, final Stack<Expression> expressionStack, final LocalVariables localVariables) { switch (intInsnNode.getOpcode()) { case Opcodes.BIPUSH: // expressionStack.add(LiteralFactory.getLiteral(intInsnNode.operand, // expressionStack.peek())); final Expression literal = new NumberLiteral(intInsnNode.operand); LOGGER.trace("Stacking literal {}", literal); expressionStack.add(literal); break; default: LOGGER.warn("IntInsnNode with OpCode {} was ignored.", intInsnNode.getOpcode()); } } /** * Converter: attempts to convert the given value {@link Expression} into a {@link BooleanLiteral} * or a {@link MethodInvocation}, or throws an {@link IllegalArgumentException} if the conversion * was not possible. * * @param value the initial value * @return a {@link BooleanLiteral} or a {@link MethodInvocation} * @throws IllegalArgumentException if the conversion is not possible */ static Expression convert(final Expression value) throws IllegalArgumentException { switch (value.getExpressionType()) { case METHOD_INVOCATION: case BOOLEAN_LITERAL: return value; case NUMBER_LITERAL: final Number numberLiteralValue = ((NumberLiteral) value).getValue(); if (numberLiteralValue.intValue() == 0) { return new BooleanLiteral(false); } else if (numberLiteralValue.intValue() == 1) { return new BooleanLiteral(true); } else { throw new IllegalArgumentException( "Could not convert number value '" + value + ") into a Boolean Literal"); } default: throw new IllegalArgumentException( "Could not convert '" + value + "' (" + value.getExpressionType() + ") into a Boolean Literal"); } } /** * The {@link AbstractInsnNode#getOpcode()} value should be one of {@code IFEQ}, {@code IFNE}, * {@code IFLT}, {@code IFGE}, {@code IFGT}, {@code IFLE}, {@code IFLT}, {@code IFGE}, * {@code IFGT},{@code IF_ICMPEQ}, {@code IF_ICMPNE}, {@code IF_ICMPLT}, {@code IF_ICMPGE}, * {@code IF_ICMPGT}, {@code IF_ICMPLE}, {@code IF_ACMPEQ}, {@code IF_ACMPNE}, {@code GOTO}, * {@code JSR}, {@code IFNULL} or {@code IFNONNULL}. * * * @param instructionCursor the cursor for the current instruction to read * @param expressionStack the stack of Expressions * @param capturedArguments the captured arguments * @param localVariables the local variables * @return the list of statements read from the jump instruction */ private List<Statement> readJumpInstruction(final InsnCursor instructionCursor, final Stack<Expression> expressionStack, final List<CapturedArgument> capturedArguments, final LocalVariables localVariables) { final JumpInsnNode jumpInsnNode = (JumpInsnNode) instructionCursor.getCurrent(); final LabelNode jumpLabel = jumpInsnNode.label; // FIXME: add support for LCMP: // Takes two two-word long integers off the stack and compares them. If // the two integers are the same, the int 0 is pushed onto the stack. If // value2 is greater than value1, the int 1 is pushed onto the stack. If // value1 is greater than value2, the int -1 is pushed onto the stack. switch (jumpInsnNode.getOpcode()) { case Opcodes.IFEQ: case Opcodes.IFNE: case Opcodes.IFLE: case Opcodes.IFLT: case Opcodes.IFGE: case Opcodes.IFGT: case Opcodes.IF_ICMPEQ: case Opcodes.IF_ICMPNE: case Opcodes.IF_ICMPLE: case Opcodes.IF_ICMPLT: case Opcodes.IF_ICMPGE: case Opcodes.IF_ICMPGT: case Opcodes.IF_ACMPEQ: case Opcodes.IF_ACMPNE: return Arrays.asList(buildControlFlowStatement(instructionCursor, expressionStack, capturedArguments, localVariables)); case Opcodes.GOTO: final InsnCursor jumpInstructionCursor = instructionCursor.duplicate(); jumpInstructionCursor.move(jumpLabel.getLabel()); return readStatements(jumpInstructionCursor, expressionStack, capturedArguments, localVariables); default: throw new AnalyzeException("Unexpected JumpInsnNode OpCode: " + jumpInsnNode.getOpcode()); } } /** * Builds a {@link ControlFlowStatement} from the given elements * * @param insnCursor the InsnCursor to read the execution branches * @param expressionStack the stack of expressions waiting to be used * @param capturedArguments the captured argument references, if any * @param localVariables the local variables, if any * @return an {@link ControlFlowStatement}. */ private Statement buildControlFlowStatement(final InsnCursor insnCursor, final Stack<Expression> expressionStack, final List<CapturedArgument> capturedArguments, final LocalVariables localVariables) { final JumpInsnNode jumpInsnNode = (JumpInsnNode) insnCursor.getCurrent(); final Expression comparisonExpression = getControlFlowExpression(jumpInsnNode, expressionStack); final LabelNode jumpLabel = jumpInsnNode.label; final InsnCursor thenInstructionCursor = insnCursor; final InsnCursor elseInstructionCursor = insnCursor.duplicate().next(); thenInstructionCursor.move(jumpLabel.getLabel()); // reading statements using a clean Expression stacks for both branches final List<Statement> thenStatements = readStatements(thenInstructionCursor, new Stack<Expression>(), capturedArguments, localVariables); final List<Statement> elseStatements = readStatements(elseInstructionCursor, new Stack<Expression>(), capturedArguments, localVariables); // insnCursor.move(thenInstructionCursor); return new ControlFlowStatement(comparisonExpression, thenStatements, elseStatements); } /** * Returns the {@link Expression} from the current {@link Expression} {@link Stack}, depending on * the current context (i.e., the current {@link JumpInsnNode} and its previous * {@link AbstractInsnNode}. * * @param jumpInsnNode the instruction * @param expressionStack the stack of expressions * @return the comparison expression (can be an {@link CompoundExpression} or some other form of * type of {@link Expression} that return a Boolean value) */ private static Expression getControlFlowExpression(final JumpInsnNode jumpInsnNode, final Stack<Expression> expressionStack) { final CompoundExpressionOperator comparisonOperator = extractComparisonOperator(jumpInsnNode); final Expression rightSideOperand = expressionStack.pop(); final Expression leftSideOperand = (expressionStack.empty() ? getDefaultComparisonOperand(rightSideOperand) : expressionStack.pop()); if (leftSideOperand.equals(new BooleanLiteral(false))) { switch (comparisonOperator) { case EQUALS: // if we have: 'expr == false', just return '!expr' return rightSideOperand.inverse(); case NOT_EQUALS: // if we have: 'expr != false', just return 'expr' return rightSideOperand; default: throw new AnalyzeException("There's no expression to compare with " + rightSideOperand + " " + comparisonOperator + " [expected something here]"); } } // ensure the operand types match by forcing the right side to be the same type as the left side final Class<?> leftSideOperandType = getOperandType(leftSideOperand); final Expression castedRightOperand = castOperand(rightSideOperand, leftSideOperandType.getName()); final CompoundExpression controlFlowExpression = new CompoundExpression(comparisonOperator, Arrays.asList(leftSideOperand, castedRightOperand)); return controlFlowExpression; } /** * Attempts to cast the given operand (if it is a Literal) to the given {@code targetType}. * * @param operand the operand to process * @param targetType the target type * @return the casted operand or the given operand if no cast could be performed */ private static Expression castOperand(final Expression operand, final String targetTypeName) { switch (operand.getExpressionType()) { case NUMBER_LITERAL: return ExpressionFactory.getLiteral((NumberLiteral) operand, targetTypeName); default: return operand; } } /** * @param operand the operand to analyze * @return the type of the operand, in particular, the {@code returnType} if the given * {@code operand} is a {@link MethodInvocation}. * @see MethodInvocation#getReturnType() */ private static Class<?> getOperandType(final Expression operand) { switch (operand.getExpressionType()) { case METHOD_INVOCATION: return ((MethodInvocation) operand).getReturnType(); default: return operand.getJavaType(); } } /** * Attempts to return the default {@link Expression} to compare against the given one, because the * generated bytecode may not have one in some cases (eg: {@code if(a == null)}). * * @param expression the {@link Expression} to compare * @return a default {@link Expression} to compare against the given one * @throws AnalyzeException when no default {@link Expression} can be provided. */ private static Expression getDefaultComparisonOperand(final Expression expression) { if (expression != null && expression.getExpressionType() == ExpressionType.METHOD_INVOCATION) { final MethodInvocation methodInvocation = (MethodInvocation) expression; // if the expression is something like 'a.equals(b)', there's no need to add an extra ' == // true' in the // equation. if (methodInvocation.getReturnType().equals(Boolean.class) || methodInvocation.getReturnType().equals(boolean.class)) { return new BooleanLiteral(false); } else if (methodInvocation.getReturnType().equals(Byte.class) || methodInvocation.getReturnType().equals(byte.class)) { return new NumberLiteral((byte) 0); } else if (methodInvocation.getReturnType().equals(Short.class) || methodInvocation.getReturnType().equals(short.class)) { return new NumberLiteral((short) 0); } else if (methodInvocation.getReturnType().equals(Integer.class) || methodInvocation.getReturnType().equals(int.class)) { return new NumberLiteral(0); } else if (methodInvocation.getReturnType().equals(Long.class) || methodInvocation.getReturnType().equals(long.class)) { return new NumberLiteral(0L); } else if (methodInvocation.getReturnType().equals(Float.class) || methodInvocation.getReturnType().equals(float.class)) { return new NumberLiteral(0f); } else if (methodInvocation.getReturnType().equals(Double.class) || methodInvocation.getReturnType().equals(float.class)) { return new NumberLiteral(0d); } else if (methodInvocation.getReturnType().equals(Character.class) || methodInvocation.getReturnType().equals(char.class)) { return new NullLiteral(); } else if (methodInvocation.getReturnType().equals(String.class)) { return new NullLiteral(); } } else if (expression != null && expression.getExpressionType() == ExpressionType.FIELD_ACCESS && (expression.getJavaType() == Boolean.class || expression.getJavaType() == boolean.class)) { return new BooleanLiteral(false); } throw new AnalyzeException("Sorry, I can't give a default comparison operand for '" + expression + "'"); } /** * Reads the given {@link TypeInsnNode} instruction. * * @param typeInsnNode the instruction to read * @param expressionStack the expression stack to put on or pop from. * @param localVariables the local variables */ private static void readTypeInstruction(final TypeInsnNode typeInsnNode, final Stack<Expression> expressionStack, final LocalVariables localVariables) { switch (typeInsnNode.getOpcode()) { case Opcodes.NEW: final Type instanceType = Type.getObjectType(typeInsnNode.desc); final ObjectInstanciation objectVariable = new ObjectInstanciation(getType(instanceType)); expressionStack.push(objectVariable); break; case Opcodes.ANEWARRAY: final Type parameterType = Type.getObjectType(typeInsnNode.desc); final NumberLiteral arrayLength = (NumberLiteral) expressionStack.pop(); final ArrayVariable arrayVariable = new ArrayVariable(getArrayType(parameterType), arrayLength.getValue().intValue()); expressionStack.push(arrayVariable); break; default: LOGGER.warn("TypeInsnNode with OpCode {} was ignored.", typeInsnNode.getOpcode()); } } /** * Reads the current ASTORE instruction, using elements from the given {@code expressionStack}. * * @param storeInsn the store instruction * @param expressionStack the stack of {@link Expression} */ private static void readArrayStoreInstruction(final AbstractInsnNode storeInsn, final Stack<Expression> expressionStack) { final Expression element = expressionStack.pop(); final NumberLiteral elementIndex = (NumberLiteral) expressionStack.pop(); final ArrayVariable targetArray = (ArrayVariable) expressionStack.pop(); targetArray.setElement(elementIndex.getValue().intValue(), element); } /** * Handler for the Loca Variables in the bytecode. The {@link LocalVariables} handles the elements * in a list and provides methods matching the <code>ALOAD</code> and <code>ASTORE</code> * instructions. */ static class LocalVariables { private final List<LocalVariableNode> localVariableNodes; private final int initialSize; private LocalVariableNode currentVariable = null; public LocalVariables(final List<LocalVariableNode> localVariableNodes) { this.localVariableNodes = localVariableNodes; this.initialSize = localVariableNodes.size(); } public List<LocalVariable> toLocalVariables() { return this.localVariableNodes.stream().limit(this.initialSize) // .filter(v -> v != null && !v.name.equals("this")) .map(var -> (var != null && !var.name.equals("this")) ? new LocalVariable(var.index, var.name, readSignature(var.desc)) : null) .collect(Collectors.toList()); } public LocalVariableNode load(final int index) { this.currentVariable = this.localVariableNodes.get(index); return this.currentVariable; } public LocalVariableNode store(final int index) { if (this.localVariableNodes.size() > index) { this.localVariableNodes.set(index, this.currentVariable); } else { this.localVariableNodes.add(index, this.currentVariable); } return this.currentVariable; } } }