Bug 430573 - [1.8][clean up][quick assist] Convert lambda to anonymous must qualify references to 'this'/'super'
Summary: [1.8][clean up][quick assist] Convert lambda to anonymous must qualify refere...
Status: RESOLVED FIXED
Alias: None
Product: JDT
Classification: Eclipse Project
Component: UI (show other bugs)
Version: 4.4   Edit
Hardware: All All
: P3 normal (vote)
Target Milestone: 4.5 M1   Edit
Assignee: Jerome Cambon CLA
QA Contact:
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2014-03-17 21:24 EDT by Markus Keller CLA
Modified: 2014-07-08 05:39 EDT (History)
4 users (show)

See Also:
noopur_gupta: review+


Attachments
My patch to review (5.03 KB, patch)
2014-05-16 10:25 EDT, Jerome Cambon CLA
no flags Details | Diff
New patch to review (16.09 KB, patch)
2014-06-18 10:15 EDT, Jerome Cambon CLA
no flags Details | Diff
New patch to review (11.93 KB, patch)
2014-06-30 09:43 EDT, Jerome Cambon CLA
jerome.cambon: review?
Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Markus Keller CLA 2014-03-17 21:24:37 EDT
Convert lambda to anonymous must qualify references to 'this'/'super':

package p;

import java.util.function.IntSupplier;

public class A {
    public static void main(String[] args) {
        new A();
    }
    
    A() {
        IntSupplier i = () -> this.m();
        System.out.println(i.getAsInt());
    }

    public int m() {
        return 7;
    }
}

=> In the anonymous, the method must return "A.this.m()", not just "this.m()".
Comment 1 Markus Keller CLA 2014-03-19 10:22:40 EDT

