java.class-file

how to map parameter annotations to corresponding method parameter in classfile using asm


As per the jvms-4.7.18 :

The i'th entry in the parameter_annotations table may, but is not required to, correspond to the i'th parameter descriptor in the method descriptor (§4.3.3).
For example, a compiler may choose to create entries in the table corresponding only to those parameter descriptors which represent explicitly declared parameters in source code. In the Java programming language, a constructor of an inner class is specified to have an implicitly declared parameter before its explicitly declared parameters (JLS §8.8.1), so the corresponding method in a class file has a parameter descriptor representing the implicitly declared parameter before any parameter descriptors representing explicitly declared parameters. If the first explicitly declared parameter is annotated in source code, then a compiler may create parameter_annotations[0] to store annotations corresponding to the second parameter descriptor.

And in asm MethodVisitor.visitParameterAnnotation​ javadoc also states that:

parameter - the parameter index. This index must be strictly smaller than the number of parameters in the method descriptor, and strictly smaller than the parameter count specified in visitAnnotableParameterCount(int, boolean). Important note: a parameter index i is not required to correspond to the i'th parameter descriptor in the method descriptor, in particular in case of synthetic parameters

So the parameter index doesn't always match. Then how should I know which parameter annotation correspond to which parameter?

At first I thought I could use MethodParameters attribute in jvms, which corresponds to MethodVisitor.visitParameter​ in asm, which has parameter access_flags in it. I just need to filter out parameters which are ACC_SYNTHETIC or ACC_MANDATED, then the remaining parameters would just match the parameter annotations. But later I found out that MethodParameters doesn't exist in classfile by default.(see my other question: how to show method parameter access_flags in java classfile) That means I can't get a parameter's access_flags in classfile, and the approach above doesn't work.

I also found related issues in asm's reposiroty:

In the discussions of above issues, it's decided that asm users should do the mapping between bytecode parameter indices and source code parameters if desired, but it doesn't mention how.

I'm also wondering how java reflection framework does the mapping, which may give me some inspiration, but I haven't checked that.

Back to the question, how should I map parameter annotations and parameters correctly?


Solution

  • I dug into the Reflection implementation code and all it does to map annotations to parameters, is to assume that the synthetic parameters are at the beginning if the number of parameters does not match the number of parameter annotations.

    This, however, is only applied to constructors of inner classes or enum types, the known use cases of the Java language where this strategy works. For constructors of local and anonymous class, it will simply ignore the mismatch. For any other case, including methods, the implementation will throw an exception if there’s a mismatch.

    So we can do a similar thing with ASM for the known use cases.

    package asmtest;
    
    public enum Example {
        ;
        Example(@Deprecated int i) {}
        public class Inner {
            Inner(@Deprecated String foo) {}
        }
    }
    
    public class ReadParameters extends ClassVisitor {
        public static void main(String[] args) throws IOException {
            for(Class<?> cl: List.of(Example.class, Example.Inner.class)) {
                System.out.println(cl);
                new ClassReader(cl.getName())
                    .accept(new ReadParameters(), ClassReader.SKIP_CODE);
                System.out.println();
            }
        }
    
        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor,
                                         String signature, String[] exceptions) {
            System.out.println(" " + name);
            return new ParameterVisitor(name, descriptor);
        }
    
        static class ParameterVisitor extends MethodVisitor {
            final Type[] parameterTypes;
            int offset;
    
            ParameterVisitor(String name, String desc) {
                super(Opcodes.ASM9);
                parameterTypes = Type.getArgumentTypes(desc);
            }
    
            @Override
            public void visitAnnotableParameterCount(int parameterCount, boolean visible) {
                offset = parameterTypes.length - parameterCount;
                for(int i = 0; i < offset; i++)
                    System.out.printf(" %3d %-20s %s%n",
                        i, parameterTypes[i].getClassName(), "(synthetic)");
            }
    
            @Override
            public AnnotationVisitor visitParameterAnnotation(
                    int parameter, String descriptor, boolean visible) {
                parameter += offset;
                System.out.printf(" %3d %-20s %s%n", parameter,
                    parameterTypes[parameter].getClassName(),
                    Type.getType(descriptor).getClassName());
                return null;
            }
        }
    
        protected ReadParameters() {
            super(Opcodes.ASM9);
        }
    }
    
    class asmtest.Example
     values
     valueOf
     <init>
       0 java.lang.String     (synthetic)
       1 int                  (synthetic)
       2 int                  java.lang.Deprecated
     $values
     <clinit>
    
    class asmtest.Example$Inner
     <init>
       0 asmtest.Example      (synthetic)
       1 java.lang.String     java.lang.Deprecated
    

    There doesn’t seem to be a smarter solution at all.


    For reference, this is how the Reflection code looks like

    (Executable)

    Annotation[][] sharedGetParameterAnnotations(Class<?>[] parameterTypes,
                                                 byte[] parameterAnnotations) {
        int numParameters = parameterTypes.length;
        if (parameterAnnotations == null)
            return new Annotation[numParameters][0];
    
        Annotation[][] result = parseParameterAnnotations(parameterAnnotations);
    
        if (result.length != numParameters &&
            handleParameterNumberMismatch(result.length, parameterTypes)) {
            Annotation[][] tmp = new Annotation[numParameters][];
            // Shift annotations down to account for any implicit leading parameters
            System.arraycopy(result, 0, tmp, numParameters - result.length, result.length);
            for (int i = 0; i < numParameters - result.length; i++) {
                tmp[i] = new Annotation[0];
            }
            result = tmp;
        }
        return result;
    }
    

    (Constructor)

    @Override
    boolean handleParameterNumberMismatch(int resultLength, Class<?>[] parameterTypes) {
        int numParameters = parameterTypes.length;
        Class<?> declaringClass = getDeclaringClass();
        if (declaringClass.isEnum()) {
            return resultLength + 2 == numParameters &&
                    parameterTypes[0] == String.class &&
                    parameterTypes[1] == int.class;
        } else if (
            declaringClass.isAnonymousClass() ||
            declaringClass.isLocalClass() )
            return false; // Can't do reliable parameter counting
        else {
            if (declaringClass.isMemberClass() &&
                ((declaringClass.getModifiers() & Modifier.STATIC) == 0)  &&
                resultLength + 1 == numParameters) {
                return true;
            } else {
                throw new AnnotationFormatError(
                          "Parameter annotations don't match number of parameters");
            }
        }
    }
    

    (Method)

    @Override
    boolean handleParameterNumberMismatch(int resultLength, Class<?>[] parameterTypes) {
        throw new AnnotationFormatError("Parameter annotations don't match number of parameters");
    }
    

    To demonstrate how Reflection deals with mismatches for local classes,

    import java.lang.reflect.Parameter;
    import java.util.Arrays;
    
    public class Example2 {
        public static void main(String[] args) {
            class Tmp {
                String[] captured = args;
                Tmp(@Deprecated int arg) {}
            }
            for(Parameter p: Tmp.class.getDeclaredConstructors()[0].getParameters()) {
                System.out.append(p + " ").flush();
                System.out.println(Arrays.toString(p.getAnnotations()));
            }
        }
    }
    
    int arg0 [@java.lang.Deprecated(forRemoval=false, since="")]
    java.lang.String[] arg1 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
            at java.base/java.lang.reflect.Parameter.getDeclaredAnnotations(Parameter.java:318)
            at java.base/java.lang.reflect.Parameter.getAnnotations(Parameter.java:358)
            at Example2.main(Example2.java:12)
    

    (So in this case, the compiler decided to add the synthetic parameter after the annotated parameter)