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

io.github.dmlloyd.classfile.impl.ClassPrinterImpl Maven / Gradle / Ivy

There is a newer version: 24.cr2
Show newest version
/*
 * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package io.github.dmlloyd.classfile.impl;


import io.github.dmlloyd.classfile.*;
import io.github.dmlloyd.classfile.AnnotationValue.*;
import io.github.dmlloyd.classfile.attribute.*;
import io.github.dmlloyd.classfile.attribute.StackMapFrameInfo.ObjectVerificationTypeInfo;
import io.github.dmlloyd.classfile.attribute.StackMapFrameInfo.SimpleVerificationTypeInfo;
import io.github.dmlloyd.classfile.attribute.StackMapFrameInfo.UninitializedVerificationTypeInfo;
import io.github.dmlloyd.classfile.attribute.StackMapFrameInfo.VerificationTypeInfo;
import io.github.dmlloyd.classfile.components.ClassPrinter.LeafNode;
import io.github.dmlloyd.classfile.components.ClassPrinter.ListNode;
import io.github.dmlloyd.classfile.components.ClassPrinter.MapNode;
import io.github.dmlloyd.classfile.components.ClassPrinter.Node;
import io.github.dmlloyd.classfile.components.ClassPrinter.Verbosity;
import io.github.dmlloyd.classfile.constantpool.*;
import io.github.dmlloyd.classfile.instruction.*;
import java.lang.constant.ConstantDesc;
import java.lang.constant.DirectMethodHandleDesc;
import io.github.dmlloyd.classfile.extras.reflect.AccessFlag;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static io.github.dmlloyd.classfile.constantpool.PoolEntry.TAG_CLASS;
import static io.github.dmlloyd.classfile.constantpool.PoolEntry.TAG_DOUBLE;
import static io.github.dmlloyd.classfile.constantpool.PoolEntry.TAG_FLOAT;
import static io.github.dmlloyd.classfile.constantpool.PoolEntry.TAG_LONG;
import static io.github.dmlloyd.classfile.constantpool.PoolEntry.TAG_STRING;
import static io.github.dmlloyd.classfile.constantpool.PoolEntry.*;
import static java.util.Objects.requireNonNull;
import static io.github.dmlloyd.classfile.impl.ClassPrinterImpl.Style.BLOCK;
import static io.github.dmlloyd.classfile.impl.ClassPrinterImpl.Style.FLOW;

public final class ClassPrinterImpl {

    public enum Style { BLOCK, FLOW }

    public record LeafNodeImpl(ConstantDesc name, ConstantDesc value) implements LeafNode {

        @Override
        public Stream walk() {
            return Stream.of(this);
        }
    }

    public static sealed class ListNodeImpl extends AbstractList implements ListNode {

        private final Style style;
        private final ConstantDesc name;
        protected final List nodes;

        public ListNodeImpl(Style style, ConstantDesc name, Stream nodes) {
            this.style = style;
            this.name = name;
            this.nodes = nodes.toList();
        }

        protected ListNodeImpl(Style style, ConstantDesc name, List nodes) {
            this.style = style;
            this.name = name;
            this.nodes = nodes;
        }

        @Override
        public ConstantDesc name() {
            return name;
        }

        @Override
        public Stream walk() {
            return Stream.concat(Stream.of(this), stream().flatMap(Node::walk));
        }

        public Style style() {
            return style;
        }

        @Override
        public Node get(int index) {
            return nodes.get(index);
        }

        @Override
        public int size() {
            return nodes.size();
        }
    }

    public static final class MapNodeImpl implements MapNode {

        private static final class PrivateListNodeImpl extends ListNodeImpl {
            PrivateListNodeImpl(Style style, ConstantDesc name, Node... n) {
                super(style, name, new ArrayList<>(List.of(n)));
            }
        }

        private final Style style;
        private final ConstantDesc name;
        private final Map map;

        public MapNodeImpl(Style style, ConstantDesc name) {
            this.style = style;
            this.name = name;
            this.map = new LinkedHashMap<>();
        }

        @Override
        public ConstantDesc name() {
            return name;
        }

        @Override
        public Stream walk() {
            return Stream.concat(Stream.of(this), values().stream().flatMap(Node::walk));
        }

        public Style style() {
            return style;
        }

        @Override
        public int size() {
            return map.size();
        }
        @Override
        public boolean isEmpty() {
            return map.isEmpty();
        }
        @Override
        public boolean containsKey(Object key) {
            return map.containsKey(key);
        }
        @Override
        public boolean containsValue(Object value) {
            return map.containsValue(value);
        }

        @Override
        public Node get(Object key) {
            return map.get(key);
        }

        @Override
        public Node put(ConstantDesc key, Node value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public Node remove(Object key) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void putAll(Map m) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void clear() {
            throw new UnsupportedOperationException();
        }

        @Override
        public Set keySet() {
            return Collections.unmodifiableSet(map.keySet());
        }

        @Override
        public Collection values() {
            return Collections.unmodifiableCollection(map.values());
        }

        @Override
        public Set> entrySet() {
            return Collections.unmodifiableSet(map.entrySet());
        }


        MapNodeImpl with(Node... nodes) {
            for (var n : nodes) {
                if (n != null) {
                    var prev = map.putIfAbsent(n.name(), n);
                    if (prev != null) {
                        //nodes with duplicite keys are joined into a list
                        if (prev instanceof PrivateListNodeImpl list) {
                            list.nodes.add(n);
                        } else {
                            map.put(n.name(), new PrivateListNodeImpl(style, n.name(), prev, n));
                        }
                    }
                }
            }
            return this;
        }
    }

    private static Node leaf(ConstantDesc name, ConstantDesc value) {
        return new LeafNodeImpl(name, value);
    }

    private static Node[] leafs(ConstantDesc... namesAndValues) {
        if ((namesAndValues.length & 1) > 0)
            throw new AssertionError("Odd number of arguments: " + Arrays.toString(namesAndValues));
        var nodes = new Node[namesAndValues.length >> 1];
        for (int i = 0, j = 0; i < nodes.length; i ++) {
            nodes[i] = leaf(namesAndValues[j++], namesAndValues[j++]);
        }
        return nodes;
    }

    private static Node list(ConstantDesc listName, ConstantDesc itemsName, Stream values) {
        return new ListNodeImpl(FLOW, listName, values.map(v -> leaf(itemsName, v)));
    }

    private static Node map(ConstantDesc mapName, ConstantDesc... keysAndValues) {
        return new MapNodeImpl(FLOW, mapName).with(leafs(keysAndValues));
    }

    private static final String NL = System.lineSeparator();

    private static final char[] DIGITS = "0123456789ABCDEF".toCharArray();

    private static void escape(int c, StringBuilder sb) {
        switch (c) {
            case '\\'  -> sb.append('\\').append('\\');
            case '"' -> sb.append('\\').append('"');
            case '\b' -> sb.append('\\').append('b');
            case '\n' -> sb.append('\\').append('n');
            case '\t' -> sb.append('\\').append('t');
            case '\f' -> sb.append('\\').append('f');
            case '\r' -> sb.append('\\').append('r');
            default -> {
                if (c >= 0x20 && c < 0x7f) {
                    sb.append((char)c);
                } else {
                    sb.append('\\').append('u').append(DIGITS[(c >> 12) & 0xf])
                            .append(DIGITS[(c >> 8) & 0xf]).append(DIGITS[(c >> 4) & 0xf]).append(DIGITS[(c) & 0xf]);
                }
            }
        }
    }

    public static void toYaml(Node node, Consumer out) {
        toYaml(0, false, new ListNodeImpl(BLOCK, null, Stream.of(node)), out);
        out.accept(NL);
    }

    private static void toYaml(int indent, boolean skipFirstIndent, Node node, Consumer out) {
        if (node instanceof LeafNode leaf) {
            out.accept(quoteAndEscapeYaml(leaf.value()));
        } else if (node instanceof ListNodeImpl list) {
            switch (list.style()) {
                case FLOW -> {
                    out.accept("[");
                    boolean first = true;
                    for (var n : list) {
                        if (first) first = false;
                        else out.accept(", ");
                        toYaml(0, false, n, out);
                    }
                    out.accept("]");
                }
                case BLOCK -> {
                    for (var n : list) {
                        out.accept(NL + "    ".repeat(indent) + "  - ");
                        toYaml(indent + 1, true, n, out);
                    }
                }
            }
        } else if (node instanceof MapNodeImpl map) {
            switch (map.style()) {
                case FLOW -> {
                    out.accept("{");
                    boolean first = true;
                    for (var n : map.values()) {
                        if (first) first = false;
                        else out.accept(", ");
                        out.accept(quoteAndEscapeYaml(n.name()) + ": ");
                        toYaml(0, false, n, out);
                    }
                    out.accept("}");
                }
                case BLOCK -> {
                    for (var n : map.values()) {
                        if (skipFirstIndent) {
                            skipFirstIndent = false;
                        } else {
                            out.accept(NL + "    ".repeat(indent));
                        }
                        out.accept(quoteAndEscapeYaml(n.name()) + ": ");
                        toYaml(n instanceof ListNodeImpl pl && pl.style() == BLOCK ? indent : indent + 1, false, n, out);
                    }
                }
            }
        }
    }

    private static String quoteAndEscapeYaml(ConstantDesc value) {
        String s = String.valueOf(value);
        if (value instanceof Number) return s;
        if (s.length() == 0) return "''";
        var sb = new StringBuilder(s.length() << 1);
        s.chars().forEach(c -> {
            switch (c) {
                case '\''  -> sb.append("''");
                default -> escape(c, sb);
            }});
        String esc = sb.toString();
        if (esc.length() != s.length()) return "'" + esc + "'";
        switch (esc.charAt(0)) {
            case '-', '?', ':', ',', '[', ']', '{', '}', '#', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`':
                return "'" + esc + "'";
        }
        for (int i = 1; i < esc.length(); i++) {
            switch (esc.charAt(i)) {
                case ',', '[', ']', '{', '}':
                    return "'" + esc + "'";
            }
        }
        return esc;
    }

    public static void toJson(Node node, Consumer out) {
        toJson(1, true, node, out);
        out.accept(NL);
    }

    private static void toJson(int indent, boolean skipFirstIndent, Node node, Consumer out) {
        if (node instanceof LeafNode leaf) {
            out.accept(quoteAndEscapeJson(leaf.value()));
        } else if (node instanceof ListNodeImpl list) {
            out.accept("[");
            boolean first = true;
            switch (list.style()) {
                case FLOW -> {
                    for (var n : list) {
                        if (first) first = false;
                        else out.accept(", ");
                        toJson(0, false, n, out);
                    }
                }
                case BLOCK -> {
                    for (var n : list) {
                        if (first) first = false;
                        else out.accept(",");
                        out.accept(NL + "    ".repeat(indent));
                        toJson(indent + 1, true, n, out);
                    }
                }
            }
            out.accept("]");
        } else if (node instanceof MapNodeImpl map) {
            switch (map.style()) {
                case FLOW -> {
                    out.accept("{");
                    boolean first = true;
                    for (var n : map.values()) {
                        if (first) first = false;
                        else out.accept(", ");
                        out.accept(quoteAndEscapeJson(n.name().toString()) + ": ");
                        toJson(0, false, n, out);
                    }
                }
                case BLOCK -> {
                    if (skipFirstIndent) out.accept("  { ");
                    else out.accept("{");
                    boolean first = true;
                    for (var n : map.values()) {
                        if (first) first = false;
                        else out.accept(",");
                        if (skipFirstIndent) skipFirstIndent = false;
                        else out.accept(NL + "    ".repeat(indent));
                        out.accept(quoteAndEscapeJson(n.name().toString()) + ": ");
                        toJson(indent + 1, false, n, out);
                    }
                }
            }
            out.accept("}");
        }
    }

    private static String quoteAndEscapeJson(ConstantDesc value) {
        String s = String.valueOf(value);
        if (value instanceof Number) return s;
        var sb = new StringBuilder(s.length() << 1);
        sb.append('"');
        s.chars().forEach(c -> escape(c, sb));
        sb.append('"');
        return sb.toString();
    }

    public static void toXml(Node node, Consumer out) {
        out.accept("");
        toXml(0, false, node, out);
        out.accept(NL);
    }

    private static void toXml(int indent, boolean skipFirstIndent, Node node, Consumer out) {
        var name = toXmlName(node.name().toString());
        if (node instanceof LeafNode leaf) {
            out.accept("<" + name + ">");
            out.accept(xmlEscape(leaf.value()));
        } else if (node instanceof ListNodeImpl list) {
            switch (list.style()) {
                case FLOW -> {
                    out.accept("<" + name + ">");
                    for (var n : list) {
                        toXml(0, false, n, out);
                    }
                }
                case BLOCK -> {
                    if (!skipFirstIndent)
                        out.accept(NL + "    ".repeat(indent));
                    out.accept("<" + name + ">");
                    for (var n : list) {
                        out.accept(NL + "    ".repeat(indent + 1));
                        toXml(indent + 1, true, n, out);
                    }
                }
            }
        } else if (node instanceof MapNodeImpl map) {
            switch (map.style()) {
                case FLOW -> {
                    out.accept("<" + name + ">");
                    for (var n : map.values()) {
                        toXml(0, false, n, out);
                    }
                }
                case BLOCK -> {
                    if (!skipFirstIndent)
                        out.accept(NL + "    ".repeat(indent));
                    out.accept("<" + name + ">");
                    for (var n : map.values()) {
                        out.accept(NL + "    ".repeat(indent + 1));
                        toXml(indent + 1, true, n, out);
                    }
                }
            }
        }
        out.accept("");
    }

    private static String xmlEscape(ConstantDesc value) {
        var s = String.valueOf(value);
        var sb = new StringBuilder(s.length() << 1);
        s.chars().forEach(c -> {
        switch (c) {
            case '<'  -> sb.append("<");
            case '>'  -> sb.append(">");
            case '"'  -> sb.append(""");
            case '&'  -> sb.append("&");
            case '\''  -> sb.append("'");
            default -> escape(c, sb);
        }});
        return sb.toString();
    }

    private static String toXmlName(String name) {
        if (Character.isDigit(name.charAt(0)))
            name = "_" + name;
        return name.replaceAll("[^A-Za-z_0-9]", "_");
    }

    private static Node[] elementValueToTree(AnnotationValue v) {
        if (v instanceof OfString cv) return leafs("string", String.valueOf(cv.stringValue()));
        else if (v instanceof OfDouble cv) return leafs("double", String.valueOf(cv.doubleValue()));
        else if (v instanceof OfFloat cv) return leafs("float", String.valueOf(cv.floatValue()));
        else if (v instanceof OfLong cv) return leafs("long", String.valueOf(cv.longValue()));
        else if (v instanceof OfInt cv) return leafs("int", String.valueOf(cv.intValue()));
        else if (v instanceof OfShort cv) return leafs("short", String.valueOf(cv.shortValue()));
        else if (v instanceof OfChar cv) return leafs("char", String.valueOf(cv.charValue()));
        else if (v instanceof OfByte cv) return leafs("byte", String.valueOf(cv.byteValue()));
        else if (v instanceof OfBoolean cv) return leafs("boolean", String.valueOf(cv.booleanValue()));
        else if (v instanceof OfClass clv) return leafs("class", clv.className().stringValue());
        else if (v instanceof OfEnum ev) return leafs("enum class", ev.className().stringValue(),
            "constant name", ev.constantName().stringValue());
        else if (v instanceof OfAnnotation av) return leafs("annotation class", av.annotation().className().stringValue());
        else if (v instanceof OfArray av) return new Node[]{new ListNodeImpl(FLOW, "array", av.values().stream().map(
            ev -> new MapNodeImpl(FLOW, "value").with(elementValueToTree(ev))))};
        else throw new IllegalStateException();
    }

    private static Node elementValuePairsToTree(List evps) {
        return new ListNodeImpl(FLOW, "values", evps.stream().map(evp -> new MapNodeImpl(FLOW, "pair").with(
                leaf("name", evp.name().stringValue()),
                new MapNodeImpl(FLOW, "value").with(elementValueToTree(evp.value())))));
    }

    private static Stream convertVTIs(CodeAttribute lr, List vtis) {
        return vtis.stream().mapMulti((vti, ret) -> {
            if (vti instanceof SimpleVerificationTypeInfo s) {
                switch (s) {
                    case DOUBLE -> {
                        ret.accept("double");
                        ret.accept("double2");
                    }
                    case FLOAT ->
                        ret.accept("float");
                    case INTEGER ->
                        ret.accept("int");
                    case LONG ->  {
                        ret.accept("long");
                        ret.accept("long2");
                    }
                    case NULL -> ret.accept("null");
                    case TOP -> ret.accept("?");
                    case UNINITIALIZED_THIS -> ret.accept("THIS");
                }
            } else if (vti instanceof ObjectVerificationTypeInfo o) {
                ret.accept(o.className().name().stringValue());
            } else if (vti instanceof UninitializedVerificationTypeInfo u) {
                ret.accept("UNINITIALIZED @" + lr.labelToBci(u.newTarget()));
            }
        });
    }

    private record ExceptionHandler(int start, int end, int handler, String catchType) {}

    public static MapNode modelToTree(CompoundElement model, Verbosity verbosity) {
        requireNonNull(verbosity); // we are using == checks in implementations
        if (model instanceof ClassModel cm) return classToTree(cm, verbosity);
        else if (model instanceof FieldModel fm) return fieldToTree(fm, verbosity);
        else if (model instanceof MethodModel mm) return methodToTree(mm, verbosity);
        else if (model instanceof CodeModel com) return codeToTree((CodeAttribute)com, verbosity);
        else throw new IllegalStateException();
    }

    private static MapNode classToTree(ClassModel clm, Verbosity verbosity) {
        return new MapNodeImpl(BLOCK, "class")
                .with(leaf("class name", clm.thisClass().asInternalName()),
                      leaf("version", clm.majorVersion() + "." + clm.minorVersion()),
                      list("flags", "flag", clm.flags().flags().stream().map(AccessFlag::name)),
                      leaf("superclass", clm.superclass().map(ClassEntry::asInternalName).orElse("")),
                      list("interfaces", "interface", clm.interfaces().stream().map(ClassEntry::asInternalName)),
                      list("attributes", "attribute", clm.attributes().stream().map(Attribute::attributeName).map(Utf8Entry::stringValue)))
                .with(constantPoolToTree(clm.constantPool(), verbosity))
                .with(attributesToTree(clm.attributes(), verbosity))
                .with(new ListNodeImpl(BLOCK, "fields", clm.fields().stream().map(f ->
                    fieldToTree(f, verbosity))))
                .with(new ListNodeImpl(BLOCK, "methods", clm.methods().stream().map(mm ->
                    methodToTree(mm, verbosity))));
    }

    private static Node[] constantPoolToTree(ConstantPool cp, Verbosity verbosity) {
        if (verbosity == Verbosity.TRACE_ALL) {
            var cpNode = new MapNodeImpl(BLOCK, "constant pool");
            for (PoolEntry e : cp) {
                cpNode.with(new MapNodeImpl(FLOW, e.index())
                        .with(leaf("tag", switch (e.tag()) {
                            case TAG_UTF8 -> "Utf8";
                            case TAG_INTEGER -> "Integer";
                            case TAG_FLOAT -> "Float";
                            case TAG_LONG -> "Long";
                            case TAG_DOUBLE -> "Double";
                            case TAG_CLASS -> "Class";
                            case TAG_STRING -> "String";
                            case TAG_FIELDREF -> "Fieldref";
                            case TAG_METHODREF -> "Methodref";
                            case TAG_INTERFACE_METHODREF -> "InterfaceMethodref";
                            case TAG_NAME_AND_TYPE -> "NameAndType";
                            case TAG_METHOD_HANDLE -> "MethodHandle";
                            case TAG_METHOD_TYPE -> "MethodType";
                            case TAG_DYNAMIC -> "Dynamic";
                            case TAG_INVOKE_DYNAMIC -> "InvokeDynamic";
                            case TAG_MODULE -> "Module";
                            case TAG_PACKAGE -> "Package";
                            default -> throw new AssertionError("Unknown CP tag: " + e.tag());
                        }))
                        .with(
                            e instanceof ClassEntry ce ? leafs(
                                "class name index", ce.name().index(),
                                "class internal name", ce.asInternalName()) :
                            e instanceof ModuleEntry me ? leafs(
                                "module name index", me.name().index(),
                                "module name", me.name().stringValue()) :
                            e instanceof PackageEntry pe ? leafs(
                                "package name index", pe.name().index(),
                                "package name", pe.name().stringValue()) :
                            e instanceof StringEntry se ? leafs(
                                    "value index", se.utf8().index(),
                                    "value", se.stringValue()) :
                            e instanceof MemberRefEntry mre ? leafs(
                                    "owner index", mre.owner().index(),
                                    "name and type index", mre.nameAndType().index(),
                                    "owner", mre.owner().name().stringValue(),
                                    "name", mre.name().stringValue(),
                                    "type", mre.type().stringValue()) :
                            e instanceof NameAndTypeEntry nte ? leafs(
                                    "name index", nte.name().index(),
                                    "type index", nte.type().index(),
                                    "name", nte.name().stringValue(),
                                    "type", nte.type().stringValue()) :
                            e instanceof MethodHandleEntry mhe ? leafs(
                                    "reference kind", DirectMethodHandleDesc.Kind.valueOf(mhe.kind()).name(),
                                    "reference index", mhe.reference().index(),
                                    "owner", mhe.reference().owner().asInternalName(),
                                    "name", mhe.reference().name().stringValue(),
                                    "type", mhe.reference().type().stringValue()) :
                            e instanceof MethodTypeEntry mte ? leafs(
                                    "descriptor index", mte.descriptor().index(),
                                    "descriptor", mte.descriptor().stringValue()) :
                            e instanceof DynamicConstantPoolEntry dcpe ? new Node[] {
                                leaf("bootstrap method handle index", dcpe.bootstrap().bootstrapMethod().index()),
                                list("bootstrap method arguments indexes",
                                        "index", dcpe.bootstrap().arguments().stream().map(en -> en.index())),
                                leaf("name and type index", dcpe.nameAndType().index()),
                                leaf("name", dcpe.name().stringValue()),
                                leaf("type", dcpe.type().stringValue())} :
                            e instanceof AnnotationConstantValueEntry ve ? leafs(
                                "value", String.valueOf(ve.constantValue())
                            ) :
                            null // not possible
                        ));
            }
            return new Node[]{cpNode};
        } else {
            return new Node[0];
        }
    }

    private static Node frameToTree(ConstantDesc name, CodeAttribute lr, StackMapFrameInfo f) {
        return new MapNodeImpl(FLOW, name).with(
                list("locals", "item", convertVTIs(lr, f.locals())),
                list("stack", "item", convertVTIs(lr, f.stack())));
    }

    private static MapNode fieldToTree(FieldModel f, Verbosity verbosity) {
        return new MapNodeImpl(BLOCK, "field")
                            .with(leaf("field name", f.fieldName().stringValue()),
                                  list("flags",
                                          "flag", f.flags().flags().stream().map(AccessFlag::name)),
                                  leaf("field type", f.fieldType().stringValue()),
                                  list("attributes",
                                          "attribute", f.attributes().stream().map(Attribute::attributeName).map(Utf8Entry::stringValue)))
                            .with(attributesToTree(f.attributes(), verbosity));
    }

    public static MapNode methodToTree(MethodModel m, Verbosity verbosity) {
        return new MapNodeImpl(BLOCK, "method")
                .with(leaf("method name", m.methodName().stringValue()),
                      list("flags",
                              "flag", m.flags().flags().stream().map(AccessFlag::name)),
                      leaf("method type", m.methodType().stringValue()),
                      list("attributes",
                              "attribute", m.attributes().stream().map(Attribute::attributeName).map(Utf8Entry::stringValue)))
                .with(attributesToTree(m.attributes(), verbosity))
                .with(codeToTree((CodeAttribute)m.code().orElse(null), verbosity));
    }

    private static MapNode codeToTree(CodeAttribute com, Verbosity verbosity) {
        if (verbosity != Verbosity.MEMBERS_ONLY && com != null) {
            var codeNode = new MapNodeImpl(BLOCK, "code");
            codeNode.with(leaf("max stack", com.maxStack()));
            codeNode.with(leaf("max locals", com.maxLocals()));
            codeNode.with(list("attributes",
                    "attribute", com.attributes().stream().map(Attribute::attributeName).map(Utf8Entry::stringValue)));
            var stackMap = new MapNodeImpl(BLOCK, "stack map frames");
            var visibleTypeAnnos = new LinkedHashMap>();
            var invisibleTypeAnnos = new LinkedHashMap>();
            List locals = List.of();
            for (var attr : com.attributes()) {
                if (attr instanceof StackMapTableAttribute smta) {
                    codeNode.with(stackMap);
                    for (var smf : smta.entries()) {
                        stackMap.with(frameToTree(com.labelToBci(smf.target()), com, smf));
                    }
                } else if (verbosity == Verbosity.TRACE_ALL && attr != null) {
                    if (attr instanceof LocalVariableTableAttribute lvta) {
                        locals = lvta.localVariables();
                        codeNode.with(new ListNodeImpl(BLOCK, "local variables",
                                IntStream.range(0, locals.size()).mapToObj(i -> {
                                    var lv = lvta.localVariables().get(i);
                                    return map(i + 1,
                                        "start", lv.startPc(),
                                        "end", lv.startPc() + lv.length(),
                                        "slot", lv.slot(),
                                        "name", lv.name().stringValue(),
                                        "type", lv.type().stringValue());
                                })));
                    } else if (attr instanceof LocalVariableTypeTableAttribute lvtta) {
                        codeNode.with(new ListNodeImpl(BLOCK, "local variable types",
                                IntStream.range(0, lvtta.localVariableTypes().size()).mapToObj(i -> {
                                    var lvt = lvtta.localVariableTypes().get(i);
                                    return map(i + 1,
                                        "start", lvt.startPc(),
                                        "end", lvt.startPc() + lvt.length(),
                                        "slot", lvt.slot(),
                                        "name", lvt.name().stringValue(),
                                        "signature", lvt.signature().stringValue());
                                })));
                    } else if (attr instanceof LineNumberTableAttribute lnta) {
                        codeNode.with(new ListNodeImpl(BLOCK, "line numbers",
                                IntStream.range(0, lnta.lineNumbers().size()).mapToObj(i -> {
                                    var ln = lnta.lineNumbers().get(i);
                                    return map(i + 1,
                                        "start", ln.startPc(),
                                        "line number", ln.lineNumber());
                                })));
                    } else if (attr instanceof CharacterRangeTableAttribute crta) {
                        codeNode.with(new ListNodeImpl(BLOCK, "character ranges",
                                IntStream.range(0, crta.characterRangeTable().size()).mapToObj(i -> {
                                    var cr = crta.characterRangeTable().get(i);
                                    return map(i + 1,
                                        "start", cr.startPc(),
                                        "end", cr.endPc(),
                                        "range start", cr.characterRangeStart(),
                                        "range end", cr.characterRangeEnd(),
                                        "flags", cr.flags());
                                })));
                    } else if (attr instanceof RuntimeVisibleTypeAnnotationsAttribute rvtaa) {
                        rvtaa.annotations().forEach(a -> forEachOffset(a, com, (off, an) ->
                                visibleTypeAnnos.computeIfAbsent(off, o -> new LinkedList<>()).add(an)));
                    } else if (attr instanceof RuntimeInvisibleTypeAnnotationsAttribute ritaa) {
                        ritaa.annotations().forEach(a -> forEachOffset(a, com, (off, an) ->
                                invisibleTypeAnnos.computeIfAbsent(off, o -> new LinkedList<>()).add(an)));
                    }
                }
            }
            codeNode.with(attributesToTree(com.attributes(), verbosity));
            if (!stackMap.containsKey(0)) {
                codeNode.with(new MapNodeImpl(FLOW, "//stack map frame @0").with(
                    list("locals", "item", convertVTIs(com, StackMapDecoder.initFrameLocals(com.parent().get()))),
                    list("stack", "item", Stream.of())));
            }
            var excHandlers = com.exceptionHandlers().stream().map(exc -> new ExceptionHandler(
                    com.labelToBci(exc.tryStart()),
                    com.labelToBci(exc.tryEnd()),
                    com.labelToBci(exc.handler()),
                    exc.catchType().map(ct -> ct.name().stringValue()).orElse(null))).toList();
            int bci = 0;
            for (var coe : com) {
                if (coe instanceof Instruction ins) {
                    var frame = stackMap.get(bci);
                    if (frame != null) {
                        codeNode.with(new MapNodeImpl(FLOW, "//stack map frame @" + bci)
                                .with(((MapNodeImpl)frame).values().toArray(new Node[2])));
                    }
                    var annos = invisibleTypeAnnos.get(bci);
                    if (annos != null) {
                        codeNode.with(typeAnnotationsToTree(FLOW, "//invisible type annotations @" + bci, annos));
                    }
                    annos = visibleTypeAnnos.get(bci);
                    if (annos != null) {
                        codeNode.with(typeAnnotationsToTree(FLOW, "//visible type annotations @" + bci, annos));
                    }
                    for (int i = 0; i < excHandlers.size(); i++) {
                        var exc = excHandlers.get(i);
                        if (exc.start() == bci) {
                            codeNode.with(map("//try block " + (i + 1) + " start",
                                    "start", exc.start(),
                                    "end", exc.end(),
                                    "handler", exc.handler(),
                                    "catch type", exc.catchType()));
                        }
                        if (exc.end() == bci) {
                            codeNode.with(map("//try block " + (i + 1) + " end",
                                    "start", exc.start(),
                                    "end", exc.end(),
                                    "handler", exc.handler(),
                                    "catch type", exc.catchType()));
                        }
                        if (exc.handler() == bci) {
                            codeNode.with(map("//exception handler " + (i + 1) + " start",
                                    "start", exc.start(),
                                    "end", exc.end(),
                                    "handler", exc.handler(),
                                    "catch type", exc.catchType()));
                        }
                    }
                    var in = new MapNodeImpl(FLOW, bci).with(leaf("opcode", ins.opcode().name()));
                    codeNode.with(in);
                    if (coe instanceof IncrementInstruction inc) in.with(leafs(
                            "slot", inc.slot(),
                            "const", inc.constant()))
                        .with(localInfoToTree(locals, inc.slot(), bci));
                    else if (coe instanceof LoadInstruction lv) in.with(leaf(
                            "slot", lv.slot()))
                        .with(localInfoToTree(locals, lv.slot(), bci));
                    else if (coe instanceof StoreInstruction lv) in.with(leaf(
                            "slot", lv.slot()))
                        .with(localInfoToTree(locals, lv.slot(), bci));
                    else if (coe instanceof FieldInstruction fa) in.with(leafs(
                        "owner", fa.owner().name().stringValue(),
                        "field name", fa.name().stringValue(),
                        "field type", fa.type().stringValue()));
                    else if (coe instanceof InvokeInstruction inv) in.with(leafs(
                        "owner", inv.owner().name().stringValue(),
                        "method name", inv.name().stringValue(),
                        "method type", inv.type().stringValue()));
                    else if (coe instanceof InvokeDynamicInstruction invd) {
                        in.with(leafs(
                            "name", invd.name().stringValue(),
                            "descriptor", invd.type().stringValue(),
                            "bootstrap method", invd.bootstrapMethod().kind().name()
                                     + " " + Util.toInternalName(invd.bootstrapMethod().owner())
                                     + "::" + invd.bootstrapMethod().methodName()));
                        in.with(list("arguments", "arg", invd.bootstrapArgs().stream()));
                    }
                    else if (coe instanceof NewObjectInstruction newo) in.with(leaf(
                        "type", newo.className().name().stringValue()));
                    else if (coe instanceof NewPrimitiveArrayInstruction newa) in.with(leafs(
                        "dimensions", 1,
                        "descriptor", newa.typeKind().upperBound().displayName()));
                    else if (coe instanceof NewReferenceArrayInstruction newa) in.with(leafs(
                        "dimensions", 1,
                        "descriptor", newa.componentType().name().stringValue()));
                    else if (coe instanceof NewMultiArrayInstruction newa) in.with(leafs(
                        "dimensions", newa.dimensions(),
                        "descriptor", newa.arrayType().name().stringValue()));
                    else if (coe instanceof TypeCheckInstruction tch) in.with(leaf(
                        "type", tch.type().name().stringValue()));
                    else if (coe instanceof ConstantInstruction cons) in.with(leaf(
                        "constant value", cons.constantValue()));
                    else if (coe instanceof BranchInstruction br) in.with(leaf(
                        "target", com.labelToBci(br.target())));
                    else if (coe instanceof LookupSwitchInstruction si) in.with(list(
                        "targets", "target", Stream.concat(Stream.of(si.defaultTarget())
                            .map(com::labelToBci), si.cases().stream()
                            .map(sc -> com.labelToBci(sc.target())))));
                    else if (coe instanceof TableSwitchInstruction si) in.with(list(
                        "targets", "target", Stream.concat(Stream.of(si.defaultTarget())
                            .map(com::labelToBci), si.cases().stream()
                            .map(sc -> com.labelToBci(sc.target())))));
                    else if (coe instanceof DiscontinuedInstruction.JsrInstruction jsr) in.with(leaf(
                        "target", com.labelToBci(jsr.target())));
                    else if (coe instanceof DiscontinuedInstruction.RetInstruction ret) in.with(leaf(
                        "slot", ret.slot()));
                    bci += ins.sizeInBytes();
                }
            }
            if (!excHandlers.isEmpty()) {
                var handlersNode = new MapNodeImpl(BLOCK, "exception handlers");
                codeNode.with(handlersNode);
                for (int i = 0; i < excHandlers.size(); i++) {
                    var exc = excHandlers.get(i);
                    handlersNode.with(map("handler " + (i + 1),
                            "start", exc.start(),
                            "end", exc.end(),
                            "handler", exc.handler(),
                            "type", exc.catchType()));
                }
            }
            return codeNode;
        }
        return null;
    }

    private static Node[] attributesToTree(List> attributes, Verbosity verbosity) {
        var nodes = new LinkedList();
        if (verbosity != Verbosity.MEMBERS_ONLY) for (var attr : attributes) {
            if (attr instanceof BootstrapMethodsAttribute bma)
                nodes.add(new ListNodeImpl(BLOCK, "bootstrap methods", bma.bootstrapMethods().stream().map(
                    bm -> {
                        var mh = bm.bootstrapMethod();
                        var mref = mh.reference();
                        var bmNode = new MapNodeImpl(FLOW, "bm");
                        bmNode.with(leafs(
                                "index", bm.bsmIndex(),
                                "kind", DirectMethodHandleDesc.Kind.valueOf(mh.kind(),
                                        mref instanceof InterfaceMethodRefEntry).name(),
                                "owner", mref.owner().asInternalName(),
                                "name", mref.nameAndType().name().stringValue()));
                        bmNode.with(list("args", "arg", bm.arguments().stream().map(LoadableConstantEntry::constantValue)));
                        return bmNode;
                    })));
            else if (attr instanceof ConstantValueAttribute cva)
                nodes.add(leaf("constant value", cva.constant().constantValue()));
            else if (attr instanceof NestHostAttribute nha)
                nodes.add(leaf("nest host", nha.nestHost().name().stringValue()));
            else if (attr instanceof NestMembersAttribute nma)
                nodes.add(list("nest members", "member", nma.nestMembers().stream()
                    .map(mp -> mp.name().stringValue())));
            else if (attr instanceof PermittedSubclassesAttribute psa)
                nodes.add(list("permitted subclasses", "subclass", psa.permittedSubclasses().stream()
                    .map(e -> e.name().stringValue())));
            if (verbosity == Verbosity.TRACE_ALL) {
                if (attr instanceof EnclosingMethodAttribute ema)
                    nodes.add(map("enclosing method",
                            "class", ema.enclosingClass().name().stringValue(),
                            "method name", ema.enclosingMethodName()
                                    .map(Utf8Entry::stringValue).orElse("null"),
                            "method type", ema.enclosingMethodType()
                                    .map(Utf8Entry::stringValue).orElse("null")));
                else if (attr instanceof ExceptionsAttribute exa)
                    nodes.add(list("exceptions", "exc", exa.exceptions().stream()
                            .map(e -> e.name().stringValue())));
                else if (attr instanceof InnerClassesAttribute ica)
                    nodes.add(new ListNodeImpl(BLOCK, "inner classes", ica.classes().stream()
                            .map(ic -> new MapNodeImpl(FLOW, "cls").with(
                                leaf("inner class", ic.innerClass().name().stringValue()),
                                leaf("outer class", ic.outerClass()
                                        .map(cle -> cle.name().stringValue()).orElse("null")),
                                leaf("inner name", ic.innerName().map(Utf8Entry::stringValue).orElse("null")),
                                list("flags", "flag", ic.flags().stream().map(AccessFlag::name))))));
                else if (attr instanceof MethodParametersAttribute mpa) {
                    var n = new MapNodeImpl(BLOCK, "method parameters");
                    for (int i = 0; i < mpa.parameters().size(); i++) {
                        var p = mpa.parameters().get(i);
                        n.with(new MapNodeImpl(FLOW, i + 1).with(
                                leaf("name", p.name().map(Utf8Entry::stringValue).orElse("null")),
                                list("flags", "flag", p.flags().stream().map(AccessFlag::name))));
                    }
                }
                else if (attr instanceof ModuleAttribute ma)
                    nodes.add(new MapNodeImpl(BLOCK, "module")
                            .with(leaf("name", ma.moduleName().name().stringValue()),
                                  list("flags","flag", ma.moduleFlags().stream().map(AccessFlag::name)),
                                  leaf("version", ma.moduleVersion().map(Utf8Entry::stringValue).orElse("null")),
                                  list("uses", "class", ma.uses().stream().map(ce -> ce.name().stringValue())),
                                  new ListNodeImpl(BLOCK, "requires", ma.requires().stream().map(req ->
                                    new MapNodeImpl(FLOW, "req").with(
                                            leaf("name", req.requires().name().stringValue()),
                                            list("flags", "flag", req.requiresFlags().stream()
                                                    .map(AccessFlag::name)),
                                            leaf("version", req.requiresVersion()
                                                    .map(Utf8Entry::stringValue).orElse(null))))),
                                  new ListNodeImpl(BLOCK, "exports", ma.exports().stream().map(exp ->
                                    new MapNodeImpl(FLOW, "exp").with(
                                            leaf("package", exp.exportedPackage().asSymbol().name()),
                                            list("flags", "flag", exp.exportsFlags().stream()
                                                    .map(AccessFlag::name)),
                                            list("to", "module", exp.exportsTo().stream()
                                                    .map(me -> me.name().stringValue()))))),
                                  new ListNodeImpl(BLOCK, "opens", ma.opens().stream().map(opn ->
                                    new MapNodeImpl(FLOW, "opn").with(
                                            leaf("package", opn.openedPackage().asSymbol().name()),
                                            list("flags", "flag", opn.opensFlags().stream()
                                                    .map(AccessFlag::name)),
                                            list("to", "module", opn.opensTo().stream()
                                                    .map(me -> me.name().stringValue()))))),
                                  new ListNodeImpl(BLOCK, "provides", ma.provides().stream()
                                          .map(prov -> new MapNodeImpl(FLOW, "prov").with(
                                                  leaf("class", prov.provides().name().stringValue()),
                                                  list("with", "cls", prov.providesWith().stream()
                                                          .map(ce -> ce.name().stringValue())))))));
                else if (attr instanceof ModulePackagesAttribute mopa)
                    nodes.add(list("module packages", "subclass", mopa.packages().stream()
                            .map(mp -> mp.asSymbol().name())));
                else if (attr instanceof ModuleMainClassAttribute mmca)
                    nodes.add(leaf("module main class", mmca.mainClass().name().stringValue()));
                else if (attr instanceof RecordAttribute ra)
                    nodes.add(new ListNodeImpl(BLOCK, "record components", ra.components().stream()
                            .map(rc -> new MapNodeImpl(BLOCK, "component")
                                    .with(leafs(
                                        "name", rc.name().stringValue(),
                                        "type", rc.descriptor().stringValue()))
                                    .with(list("attributes", "attribute", rc.attributes().stream()
                                            .map(Attribute::attributeName).map(Utf8Entry::stringValue)))
                                    .with(attributesToTree(rc.attributes(), verbosity)))));
                else if (attr instanceof AnnotationDefaultAttribute ada)
                    nodes.add(new MapNodeImpl(FLOW, "annotation default").with(elementValueToTree(ada.defaultValue())));
                else if (attr instanceof RuntimeInvisibleAnnotationsAttribute aa)
                    nodes.add(annotationsToTree("invisible annotations", aa.annotations()));
                else if (attr instanceof RuntimeVisibleAnnotationsAttribute aa)
                    nodes.add(annotationsToTree("visible annotations", aa.annotations()));
                else if (attr instanceof RuntimeInvisibleParameterAnnotationsAttribute aa)
                    nodes.add(parameterAnnotationsToTree("invisible parameter annotations", aa.parameterAnnotations()));
                else if (attr instanceof RuntimeVisibleParameterAnnotationsAttribute aa)
                    nodes.add(parameterAnnotationsToTree("visible parameter annotations", aa.parameterAnnotations()));
                else if (attr instanceof RuntimeInvisibleTypeAnnotationsAttribute aa)
                    nodes.add(typeAnnotationsToTree(BLOCK, "invisible type annotations", aa.annotations()));
                else if (attr instanceof RuntimeVisibleTypeAnnotationsAttribute aa)
                    nodes.add(typeAnnotationsToTree(BLOCK, "visible type annotations", aa.annotations()));
                else if (attr instanceof SignatureAttribute sa)
                    nodes.add(leaf("signature", sa.signature().stringValue()));
                else if (attr instanceof SourceFileAttribute sfa)
                    nodes.add(leaf("source file", sfa.sourceFile().stringValue()));
            }
        }
        return nodes.toArray(Node[]::new);
    }

    private static Node annotationsToTree(String name, List annos) {
        return new ListNodeImpl(BLOCK, name, annos.stream().map(a ->
                new MapNodeImpl(FLOW, "anno")
                        .with(leaf("annotation class", a.className().stringValue()))
                        .with(elementValuePairsToTree(a.elements()))));

    }

    private static Node typeAnnotationsToTree(Style style, String name, List annos) {
        return new ListNodeImpl(style, name, annos.stream().map(a ->
                new MapNodeImpl(FLOW, "anno")
                        .with(leaf("annotation class", a.annotation().className().stringValue()),
                              leaf("target info", a.targetInfo().targetType().name()))
                        .with(elementValuePairsToTree(a.annotation().elements()))));

    }

    private static MapNodeImpl parameterAnnotationsToTree(String name, List> paramAnnotations) {
        var node = new MapNodeImpl(BLOCK, name);
        for (int i = 0; i < paramAnnotations.size(); i++) {
            var annos = paramAnnotations.get(i);
            if (!annos.isEmpty()) {
                node.with(new ListNodeImpl(FLOW, "parameter " + (i + 1), annos.stream().map(a ->
                                new MapNodeImpl(FLOW, "anno")
                                        .with(leaf("annotation class", a.className().stringValue()))
                                        .with(elementValuePairsToTree(a.elements())))));
            }
        }
        return node;
    }

    private static Node[] localInfoToTree(List locals, int slot, int bci) {
        if (locals != null) {
            for (var l : locals) {
                if (l.slot() == slot && l.startPc() <= bci && l.length() + l.startPc() >= bci) {
                    return leafs("type", l.type().stringValue(),
                                 "variable name", l.name().stringValue());
                }
            }
        }
        return new Node[0];
    }

    private static void forEachOffset(TypeAnnotation ta, CodeAttribute lr, BiConsumer consumer) {
        if (ta.targetInfo() instanceof TypeAnnotation.OffsetTarget ot)
            consumer.accept(lr.labelToBci(ot.target()), ta);
        else if (ta.targetInfo() instanceof TypeAnnotation.TypeArgumentTarget tat)
            consumer.accept(lr.labelToBci(tat.target()), ta);
        else if (ta.targetInfo() instanceof TypeAnnotation.LocalVarTarget lvt)
            lvt.table().forEach(lvti -> consumer.accept(lr.labelToBci(lvti.startLabel()), ta));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy