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

Extracting lambda expression - without {} - to a new method creates valid java code #6595

Open
wants to merge 16 commits into
base: master
Choose a base branch
from

Conversation

Honza-cz
Copy link

resolves #6311

Fixes the issue linked to extracting parameterized lambda into a method. I tested manually all cases I have in mind. The fix itself is not nicest, but it works. I was unable to write meaningful automated tests for that class.

source code:

public class ExtractMethodDemo11 {
    
    public static void main(String[] args) {
        String unused = "unused";
        String first = "hello";
        String second = "world";

        System.out.println(
                java.util.Optional.ofNullable("-")
                        .map(t -> first + t + second)
                        .orElseThrow()
        );
    }
}

extracting first + t + second:

public class ExtractMethodDemo11 {
    
    public static void main(String[] args) {
        String unused = "unused";
        String first = "hello";
        String second = "world";

        System.out.println(java.util.Optional.ofNullable("-")
                        .map(t -> inner(first, t, second))
                        .orElseThrow()
        );
    }

    private static String inner(String first, String t, String second) {
        return first + t + second;
    }
}

extracting t -> first + t + second:

public class ExtractMethodDemo11 {
   
    public static void main(String[] args) {
        String unused = "unused";
        String first = "hello";
        String second = "world";

        System.out.println(java.util.Optional.ofNullable("-")
                        .map(inner(first, second))
                        .orElseThrow()
        );
    }

    private static Function<String, String> inner(String first, String second) {
        return t -> first + t + second;
    }
}

As I mentioned in bug, lambda with {} still cannot be extracted correctly to a method. The problem is with return type of the newly created method which is taken not from lambda, but from enclosing method. I was not able to resolve this issue yet.

if (localVariables.contains(e) && usedLocalVariables.get(e) == null) {

final boolean isUsedInLambda = isLambda
&& treesSeensInSelection.contains(node)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: if PHASE_INSIDE_SELECTION is active, then the node is already added in scan() line 106 ?

Copy link
Author

@Honza-cz Honza-cz Oct 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, scan method is called before visit* methods
image

@Override
public Void visitLambdaExpression(LambdaExpressionTree node, Void p) {
nesting++;
if (node.equals(firstInSelection)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use == here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

if (node.equals(firstInSelection)) {
phase = PHASE_INSIDE_SELECTION;
}
isLambda = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm ... what about nested lambdas ? Suppose a code like

Runnable r = () -> new Worker() { 
   public void n() {
     doSomething(variable);
   }
   public void someOverrideMethod() {
      someExecutor.execute(
// selection starts here
       () -> whateverMethodCall(variable);
// selection ends here
      );
   }
}.run();

The lambda flag becomes true on the outermost lambda, which is still unrelated to the selection

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was checking this use case with nested lambda. It is true, that flag is set for whole lambda. On the other hand it seems to fix the issue. I realized a new occurrence of issue, tested on netbeans 19:

static void dummy(final String first) {
    String unused = "unused";
    String second = "world";

    Runnable r = () -> {
        Runnable g = () -> {
            final String m = first + second + m1;
            System.out.println("m" + m);
        };
    };
}

select first + second + m1 => extract => non compilable code:

static void dummy(final String first) {
    String unused = "unused";
    String second = "world";

    Runnable r = () -> {
        Runnable g = () -> {
            final String m = aaa();
            System.out.println("m" + m);
        };
    };
}

private static String aaa() {
    return first + second + m1;
}

with my version it create valid code:

static void dummy(final String first) {
    String unused = "unused";
    String second = "world";

    Runnable r = () -> {
        Runnable g = () -> {
            final String m = aaa(first, second);
            System.out.println("m" + m);
        };
    };
}

private static String aaa(final String first, String second) {
    return first + second + m1;
}

I was testing nested lambada and it works fine, just some a sample:

public static void main(String[] args) {
    String unused = "unused";
    String first = "hello";
    String second = "world";

    Function<String,Integer> f1 = t -> {
        Function<String,String> f2 = u -> {
            final String m = first + second + m1;
            System.out.println("m" + m);

            final Function<String, Integer> len = s -> (u + s + m + first + m1).length();
            System.out.println("len" + len.apply("hello"));

            return (u + m + first + m1);
        };

        return f2.apply(t).length();

    };

}

=> various extract to method:

public static void main(String[] args) {
    String unused = "unused";
    String first = "hello";
    String second = "world";

    Function<String, Integer> f1 = t -> {
        Function<String, String> f2 = u -> {
            final String m = z1(first, second);
            System.out.println("m" + m);

            Function<String, Integer> len = z3(u, m, first);
            System.out.println("len" + len.apply("hello"));

            return (u + m + first + m1);
        };

        return z4(f2, t);

    };

}

private static int z4(Function<String, String> f2, String t) {
    return f2.apply(t).length();
}

private static Function<String, Integer> z3(String u, final String m, String first) {
    final Function<String, Integer> len = s -> z2(u, s, m, first);
    return len;
}

private static int z2(String u, String s, final String m, String first) {
    return (u + s + m + first + m1).length();
}

private static String z1(String first, String second) {
    return first + second + m1;
}

What didn't work if I select whole statement with return return f2.apply(t).length(); but this is another issue which I was not able to fix yet (and announced it in the description above)

I admit that isLambda flag is a bit magic flag, but it works in case of lambda or non-lambda code. At least I was not able to find a use case, which could be broken by this flag.

Do you want me to dig deeper?

@mbien mbien added the Java [ci] enable extra Java tests (java.completion, java.source.base, java.hints, refactoring.java, form) label Oct 20, 2023
@mbien mbien added the hints label Jan 7, 2024
Copy link
Member

@mbien mbien left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @Honza-cz
could you add a test case analog to:

public void testIntroduceMethod179258() throws Exception {
performFixTest("package test;\n" +
"public class Test {\n" +
" public static void test() {\n" +
" String test = null;\n" +
" |test = \"foo\";\n" +
" if (test == null) {\n" +
" System.err.println(1);\n" +
" } else {\n" +
" System.err.println(2);\n" +
" }|\n" +
" }\n" +
"}",
("package test;\n" +
"public class Test {\n" +
" public static void test() {\n" +
" String test = null;\n" +
" name();\n" +
" }\n" +
" private static void name() {\n" +
" String test;\n" +
" test = \"foo\";\n" +
" if (test == null) {\n" +
" System.err.println(1);\n" +
" } else {\n" +
" System.err.println(2);\n" +
" }\n" +
" }\n" +
"}")
.replaceAll("[ \t\n]+", " "),
new DialogDisplayerImpl3("name", EnumSet
.of(Modifier.PRIVATE), true));
}

| marks start/end of the selection which is supposed to be extracted.

regarding paperwork:

  • please make sure the commit has your full name as author, you can check it by looking at the patch file of the PR: https://github.com/apache/netbeans/pull/6595.patch
  • please squash everything to one commit and rebase it on latest master once ready to merge

@mbien mbien added the ci:dev-build [ci] produce a dev-build zip artifact (7 days expiration, see link on workflow summary page) label May 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ci:dev-build [ci] produce a dev-build zip artifact (7 days expiration, see link on workflow summary page) hints Java [ci] enable extra Java tests (java.completion, java.source.base, java.hints, refactoring.java, form)
Projects
None yet
3 participants