*** This bug has been marked as a duplicate of bug 421479 ***
Comment 2 Markus Keller CLA 2014-03-25 07:08:13 EDT
Not a dup, this is about the other direction.
Comment 3 Jerome Cambon CLA 2014-04-29 11:11:54 EDT
FYI, I am working on this one. Hope to have a patch soon.
Comment 4 Jerome Cambon CLA 2014-04-30 03:35:52 EDT
I have a patch now. Hope to be able to publish it soon.
Comment 5 Jerome Cambon CLA 2014-05-16 10:25:36 EDT
Created attachment 243182 [details]
My patch to review
Comment 6 Noopur Gupta CLA 2014-06-16 08:39:44 EDT
(In reply to Jerome Cambon from comment #5)
> Created attachment 243182 [details] [diff]
> My patch to review

- Add an explicit CoO sign-off comment:
http://wiki.eclipse.org/Development_Resources/Contributing_via_Git#via_Bugzilla

- Update copyright header and follow JDT UI coding conventions:
https://wiki.eclipse.org/JDT_UI/How_to_Contribute#Coding_Conventions

- Add test cases in AssistQuickFixTest18.java

- Note that lambda being converted can also be present in an anonymous class. Modified example from comment #0:
    A() {
        Runnable r = new Runnable() {
            int x= 10;
            @Override
            public void run() {
                IntSupplier i = () -> this.x;
            }
        };
    }
In this case, we get:
java.lang.IllegalArgumentException: Invalid identifier : ><
	at org.eclipse.jdt.core.dom.SimpleName.setIdentifier(SimpleName.java:195)
	at org.eclipse.jdt.core.dom.AST.newSimpleName(AST.java:2202)
	at org.eclipse.jdt.internal.corext.fix.LambdaExpressionsFix$CreateAnonymousClassCreationOperation.rewriteAST(LambdaExpressionsFix.java:453)

- Instead of creating separate Finder classes, we should have a single class and the qualification can also be added in that class while visiting the AST.

- We should qualify only the *unqualified* 'super' and 'this' references *directly* present in lambda body.

(Refer LambdaExpressionsFix.SuperThisReferenceFinder and bug 421479 for the other direction i.e. conversion of anonymous to lambda.)
Comment 7 Jerome Cambon CLA 2014-06-16 10:57:19 EDT
This contribution complies with http://www.eclipse.org/legal/CoO.php
Comment 8 Jerome Cambon CLA 2014-06-18 10:08:12 EDT
(In reply to Noopur Gupta from comment #6)
> - Add an explicit CoO sign-off comment:
> http://wiki.eclipse.org/Development_Resources/
> Contributing_via_Git#via_Bugzilla

Done

> 
> - Update copyright header and follow JDT UI coding conventions:
> https://wiki.eclipse.org/JDT_UI/How_to_Contribute#Coding_Conventions

Done

> 
> - Add test cases in AssistQuickFixTest18.java

Done.
(testConvertToAnonymousClassCreation11 is failing, see below)

> 
> - Note that lambda being converted can also be present in an anonymous
> class. Modified example from comment #0:
>     A() {
>         Runnable r = new Runnable() {
>             int x= 10;
>             @Override
>             public void run() {
>                 IntSupplier i = () -> this.x;
>             }
>         };
>     }
> In this case, we get:
> java.lang.IllegalArgumentException: Invalid identifier : ><
> 	at org.eclipse.jdt.core.dom.SimpleName.setIdentifier(SimpleName.java:195)
> 	at org.eclipse.jdt.core.dom.AST.newSimpleName(AST.java:2202)
> 	at
> org.eclipse.jdt.internal.corext.fix.
> LambdaExpressionsFix$CreateAnonymousClassCreationOperation.
> rewriteAST(LambdaExpressionsFix.java:453)

Indeed, good catch. 
I am now checking if the declaring class is valid, and removing the 'this' reference to avoid compilation error.
I still have an issue (see testConvertToAnonymousClassCreation11, which is failing).
Note this code works well if I replace:
"IntSupplier i = () -> this.x;" by "IntSupplier i = () -> {return this.x;};"
Any hint on this ?

> 
> - Instead of creating separate Finder classes, we should have a single class
> and the qualification can also be added in that class while visiting the AST.

Done. Indeed, this simplify a bit the code.

> 
> - We should qualify only the *unqualified* 'super' and 'this' references
> *directly* present in lambda body.

Done.
(Not sure I understand what you mean by "directly" though)

> 
> (Refer LambdaExpressionsFix.SuperThisReferenceFinder and bug 421479 for the
> other direction i.e. conversion of anonymous to lambda.)
Comment 9 Jerome Cambon CLA 2014-06-18 10:15:16 EDT
Created attachment 244333 [details]
New patch to review
Comment 10 Noopur Gupta CLA 2014-06-20 09:59:56 EDT
- Copyright is still not updated in the test file.

- In LambdaExpressionsFix.CreateAnonymousClassCreationOperation#rewriteAST, lambdaExpression.resolveMethodBinding() can return null (=> null check is missing).
However, instead of using bindings to get the parent type, use: ASTResolving.findParentType(ASTNode node).

- The new class 'SuperThisQualifier' should extend HierarchicalASTVisitor instead of ASTVisitor (Refer LambdaExpressionsFix.SuperThisReferenceFinder).

- SuperThisQualifier#perform should accept LambdaExpression instead of ASTNode. Also pass TextEditGroup to it.

- > > We should qualify only the *unqualified* 'super' and 'this' references
  > > *directly* present in lambda body.
> Not sure I understand what you mean by "directly" though

class TX {
    IntSupplier i = () -> {
        class CX {
            int n=10;
            {
                this.n= 0;
            }
        }
        return -1;
    };
}
Here, this.n is not directly present in lambda and hence should not be affected. 
We should not visit AnonymousClassDeclaration and BodyDeclaration within lambda (Refer LambdaExpressionsFix.SuperThisReferenceFinder).

- > > Note that lambda being converted can also be present in an anonymous class
> I am now checking if the declaring class is valid, and removing the 'this'
> reference to avoid compilation error.

It can happen with 'super' (field access / method invocation) also (not just 'this'). And, removing 'this' or 'super' is not the correct solution.
In the following example, if you remove 'this' while conversion, it will refer to the local 'x' with value 2 which is wrong.
        new Runnable() {
            int x= 1;
            public void run() {
                IntSupplier i = () -> {
                    int x= 2;
                    return this.x;
                };
            }
        };

Markus, how should it be handled?

- For the tests, you could group similar examples within a single lambda in test methods instead of creating different test methods for individual cases.
Comment 11 Markus Keller CLA 2014-06-20 13:50:54 EDT
(In reply to Noopur Gupta from comment #10)
> It can happen with 'super' (field access / method invocation) also (not just
> 'this'). And, removing 'this' or 'super' is not the correct solution.
> In the following example, if you remove 'this' while conversion, it will
> refer to the local 'x' with value 2 which is wrong.
>         new Runnable() {
>             int x= 1;
>             public void run() {
>                 IntSupplier i = () -> {
>                     int x= 2;
>                     return this.x;
>                 };
>             }
>         };
> 
> Markus, how should it be handled?

It can't be solved without major rewrites (e.g. renaming one of the 'x' variables or adding a getter inside 'new Runnable() {...}' that returns 'this.x'.

Let's assume this is sufficiently uncommon that it doesn't require an actual solution. It would be highly unexpected to make such major changes in this Quick Assist / Clean Up, but it would also be strange if we just disabled the conversion in this uncommon case. I think the best solution is to generate an invalid qualification like this to make the user aware of the problem:

                    public int getAsInt() {
                        int x= 2;
                        return Runnable.this.x;
                    }

(You should get this for free if you use ImportRewrite, see below.)

Implementation notes:

- Don't pass an unqualified String className to ThisOrSuperQualifier#perform(..). Pass the ITypeBinding itself and use ImportRewrite#addImport(..) to turn the binding into a name when you need it. Make sure it also works fine with generic outer types (maybe need to call ITypeBinding#getTypeDeclaration() somewhere to strip type arguments).

- Don't use ASTNode.copySubtree unless absolutely necessary. This loses all formatting and comments. Use e.g. this to rewrite an ASTNode property:
fRewrite.set(node, SuperMethodInvocation.QUALIFIER_PROPERTY, newQualifier, null);
For List-valued properties, you would use ASTRewrite#getListRewrite(..).
Comment 12 Jerome Cambon CLA 2014-06-24 11:54:17 EDT
(In reply to Markus Keller from comment #11)
> Implementation notes:
> 
> - Don't pass an unqualified String className to
> ThisOrSuperQualifier#perform(..). Pass the ITypeBinding itself and use
> ImportRewrite#addImport(..) to turn the binding into a name when you need
> it. Make sure it also works fine with generic outer types (maybe need to
> call ITypeBinding#getTypeDeclaration() somewhere to strip type arguments).

Ok Markus, but Noopur suggested to use ASTResolving.findParentType() instead of using bindings in comment 10. What is the best?
Comment 13 Noopur Gupta CLA 2014-06-24 12:41:49 EDT
(In reply to Jerome Cambon from comment #12)
> (In reply to Markus Keller from comment #11)
> > Implementation notes:
> > 
> > - Don't pass an unqualified String className to
> > ThisOrSuperQualifier#perform(..). Pass the ITypeBinding itself and use
> > ImportRewrite#addImport(..) to turn the binding into a name when you need
> > it. Make sure it also works fine with generic outer types (maybe need to
> > call ITypeBinding#getTypeDeclaration() somewhere to strip type arguments).
> 
> Ok Markus, but Noopur suggested to use ASTResolving.findParentType() instead
> of using bindings in comment 10. What is the best?

Use ASTResolving.findParentType() to get the parent type node and call #resolveBinding on it to get the corresponding ITypeBinding. Handle anonymous classes and generic types as suggested by Markus.
Comment 14 Jerome Cambon CLA 2014-06-25 12:26:22 EDT
(In reply to Markus Keller from comment #11)
> Implementation notes:
> 
> - Don't pass an unqualified String className to
> ThisOrSuperQualifier#perform(..). Pass the ITypeBinding itself and use
> ImportRewrite#addImport(..) to turn the binding into a name when you need
> it. Make sure it also works fine with generic outer types (maybe need to
> call ITypeBinding#getTypeDeclaration() somewhere to strip type arguments).

My last issue is related to this.
I got indeed an exception with the generic types.
I have a generic type (e.g. 'F<Object>', 'F' being a functional interface), and I need a SimpleType ('F').
I tried ITypeBinding#getTypeDeclaration() as suggested, but it doesn't help: it returns the same ITypeBinding.
I don't find a method from ITypeBinding that allow to strip this. Any advice?
Comment 15 Noopur Gupta CLA 2014-06-26 09:27:20 EDT
(In reply to Jerome Cambon from comment #14)
> (In reply to Markus Keller from comment #11)
> > Implementation notes:
> > 
> > - Don't pass an unqualified String className to
> > ThisOrSuperQualifier#perform(..). Pass the ITypeBinding itself and use
> > ImportRewrite#addImport(..) to turn the binding into a name when you need
> > it. Make sure it also works fine with generic outer types (maybe need to
> > call ITypeBinding#getTypeDeclaration() somewhere to strip type arguments).
> 
> My last issue is related to this.
> I got indeed an exception with the generic types.
> I have a generic type (e.g. 'F<Object>', 'F' being a functional interface),
> and I need a SimpleType ('F').
> I tried ITypeBinding#getTypeDeclaration() as suggested, but it doesn't help:
> it returns the same ITypeBinding.
> I don't find a method from ITypeBinding that allow to strip this. Any advice?

How are you using ITypeBinding with ImportRewrite#addImport(..) to get the name? And where are you getting the exception? - Please attach the example and patch.

See if ITypeBinding.getErasure() is required.

Try ImportRewrite.addImport(ITypeBinding binding) which returns a String that can be used to create the new Name.
Comment 16 Noopur Gupta CLA 2014-06-26 10:09:08 EDT
(In reply to Jerome Cambon from comment #14)
> (In reply to Markus Keller from comment #11)
> > Implementation notes:
> > 
> > - Don't pass an unqualified String className to
> > ThisOrSuperQualifier#perform(..). Pass the ITypeBinding itself and use
> > ImportRewrite#addImport(..) to turn the binding into a name when you need
> > it. Make sure it also works fine with generic outer types (maybe need to
> > call ITypeBinding#getTypeDeclaration() somewhere to strip type arguments).
> 
> My last issue is related to this.
> I got indeed an exception with the generic types.
> I have a generic type (e.g. 'F<Object>', 'F' being a functional interface),
> and I need a SimpleType ('F').
> I tried ITypeBinding#getTypeDeclaration() as suggested, but it doesn't help:
> it returns the same ITypeBinding.
> I don't find a method from ITypeBinding that allow to strip this. Any advice?

For anonymous class binding, Bindings.normalizeTypeBinding(ITypeBinding binding) can also be helpful before calling ITypeBinding#getTypeDeclaration().
Comment 17 Jerome Cambon CLA 2014-06-30 09:43:42 EDT
Created attachment 244673 [details]
New patch to review

This patch includes all the comments from Noopur and Markus.
Comment 18 Noopur Gupta CLA 2014-07-02 09:15:47 EDT
(In reply to Jerome Cambon from comment #17)
> Created attachment 244673 [details] [diff]
> New patch to review
> 
> This patch includes all the comments from Noopur and Markus.

Patch looks good. 

I have made the following changes:
- Renamed ThisOrSuperQualifier to SuperThisQualifier and reordered the methods.
- Passed "TextEditGroup" to #perform.
- Extracted "cuRewrite.getImportRewrite()" and "cuRewrite.getASTRewrite()" to fields.
- Renamed #getNewDeclaringClass to #getQualifierTypeName and added part of its functionality to CreateAnonymousClassCreationOperation#rewriteAST with null check.
- Used #newName instead of #newSimpleName in #getQualifierTypeName as #addImport may return a qualified name also.

Released the patch in master branch:
http://git.eclipse.org/c/jdt/eclipse.jdt.ui.git/commit/?id=235b7bd4c74d4f3e28f322f6c94c6e7c20df2c43

Keeping the bug open for any comments from Markus.
Comment 19 Noopur Gupta CLA 2014-07-08 05:39:17 EDT
.