javaparser icon indicating copy to clipboard operation
javaparser copied to clipboard

Is there any way to get the implicit type arguments for a method?

Open MarioIshac opened this issue 5 years ago • 14 comments

In this following, assume there is a class Creator with a static method create that receives one type argument and one argument of that type (the value).

String myString = Creator.create("ABC");

Is there anyway for JavaParser to resolve the type arguments given to the method create solely from Creator.create("ABC")? It should return String. Note that these type arguments won't necessarily align with the type on the left side, so I can't look at the left side of the variable declaration.

MarioIshac avatar Mar 24 '19 21:03 MarioIshac

@ftomassetti ?

matozoid avatar Mar 24 '19 22:03 matozoid

I cannot think of one on the top of my head. The resolution process has to figure out the values for the type arguments, the problem is remembering how to get those values. There is one class called MethodUsage that is basically a combination of a MethodDeclaration and the type parameters (both the type parameters on the method and the type parameters in containing classes/interfaces).

So the question becomes, how do we go from a MethodCallExpr to a MethodUsage? In general we should make that easier (i.e., add a method for that).

For now I would use the JavaParserFacade method named solveMethodAsUsage. It takes a MethodCallExpr and return a MethodUsage.

ftomassetti avatar Mar 25 '19 08:03 ftomassetti

@ftomassetti I have an instance of MethodUsage (forgot to say that in the original question, sorry about that). When looking through the possible methods, it only allows me to get the method declaration (which would in turn allow me to get the type parameters) and the types of the arguments. It doesn't provide a method to get the type arguments. Am I missing something?

MarioIshac avatar Mar 26 '19 01:03 MarioIshac

You should have a method named typeParametersMap

On Tue, 26 Mar 2019 at 02:01, Mario Ishac [email protected] wrote:

@ftomassetti https://github.com/ftomassetti I have an instance of MethodUsage (forgot to say that in the original question, sorry about that). When looking through the possible methods, it only allows me to get the method declaration (which would in turn allow me to get the type parameters) and the types of the arguments. It doesn't provide a method to get the type arguments. Am I missing something?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/javaparser/javaparser/issues/2135#issuecomment-476433497, or mute the thread https://github.com/notifications/unsubscribe-auth/AAazJhrtbz9n4B_to3k5upSB3TkH0N3bks5vaXFOgaJpZM4cFx1g .

-- Website at https://tomassetti.me GitHub https://github.com/ftomassetti

ftomassetti avatar Mar 26 '19 08:03 ftomassetti

You should have a method named typeParametersMap On Tue, 26 Mar 2019 at 02:01, Mario Ishac @.***> wrote: @ftomassetti https://github.com/ftomassetti I have an instance of MethodUsage (forgot to say that in the original question, sorry about that). When looking through the possible methods, it only allows me to get the method declaration (which would in turn allow me to get the type parameters) and the types of the arguments. It doesn't provide a method to get the type arguments. Am I missing something? — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#2135 (comment)>, or mute the thread https://github.com/notifications/unsubscribe-auth/AAazJhrtbz9n4B_to3k5upSB3TkH0N3bks5vaXFOgaJpZM4cFx1g . -- Website at https://tomassetti.me GitHub https://github.com/ftomassetti

Sorry that I haven't responded in a while, but upon testing with this method it seems that it returns type parameter-argument mappings for type arguments explicitly used, not implicitly. A MethodUsage corresponding to this.methodWithTypeParameters("B") would have an empty type parameters-arguments map, even if there was an inferred type argument of String.

Is there any way to get the implicit mappings as well?

MarioIshac avatar Apr 11 '19 23:04 MarioIshac

mmm, I think so because internally we need that stuff. I do not remember how to access it and if those are exposed externally.

ftomassetti avatar Apr 13 '19 07:04 ftomassetti

This is the point where we ask for help from our users, like user @TheeNinja :-)

matozoid avatar Apr 14 '19 20:04 matozoid

@ftomassetti @matozoid In the process of building a unit test for the expected behavior, I discovered that the implicit type arguments are actually discovered in a MethodUsage where the implicit type arguments serve as the type of a method argument, such as in the example I provided (method create). I had overgeneralized the issue, the issue appears when doing things like Stream.Builder<String> builder = Stream.builder() (the line from my application where I noticed the issue) as the method solveMethodAsUsage(String name, List<ResolvedType> argumentsTypes) in MethodCallExprContent only searches for implicit type arguments through the method resolveMethodTypeParameters(MethodUsage methodUsage, List<ResolvedType> actualParamTypes) . Both of these methods receive only argument types as information (and no return type of the method) so it makes sense that they modify the type parameter-argument map solely based on that information.

I would need to pass the information of where the method is called in addition to the arguments provided in the call. This is because the type argument corresponding to the method return type is inferred from there. It would have to be inferred differently in String a = methodWithTypeParameter() compared to methodThatTakesAString(methodWithTypeParameter()) (in both cases the inferred type would be a String). Do you know if there is any way to access this surrounding information from an instance of MethodCallExpr or MethodUsage so I can make the PR?

