Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API for resolving return type, field type, parameter type with replacement of the actual type arguments #735

Open
vlsi opened this issue Nov 29, 2022 · 17 comments

Comments

@vlsi
Copy link

vlsi commented Nov 29, 2022

Here's a sample case:

interface Base<T> {
    T getT();
}

abstract class Derived implements Base<String> {
}

I want a runtime API to resolve the type of getT in Derived class as String.

In other words, something like TypeParameterResolver#resolveReturnType from mybatis https://github.com/mybatis/mybatis-3/blob/4b72ae4ffbf1029162233d3eb77c2f0b891c24fa/src/main/java/org/apache/ibatis/reflection/TypeParameterResolver.java#L50-L59, however, mybatis does not consider TypeParameterResolver its API.

WDYT?

@lukehutch
Copy link
Member

This API already exists in ClassGraph but it is probable that it doesn't work in some cases, because I never looked closely at how type parameters are resolved by the Java compiler.

Off the top of my head (I can't write some test code right now), just get a type signature for the generic class in question, then get the type arguments from the type signature, the call .resolve() to get a type parameter for each type argument.

@vlsi
Copy link
Author

vlsi commented Nov 29, 2022

I'm afraid it looks like ClassGraph does not expose reflection API.
I don't really want to scan classes, and I need to get data out of the existing java.lang.reflect.Method instances.

Just in case, I tried "scanning" the class by its name, and I could not find APIs to resolve that getT would have String type in Derived (I tried classInfo.methodInfo.first().typeSignature.resultType. in debugger)

@lukehutch
Copy link
Member

lukehutch commented Nov 29, 2022

I tried this out --

ClassTypeSignature ifaceSig = derivedClassInfo.getInterfaces().get(0).getTypeSignature();

gives

pkg.Base<T>

while

ClassRefTypeSignature ifaceSig2 = derivedClassInfo.getTypeSignature().getSuperinterfaceSignatures().get(0);

gives

pkg.Base<java.lang.String>

Please confirm if the latter form already gives you what you want.

@vlsi
Copy link
Author

vlsi commented Nov 29, 2022

derivedClassInfo.getTypeSignature().getSuperinterfaceSignatures().get(0);

Well, do you expect that everybody reimplements type parameter substitution rules?
I do not know the shape of incoming classes, so I can't hard-code .get(0).

I know java.lang.reflect.ParameterizedType#getActualTypeArguments exists, so having derivedClassInfo.getTypeSignature().getSuperinterfaceSignatures().get(0); provides no extra value on top of the regular reflection.

What I ask is a higher-level API: "give me the actual type with all type parameters substituted with the corresponding actual type arguments".

@vlsi
Copy link
Author

vlsi commented Nov 29, 2022

Just in case, I'm not like "implement the feature for me" (the feature can be borrowed from mybatis or spring-framework).
I want to figure out whether you think classgraph/classgraph might be a suitable home for such a higher-level reflection API.

@lukehutch
Copy link
Member

What I ask is a higher-level API: "give me the actual type with all type parameters substituted with the corresponding actual type arguments".

Did you not see the second code example, which does exactly what you are asking for? There's one way to get the type signature that uses type variables, and there's another way to get the type signature that includes the full concrete type. Take another look at what I sent:

ClassRefTypeSignature ifaceSig2 = derivedClassInfo.getTypeSignature().getSuperinterfaceSignatures().get(0);

gives

pkg.Base<java.lang.String>

However, like I said earlier, there is already support for doing the substitution you are asking for:

https://github.com/classgraph/classgraph/blob/latest/src/main/java/io/github/classgraph/TypeVariableSignature.java#L93

To use this though, you need a TypeVariableSignature, which is only contained within a TypeSignature of a few things, like the throws part of a method's type signature.

As I said, the support for resolving type variables is probably incomplete. So what would be needed to ensure you could get the concrete type of the interface signature using either of the two APIs that I showed you, you would need to copy this logic from TypeVariableSignature across to TypeParameter, and probably update it to also resolve type variables from the enclosing class' TypeSignature. You may have to do a bit of extra work to link a TypeParameter to the containing TypeSignature, in order to do the resolution properly.

Code contributions are welcome. Since there is already a way to get exactly the answer you need, adding a second mechanism to do the same thing is not a high priority, and I don't have a lot of time to work on this.

@vlsi
Copy link
Author

vlsi commented Nov 30, 2022

Did you not see the second code example, which does exactly what you are asking for?

Would you please clarify what you mean by "second code example"?
I see no examples that suggest a way to resolve the return type of the method.

@lukehutch
Copy link
Member

I literally quoted the code again that I was talking about. And then I showed again how it returns the actual concrete type you are looking for.

@vlsi
Copy link
Author

vlsi commented Nov 30, 2022

I'm afraid you quoted pkg.Base<java.lang.String> which is not what I need.
I need the return type of getT method, not the superinterface type of Derived class.

@lukehutch
Copy link
Member

lukehutch commented Nov 30, 2022

Ah! I couldn't understand why we weren't understanding each other. I'm not at a computer, but getting the type signature of the method is the place to start.

@lukehutch
Copy link
Member

        ClassInfo ci = scanResult.getClassInfo(Derived.class.getName());
        System.out.println(ci.getMethodInfo().get(0).getTypeSignatureOrTypeDescriptor().getResultType());

prints

java.lang.String

@vlsi
Copy link
Author

vlsi commented Nov 30, 2022

Could you share the full code for Derived class you use in your test?

public interface Base<T> {
    T getT();
}

public abstract class Derived implements Base<String> {
}

Test code:

    @Test
    fun abc() {
        ClassGraph().enableAllInfo().acceptClasses(Derived::class.java.name).scan().use { scanResult ->
            val klass = scanResult.getClassInfo(Derived::class.java.name)
            val methodInfo = klass.methodInfo.first()
            println("methodInfo: ${methodInfo.typeSignatureOrTypeDescriptor.resultType}")
        }
    }

The output is methodInfo: T

@lukehutch
Copy link
Member

I used exactly the code you first gave, except that I had to define the getT method to return null in the implementation, because you have to define all interface methods in Java.

I think you're using the Kotlin compiler? That has had bugs in code generation before that have affected ClassGraph scanning. Can you please put together a complete test project so that I can reproduce the problem?

@vlsi
Copy link
Author

vlsi commented Dec 1, 2022

I am inclined that implementing getT within Derived spoils the test case.
Just in case, I tried with both Java and Kotlin, and the outcome is the same: classgraph does not resolve the type.

Please try with the abstract Derived class.

interface Base<T> {
    T get();
}

class Derived1 implements Base<String> {
    public String get() {
        return null;
    }
}

abstract class Derived2 implements Base<String> {
}

If I compile it with Java bytecode 1.8, then I get the following:

% javap -p Derived1.class
Compiled from "Derived1.java"
class com.Derived1 implements com.Base<java.lang.String> {
  com.Derived1();
  public java.lang.String get();
  public java.lang.Object get();
}

% javap -p Derived2.class
Compiled from "Derived1.java"
abstract class com.Derived2 implements com.Base<java.lang.String> {
  com.Derived2();
}
% javap -verbose Derived1.class
  Last modified 1 дек. 2022 г.; size 525 bytes
  MD5 checksum 93f604eb1f20ef1abafaf6f4a6b3f3e8
  Compiled from "Derived1.java"
class com.Derived1 extends java.lang.Object implements com.Base<java.lang.String>
  minor version: 0
  major version: 52
  flags: (0x0020) ACC_SUPER
  this_class: #3                          // com/Derived1
  super_class: #4                         // java/lang/Object
  interfaces: 1, fields: 0, methods: 3, attributes: 2
Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = Methodref          #3.#21         // com/Derived1.get:()Ljava/lang/String;
   #3 = Class              #22            // com/Derived1
   #4 = Class              #23            // java/lang/Object
   #5 = Class              #24            // com/Base
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcom/Derived1;
  #13 = Utf8               get
  #14 = Utf8               ()Ljava/lang/String;
  #15 = Utf8               ()Ljava/lang/Object;
  #16 = Utf8               Signature
  #17 = Utf8               Ljava/lang/Object;Lcom/Base<Ljava/lang/String;>;
  #18 = Utf8               SourceFile
  #19 = Utf8               Derived1.java
  #20 = NameAndType        #6:#7          // "<init>":()V
  #21 = NameAndType        #13:#14        // get:()Ljava/lang/String;
  #22 = Utf8               com/Derived1
  #23 = Utf8               java/lang/Object
  #24 = Utf8               com/Base
{
  com.Derived1();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/Derived1;

  public java.lang.String get();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aconst_null
         1: areturn
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       2     0  this   Lcom/Derived1;

  public java.lang.Object get();
    descriptor: ()Ljava/lang/Object;
    flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #2                  // Method get:()Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/Derived1;
}
Signature: #17                          // Ljava/lang/Object;Lcom/Base<Ljava/lang/String;>;
SourceFile: "Derived1.java"

lukehutch added a commit that referenced this issue Dec 14, 2022
@lukehutch
Copy link
Member

OK, the reason why ClassGraph returns T as the result type for Derived2.get() is because that function is only defined in the superclass, Base. It is not defined in Derived2 at all. When it is defined, as in Derived1, the type is returned as String. I actually think this is the correct behavior, even if it is not the most useful behavior. Do you disagree?

@vlsi
Copy link
Author

vlsi commented Dec 14, 2022

Do you disagree?

That is a very different question from what I asked in the very first comment of the issue.

What I want is an API that would resolve method return type taking into account all the generics.
Of course, the method is not defined in Derived2. However, what I need is an API that would tell "ok, Base#get() method would return String if called in Derived2 class"

@lukehutch
Copy link
Member

I agree that a resolve method could be added to TypeParameter, just as exists right now for TypeVariableSignature. I provided the link to the code for that in this comment, and instructions about other changes that need to be made (specifically, the TypeParameter will need to know what the enclosing method and/or class is): #735 (comment) .

I have extremely limited time to work on ClassGraph at this point, so I would appreciate it if you could please take a shot at implementing this, since I don't know when I could afford the time to work on it. You are the first person to request this feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants