LambdaCaptureSafety.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer 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
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
/**
* Validates local-variable capture rules before a loop is rewritten as a lambda.
*
* <p>Variables declared inside the original body, the generated lambda parameter,
* fields, and accumulators removed from the lambda are not external captures. Every
* remaining local must be explicitly {@code final} or effectively final according
* to its resolved JDT variable binding. Incomplete binding identity is rejected
* rather than allowing a potentially uncompilable cleanup proposal.</p>
*/
final class LambdaCaptureSafety {
private LambdaCaptureSafety() {
// utility class
}
/**
* Returns whether a generated lambda would capture an unsafe local variable.
*
* @param body original loop body with resolved bindings
* @param liftedVariableNames external accumulator names removed from the lambda
* @param additionalLocalBindings declarations outside {@code body} that become
* lambda-local, such as an enhanced-for parameter
* @return {@code true} when binding identity is incomplete or a captured local is
* neither explicitly final nor effectively final
*/
static boolean hasUnsafeCapture(Statement body, Set<String> liftedVariableNames,
IVariableBinding... additionalLocalBindings) {
if (body == null) {
return true;
}
Set<String> localBindingKeys= new HashSet<>();
for (IVariableBinding binding : additionalLocalBindings) {
if (!addBindingKey(localBindingKeys, binding)) {
return true;
}
}
AtomicBoolean incompleteBindings= new AtomicBoolean();
body.accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (!addBindingKey(localBindingKeys, node.resolveBinding())) {
incompleteBindings.set(true);
}
return true;
}
@Override
public boolean visit(SingleVariableDeclaration node) {
if (!addBindingKey(localBindingKeys, node.resolveBinding())) {
incompleteBindings.set(true);
}
return true;
}
});
if (incompleteBindings.get()) {
return true;
}
Set<String> liftedBindingKeys= resolveLiftedBindingKeys(body, liftedVariableNames);
AtomicBoolean unsafe= new AtomicBoolean();
body.accept(new ASTVisitor() {
@Override
public boolean visit(SimpleName node) {
IBinding binding= node.resolveBinding();
if (!(binding instanceof IVariableBinding variableBinding)) {
return true;
}
IVariableBinding declaration= variableBinding.getVariableDeclaration();
String key= declaration.getKey();
if (key == null) {
unsafe.set(true);
return false;
}
boolean finalLocal= Modifier.isFinal(declaration.getModifiers()) || declaration.isEffectivelyFinal();
if (!declaration.isField() && !localBindingKeys.contains(key)
&& !liftedBindingKeys.contains(key) && !finalLocal) {
unsafe.set(true);
return false;
}
return true;
}
});
return unsafe.get();
}
private static Set<String> resolveLiftedBindingKeys(Statement body, Set<String> liftedVariableNames) {
if (liftedVariableNames.isEmpty()) {
return Set.of();
}
Set<String> result= new HashSet<>();
body.accept(new ASTVisitor() {
@Override
public boolean visit(SimpleName node) {
if (liftedVariableNames.contains(node.getIdentifier())
&& node.resolveBinding() instanceof IVariableBinding variableBinding) {
String key= variableBinding.getVariableDeclaration().getKey();
if (key != null) {
result.add(key);
}
}
return true;
}
});
return result;
}
private static boolean addBindingKey(Set<String> keys, IVariableBinding binding) {
if (binding == null) {
return false;
}
String key= binding.getVariableDeclaration().getKey();
if (key == null) {
return false;
}
keys.add(key);
return true;
}
}