MarioIshac avatar Apr 15 '19 06:04 MarioIshac

If @ftomassetti can't remember, then you are the new expert, @TheeNinja :-)

matozoid avatar Apr 16 '19 20:04 matozoid

@matozoid @ftomassetti I'm trying to become the new expert, but based on my unit tests it seems there's many more problems than just not accounting for the return type. I'll continue trying to fix them, but I've reached a little dilemma I've been stuck at for a few hours: From MethodCallExprContext:

    public Optional<MethodUsage> solveMethodAsUsage(String name, List<ResolvedType> argumentsTypes) {
        if (wrappedNode.getScope().isPresent()) {
            Expression scope = wrappedNode.getScope().get();

            // Considers calls of the form <name>.method(), name can be class meaning that this if block considers static methods as well.
            if (scope instanceof NameExpr) {
                String className = ((NameExpr) scope).getName().getId();
                SymbolReference<ResolvedTypeDeclaration> ref = solveType(className);
                if (ref.isSolved()) {
                    SymbolReference<ResolvedMethodDeclaration> m = MethodResolutionLogic.solveMethodInType(ref.getCorrespondingDeclaration(), name, argumentsTypes);
                    if (m.isSolved()) {
                        MethodUsage methodUsage = new MethodUsage(m.getCorrespondingDeclaration());
                        methodUsage = resolveMethodTypeParametersFromExplicitList(typeSolver, methodUsage);
                        methodUsage = resolveMethodTypeParameters(methodUsage, argumentsTypes);
                        return Optional.of(methodUsage);
                    } else {
                        throw new UnsolvedSymbolException(ref.getCorrespondingDeclaration().toString(),
                                "Method '" + name + "' with parameterTypes " + argumentsTypes);
                    }
                }
            }
            ResolvedType typeOfScope = JavaParserFacade.get(typeSolver).getType(scope);
            // we can replace the parameter types from the scope into the typeParametersValues

            Map<ResolvedTypeParameterDeclaration, ResolvedType> inferredTypes = new HashMap<>();
            for (int i = 0; i < argumentsTypes.size(); i++) {
                // by replacing types I can also find new equivalences
                // for example if I replace T=U with String because I know that T=String I can derive that also U equal String
                ResolvedType originalArgumentType = argumentsTypes.get(i);
                ResolvedType updatedArgumentType = usingParameterTypesFromScope(typeOfScope, originalArgumentType, inferredTypes);
                argumentsTypes.set(i, updatedArgumentType);
            }
            for (int i = 0; i < argumentsTypes.size(); i++) {
                ResolvedType updatedArgumentType = applyInferredTypes(argumentsTypes.get(i), inferredTypes);
                argumentsTypes.set(i, updatedArgumentType);
            }

            return solveMethodAsUsage(typeOfScope, name, argumentsTypes, this);
        } else {
            Context parentContext = getParent();
            while (parentContext instanceof MethodCallExprContext || parentContext instanceof ObjectCreationContext) {
                parentContext = parentContext.getParent();
            }
            return parentContext.solveMethodAsUsage(name, argumentsTypes);
        }
    }

Why is there different behaviors regarding inference of type arguments based on whether the scope is a name expression or not? If it is, it seems exception and return types are not accounted for in type argument resolution. If it is not, this line executes: solveMethodAsUsage(typeOfScope, name, argumentsTypes, this);, and this variation of solveMethodAsUsage seems to account for those in addition to the argument types. I'm trying to eliminate this duplicated functionality.

I'll probably have the answer to my question after some more thought, but if by change you guys know it'll make my job easier.

MarioIshac avatar Apr 25 '19 06:04 MarioIshac

Oof, we need @ftomassetti for this.

matozoid avatar Apr 25 '19 19:04 matozoid

I am currently facing a similar challenge: How to resolve the actual type arguments in a scenario like new HashMap<>() ? There are various scenarios where the Java Compiler can infer the type arguments.

The discussion so far was aiming to infer the type arguments from the parameters, but this doesn't work in cases like mine. Is there a way to get the contextual type information of such a constructor call?

Danielku15 avatar Apr 10 '22 15:04 Danielku15

Is there a way to get the contextual type information of such a constructor call?

Not directly from the constructor call (i.e., ObjectInitializerExpr) as there is not enough information available within that node, but it is usually possible to get the declared type.

For example, if the result of the object initialisation is assigned to a variable, you can resolve that variable to examine the declared type of that variable. I show this within a sample below.

@Danielku15 -- If the sample below doesn't match your use-case, could you share a small snippet of what you are trying to do please?

I am currently facing a similar challenge: How to resolve the actual type arguments in a scenario like new HashMap<>() ?

Is it sufficient for your needs to navigate to the variable declaration, then inspect the declared type?

