LambdaGenerator.java
/*******************************************************************************
* Copyright (c) 2025 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
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.PrefixExpression;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.sandbox.jdt.internal.corext.util.ExpressionHelper;
/**
* Generates lambda expressions and method references for stream operations.
*
* <p>This class is responsible for creating the lambda expressions and method
* references used in stream pipeline operations. It handles:</p>
*
* <ul>
* <li>Accumulator lambdas: {@code (a, b) -> a + b}</li>
* <li>Predicate lambda bodies for filter/match operations</li>
* <li>Unique variable name generation to avoid scope conflicts</li>
* </ul>
*
* <p><b>Variable Name Generation:</b></p>
* <p>The generator ensures lambda parameter names don't conflict with variables
* already in scope by tracking used variable names and generating unique alternatives.</p>
*
* <p><b>Usage Example:</b></p>
* <pre>{@code
* LambdaGenerator generator = new LambdaGenerator(ast);
* generator.setUsedVariableNames(scopeVariables);
*
* // Create an accumulator expression for REDUCE operations
* Expression accExpr = generator.createAccumulatorExpression(ReducerType.SUM, "int", false);
*
* // Create a predicate lambda body for FILTER operations
* Expression predicateBody = generator.createPredicateLambdaBody(condition);
* }</pre>
*
* @see ProspectiveOperation
* @see StreamPipelineBuilder
* @see ReducerType
*/
public final class LambdaGenerator {
private final AST ast;
private Set<String> usedVariableNames = new HashSet<>();
private Set<String> neededVariables = new HashSet<>();
/**
* Creates a new LambdaGenerator for the given AST.
*
* @param ast the AST to create nodes in (must not be null)
* @throws IllegalArgumentException if ast is null
*/
public LambdaGenerator(AST ast) {
if (ast == null) {
throw new IllegalArgumentException("ast cannot be null");
}
this.ast = ast;
}
/**
* Sets the collection of variable names already in use in the current scope.
* This is used to generate unique lambda parameter names.
*
* @param usedNames the collection of variable names in use (may be null)
*/
public void setUsedVariableNames(Collection<String> usedNames) {
if (usedNames != null) {
this.usedVariableNames = new HashSet<>(usedNames);
}
}
/**
* Sets the collection of variables needed/referenced by the operation.
* This is combined with usedVariableNames to ensure unique parameter names.
*
* @param needed the set of needed variable names (may be null)
*/
public void setNeededVariables(Set<String> needed) {
if (needed != null) {
this.neededVariables = new HashSet<>(needed);
}
}
/**
* Generates a unique variable name that doesn't collide with existing variables in scope.
*
* @param baseName the base name to use (e.g., "a", "_item", "accumulator")
* @return a unique variable name that doesn't exist in the current scope
*/
public String generateUniqueVariableName(String baseName) {
Set<String> allUsedNames = new HashSet<>(neededVariables);
allUsedNames.addAll(usedVariableNames);
if (!allUsedNames.contains(baseName)) {
return baseName;
}
int counter = 2;
String candidate = baseName + counter;
while (allUsedNames.contains(candidate)) {
counter++;
candidate = baseName + counter;
}
return candidate;
}
/**
* Creates a general accumulator lambda like (a, b) -> a + b.
* Used as a fallback for custom aggregation patterns.
*
* @return a LambdaExpression with generic parameters
*/
public LambdaExpression createAccumulatorLambda() {
LambdaExpression lambda = ast.newLambdaExpression();
String param1Name = generateUniqueVariableName("a");
String param2Name = generateUniqueVariableName("b");
VariableDeclarationFragment paramA = ast.newVariableDeclarationFragment();
paramA.setName(ast.newSimpleName(param1Name));
VariableDeclarationFragment paramB = ast.newVariableDeclarationFragment();
paramB.setName(ast.newSimpleName(param2Name));
lambda.parameters().add(paramA);
lambda.parameters().add(paramB);
InfixExpression operationExpr = ast.newInfixExpression();
operationExpr.setLeftOperand(ast.newSimpleName(param1Name));
operationExpr.setRightOperand(ast.newSimpleName(param2Name));
operationExpr.setOperator(InfixExpression.Operator.PLUS);
lambda.setBody(operationExpr);
return lambda;
}
/**
* Creates a lambda body for predicate expressions (FILTER, ANYMATCH, etc.).
*
* <p>Wraps InfixExpressions in parentheses for clarity. Does NOT wrap:
* PrefixExpression with NOT, MethodInvocation, SimpleName, BooleanLiteral.</p>
*
* @param expression the predicate expression
* @return the lambda body expression, possibly wrapped in parentheses
*/
public Expression createPredicateLambdaBody(Expression expression) {
// Unwrap parentheses to check the actual expression type
Expression unwrapped = ExpressionHelper.getUnparenthesized(expression);
// Don't wrap PrefixExpression with NOT - already has proper precedence
if (unwrapped instanceof PrefixExpression) {
PrefixExpression prefix = (PrefixExpression) unwrapped;
if (prefix.getOperator() == PrefixExpression.Operator.NOT) {
return (Expression) ASTNode.copySubtree(ast, unwrapped);
}
}
// Only wrap InfixExpressions for readability
if (unwrapped instanceof InfixExpression) {
ParenthesizedExpression parenExpr = ast.newParenthesizedExpression();
parenExpr.setExpression((Expression) ASTNode.copySubtree(ast, unwrapped));
return parenExpr;
}
// For all other expressions, no parentheses needed
return (Expression) ASTNode.copySubtree(ast, unwrapped);
}
/**
* Creates the accumulator expression for a REDUCE operation.
* Delegates to the ReducerType enum which encapsulates the logic for each reducer type.
*
* @param reducerType the type of reducer
* @param accumulatorType the type of the accumulator variable
* @param isNullSafe whether the operation is null-safe
* @return an Expression suitable for the second argument of reduce()
*/
public Expression createAccumulatorExpression(ReducerType reducerType,
String accumulatorType, boolean isNullSafe) {
if (reducerType == null) {
return createAccumulatorLambda();
}
return reducerType.createAccumulatorExpression(ast, accumulatorType, isNullSafe);
}
}