ProspectiveOperation.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.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
/**
* Represents a prospective stream operation extracted from a loop body.
*
* <p>
* This class encapsulates a single operation in a stream pipeline being
* constructed from an enhanced for-loop. Each operation corresponds to a
* stream method (filter, map, forEach, reduce, etc.) and maintains information
* about the expression to transform, variables consumed/produced, and any
* special handling required.
* </p>
*
* <p><b>Operation Types:</b></p>
* <ul>
* <li><b>FILTER</b>: Conditional filtering ({@code .filter(predicate)})</li>
* <li><b>MAP</b>: Transformation ({@code .map(function)})</li>
* <li><b>FOREACH</b>: Terminal action ({@code .forEach(consumer)})</li>
* <li><b>REDUCE</b>: Aggregation ({@code .reduce(identity, accumulator)})</li>
* <li><b>ANYMATCH</b>: Short-circuit match ({@code .anyMatch(predicate)})</li>
* <li><b>NONEMATCH</b>: Short-circuit non-match ({@code .noneMatch(predicate)})</li>
* <li><b>ALLMATCH</b>: Short-circuit all-match ({@code .allMatch(predicate)})</li>
* </ul>
*
* <p><b>Variable Tracking:</b></p>
* <p>
* The class tracks three types of variables:
* <ul>
* <li><b>Consumed variables</b>: Variables read by this operation</li>
* <li><b>Produced variable</b>: Variable created by MAP operations (e.g., {@code int x = ...})</li>
* <li><b>Accumulator variable</b>: Variable modified by REDUCE operations (e.g., {@code sum += ...})</li>
* </ul>
* This tracking enables proper scoping and validation in the stream pipeline.
* </p>
*
* <p><b>Reducer Patterns:</b></p>
* <p>
* For REDUCE operations, this class supports various reducer types:
* <ul>
* <li>INCREMENT/DECREMENT: {@code i++}, {@code i--}</li>
* <li>SUM: {@code sum += x} → {@code .reduce(sum, Integer::sum)}</li>
* <li>PRODUCT: {@code product *= x} → {@code .reduce(product, (a,b) -> a*b)}</li>
* <li>MAX/MIN: {@code max = Math.max(max, x)} → {@code .reduce(max, Integer::max)}</li>
* <li>STRING_CONCAT: {@code str += s} → {@code .reduce(str, String::concat)} (when null-safe)</li>
* </ul>
* </p>
*
* <p><b>Lambda Generation:</b></p>
* <p>
* The {@link #getArguments(AST, String)} method generates lambda expressions
* or method references appropriate for each operation type. It handles:
* <ul>
* <li>Parameter naming based on variable tracking</li>
* <li>Identity element generation for reducers</li>
* <li>Method reference optimization (e.g., Integer::sum vs explicit lambda)</li>
* <li>Expression copying and AST node creation</li>
* </ul>
* </p>
*
* <p><b>Thread Safety:</b> This class is not thread-safe.</p>
*
* <p>
* This class is final to prevent subclassing and potential finalizer attacks,
* since constructors call analysis methods that could potentially throw
* exceptions.
* </p>
*
* @see StreamPipelineBuilder
* @see OperationType
* @see ReducerType
* @see StreamConstants
*/
public final class ProspectiveOperation {
/**
* The original expression being analyzed or transformed.
* <p>
* This is set directly when the {@link ProspectiveOperation} is constructed
* with an {@link Expression}. If constructed with a
* {@link org.eclipse.jdt.core.dom.Statement}, this is set to the expression
* contained within the statement (if applicable, e.g., for
* {@link org.eclipse.jdt.core.dom.ExpressionStatement}).
*/
private Expression originalExpression;
/**
* The original statement being analyzed or transformed.
* <p>
* This is set when the {@link ProspectiveOperation} is constructed with a
* {@link org.eclipse.jdt.core.dom.Statement}. If the statement is an
* {@link org.eclipse.jdt.core.dom.ExpressionStatement}, its expression is also
* extracted and stored in {@link #originalExpression}. Otherwise,
* {@link #originalExpression} may be null.
*/
private org.eclipse.jdt.core.dom.Statement originalStatement;
private OperationType operationType;
private Set<String> neededVariables = new HashSet<>();
/**
* The name of the loop variable associated with this operation, if applicable.
* <p>
* This is set when the {@link ProspectiveOperation} is constructed with a
* statement and a loop variable name. It is used to track the variable iterated
* over in enhanced for-loops or similar constructs.
*/
private String loopVariableName;
/**
* The name of the variable produced by this operation (for MAP operations).
* This is used to track variable names through the stream pipeline.
*/
private String producedVariableName;
/**
* The name of the accumulator variable for REDUCE operations. Used to track
* which variable is being accumulated (e.g., "i" in "i++").
*/
private String accumulatorVariableName;
/**
* The reducer type for REDUCE operations (INCREMENT, DECREMENT, SUM, etc.).
*/
private ReducerType reducerType;
/**
* The type of the accumulator variable for REDUCE operations (e.g., "int", "double", "long").
* Used to generate the correct method reference (Integer::sum vs Double::sum).
*/
private String accumulatorType;
/**
* Indicates if this operation is null-safe (e.g., variables are annotated with @NotNull).
* When true for STRING_CONCAT, String::concat method reference can be used safely.
*/
private boolean isNullSafe = false;
/**
* The collector type for COLLECT operations (TO_LIST, TO_SET, etc.).
*/
private CollectorType collectorType;
/**
* The name of the target collection variable for COLLECT operations (e.g., "result" in "result.add(item)").
*/
private String targetVariable;
/**
* Set of variables consumed by this operation. Used for tracking variable scope
* and preventing leaks.
*/
private Set<String> consumedVariables = new HashSet<>();
/**
* Collection of variable names already in use in the scope. Used to generate
* unique lambda parameter names that don't clash with existing variables.
*/
private Collection<String> usedVariableNames = new HashSet<>();
// Collects all used/referenced variables
private void collectNeededVariables(Expression expression) {
if (expression == null)
return;
expression.accept(new ASTVisitor() {
@Override
public boolean visit(SimpleName node) {
// Only collect SimpleName nodes that are actual variable references,
// not part of qualified names (e.g., System.out) or method/field names
ASTNode parent = node.getParent();
// Skip if this is any part of a qualified name (e.g., "System" or "out" in "System.out")
if (parent instanceof org.eclipse.jdt.core.dom.QualifiedName) {
// Skip both qualifier and name parts of qualified names
return super.visit(node);
}
// Skip if this is any part of a field access (e.g., explicit field accesses)
if (parent instanceof org.eclipse.jdt.core.dom.FieldAccess) {
// Skip both the expression (qualifier) and the name (field name)
return super.visit(node);
}
// Skip if this is the name part of a method invocation (e.g., "println" in "out.println()")
if (parent instanceof MethodInvocation) {
MethodInvocation mi = (MethodInvocation) parent;
if (mi.getName() == node) {
return super.visit(node); // Skip method name
}
// Skip class names used as method invocation receivers (e.g., "Math" in "Math.max()")
// but NOT variable references (e.g., "list" in "list.forEach()")
if (mi.getExpression() == node) {
IBinding binding = node.resolveBinding();
// If it's a type binding (class name), skip it
// If it's a variable binding, we want to collect it
if (binding != null && !(binding instanceof IVariableBinding)) {
return super.visit(node); // Skip class name
}
// Fallback: If binding resolution failed, check if the name starts with uppercase
// (convention for class names in Java) and has no local variable conflict
if (binding == null) {
String name = node.getIdentifier();
if (name.length() > 0 && Character.isUpperCase(name.charAt(0))) {
// Likely a class name like Math, System, Integer, etc.
return super.visit(node);
}
}
}
}
// Skip if this is part of a type reference (e.g., class names)
if (parent instanceof org.eclipse.jdt.core.dom.Type) {
return super.visit(node);
}
// Skip if this is the type name in a constructor invocation (e.g., "MyClass" in "new MyClass()")
if (parent instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation) {
return super.visit(node);
}
// Skip if this is the name of a type declaration (e.g., class or interface name)
if (parent instanceof org.eclipse.jdt.core.dom.TypeDeclaration) {
org.eclipse.jdt.core.dom.TypeDeclaration typeDecl = (org.eclipse.jdt.core.dom.TypeDeclaration) parent;
if (typeDecl.getName() == node) {
return super.visit(node);
}
}
// Otherwise, this is a variable reference - collect it
neededVariables.add(node.getIdentifier());
return super.visit(node);
}
});
}
/**
* Constructor for operations with an expression.
*
* @param expression the expression to process (must not be null)
* @param operationType the type of operation (must not be null)
*/
public ProspectiveOperation(Expression expression, OperationType operationType) {
assert expression != null : "expression cannot be null";
assert operationType != null : "operationType cannot be null";
this.originalExpression = expression;
this.operationType = operationType;
collectNeededVariables(expression);
updateConsumedVariables();
}
/**
* Constructor for operations with a statement.
*
* @param statement the statement to process (must not be null)
* @param operationType the type of operation (MAP, FOREACH, etc.) (must not be
* null)
* @param loopVarName the loop variable name; for side-effect MAP operations,
* this represents the variable to be returned in the
* lambda body (may be the current variable name in the
* pipeline, not necessarily the original loop variable)
*/
public ProspectiveOperation(org.eclipse.jdt.core.dom.Statement statement, OperationType operationType,
String loopVarName) {
assert statement != null : "statement cannot be null";
assert operationType != null : "operationType cannot be null";
this.originalStatement = statement;
this.operationType = operationType;
this.loopVariableName = loopVarName;
if (statement instanceof org.eclipse.jdt.core.dom.ExpressionStatement) {
this.originalExpression = ((org.eclipse.jdt.core.dom.ExpressionStatement) statement).getExpression();
collectNeededVariables(this.originalExpression);
}
updateConsumedVariables();
}
/**
* Constructor for MAP operations with a produced variable name. Used when a
* variable declaration creates a new variable in the stream pipeline.
*
* @param expression the expression that produces the new variable (must
* not be null)
* @param operationType the type of operation (should be MAP) (must not be
* null)
* @param producedVarName the name of the variable produced by this operation
*/
public ProspectiveOperation(Expression expression, OperationType operationType, String producedVarName) {
assert expression != null : "expression cannot be null";
assert operationType != null : "operationType cannot be null";
this.originalExpression = expression;
this.operationType = operationType;
this.producedVariableName = producedVarName;
collectNeededVariables(expression);
updateConsumedVariables();
}
/**
* Constructor for REDUCE operations with accumulator variable and reducer type.
* Used when a reducer pattern (i++, sum += x, etc.) is detected.
*
* @param statement the statement containing the reducer (must not be
* null)
* @param accumulatorVarName the name of the accumulator variable (e.g., "i",
* "sum") (must not be null)
* @param reducerType the type of reducer (INCREMENT, SUM, etc.) (must
* not be null)
*/
public ProspectiveOperation(org.eclipse.jdt.core.dom.Statement statement, String accumulatorVarName,
ReducerType reducerType) {
assert statement != null : "statement cannot be null";
assert accumulatorVarName != null : "accumulatorVarName cannot be null";
assert reducerType != null : "reducerType cannot be null";
this.originalStatement = statement;
this.operationType = OperationType.REDUCE;
this.accumulatorVariableName = accumulatorVarName;
this.reducerType = reducerType;
if (statement instanceof org.eclipse.jdt.core.dom.ExpressionStatement) {
this.originalExpression = ((org.eclipse.jdt.core.dom.ExpressionStatement) statement).getExpression();
collectNeededVariables(this.originalExpression);
}
updateConsumedVariables();
}
/** Returns the operation type. */
public OperationType getOperationType() {
return this.operationType;
}
/**
* Generate the lambda arguments for this operation Based on NetBeans
* ProspectiveOperation.getArguments()
*
* @param ast the AST to create nodes in (must not be null)
* @param paramName the parameter name to use for the lambda (may be null,
* defaults to "item")
* @return a list of expressions to use as arguments for the stream method
* @throws IllegalArgumentException if ast is null
*/
public List<Expression> getArguments(AST ast, String paramName) {
if (ast == null) {
throw new IllegalArgumentException("ast cannot be null");
}
List<Expression> args = new ArrayList<>();
// REDUCE and COLLECT have special argument handling
if (operationType.hasSpecialArgumentHandling()) {
if (operationType == OperationType.REDUCE) {
return getArgumentsForReducer(ast);
} else if (operationType == OperationType.COLLECT) {
return getArgumentsForCollector(ast);
}
}
// Create lambda expression for MAP, FILTER, FOREACH, ANYMATCH, NONEMATCH, ALLMATCH
LambdaExpression lambda = ast.newLambdaExpression();
// Create parameter with defensive null check
// Use the provided paramName, or generate a unique default name
VariableDeclarationFragment param = ast.newVariableDeclarationFragment();
String effectiveParamName;
if (paramName != null && !paramName.isEmpty()) {
effectiveParamName = paramName;
} else {
// Generate a unique default name to avoid clashes
effectiveParamName = generateUniqueVariableName("item");
}
param.setName(ast.newSimpleName(effectiveParamName));
lambda.parameters().add(param);
// For single parameter without type annotation, don't use parentheses
lambda.setParentheses(false);
// Create lambda body using OperationType delegation
OperationType.LambdaBodyContext context = new OperationType.LambdaBodyContext(
originalExpression, originalStatement, loopVariableName);
ASTNode lambdaBody = operationType.createLambdaBody(ast, context);
if (lambdaBody != null) {
lambda.setBody(lambdaBody);
} else if (originalExpression != null) {
// Fallback: use expression as body
lambda.setBody(ASTNode.copySubtree(ast, originalExpression));
} else {
// Defensive: neither originalExpression nor originalStatement is available
throw new IllegalStateException(
"Cannot create lambda: both originalExpression and originalStatement are null for operationType "
+ operationType);
}
args.add(lambda);
return args;
}
/**
* Returns the variable name produced by this operation (for MAP operations).
* This is used to track variable names through the stream pipeline.
*/
public String getProducedVariableName() {
return producedVariableName;
}
/**
* Returns the accumulator variable name for REDUCE operations.
*
* @return the accumulator variable name, or null if not a REDUCE operation
*/
public String getAccumulatorVariableName() {
return accumulatorVariableName;
}
/**
* Returns the reducer type for REDUCE operations.
*
* @return the reducer type, or null if not a REDUCE operation
*/
public ReducerType getReducerType() {
return reducerType;
}
/**
* Sets whether this operation is null-safe.
*
* @param isNullSafe true if the operation is null-safe
*/
public void setNullSafe(boolean isNullSafe) {
this.isNullSafe = isNullSafe;
}
/**
* Returns whether this operation is null-safe.
*
* @return true if the operation is null-safe
*/
public boolean isNullSafe() {
return isNullSafe;
}
/**
* Sets the collector type for COLLECT operations.
*
* @param collectorType the collector type (TO_LIST, TO_SET, etc.)
*/
public void setCollectorType(CollectorType collectorType) {
this.collectorType = collectorType;
}
/**
* Returns the collector type for COLLECT operations.
*
* @return the collector type, or null if not a COLLECT operation
*/
public CollectorType getCollectorType() {
return collectorType;
}
/**
* Sets the target collection variable for COLLECT operations.
*
* @param targetVariable the target variable name (e.g., "result")
*/
public void setTargetVariable(String targetVariable) {
this.targetVariable = targetVariable;
}
/**
* Returns the target collection variable for COLLECT operations.
*
* @return the target variable name, or null if not a COLLECT operation
*/
public String getTargetVariable() {
return targetVariable;
}
/**
* Sets the accumulator type for REDUCE operations.
* This is used to generate the correct method reference (e.g., Integer::sum vs Double::sum).
*
* @param accumulatorType the type of the accumulator variable (e.g., "int", "double", "long")
*/
public void setAccumulatorType(String accumulatorType) {
this.accumulatorType = accumulatorType;
}
/**
* Returns the accumulator type for REDUCE operations.
*
* @return the accumulator type (e.g., "int", "double", "long"), or null if not set
*/
public String getAccumulatorType() {
return accumulatorType;
}
/**
* Sets the collection of variable names already in use in the current scope.
* This is used to generate unique lambda parameter names that don't clash
* with existing variables.
*
* @param usedNames the collection of variable names in use (may be null)
*/
public void setUsedVariableNames(Collection<String> usedNames) {
if (usedNames != null) {
this.usedVariableNames = usedNames;
}
}
/**
* Generates a unique variable name that doesn't collide with existing variables in scope.
*
* <p>This method ensures the generated lambda parameter name doesn't conflict with other
* variables visible at the transformation point. If the base name is already in use,
* a numeric suffix is appended (e.g., "a2", "a3", etc.).</p>
*
* @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
*/
private String generateUniqueVariableName(String baseName) {
// Combine neededVariables (from expression analysis) with usedVariableNames (from scope)
Set<String> allUsedNames = new HashSet<>(neededVariables);
allUsedNames.addAll(usedVariableNames);
// If base name is not used, return it
if (!allUsedNames.contains(baseName)) {
return baseName;
}
// Otherwise, append a number until we find an unused name
int counter = 2;
String candidate = baseName + counter;
while (allUsedNames.contains(candidate)) {
counter++;
candidate = baseName + counter;
}
return candidate;
}
/**
* Returns the set of variables consumed by this operation. This includes all
* SimpleName references in the operation's expression.
*
* @return the set of consumed variable names
*/
public Set<String> getConsumedVariables() {
return consumedVariables;
}
/**
* Updates the consumed variables set by collecting all SimpleName references.
* This is called during operation construction to track variable usage.
*/
private void updateConsumedVariables() {
consumedVariables.addAll(neededVariables);
}
/** Determines the arguments for reduce() operation. */
private List<Expression> getArgumentsForReducer(AST ast) {
List<Expression> arguments = new ArrayList<>();
if (operationType == OperationType.REDUCE) {
// First argument: identity element (the accumulator variable reference)
if (accumulatorVariableName != null) {
arguments.add(ast.newSimpleName(accumulatorVariableName));
} else {
// Fallback to default identity
Expression identity = getIdentityElement(ast);
if (identity != null)
arguments.add(identity);
}
// Second argument: accumulator function (method reference or lambda)
Expression accumulator = createAccumulatorExpression(ast);
if (accumulator != null)
arguments.add(accumulator);
}
return arguments;
}
/**
* Determines the arguments for collect() operation.
* Returns a single argument: the Collectors method invocation (e.g., Collectors.toList()).
*/
private List<Expression> getArgumentsForCollector(AST ast) {
List<Expression> arguments = new ArrayList<>();
if (operationType == OperationType.COLLECT && collectorType != null) {
// Single argument: Collectors.toList() or Collectors.toSet()
Expression collector = collectorType.createCollectorExpression(ast);
arguments.add(collector);
}
return arguments;
}
/**
* Creates the accumulator expression for REDUCE operations. Returns a method
* reference (e.g., Integer::sum, Long::sum, Double::sum) when possible, or a lambda otherwise.
* The method reference type is determined by the accumulator variable type.
*/
private Expression createAccumulatorExpression(AST ast) {
LambdaGenerator generator = new LambdaGenerator(ast);
generator.setNeededVariables(neededVariables);
generator.setUsedVariableNames(usedVariableNames);
return generator.createAccumulatorExpression(reducerType, accumulatorType, isNullSafe);
}
/** Determines the identity element (0, 1) for reduce() operation. */
private Expression getIdentityElement(AST ast) {
if (operationType == OperationType.REDUCE && originalExpression instanceof Assignment) {
Assignment assignment = (Assignment) originalExpression;
if (assignment.getOperator() == Assignment.Operator.PLUS_ASSIGN) {
return ast.newNumberLiteral("0");
} else if (assignment.getOperator() == Assignment.Operator.TIMES_ASSIGN) {
return ast.newNumberLiteral("1");
}
}
return null;
}
@Override
public String toString() {
return "ProspectiveOperation{" + "expression=" + originalExpression + ", operationType=" + operationType
+ ", neededVariables=" + neededVariables + '}';
}
}