Here is an example showing how this might be done (caution: with defensive guards/error-checking removed -- not suitable for production code):

import java.util.HashMap;

class Scratch {
    HashMap<Integer, String> map;
    public Scratch() {
        map = new HashMap<>();
    }
}
package com.github.javaparser.symbolsolver;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseResult;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.expr.AssignExpr;
import com.github.javaparser.ast.expr.NameExpr;
import com.github.javaparser.ast.expr.ObjectCreationExpr;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.resolution.types.ResolvedReferenceType;
import com.github.javaparser.resolution.types.ResolvedType;
import com.github.javaparser.symbolsolver.model.resolution.TypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
import org.junit.jupiter.api.Test;

import java.util.List;

    @Test
    public void issue2315_sample__inferred_type_arguments__getting_declared_type_parameters() {
        String inputText = "" +
                "import java.util.HashMap;\n" +
                "\n" +
                "class Scratch {\n" +
                "    HashMap<Integer, String> map;\n" +
                "    public Scratch() {\n" +
                "        map = new HashMap<>();\n" +
                "    }\n" +
                "}\n" +
                "";


        JavaParser javaParser = new JavaParser();

        // Set up a minimal type solver that only looks at the classes used to run this sample.
        // Configure JavaParser to use type resolution
        TypeSolver typeSolver = new ReflectionTypeSolver();
        JavaSymbolSolver javaSymbolSolver = new JavaSymbolSolver(typeSolver);
        javaParser.getParserConfiguration()
                .setSymbolResolver(javaSymbolSolver);

        // Attempt parsing
        ParseResult<CompilationUnit> parseResult = javaParser.parse(inputText);

        // Get a usage of implicit type parameters from the input source
        ObjectCreationExpr objectCreationExpr = parseResult
                .getResult()
                .get()
                .findAll(ObjectCreationExpr.class)
                .get(0);


        // Initialised type details (as part of the `ObjectCreationExpr` -- i.e., `new HashMap<>()`)
        ClassOrInterfaceType initialisedType = objectCreationExpr.getType();
        NodeList<Type> initialisedTypeArguments = initialisedType.getTypeArguments().get();
        System.out.println("initialisedType = " + initialisedType);
        System.out.println("initialisedTypeArguments = " + initialisedTypeArguments);


        // Get variable associated with this `ObjectCreationExpr`
        NameExpr variableNameExpr = objectCreationExpr
                .findAncestor(AssignExpr.class)
                .get()
                .getTarget()
                .asNameExpr();

        // Resolve the variable name, to get the declared type
        ResolvedReferenceType resolvedDeclaredType = variableNameExpr
                .calculateResolvedType()
                .asReferenceType();

        // Examine the _declared_ type parameters
        List<ResolvedType> resolvedDeclaredTypeParameters = resolvedDeclaredType.typeParametersValues();
        System.out.println("resolvedDeclaredTypeParameters = " + resolvedDeclaredTypeParameters);

    }
}

This outputs the following -- note that the declared type parameters are resolved and shown below:

initialisedType = HashMap<>
initialisedTypeArguments = []
resolvedDeclaredTypeParameters = [ReferenceType{java.lang.Integer, typeParametersMap=TypeParametersMap{nameToValue={}}}, ReferenceType{java.lang.String, typeParametersMap=TypeParametersMap{nameToValue={}}}]

MysterAitch avatar Apr 17 '22 16:04 MysterAitch

@MysterAitch This is what I basically tried and it works.. somehow. But as you can imagine: handling all scenarios is quite a big deal. I have now covered 5 cases in my code and it is sufficient for my needs, still I'm not really happy. Luckily my project is anyhow just a transpiler which is not meant to be perfect and needs manual cleanup of the generated code. The cases I covered now:

  1. Map<String, String> x = new HashMap<>() looks for a VariableDeclarator node and tries to pull the types from there.
  2. new Other(new HashMap<>()) looks for parent ObjectCreationExpr, then looks for the right constructor parameter definition and pulls the type arguments from there.
  3. Method(new HashMap<>()) looks for a parent MethodCallExpr, then looks for the right method parameter definition and pulls the type arguments from there.
  4. x.y = new HashMap<>() looks for a AssignExpr, then tries to resolve type of this AssignExpr through calculateResolvedType() and pulls the type arguments from there.
  5. return new HashMap<>() looks for the parent MethodDeclaration and then pulls the type arguments from the return type.

This works mostly for the happy paths. But as soon there are further generic types included in this scope it starts failing.

You can look at the related code here: https://github.com/Danielku15/SigningServer/blob/77dfa94466d6f781dda8df2a3922f533add1e4d6/tools/apksig-transpiler/src/main/kotlin/CSharpAstTransformer.kt#L792-L874

(Disclaimer: it's not fully clean-code but as it is just a small tool to serve my needs on translating Java to C# code I'm fine with it 😁).

Danielku15 avatar Apr 18 '22 11:04 Danielku15