Bug 421479 - [1.8][clean up][quick assist] convert anonymous to lambda must consider lost scope of interface
Summary: [1.8][clean up][quick assist] convert anonymous to lambda must consider lost ...
Status: ASSIGNED
Alias: None
Product: JDT
Classification: Eclipse Project
Component: UI (show other bugs)
Version: 4.4   Edit
Hardware: All All
: P2 normal (vote)
Target Milestone: ---   Edit
Assignee: JDT-UI-Inbox CLA
QA Contact:
URL:
Whiteboard: stalebug
Keywords:
: 475260 (view as bug list)
Depends on: 403749
Blocks:
  Show dependency tree
 
Reported: 2013-11-11 14:09 EST by Markus Keller CLA
Modified: 2023-08-03 12:20 EDT (History)
7 users (show)

See Also:
noopur_gupta: review+


Attachments
WIP Patch+Tests (11.84 KB, patch)
2014-05-13 08:35 EDT, Martin Mathew CLA
no flags Details | Diff
Patch+Tests (12.28 KB, patch)
2014-05-14 07:16 EDT, Martin Mathew CLA
no flags Details | Diff
Updated patch + Tests (25.46 KB, patch)
2014-05-21 07:59 EDT, Martin Mathew CLA
no flags Details | Diff
Updated patch + Tests (27.07 KB, patch)
2014-05-22 18:08 EDT, Martin Mathew CLA
no flags Details | Diff
Updated patch + Tests (28.63 KB, patch)
2014-05-22 19:26 EDT, Martin Mathew CLA
no flags Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Markus Keller CLA 2013-11-11 14:09:37 EST
When converting an anonymous to a lambda expression, we have to consider that names declared in the functional interface are not in scope any more. JLS7 6.3: "The scope of a declaration of a member m declared in or inherited by a class type C (ยง8.1.6) is the entire body of C, including any nested type declarations."

package jsr335.funint;

@FunctionalInterface
interface Func {
    enum En { A }
    void foo();
    String NAME = "";
    static String staticName() { return NAME; }
    default String getName() { return NAME; }
}

class Test {
    void bar() {
        Func f = new Func() {
            @Override
            public void foo() {
                System.out.println(En.A);
                System.out.println(NAME);
// already not in scope, since static interface methods are not inherited:
                System.out.println(staticName());
                
// OK:
                System.out.println(Test.this);
                
// accessing this or super:
                System.out.println(getName());
                foo();
                System.out.println(this);
                System.out.println(super.toString());
                Class<? extends Func> tc= this.getClass();
                Class<? extends Object> sc= super.getClass();
                System.out.println(Func.super.getName());
            }
        };
    }
}
Comment 1 Markus Keller CLA 2014-03-19 10:22:41 EDT
*** Bug 430573 has been marked as a duplicate of this bug. ***
Comment 2 Markus Keller CLA 2014-05-08 05:18:10 EDT
Manju, please handle this with priority. If the fix is simple enough, then we should fix this for RC1.

Conversion of a full-source Eclipse SDK workspace revealed that this is a common problem, e.g.:

- org.eclipse.ui.ide
CopyFilesAndFoldersOperation.java

- org.eclipse.pde.ui
FeatureImportOperation.java
PluginImportHelper.java

Note: The problem "Cannot reference a field before it is defined" (happens e.g. in AbstractTextEditor) is not part of this bug. That's a bug 421926 (a bug in the JLS, for which we won't implement a workaround for now).
Comment 3 Martin Mathew CLA 2014-05-13 08:35:17 EDT
Created attachment 243021 [details]
WIP Patch+Tests

There are two issues to be handled in this bug:
a) qualify references to static fields and types that are no longer in scope in the lambda
b) add missing cases to SuperThisReferenceFinder, so that all implicit references to 'this' block the quick assist (e.g. the recursive call to foo())

This patch handles the first case and a partial fix for the second case. 

Markus, let me know if i am on the right direction. I am yet to test all the different cases.
Comment 4 Martin Mathew CLA 2014-05-14 07:16:34 EDT
Created attachment 243077 [details]
Patch+Tests

With this patch i have covered all the cases that were encountered and the tests depicts the cases.

