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

About adapting type parameter of generic method, using actual invocation in AST. #4838

Open
jiseongg opened this issue Aug 12, 2022 · 7 comments

Comments

@jiseongg
Copy link

Hi,

I'm looking for the way to extract type adapting rule from AST. For generic class, there is ClassTypingContext. Is there any equivalent concept in CtExecutable? I've tried MethodTypingContext but I'm not sure it can be used for my purpose.

Example:

// A.java
public class A {
  public static <E> void genericMethod(E e) {
    System.out.println("genericMethod" + e.toString());
  }
}

// TestA.java
public class TestA {
  public void test() {
    // here!
    A.genericMethod("e");
  }
}

I want to extract the information from the AST of TestA above, which tells me that the type parameter E is actually in java.lang.String.

Below is what I've tried roughly and it cause errors. Also, note that I'm not sure about the actual usage of MethodTypingContext

// let's say method is `TestA.test()`
CtTypeParameter e = method.getFormalCtTypeParameters().get(0);

// let's say A.genericMethod("e") is `invocation`
new MethodTypingContext().setExecutableReference(invocation.getExecutable()).adaptType(e)
@I-Al-Istannen
Copy link
Collaborator

Hey,

the tl;dr is that you are currently out of luck, there is no inbuilt solution.

What Spoon offers currently

I recently rewrote type adaption and created the TypeAdaptor class. One feature from the old implementation which I didn't see used anywhere and has quite some limitations was MethodTypingContext#setInvocation. This is kinda what you want to do here. You want to adapt the method defined in A to the invocation used in TestA. To do this using the old API you would do:

new MethodTypingContext()
    .setClassTypingContext(new ClassTypingContext(classA))
    .setInvocation(invocation)
    .adaptType(e) // => String?

So, just use the old API, set the invocation and done. Easy, right? Not quite. Spoon only considers explicitly annotated type arguments in invocations. The actual type arguments of the invocation A.genericMethod("e") is an empty List. If you change the code to A.<String>genericMethod("e"), the snippet above does in fact return String though.
Just be aware that the way the old type adaption reaches that conclusion can be a bit meandering and inefficient.

How that could be improved

To be usable, Spoon would need to ask JDT for the types it inferred for invocations and then use them as actual type arguments. This might be possible if somebody implements it, but is then limited to code parsed by JDT. If you create your own code using the spoon API, you would need to supply them yourself. If you transform the model and you aren't really diligent, that information will be incorrect or missing.

(Yes, this section exists just to yakbait somebody into implementing this. The alternative is implementing the java type inference rules in spoon and adapting them for each java version, which could be an interesting project but is a really, really bad idea:tm:)

How you could maybe fake it yourself

You might, however, be able to just do it yourself. Depending on your usecase, using the type of the arguments of the CtInvocation could be enough. In this case you would ask the String literal for its type, which is String, and then just use that type for the first parameter. Note that you still might end up with unresolved generic types, but that is unavoidable. If you have a generic method and it calls another with its generic argument, the actual type is likely unknown inside the method and you would need to propagate the relationships from the outermost call inwards (which could still not be enough if you have a generic library method or whatever that isn't directly called).

@jiseongg
Copy link
Author

jiseongg commented Aug 12, 2022

I really appreciate for the detailed answer. The code snippet you attached seems useful to me. The good news is, I'm using Spoon version where MethodTypingContext#setInvocation exists.

Spoon only considers explicitly annotated type arguments in invocations. The actual type arguments of the invocation A.genericMethod("e") is an empty List.

Actually, in the Java programs I'm working with, explicit annotation rarely occur. I think I have to do it by myself. Thank you for your detailed hint.

@I-Al-Istannen
Copy link
Collaborator

@jiseongg With #4844 merged, this should now work:

	@ModelTest("/tmp/test")
	void better(Factory factory) {
		CtClass<?> classA = factory.Class().get("A");
		CtClass<?> classTestA = factory.Class().get("TestA");
		CtInvocation<?> invocation = classTestA.getMethodsByName("test").get(0)
			.getBody()
			.getStatement(0);

		CtTypeParameter e = classA.getMethodsByName("genericMethod").get(0)
			.getFormalCtTypeParameters().get(0);

		System.out.println(
			new MethodTypingContext()
				.setClassTypingContext(new ClassTypingContext(classA))
				.setInvocation(invocation)
				.adaptType(e)
		);
	}

I am not a fan of the old class typing context though, but I am also not quite sure what exactly you need here. The main question I have is whether you need to resolve generics defined foe the enclosing class, e.g.

class Top<T> {
  void foo(T t);
}

class Bottom<R> extends Top<R> {}

Do you want to resolve the type of the parameter T to R for an invocation of Bottom#foo? That requires you knowing what exactly the context is (the input setClassTypingContext in the example above, here: Bottom).

If you do not require this, what you desire should be exactly the result of calling invocation.getActualTypeArguments().
If you do require this, we could maybe come up with some okay-ish newer API :)

