diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/LambdaExpression.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/LambdaExpression.java index 0cf0f123d9c..ac702348165 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/LambdaExpression.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/LambdaExpression.java @@ -47,6 +47,7 @@ import static org.eclipse.jdt.internal.compiler.ast.ExpressionContext.INVOCATION_CONTEXT; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -985,9 +986,7 @@ private LambdaExpression cachedResolvedCopy(TypeBinding targetType, boolean anyT return null; targetType = copy.expectedType; // possibly updated local types - if (this.copiesPerTargetType == null) - this.copiesPerTargetType = new HashMap<>(); - this.copiesPerTargetType.put(targetType, copy); + this.copiesPerTargetType.put(targetType, copy); // copy() has linked this lambda to the original's cache } if (!requireExceptionAnalysis) return copy; @@ -1116,7 +1115,7 @@ LambdaExpression copy() { if (copy != null) { // ==> syntax errors == null if (copy.sourceStart != this.sourceStart || copy.sourceEnd != this.sourceEnd) return null; // something wrong - copy.original = this; + shareInferenceCaches(copy); copy.assistNode = this.assistNode; copy.enclosingScope = this.enclosingScope; copy.text = this.text; // discard redundant textual copy @@ -1124,6 +1123,44 @@ LambdaExpression copy() { return copy; } + private void shareInferenceCaches(LambdaExpression copy) { + // A speculative copy can contain nested lambdas. Link each collected lambda to its + // original lambda and share the per-target inference cache. + // A cache entry is a resolved lambda copy and owns its parameter bindings. Nested + // lambdas may use parameters from an enclosing lambda, so collectLambdas() stops + // below a lambda with parameters. Caches below that point stay local to the + // enclosing copy. + // Both traversals start at the root and visit nested lambdas in source order. + List sourceLambdas = collectLambdas(this); + List copiedLambdas = collectLambdas(copy); + if (sourceLambdas.size() != copiedLambdas.size()) + throw new CopyFailureException(); + for (int i = 0; i < sourceLambdas.size(); i++) { + LambdaExpression sourceLambda = sourceLambdas.get(i); + LambdaExpression copiedLambda = copiedLambdas.get(i); + if (sourceLambda.sourceStart != copiedLambda.sourceStart || sourceLambda.sourceEnd != copiedLambda.sourceEnd) + throw new CopyFailureException(); + LambdaExpression originalLambda = sourceLambda.original; + copiedLambda.original = originalLambda; + if (originalLambda.copiesPerTargetType == null) + originalLambda.copiesPerTargetType = new HashMap<>(); + sourceLambda.copiesPerTargetType = originalLambda.copiesPerTargetType; + copiedLambda.copiesPerTargetType = originalLambda.copiesPerTargetType; + } + } + + private static List collectLambdas(LambdaExpression root) { + List lambdas = new ArrayList<>(); + root.traverse(new ASTVisitor() { + @Override + public boolean visit(LambdaExpression lambda, BlockScope skope) { + lambdas.add(lambda); + return lambda.arguments.length == 0; // nested lambdas may use these parameters + } + }, root.enclosingScope); + return lambdas; + } + public void returnsExpression(Expression expression, TypeBinding resultType) { if (this.original == this) // Not in overload resolution context. result expressions not relevant. return; diff --git a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/NestedLambdaInferenceTest.java b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/NestedLambdaInferenceTest.java new file mode 100644 index 00000000000..2ff384e13f0 --- /dev/null +++ b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/NestedLambdaInferenceTest.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * Copyright (c) 2026 François Martin and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * François Martin - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.core.tests.compiler.regression; + +import junit.framework.Test; + +public class NestedLambdaInferenceTest extends AbstractRegressionTest { + private static final int NESTING_DEPTH = 24; + + public NestedLambdaInferenceTest(String name) { + super(name); + } + + public static Test suite() { + return buildMinimalComplianceTestSuite(NestedLambdaInferenceTest.class, F_1_8); + } + + // https://github.com/eclipse-jdt/eclipse.jdt.core/issues/5206 + public void testIssue5206GenericRouteChain() { + runNestedLambdaTest("GenericRouteChain", """ + V route(Red marker, Work work) { return null; } + V route(Blue marker, Work work) { return null; } + V route(Green marker, Work work) { return null; } + V route(Gold marker, Work work) { return null; } + V route(Object marker, Work work) { return null; } + + V select(Red marker, Work work) { return null; } + V select(Blue marker, Work work) { return null; } + V select(Green marker, Work work) { return null; } + V select(Gold marker, Work work) { return null; } + V select(Object marker, Work work) { return null; } + """); + } + + // https://github.com/eclipse-jdt/eclipse.jdt.core/issues/5206 + public void testIssue5206ConcreteRouteChain() { + runNestedLambdaTest("ConcreteRouteChain", """ + String route(Red marker, Work work) { return ""; } + String route(Blue marker, Work work) { return ""; } + String route(Green marker, Work work) { return ""; } + String route(Gold marker, Work work) { return ""; } + String route(Object marker, Work work) { return ""; } + + String select(Red marker, Work work) { return ""; } + String select(Blue marker, Work work) { return ""; } + String select(Green marker, Work work) { return ""; } + String select(Gold marker, Work work) { return ""; } + String select(Object marker, Work work) { return ""; } + """); + } + + // https://github.com/eclipse-jdt/eclipse.jdt.core/issues/5206 + public void testIssue5206InterleavedParameterizedLambdas() { + this.runConformTest(new String[] { + "InterleavedLambdas.java", + """ + public class InterleavedLambdas { + interface Producer { + T produce(); + } + interface Mapper { + R map(T value); + } + + static T produce(Producer producer) { + return producer.produce(); + } + static R map(T value, Mapper mapper) { + return mapper.map(value); + } + + public static void main(String[] args) { + String result = produce(() -> + map("left", left -> + produce(() -> + map(7, number -> left + number))) + + produce(() -> "!")); + System.out.print(result); + } + } + """ + }, + "left7!"); + } + + private void runNestedLambdaTest(String className, String overloads) { + this.runConformTest(new String[] { + className + ".java", + """ + class %s { + interface Work { + V perform(); + } + static final Work TERMINAL = null; + + void test() { + %s; + } + static class Red { } + static class Blue { } + static class Green { } + static class Gold { } + %s + } + """.formatted(className, createNestedInvocation(), overloads.indent(4).stripTrailing()) + }); + } + + private static String createNestedInvocation() { + String invocation = "route(null, TERMINAL)"; + for (int level = 1; level < NESTING_DEPTH; level++) { + String selector = level % 2 == 0 ? "route" : "select"; + invocation = selector + "(null, () -> " + invocation + ")"; + } + return invocation; + } +} diff --git a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/TestAll.java b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/TestAll.java index b8875da2eae..14087ca36b5 100644 --- a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/TestAll.java +++ b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/TestAll.java @@ -131,6 +131,7 @@ public static Test suite() { standardTests.add(NullTypeAnnotationTest.class); standardTests.add(NegativeLambdaExpressionsTest.class); standardTests.add(LambdaExpressionsTest.class); + standardTests.add(NestedLambdaInferenceTest.class); standardTests.add(LambdaRegressionTest.class); standardTests.add(SerializableLambdaTest.class); standardTests.add(OverloadResolutionTest8.class);