Markus, let me know if this is correct.
Comment 5 Markus Keller CLA 2014-05-14 20:11:33 EDT
(In reply to Manju Mathew from comment #4)
LambdaExpressionsFix.CreateLambdaOperation#qualifyNameNodes(..) looks like it does too much String-handling. It should use ImportRewrite to convert the ITypeBinding into ASTNodes. Pass the CURewrite to qualifyNameNodes, so that the ImportRewrite only gets created when it's really used.

Since we later use the ImportRemover, we also have to call importRemover.registerAddedImports(..) on the result of addImports(..).


Example:

interface Func<T> {
    void go();
    String NAME = Func.class.getName();
}

public class Test {
    void foo() {
        Func<String> f= new Func<String>() {
            @Override
            public void go() {
                System.out.println(NAME);
            }
        };
    }
}

java.lang.IllegalArgumentException: Invalid identifier : >Func<String><
	at org.eclipse.jdt.core.dom.SimpleName.setIdentifier(SimpleName.java:199)
	at org.eclipse.jdt.core.dom.AST.newSimpleName(AST.java:2202)
	at org.eclipse.jdt.core.dom.AST.newName(AST.java:1929)
	at org.eclipse.jdt.internal.corext.fix.LambdaExpressionsFix$CreateLambdaOperation.qualifyNameNodes(LambdaExpressionsFix.java:343)
	at org.eclipse.jdt.internal.corext.fix.LambdaExpressionsFix$CreateLambdaOperation.rewriteAST(LambdaExpressionsFix.java:315)
...


The InterfaceAccessQualifier#visit(QualifiedName) method also shouldn't use getFullyQualifiedName() but compare bindings. String#startsWith(..) always misses some cases when you try to compare packages.
Comment 6 Noopur Gupta CLA 2014-05-21 03:05:39 EDT
package jsr335.funint;

@FunctionalInterface
interface Func {
	class C {
		static  class CIn { static int i= 0; }		
		static String NAME = "";
	}
    void foo();
}

class Test {
	static class C {
		static  class CIn { static int i= 1; }		
		static String NAME = "";
	}
    void bar() {
        Func f = new Func() {
            @Override
            public void foo() {
                System.out.println(C.CIn.i);
                System.out.println(C.NAME);
            }
        };
    }
}

Manju, with the latest patch, apply quick assist in the above example.
- Additional import is added: import jsr335.funint.Func.C;
- C.CIn.i is not qualified with Func. As a result, it gives value 0 before conversion and value 1 after the conversion.
Comment 7 Martin Mathew CLA 2014-05-21 07:59:35 EDT
Created attachment 243339 [details]
Updated patch + Tests

Thanks Noopur for the usecase. I have taken care of all the cases mentioned and corresponding tests are also added.

Markus and Noopur, kindly review.
Comment 8 Noopur Gupta CLA 2014-05-21 14:51:33 EDT
Apply the conversion in the following example:

interface FI {
	int e= 0;
	void run(int x);
	class Test {
		public static void test(int e) {
			FI fi = new FI() {
				@Override
				public void run(int x) {
					System.out.println(e);
				}
			};
			fi.run(e);
		}			
	}
}

'e' in System.out.println(e) refers to the field "int e= 0" in FI.
After conversion, it refers to the method parameter 'e' in #test.
Comment 9 Noopur Gupta CLA 2014-05-21 14:54:18 EDT
Tested the patch and other cases look fine.
It can be released after fixing the issue mentioned in comment #8.
Comment 10 Noopur Gupta CLA 2014-05-21 15:07:27 EDT
Please ignore the example in comment #8, it works correctly.

However, for the case in comment #6, we get the result as:
        Func f = () -> {
		System.out.println(jsr335.funint.Func.C.CIn.i);
		System.out.println(Func.C.NAME);
	};

The 1st statement has package name included in the qualifier and the 2nd one doesn't. Is it OK or should it be consistent?
Comment 11 Noopur Gupta CLA 2014-05-21 15:17:38 EDT
(In reply to Noopur Gupta from comment #10)
> Please ignore the example in comment #8, it works correctly.

The example which does not work is:

interface FI extends FIS {
	void run(int x);
	public static void test(int e) {
		FI fi = new FI() {
			@Override
			public void run(int x) {
				System.out.println(e);
			}
		};
		fi.run(e);
	}			
}

interface FIS {
	int e= 0;	
}
Comment 12 Markus Keller CLA 2014-05-21 20:02:38 EDT
The LambdaExpressionsFix.CreateLambdaOperation#isTypeImported(..) is not good. E.g. AssistQuickFixTest18.testConvertToLambda21() already fails if you change the import to "import test1.Func2.En.*;".

In InterfaceAccessQualifier#visit(QualifiedName), I don't see why we should use resolveTypeBinding(). Why is the type of a variable relevant? I guess this just works by accident in some special cases with enums, where the type of the enum constant is the same as the declaring type. But in general, we only care about the declaring type of a variable, or about the type itself if the name refers to a type.

The same applies to qualifyNameNodes(..): resolveTypeBinding() should not be used there. We're only interested in resolveBinding().

I would also expect that the InterfaceAccessQualifier produces a list of SimpleNames. Everything after the first '.' doesn't need to be considered for qualification. I'm also not sure why we need the list at all. Can't we just add the qualifications while we visit the AST?

The "importRemover.registerAddedImports(addImport);" call should go right after the addImport(..) call (not in an else branch). We don't know whether an import was added, so we have to register the type all the time.
Comment 13 Martin Mathew CLA 2014-05-22 18:08:12 EDT
Created attachment 243415 [details]
Updated patch + Tests

1. #isTypeImported(..)is updated to consider onDemand import statements.
2. Updated to use #resolveBinding instead of #resolveTypeBinding
3. InterfaceAccessQualifier now qualifies the nodes during the AST visit.
3. importRemover.registerAddedImports(addImport); is added soon after the #addImport(..)
4. Added appropriate tests.

Markus and Noopur, kindly have a look.
Comment 14 Martin Mathew CLA 2014-05-22 18:26:10 EDT
Still working on a soln for the case reported in comment 11.
Comment 15 Markus Keller CLA 2014-05-22 18:57:29 EDT
(In reply to Markus Keller from comment #12)
> The LambdaExpressionsFix.CreateLambdaOperation#isTypeImported(..) is not
> good.

This was not a criticism of the implementation, but of the whole approach. It's the ImportRewrite's job to deal with imports, and we should not try to rebuild that. Please explain which problem can't be solved by the ImportRewrite.
Comment 16 Martin Mathew CLA 2014-05-22 19:26:33 EDT
Created attachment 243417 [details]
Updated patch + Tests

(In reply to Markus Keller from comment #15)
> Please explain which problem can't be solved by the ImportRewrite.

When i tried to explain to myself why the method was needed in the first place i realized the mistake. It was introduced in the initial phase of the development of this issue and never validated whether it was actually required.

This patch also contains the fix for the usecase reported in comment 11.
Comment 17 Noopur Gupta CLA 2015-08-18 08:22:47 EDT
*** Bug 475260 has been marked as a duplicate of this bug. ***
Comment 18 Pierre-Yves Bigourdan CLA 2019-08-22 08:20:37 EDT
I noticed that converting to lambda expressions still fails in Eclipse 4.12 when the functional interface contains generics. For instance:
@FunctionalInterface
public interface Calculator<M> {
    double calculate(int a);

    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}

Quick assist/clean up will transform a bit of code similar to the following:
Calculator<?> calculator = new Calculator<>() {
    @Override
    public double calculate(int a) {
        return sqrt(a) + 1;
    }
};
to:
Calculator<?> calculator = a -> sqrt(a) + 1;

This results in invalid code. When I remove the <M> generics, the invalid fix is not suggested.
Comment 19 Eclipse Genie CLA 2021-08-12 16:40:02 EDT
This bug hasn't had any activity in quite some time. Maybe the problem got resolved, was a duplicate of something else, or became less pressing for some reason - or maybe it's still relevant but just hasn't been looked at yet.

If you have further information on the current state of the bug, please add it. The information can be, for example, that the problem still occurs, that you still want the feature, that more information is needed, or that the bug is (for whatever reason) no longer relevant.

--
The automated Eclipse Genie.
Comment 20 Eclipse Genie CLA 2023-08-03 12:20:43 EDT
This bug hasn't had any activity in quite some time. Maybe the problem got resolved, was a duplicate of something else, or became less pressing for some reason - or maybe it's still relevant but just hasn't been looked at yet.

If you have further information on the current state of the bug, please add it. The information can be, for example, that the problem still occurs, that you still want the feature, that more information is needed, or that the bug is (for whatever reason) no longer relevant.

--
The automated Eclipse Genie.