/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you under
 * the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.elasticsearch.test.loggerusage;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.Supplier;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicInterpreter;
import org.objectweb.asm.tree.analysis.BasicValue;
import org.objectweb.asm.tree.analysis.Frame;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;

public class ESLoggerUsageChecker {
    public static final Type LOGGER_CLASS = Type.getType(Logger.class);
    public static final Type THROWABLE_CLASS = Type.getType(Throwable.class);
    public static final Type STRING_CLASS = Type.getType(String.class);
    public static final Type STRING_ARRAY_CLASS = Type.getType(String[].class);
    public static final Type PARAMETERIZED_MESSAGE_CLASS = Type.getType(ParameterizedMessage.class);
    public static final Type OBJECT_CLASS = Type.getType(Object.class);
    public static final Type OBJECT_ARRAY_CLASS = Type.getType(Object[].class);
    public static final Type SUPPLIER_ARRAY_CLASS = Type.getType(Supplier[].class);
    public static final Type MARKER_CLASS = Type.getType(Marker.class);
    public static final List<String> LOGGER_METHODS = Arrays.asList("trace", "debug", "info", "warn", "error", "fatal");
    public static final String IGNORE_CHECKS_ANNOTATION = "org.elasticsearch.common.SuppressLoggerChecks";

    @SuppressForbidden(reason = "command line tool")
    public static void main(String... args) throws Exception {
        System.out.println("checking for wrong usages of ESLogger...");
        boolean[] wrongUsageFound = new boolean[1];
        checkLoggerUsage(wrongLoggerUsage -> {
            System.err.println(wrongLoggerUsage.getErrorLines());
            wrongUsageFound[0] = true;
        }, args);
        if (wrongUsageFound[0]) {
            throw new Exception("Wrong logger usages found");
        } else {
            System.out.println("No wrong usages found");
        }
    }