@jiseongg
Copy link
Author

jiseongg commented Aug 24, 2022

@I-Al-Istannen, thank you for your effort to make such PR. It is what I need.

For the additional example you gave, I'm not sure I need such feature right now. I think I have to share how I'm using Spoon. What I'm doing with Spoon is analyzing existing Java projects statically to gather some useful information and utilize it to enhance automatic test case generation engine. Until now, it has been mostly about generics. For example, automatic test case generator goes through difficulties to test A#genericMethod since it should decide which types of arguments to feed it. Rather than randomly deciding this, I'm trying to refer existing test cases. (In the example above, A.genericMethod("e").)

Considering my use case, with the example you've given, the most desirable feature I want is the way to resolve T in the signature foo(T) to Integer from following code snippet.

class new TestBottom {
  public class test() {
    Bottom<Integer> bot = new Bottom<>();
    bot.foo(3);
  }
}

But, I suppose this is possible already, isn't it? I've used spoon like below.

CtClass<?> testBottom = factory.Class().get("TestBottom");
CtInvocation<?> invocation = bottom.getMethodsByName("test").get(0)
			.getBody()
			.getStatement(1);
CtTypeReference<?> receiverObjectType = invocation.getTarget().getType(); // this is `Bottom<Integer>`

CtClass<?> top = factory.Class().get("Top");
CtTypeParameter t = top.getFormalCtTypeParameters().get(0);
CtTypeReference<?> resolvedT = new ClassTypingContext(receiverObjectType).adaptType(t); // this is Integer

I'm sorry if there is some misusage causing misunderstanding, since I'm not in my workplace now.

To sum up,

Do you want to resolve the type of the parameter T to R for an invocation of Bottom#foo?

until now, I haven't experienced the situation I had to resolve CtTypeParameter to CtTypeParameter. The result of type parameter resolution I desire was non-generic type. (But, I guess resolve T to R may be done internally, in my case.) But I think I've done it by using existing APIs somehow.

@I-Al-Istannen
Copy link
Collaborator

Well, I would change the last line to new TypeAdaptor(receiverObjectType).adaptType(t) :P

Apart from that, your code correctly handles one special case. If that is all you need, you are fine :)


If you have a method

public class Test<T> {
  public void foo(List<T> list) {}
}

you will need to adapt the parameter's type instead though:

		CtExecutable<?> calledExecutable = invocation.getExecutable().getExecutableDeclaration();
		CtTypeReference<?> parameterType = calledExecutable.getParameters().get(0).getType();
		System.out.println(new TypeAdaptor(receiverObjectType).adaptType(parameterType));

But this still works just fine, no problem there.


Where things start to break down are cases like:

class Test {
  public static <T> void foo(T t) {}
}

// and somewhere an invocation
Test.foo(20);

With my PR, the type the eclipse compiler inferred for such a call will be stored in CtInvocation#getActualTypeArguments, so
invocation.getActualTypeArguments() would return [java.lang.Integer].

To solve this, you can use

		CtExecutable<?> calledExecutable = invocation.getExecutable().getExecutableDeclaration();
		CtTypeReference<?> parameterType = calledExecutable.getParameters().get(1).getType();
		System.out.println(
			new MethodTypingContext()
				.setClassTypingContext(new ClassTypingContext(receiverObjectType))
				.setInvocation(invocation)
				.adaptType(parameterType)
		);

which returns java.lang.Integer for the above case. I am wondering whether you need an API to do this and where it would fit into spoon.

@jiseongg
Copy link
Author

jiseongg commented Aug 24, 2022

Oh, I've just understood what you're wondering. I think it'll be great if such API exists. (regardless whether this kind of operation has been used a lot historically.) Making new TypeAdaptor support this would be great.

One thing I wonder about the code

new MethodTypingContext()
    .setClassTypingContext(new ClassTypingContext(receiverObjectType))
    .setInvocation(invocation)
    .adaptType(parameterType)

is whether setClassTypingContext should be required. I think the use case of this operation occurs for resolving CtTypeParameter declared in CtMethod, not in CtType.

@I-Al-Istannen
Copy link
Collaborator

The class typing context (or, equivalently, the constructor argument of TypeAdaptor) is needed. If you have a method:

class Top<T> {
  public void foo(T t) {}
}
class Sub extends Top<String> {}

And you are resolving new Sub().foo(null), you need to be able to understand that the implementation in Sub is called and that T in that context refers to String. Therefore, you need to pass Sub.class as the typing context.


For now you can use the method typing context, I will see if I can come up with something. It's not really type adaption, it is adapting the method first and then replacing its formal type parameters with the actual types recursively.

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