TextPrinter.java

/*
 *  Copyright (c) 2001-2024, Jean Tessier
 *  All rights reserved.
 *  
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *  
 *      * Redistributions of source code must retain the above copyright
 *        notice, this list of conditions and the following disclaimer.
 *  
 *      * 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.
 *  
 *      * Neither the name of Jean Tessier nor the names of his 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 REGENTS 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 com.jeantessier.classreader;

import java.io.*;
import java.util.*;

public class TextPrinter extends Printer {
    private boolean top = true;

    public TextPrinter(PrintWriter out) {
        super(out);
    }
    
    public void visitClassfile(Classfile classfile) {
        top = true;
        classfile.getConstantPool().accept(this);
        top = false;

        eol();

        append(classfile.getDeclaration()).append(" {").eol();

        visitClassfileFields(classfile);
        visitClassfileMethods(classfile);

        append("}").eol();
    }

    public void visitClass_info(Class_info entry) {
        if (top) {
            top = false;
            append(currentIndex()).append(": Class ").append(entry).eol();
            top = true;
        } else {
            append(entry);
        }
    }

    public void visitFieldRef_info(FieldRef_info entry) {
        if (top) {
            top = false;
            append(currentIndex()).append(": Field ").append(entry).eol();
            top = true;
        } else {
            append(entry);
        }
    }

    public void visitMethodRef_info(MethodRef_info entry) {
        if (top) {
            top = false;
            append(currentIndex()).append(": Method ").append(entry).eol();
            top = true;
        } else {
            append(entry);
        }
    }

    public void visitInterfaceMethodRef_info(InterfaceMethodRef_info entry) {
        if (top) {
            top = false;
            append(currentIndex()).append(": Interface Method ").append(entry).eol();
            top = true;
        } else {
            append(entry);
        }
    }

    public void visitString_info(String_info entry) {
        if (top) {
            top = false;
            append(currentIndex()).append(": String \"");
            entry.getRawValue().accept(this);
            append("\"").eol();
            top = true;
        } else {
            entry.getRawValue().accept(this);
        }
    }

    public void visitInteger_info(Integer_info entry) {
        if (top) {
            append(currentIndex()).append(": Integer ").append(entry.getValue()).eol();
        } else {
            append(entry.getValue());
        }
    }

    public void visitFloat_info(Float_info entry) {
        if (top) {
            append(currentIndex()).append(": Float ").append(entry.getValue()).eol();
        } else {
            append(entry.getValue());
        }
    }

    public void visitLong_info(Long_info entry) {
        if (top) {
            append(currentIndex()).append(": Long ").append(entry.getValue()).eol();
        } else {
            append(entry.getValue());
        }
    }

    public void visitDouble_info(Double_info entry) {
        if (top) {
            append(currentIndex()).append(": Double ").append(entry.getValue()).eol();
        } else {
            append(entry.getValue());
        }
    }

    public void visitNameAndType_info(NameAndType_info entry) {
        if (top) {
            top = false;
            append(currentIndex()).append(": Name and Type ");
            entry.getRawName().accept(this);
            append(" ");
            entry.getRawType().accept(this);
            eol();
            top = true;
        } else {
            entry.getRawName().accept(this);
            append(" ");
            entry.getRawType().accept(this);
        }
    }

    public void visitUTF8_info(UTF8_info entry) {
        if (top) {
            append(currentIndex()).append(": \"").append(entry.getValue()).append("\"").eol();
        } else {
            append(entry.getValue());
        }
    }

    public void visitMethodHandle_info(MethodHandle_info entry) {
        if (top) {
            top = false;
            append(currentIndex()).append(": Method Handle ");
            append(entry.getReferenceKind().getDescription());
            append(" ");
            entry.getReference().accept(this);
            eol();
            top = true;
        } else {
            append(entry.getReferenceKind().getDescription());
            append(" ");
            entry.getReference().accept(this);
        }
    }

    public void visitMethodType_info(MethodType_info entry) {
        if (top) {
            top = false;
            append(currentIndex()).append(": Method Type ").append(entry).eol();
            top = true;
        } else {
            append(entry);
        }
    }

    public void visitDynamic_info(Dynamic_info entry) {
        if (top) {
            top = false;
            append(currentIndex()).append(": Dynamic ");
            append(entry.getBootstrapMethodAttrIndex());
            append(" ");
            append(entry);
            eol();
            top = true;
        } else {
            append(entry);
        }
    }

    public void visitInvokeDynamic_info(InvokeDynamic_info entry) {
        if (top) {
            top = false;
            append(currentIndex()).append(": Invoke Dynamic ");
            append(entry.getBootstrapMethodAttrIndex());
            append(" ");
            append(entry);
            eol();
            top = true;
        } else {
            append(entry);
        }
    }

    public void visitModule_info(Module_info entry) {
        if (top) {
            top = false;
            append(currentIndex()).append(": Module ").append(entry).eol();
            top = true;
        } else {
            append(entry);
        }
    }

    public void visitPackage_info(Package_info entry) {
        if (top) {
            top = false;
            append(currentIndex()).append(": Package ").append(entry).eol();
            top = true;
        } else {
            append(entry);
        }
    }

    public void visitUnusableEntry(UnusableEntry entry) {
        if (top) {
            append(currentIndex()).append(": ").append(entry).eol();
        } else {
            append(entry);
        }
    }

    public void visitField_info(Field_info entry) {
        append("    ").append(entry.getDeclaration()).append(";").eol();
    }

    public void visitMethod_info(Method_info entry) {
        eol();
        append("    ");
        append(entry.getDeclaration());
        if (!entry.isStaticInitializer()) {
            append(";");
        }
        eol();

        // As per the Class File Format (paragraph 4.8.3):
        // - abstract and native methods must *not* have a Code attribute
        // - all other methods must have exactly one Code attribute.
        if (!entry.isAbstract() && !entry.isNative()) {
            entry.getCode().accept(this);
        }
    }

    public void visitCode_attribute(Code_attribute attribute) {
        append("        CODE").eol();
        visitInstructions(attribute);

        Collection<? extends ExceptionHandler> exceptionHandlers = attribute.getExceptionHandlers();
        if (!exceptionHandlers.isEmpty()) {
            append("        EXCEPTION HANDLING").eol();
            visitExceptionHandlers(exceptionHandlers);
        }
    }

    public void visitInstruction(Instruction instruction) {
        append("        ").append(instruction.getStart()).append(":\t").append(instruction.getMnemonic());
        appendIndexedConstantPoolEntry(instruction);
        appendIndexedLocalVariable(instruction);
        appendDynamicConstantPoolEntries(instruction);
        appendOffset(instruction);
        appendValue(instruction);
        eol();

        super.visitInstruction(instruction);
    }

    public void visitExceptionHandler(ExceptionHandler handler) {
        append("        ").append(handler.getStartPC()).append("-").append(handler.getEndPC()).append(": ").append(handler.getHandlerPC());
        if (handler.hasCatchType()) {
            append(" (").append(handler.getCatchType()).append(")");
        }
        eol();
    }

    private Printer appendIndexedConstantPoolEntry(Instruction instruction) {
        switch (instruction.getOpcode()) {
            case 0x12: // ldc
            case 0x13: // ldc_w
            case 0x14: // ldc2_w
            case 0xb2: // getstatic
            case 0xb3: // putstatic
            case 0xb4: // getfield
            case 0xb5: // putfield
            case 0xb6: // invokevirtual
            case 0xb7: // invokespecial
            case 0xb8: // invokestatic
            case 0xb9: // invokeinterface
            case 0xbb: // new
            case 0xbd: // anewarray
            case 0xc0: // checkcast
            case 0xc1: // instanceof
            case 0xc5: // multianewarray
                append(" ");
                instruction.getIndexedConstantPoolEntry().accept(this);
                break;
            case 0xba: // invokedynamic
                var indexedEntry = instruction.getIndexedConstantPoolEntry();
                if (indexedEntry instanceof Dynamic_info dynamic_info) {
                    append(" ").append(dynamic_info.getName());
                } else if (indexedEntry instanceof InvokeDynamic_info invokeDynamic_info) {
                    append(" ").append(invokeDynamic_info.getName());
                }
                // TODO: Replace with type pattern matching in switch expression in Java 21
                // switch (instruction.getIndexedConstantPoolEntry()) {
                //     case Dynamic_info entry -> append(" ").append(entry.getName());
                //     case InvokeDynamic_info entry -> append(" ").append(entry.getName());
                //     default -> append("");
                // }
                break;
            default:
                // Do nothing
                break;
        }
        return this;
    }

    private Printer appendIndexedLocalVariable(Instruction instruction) {
        switch (instruction.getOpcode()) {
            case 0x1a: // iload_0
            case 0x1e: // lload_0
            case 0x22: // fload_0
            case 0x26: // dload_0
            case 0x2a: // aload_0
            case 0x3b: // istore_0
            case 0x3f: // lstore_0
            case 0x43: // fstore_0
            case 0x47: // dstore_0
            case 0x4b: // astore_0
            case 0x1b: // iload_1
            case 0x1f: // lload_1
            case 0x23: // fload_1
            case 0x27: // dload_1
            case 0x2b: // aload_1
            case 0x3c: // istore_1
            case 0x40: // lstore_1
            case 0x44: // fstore_1
            case 0x48: // dstore_1
            case 0x4c: // astore_1
            case 0x1c: // iload_2
            case 0x20: // lload_2
            case 0x24: // fload_2
            case 0x28: // dload_2
            case 0x2c: // aload_2
            case 0x3d: // istore_2
            case 0x41: // lstore_2
            case 0x45: // fstore_2
            case 0x49: // dstore_2
            case 0x4d: // astore_2
            case 0x1d: // iload_3
            case 0x21: // lload_3
            case 0x25: // fload_3
            case 0x29: // dload_3
            case 0x2d: // aload_3
            case 0x3e: // istore_3
            case 0x42: // lstore_3
            case 0x46: // fstore_3
            case 0x4a: // dstore_3
            case 0x4e: // astore_3
                appendLocalVariable(instruction.getIndexedLocalVariable());
                break;
            case 0x15: // iload
            case 0x16: // llload
            case 0x17: // fload
            case 0x18: // dload
            case 0x19: // aload
            case 0x36: // istore
            case 0x37: // lstore
            case 0x38: // fstore
            case 0x39: // dstore
            case 0x3a: // astore
            case 0xa9: // ret
            case 0x84: // iinc
            case 0xc4: // wide
                appendLocalVariable(instruction.getIndexedLocalVariable());
                append(" (#").append(instruction.getIndex()).append(")");
                break;
            default:
                // Do nothing
                break;
        }
        return this;
    }

    private Printer appendDynamicConstantPoolEntries(Instruction instruction) {
        switch (instruction.getOpcode()) {
            case 0xba: // invokedynamic
                instruction.getDynamicConstantPoolEntries().forEach(entry -> {
                    append(" ");
                    entry.accept(this);
                });
                break;
            default:
                // Do nothing
                break;
        }
        return this;
    }

    private Printer appendLocalVariable(LocalVariable localVariable) {
        if (localVariable != null) {
            append(" ").append(DescriptorHelper.getType(localVariable.getDescriptor())).append(" ").append(localVariable.getName());
        }
        return this;
    }

    private Printer appendOffset(Instruction instruction) {
        switch (instruction.getOpcode()) {
            case 0x99: // ifeq
            case 0x9a: // ifne
            case 0x9b: // iflt
            case 0x9c: // ifge
            case 0x9d: // ifgt
            case 0x9e: // ifle
            case 0x9f: // if_icmpeq
            case 0xa0: // if_icmpne
            case 0xa1: // if_icmplt
            case 0xa2: // if_icmpge
            case 0xa3: // if_icmpgt
            case 0xa4: // if_icmple
            case 0xa5: // if_acmpeq
            case 0xa6: // if_acmpne
            case 0xa7: // goto
            case 0xa8: // jsr
            case 0xc6: // ifnull
            case 0xc7: // ifnonnull
            case 0xc8: // goto_w
            case 0xc9: // jsr_w
                append(" ").append(instruction.getStart() + instruction.getOffset()).append(" (");
                if (instruction.getOffset() >= 0) {
                    append("+");
                }
                append(instruction.getOffset());
                append(")");
                break;
            default:
                // Do nothing
                break;
        }
        return this;
    }

    private Printer appendValue(Instruction instruction) {
        switch (instruction.getOpcode()) {
            case 0x10: // bipush
            case 0x11: // sipush
            case 0x84: // iinc
                append(" ").append(instruction.getValue());
                break;
            case 0xaa: // tableswitch
                append(" default:").appendSwitchDefault(instruction);
                append(" | ");
                appendTableSwitch(instruction, " | ");
                break;
            case 0xab: // lookupswitch
                append(" default:").appendSwitchDefault(instruction);
                append(" | ");
                appendLookupSwitch(instruction, " | ");
                break;
            case 0xc4: // wide
                if (instruction.getByte(1) == 0x84 /* iinc */) {
                    append(" ").append(instruction.getValue());
                }
                break;
            default:
                // Do nothing
                break;
        }
        return this;
    }
}