    private static void checkLoggerUsage(Consumer<WrongLoggerUsage> wrongUsageCallback, String... classDirectories)
        throws IOException {
        for (String classDirectory : classDirectories) {
            Path root = Paths.get(classDirectory);
            if (Files.isDirectory(root) == false) {
                throw new IllegalArgumentException(root + " should be an existing directory");
            }
            Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    if (Files.isRegularFile(file) && file.getFileName().toString().endsWith(".class")) {
                        try (InputStream in = Files.newInputStream(file)) {
                            ESLoggerUsageChecker.check(wrongUsageCallback, in);
                        }
                    }
                    return super.visitFile(file, attrs);
                }
            });
        }
    }

    public static void check(Consumer<WrongLoggerUsage> wrongUsageCallback, InputStream inputStream) throws IOException {
        check(wrongUsageCallback, inputStream, s -> true);
    }

    // used by tests
    static void check(Consumer<WrongLoggerUsage> wrongUsageCallback, InputStream inputStream, Predicate<String> methodsToCheck)
        throws IOException {
        ClassReader cr = new ClassReader(inputStream);
        cr.accept(new ClassChecker(wrongUsageCallback, methodsToCheck), 0);
    }

    public static class WrongLoggerUsage {
        private final String className;
        private final String methodName;
        private final String logMethodName;
        private final int line;
        private final String errorMessage;

        public WrongLoggerUsage(String className, String methodName, String logMethodName, int line, String errorMessage) {
            this.className = className;
            this.methodName = methodName;
            this.logMethodName = logMethodName;
            this.line = line;
            this.errorMessage = errorMessage;
        }

        @Override
        public String toString() {
            return "WrongLoggerUsage{" +
                "className='" + className + '\'' +
                ", methodName='" + methodName + '\'' +
                ", logMethodName='" + logMethodName + '\'' +
                ", line=" + line +
                ", errorMessage='" + errorMessage + '\'' +
                '}';
        }

        /**
         * Returns an error message that has the form of stack traces emitted by {@link Throwable#printStackTrace}
         */
        public String getErrorLines() {
            String fullClassName = Type.getObjectType(className).getClassName();
            String simpleClassName = fullClassName.substring(fullClassName.lastIndexOf(".") + 1, fullClassName.length());
            int innerClassIndex = simpleClassName.indexOf("$");
            if (innerClassIndex > 0) {
                simpleClassName = simpleClassName.substring(0, innerClassIndex);
            }
            simpleClassName = simpleClassName + ".java";
            StringBuilder sb = new StringBuilder();
            sb.append("Bad usage of ");
            sb.append(LOGGER_CLASS.getClassName()).append("#").append(logMethodName);
            sb.append(": ");
            sb.append(errorMessage);
            sb.append("\n\tat ");
            sb.append(fullClassName);
            sb.append(".");
            sb.append(methodName);
            sb.append("(");
            sb.append(simpleClassName);
            sb.append(":");
            sb.append(line);
            sb.append(")");
            return sb.toString();
        }
    }

    private static class ClassChecker extends ClassVisitor {
        private String className;
        private boolean ignoreChecks;
        private final Consumer<WrongLoggerUsage> wrongUsageCallback;
        private final Predicate<String> methodsToCheck;

        ClassChecker(Consumer<WrongLoggerUsage> wrongUsageCallback, Predicate<String> methodsToCheck) {
            super(Opcodes.ASM5);
            this.wrongUsageCallback = wrongUsageCallback;
            this.methodsToCheck = methodsToCheck;
        }

        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            this.className = name;
        }

        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            if (IGNORE_CHECKS_ANNOTATION.equals(Type.getType(desc).getClassName())) {
                ignoreChecks = true;
            }
            return super.visitAnnotation(desc, visible);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            if (ignoreChecks == false && methodsToCheck.test(name)) {
                return new MethodChecker(this.className, access, name, desc, wrongUsageCallback);
            } else {
                return super.visitMethod(access, name, desc, signature, exceptions);
            }
        }
    }

    private static class MethodChecker extends MethodVisitor {
        private final String className;
        private final Consumer<WrongLoggerUsage> wrongUsageCallback;
        private boolean ignoreChecks;

        MethodChecker(String className, int access, String name, String desc, Consumer<WrongLoggerUsage> wrongUsageCallback) {
            super(Opcodes.ASM5, new MethodNode(access, name, desc, null, null));
            this.className = className;
            this.wrongUsageCallback = wrongUsageCallback;
        }

        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            if (IGNORE_CHECKS_ANNOTATION.equals(Type.getType(desc).getClassName())) {
                ignoreChecks = true;
            }
            return super.visitAnnotation(desc, visible);
        }

        @Override
        public void visitEnd() {
            if (ignoreChecks == false) {
                findBadLoggerUsages((MethodNode) mv);
            }
            super.visitEnd();
        }

        public void findBadLoggerUsages(MethodNode methodNode) {
            Analyzer<BasicValue> stringPlaceHolderAnalyzer = new Analyzer<>(new PlaceHolderStringInterpreter());
            Analyzer<BasicValue> arraySizeAnalyzer = new Analyzer<>(new ArraySizeInterpreter());
            try {
                stringPlaceHolderAnalyzer.analyze(className, methodNode);
                arraySizeAnalyzer.analyze(className, methodNode);
            } catch (AnalyzerException e) {
                throw new RuntimeException("Internal error: failed in analysis step", e);
            }
            Frame<BasicValue>[] logMessageFrames = stringPlaceHolderAnalyzer.getFrames();
            Frame<BasicValue>[] arraySizeFrames = arraySizeAnalyzer.getFrames();
            AbstractInsnNode[] insns = methodNode.instructions.toArray();
            int lineNumber = -1;
            for (int i = 0; i < insns.length; i++) {
                AbstractInsnNode insn = insns[i];
                if (insn instanceof LineNumberNode) {
                    LineNumberNode lineNumberNode = (LineNumberNode) insn;
                    lineNumber = lineNumberNode.line;
                }
                if (insn.getOpcode() == Opcodes.INVOKEINTERFACE) {
                    MethodInsnNode methodInsn = (MethodInsnNode) insn;
                    if (Type.getObjectType(methodInsn.owner).equals(LOGGER_CLASS)) {
                        if (LOGGER_METHODS.contains(methodInsn.name) == false) {
                            continue;
                        }

                        Type[] argumentTypes = Type.getArgumentTypes(methodInsn.desc);
                        int markerOffset = 0;
                        if (argumentTypes[0].equals(MARKER_CLASS)) {
                            markerOffset = 1;
                        }

                        int lengthWithoutMarker = argumentTypes.length - markerOffset;

                        if (lengthWithoutMarker == 2 &&
                            argumentTypes[markerOffset + 0].equals(STRING_CLASS) &&
                            (argumentTypes[markerOffset + 1].equals(OBJECT_ARRAY_CLASS) ||
                                argumentTypes[markerOffset + 1].equals(SUPPLIER_ARRAY_CLASS))) {
                            // VARARGS METHOD: debug(Marker?, String, (Object...|Supplier...))
                            checkArrayArgs(methodNode, logMessageFrames[i], arraySizeFrames[i], lineNumber, methodInsn, markerOffset + 0,
                                markerOffset + 1);
                        } else if (lengthWithoutMarker >= 2 &&
                            argumentTypes[markerOffset + 0].equals(STRING_CLASS) &&
                            argumentTypes[markerOffset + 1].equals(OBJECT_CLASS)) {
                            // MULTI-PARAM METHOD: debug(Marker?, String, Object p0, ...)
                            checkFixedArityArgs(methodNode, logMessageFrames[i], lineNumber, methodInsn, markerOffset + 0,
                                lengthWithoutMarker - 1);
                        } else if ((lengthWithoutMarker == 1 || lengthWithoutMarker == 2) &&
                            lengthWithoutMarker == 2 ? argumentTypes[markerOffset + 1].equals(THROWABLE_CLASS) : true) {
                            // all the rest: debug(Marker?, (Message|MessageSupplier|CharSequence|Object|String|Supplier), Throwable?)
                            checkFixedArityArgs(methodNode, logMessageFrames[i], lineNumber, methodInsn, markerOffset + 0, 0);
                        } else {
                            throw new IllegalStateException("Method invoked on " + LOGGER_CLASS.getClassName() +
                                " that is not supported by logger usage checker");
                        }
                    }
                } else if (insn.getOpcode() == Opcodes.INVOKESPECIAL) { // constructor invocation
                    MethodInsnNode methodInsn = (MethodInsnNode) insn;
                    if (Type.getObjectType(methodInsn.owner).equals(PARAMETERIZED_MESSAGE_CLASS)) {
                        Type[] argumentTypes = Type.getArgumentTypes(methodInsn.desc);
                        if (argumentTypes.length == 2 &&
                            argumentTypes[0].equals(STRING_CLASS) &&
                            argumentTypes[1].equals(OBJECT_ARRAY_CLASS)) {
                            checkArrayArgs(methodNode, logMessageFrames[i], arraySizeFrames[i], lineNumber, methodInsn, 0, 1);
                        } else if (argumentTypes.length == 2 &&
                            argumentTypes[0].equals(STRING_CLASS) &&
                            argumentTypes[1].equals(OBJECT_CLASS)) {
                            checkFixedArityArgs(methodNode, logMessageFrames[i], lineNumber, methodInsn, 0, 1);
                        } else if (argumentTypes.length == 3 &&
                            argumentTypes[0].equals(STRING_CLASS) &&
                            argumentTypes[1].equals(OBJECT_CLASS) &&
                            argumentTypes[2].equals(OBJECT_CLASS)) {
                            checkFixedArityArgs(methodNode, logMessageFrames[i], lineNumber, methodInsn, 0, 2);
                        } else if (argumentTypes.length == 3 &&
                            argumentTypes[0].equals(STRING_CLASS) &&
                            argumentTypes[1].equals(OBJECT_ARRAY_CLASS) &&
                            argumentTypes[2].equals(THROWABLE_CLASS)) {
                            checkArrayArgs(methodNode, logMessageFrames[i], arraySizeFrames[i], lineNumber, methodInsn, 0, 1);
                        } else if (argumentTypes.length == 3 &&
                            argumentTypes[0].equals(STRING_CLASS) &&
                            argumentTypes[1].equals(STRING_ARRAY_CLASS) &&
                            argumentTypes[2].equals(THROWABLE_CLASS)) {
                            checkArrayArgs(methodNode, logMessageFrames[i], arraySizeFrames[i], lineNumber, methodInsn, 0, 1);
                        } else {
                            throw new IllegalStateException("Constructor invoked on " + PARAMETERIZED_MESSAGE_CLASS.getClassName() +
                                " that is not supported by logger usage checker");
                        }
                    }
                }
            }
        }

        private void checkFixedArityArgs(MethodNode methodNode, Frame<BasicValue> logMessageFrame, int lineNumber,
                                         MethodInsnNode methodInsn, int messageIndex, int positionalArgsLength) {
            PlaceHolderStringBasicValue logMessageLength = checkLogMessageConsistency(methodNode, logMessageFrame, lineNumber, methodInsn,
                messageIndex, positionalArgsLength);
            if (logMessageLength == null) {
                return;
            }
            if (logMessageLength.minValue != positionalArgsLength) {
                wrongUsageCallback.accept(new WrongLoggerUsage(className, methodNode.name, methodInsn.name, lineNumber,
                    "Expected " + logMessageLength.minValue + " arguments but got " + positionalArgsLength));
                return;
            }
        }

        private void checkArrayArgs(MethodNode methodNode, Frame<BasicValue> logMessageFrame, Frame<BasicValue> arraySizeFrame,
                                    int lineNumber, MethodInsnNode methodInsn, int messageIndex, int arrayIndex) {
            BasicValue arraySizeObject = getStackValue(arraySizeFrame, methodInsn, arrayIndex);
            if (arraySizeObject instanceof ArraySizeBasicValue == false) {
                wrongUsageCallback.accept(new WrongLoggerUsage(className, methodNode.name, methodInsn.name, lineNumber,
                    "Could not determine size of array"));
                return;
            }
            ArraySizeBasicValue arraySize = (ArraySizeBasicValue) arraySizeObject;
            PlaceHolderStringBasicValue logMessageLength = checkLogMessageConsistency(methodNode, logMessageFrame, lineNumber, methodInsn,
                messageIndex, arraySize.minValue);
            if (logMessageLength == null) {
                return;
            }
            if (arraySize.minValue != arraySize.maxValue) {
                wrongUsageCallback.accept(new WrongLoggerUsage(className, methodNode.name, methodInsn.name, lineNumber,
                    "Multiple parameter arrays with conflicting sizes"));
                return;
            }
            assert logMessageLength.minValue == logMessageLength.maxValue && arraySize.minValue == arraySize.maxValue;
            if (logMessageLength.minValue != arraySize.minValue) {
                wrongUsageCallback.accept(new WrongLoggerUsage(className, methodNode.name, methodInsn.name, lineNumber,
                    "Expected " + logMessageLength.minValue + " arguments but got " + arraySize.minValue));
                return;
            }
        }

        private PlaceHolderStringBasicValue checkLogMessageConsistency(MethodNode methodNode, Frame<BasicValue> logMessageFrame,
                                                                       int lineNumber, MethodInsnNode methodInsn, int messageIndex,
                                                                       int argsSize) {
            BasicValue logMessageLengthObject = getStackValue(logMessageFrame, methodInsn, messageIndex);
            if (logMessageLengthObject instanceof PlaceHolderStringBasicValue == false) {
                if (argsSize > 0) {
                    wrongUsageCallback.accept(new WrongLoggerUsage(className, methodNode.name, methodInsn.name, lineNumber,
                        "First argument must be a string constant so that we can statically ensure proper place holder usage"));
                } else {
                    // don't check logger usage for logger.warn(someObject)
                }
                return null;
            }
            PlaceHolderStringBasicValue logMessageLength = (PlaceHolderStringBasicValue) logMessageLengthObject;
            if (logMessageLength.minValue != logMessageLength.maxValue) {
                wrongUsageCallback.accept(new WrongLoggerUsage(className, methodNode.name, methodInsn.name, lineNumber,
                    "Multiple log messages with conflicting number of place holders"));
                return null;
            }
            return logMessageLength;
        }
    }

    private static int calculateNumberOfPlaceHolders(String message) {
        int count = 0;
        for (int i = 1; i < message.length(); i++) {
            if (message.charAt(i - 1) == '{' && message.charAt(i) == '}') {
                count++;
                i += 1;
            }
        }
        return count;
    }

    private static BasicValue getStackValue(Frame<BasicValue> f, MethodInsnNode methodInsn, int index) {
        int relIndex = Type.getArgumentTypes(methodInsn.desc).length - 1 - index;
        int top = f.getStackSize() - 1;
        return relIndex <= top ? f.getStack(top - relIndex) : null;
    }

    private static class IntMinMaxTrackingBasicValue extends BasicValue {
        protected final int minValue;
        protected final int maxValue;

        IntMinMaxTrackingBasicValue(Type type, int value) {
            super(type);
            this.minValue = value;
            this.maxValue = value;
        }

        IntMinMaxTrackingBasicValue(Type type, int minValue, int maxValue) {
            super(type);
            this.minValue = minValue;
            this.maxValue = maxValue;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            if (!super.equals(o)) return false;

            IntMinMaxTrackingBasicValue that = (IntMinMaxTrackingBasicValue) o;

            if (minValue != that.minValue) return false;
            return maxValue == that.maxValue;

        }

        @Override
        public int hashCode() {
            int result = super.hashCode();
            result = 31 * result + minValue;
            result = 31 * result + maxValue;
            return result;
        }

        @Override
        public String toString() {
            return "IntMinMaxTrackingBasicValue{" +
                "minValue=" + minValue +
                ", maxValue=" + maxValue +
                '}';
        }
    }

    private static final class PlaceHolderStringBasicValue extends IntMinMaxTrackingBasicValue {
        public static final Type STRING_OBJECT_TYPE = Type.getObjectType("java/lang/String");

        PlaceHolderStringBasicValue(int placeHolders) {
            super(STRING_OBJECT_TYPE, placeHolders);
        }

        PlaceHolderStringBasicValue(int minPlaceHolders, int maxPlaceHolders) {
            super(STRING_OBJECT_TYPE, minPlaceHolders, maxPlaceHolders);
        }
    }

    private static final class ArraySizeBasicValue extends IntMinMaxTrackingBasicValue {
        ArraySizeBasicValue(Type type, int minArraySize, int maxArraySize) {
            super(type, minArraySize, maxArraySize);
        }
    }

    private static final class IntegerConstantBasicValue extends IntMinMaxTrackingBasicValue {
        IntegerConstantBasicValue(Type type, int constant) {
            super(type, constant);
        }

        IntegerConstantBasicValue(Type type, int minConstant, int maxConstant) {
            super(type, minConstant, maxConstant);
        }
    }

    private static final class PlaceHolderStringInterpreter extends BasicInterpreter {
        @Override
        public BasicValue newOperation(AbstractInsnNode insnNode) throws AnalyzerException {
            if (insnNode.getOpcode() == Opcodes.LDC) {
                Object constant = ((LdcInsnNode) insnNode).cst;
                if (constant instanceof String) {
                    return new PlaceHolderStringBasicValue(calculateNumberOfPlaceHolders((String) constant));
                }
            }
            return super.newOperation(insnNode);
        }

        @Override
        public BasicValue merge(BasicValue value1, BasicValue value2) {
            if (value1 instanceof PlaceHolderStringBasicValue && value2 instanceof PlaceHolderStringBasicValue
                && value1.equals(value2) == false) {
                PlaceHolderStringBasicValue c1 = (PlaceHolderStringBasicValue) value1;
                PlaceHolderStringBasicValue c2 = (PlaceHolderStringBasicValue) value2;
                return new PlaceHolderStringBasicValue(Math.min(c1.minValue, c2.minValue), Math.max(c1.maxValue, c2.maxValue));
            }
            return super.merge(value1, value2);
        }
    }

    private static final class ArraySizeInterpreter extends BasicInterpreter {
        @Override
        public BasicValue newOperation(AbstractInsnNode insnNode) throws AnalyzerException {
            switch (insnNode.getOpcode()) {
                case ICONST_0: return new IntegerConstantBasicValue(Type.INT_TYPE, 0);
                case ICONST_1: return new IntegerConstantBasicValue(Type.INT_TYPE, 1);
                case ICONST_2: return new IntegerConstantBasicValue(Type.INT_TYPE, 2);
                case ICONST_3: return new IntegerConstantBasicValue(Type.INT_TYPE, 3);
                case ICONST_4: return new IntegerConstantBasicValue(Type.INT_TYPE, 4);
                case ICONST_5: return new IntegerConstantBasicValue(Type.INT_TYPE, 5);
                case BIPUSH:
                case SIPUSH: return new IntegerConstantBasicValue(Type.INT_TYPE, ((IntInsnNode)insnNode).operand);
                case Opcodes.LDC: {
                    Object constant = ((LdcInsnNode)insnNode).cst;
                    if (constant instanceof Integer) {
                        return new IntegerConstantBasicValue(Type.INT_TYPE, (Integer)constant);
                    } else {
                        return super.newOperation(insnNode);
                    }
                }
                default: return super.newOperation(insnNode);
            }
        }

        @Override
        public BasicValue merge(BasicValue value1, BasicValue value2) {
            if (value1 instanceof IntegerConstantBasicValue && value2 instanceof IntegerConstantBasicValue) {
                IntegerConstantBasicValue c1 = (IntegerConstantBasicValue) value1;
                IntegerConstantBasicValue c2 = (IntegerConstantBasicValue) value2;
                return new IntegerConstantBasicValue(Type.INT_TYPE, Math.min(c1.minValue, c2.minValue), Math.max(c1.maxValue, c2.maxValue));
            } else if (value1 instanceof ArraySizeBasicValue && value2 instanceof ArraySizeBasicValue) {
                ArraySizeBasicValue c1 = (ArraySizeBasicValue) value1;
                ArraySizeBasicValue c2 = (ArraySizeBasicValue) value2;
                return new ArraySizeBasicValue(Type.INT_TYPE, Math.min(c1.minValue, c2.minValue), Math.max(c1.maxValue, c2.maxValue));
            }
            return super.merge(value1, value2);
        }

        @Override
        public BasicValue unaryOperation(AbstractInsnNode insnNode, BasicValue value) throws AnalyzerException {
            if (insnNode.getOpcode() == Opcodes.ANEWARRAY && value instanceof IntegerConstantBasicValue) {
                IntegerConstantBasicValue constantBasicValue = (IntegerConstantBasicValue) value;
                String desc = ((TypeInsnNode) insnNode).desc;
                return new ArraySizeBasicValue(Type.getType("[" + Type.getObjectType(desc)), constantBasicValue.minValue,
                    constantBasicValue.maxValue);
            }
            return super.unaryOperation(insnNode, value);
        }

        @Override
        public BasicValue ternaryOperation(AbstractInsnNode insnNode, BasicValue value1, BasicValue value2, BasicValue value3)
            throws AnalyzerException {
            if (insnNode.getOpcode() == Opcodes.AASTORE && value1 instanceof ArraySizeBasicValue) {
                return value1;
            }
            return super.ternaryOperation(insnNode, value1, value2, value3);
        }
    }
}
