PipelineAssembler.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
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;
import java.util.Collection;
import java.util.List;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.PrefixExpression;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.Statement;
/**
* Assembles stream pipeline method invocations from a list of operations.
*
* <p>This class is responsible for the final assembly phase of the stream
* conversion, taking a list of {@link ProspectiveOperation}s and constructing
* the actual AST nodes for the stream pipeline.</p>
*
* <p><b>Responsibilities:</b></p>
* <ul>
* <li>Building the method invocation chain (stream().filter().map()...)</li>
* <li>Determining if .stream() prefix is needed</li>
* <li>Wrapping the pipeline in appropriate statement types</li>
* <li>Handling special cases (REDUCE assignments, match IF statements)</li>
* </ul>
*
* <p><b>Usage Example:</b></p>
* <pre>{@code
* PipelineAssembler assembler = new PipelineAssembler(forLoop, operations, loopVarName);
* assembler.setUsedVariableNames(scopeVars);
* assembler.setReduceDetector(reduceDetector);
*
* MethodInvocation pipeline = assembler.buildPipeline();
* Statement replacement = assembler.wrapPipeline(pipeline);
* }</pre>
*
* @see StreamPipelineBuilder
* @see ProspectiveOperation
*/
public class PipelineAssembler {
private final EnhancedForStatement forLoop;
private final AST ast;
private final List<ProspectiveOperation> operations;
private final String loopVariableName;
private Collection<String> usedVariableNames;
private ReducePatternDetector reduceDetector;
private CollectPatternDetector collectDetector;
private boolean needsArraysImport = false;
private boolean usesDirectToList = false;
/**
* Creates a new PipelineAssembler.
*
* @param forLoop the enhanced for-loop being converted
* @param operations the list of operations to assemble
* @param loopVariableName the loop variable name
*/
public PipelineAssembler(EnhancedForStatement forLoop,
List<ProspectiveOperation> operations,
String loopVariableName) {
this.forLoop = forLoop;
this.ast = forLoop.getAST();
this.operations = operations;
this.loopVariableName = loopVariableName;
}
/**
* Sets the collection of variable names in use (for unique name generation).
*
* @param usedNames the used variable names
*/
public void setUsedVariableNames(Collection<String> usedNames) {
this.usedVariableNames = usedNames;
}
/**
* Returns whether the pipeline needs the java.util.Arrays import.
* This is true when iterating over an array.
*
* @return true if Arrays import is needed
*/
public boolean needsArraysImport() {
return needsArraysImport;
}
/**
* Returns whether the pipeline needs the java.util.stream.Collectors import.
* This is true when using .collect(Collectors.toList()) or .collect(Collectors.toSet()),
* but false when using Java 16+ .toList() directly.
*
* @return true if Collectors import is needed
*/
public boolean needsCollectorsImport() {
// If using direct .toList(), no Collectors import is needed
if (usesDirectToList) {
return false;
}
// Check if any operation is a COLLECT operation
return operations.stream()
.anyMatch(op -> op.getOperationType() == OperationType.COLLECT);
}
/**
* Sets the reduce pattern detector (for accumulator variable access).
*
* @param detector the reduce pattern detector
*/
public void setReduceDetector(ReducePatternDetector detector) {
this.reduceDetector = detector;
}
/**
* Sets the collect pattern detector (for target variable access).
*
* @param detector the collect pattern detector
*/
public void setCollectDetector(CollectPatternDetector detector) {
this.collectDetector = detector;
}
/**
* Sets whether the pipeline uses Java 16+ direct .toList() method.
* When true, no Collectors import is needed.
*
* @param usesDirectToList true if using .toList() instead of .collect(Collectors.toList())
*/
public void setUsesDirectToList(boolean usesDirectToList) {
this.usesDirectToList = usesDirectToList;
}
/**
* Builds the stream pipeline method invocation chain.
*
* @return the assembled pipeline, or null if operations are empty
*/
public MethodInvocation buildPipeline() {
if (operations.isEmpty()) {
return null;
}
boolean needsStream = requiresStreamPrefix();
if (needsStream) {
return buildStreamPipeline();
} else {
return buildDirectForEach();
}
}
/**
* Builds a pipeline starting with .stream() or Arrays.stream() for arrays.
*/
private MethodInvocation buildStreamPipeline() {
// Start with .stream() or Arrays.stream() for arrays
MethodInvocation pipeline = createStreamSource();
// Chain each operation
for (int i = 0; i < operations.size(); i++) {
ProspectiveOperation op = operations.get(i);
if (usedVariableNames != null) {
op.setUsedVariableNames(usedVariableNames);
}
MethodInvocation next = ast.newMethodInvocation();
next.setExpression(pipeline);
next.setName(ast.newSimpleName(op.getOperationType().getMethodName()));
String paramName = getVariableNameFromPreviousOp(i);
List<Expression> args = op.getArguments(ast, paramName);
for (Expression arg : args) {
next.arguments().add(arg);
}
pipeline = next;
}
return pipeline;
}
/**
* Builds a direct forEach without .stream() for collections,
* or Arrays.stream().forEach() for arrays.
*/
private MethodInvocation buildDirectForEach() {
ProspectiveOperation op = operations.get(0);
if (usedVariableNames != null) {
op.setUsedVariableNames(usedVariableNames);
}
MethodInvocation pipeline;
// Arrays need Arrays.stream(items).forEach() since arrays don't have forEach
if (isArrayIteration()) {
// Create Arrays.stream(items).forEach(...)
MethodInvocation streamSource = createStreamSource();
pipeline = ast.newMethodInvocation();
pipeline.setExpression(streamSource);
pipeline.setName(ast.newSimpleName(StreamConstants.FOR_EACH_METHOD));
} else {
// Create collection.forEach(...)
pipeline = ast.newMethodInvocation();
pipeline.setExpression((Expression) ASTNode.copySubtree(ast, forLoop.getExpression()));
pipeline.setName(ast.newSimpleName(StreamConstants.FOR_EACH_METHOD));
}
List<Expression> args = op.getArguments(ast, loopVariableName);
for (Expression arg : args) {
pipeline.arguments().add(arg);
}
return pipeline;
}
/**
* Creates the stream source expression.
* For collections: {@code collection.stream()}
* For arrays: {@code Arrays.stream(array)}
*
* @return the stream source MethodInvocation
*/
private MethodInvocation createStreamSource() {
MethodInvocation streamSource = ast.newMethodInvocation();
if (isArrayIteration()) {
// Arrays.stream(items)
needsArraysImport = true;
streamSource.setExpression(ast.newSimpleName(StreamConstants.ARRAYS_CLASS_NAME));
streamSource.setName(ast.newSimpleName(StreamConstants.STREAM_METHOD));
streamSource.arguments().add(ASTNode.copySubtree(ast, forLoop.getExpression()));
} else {
// collection.stream()
streamSource.setExpression((Expression) ASTNode.copySubtree(ast, forLoop.getExpression()));
streamSource.setName(ast.newSimpleName(StreamConstants.STREAM_METHOD));
}
return streamSource;
}
/**
* Checks if the for-loop iterates over an array.
*
* @return true if the loop expression is an array type
*/
private boolean isArrayIteration() {
Expression expr = forLoop.getExpression();
if (expr != null) {
ITypeBinding typeBinding = expr.resolveTypeBinding();
if (typeBinding != null) {
return typeBinding.isArray();
}
}
return false;
}
/**
* Wraps the pipeline in an appropriate statement type.
*
* @param pipeline the pipeline to wrap
* @return the wrapped statement
*/
public Statement wrapPipeline(MethodInvocation pipeline) {
if (pipeline == null) {
return null;
}
// Check for match operations
if (hasOperationType(OperationType.ANYMATCH)) {
return wrapAnyMatch(pipeline);
}
if (hasOperationType(OperationType.NONEMATCH)) {
return wrapNegatedMatch(pipeline, false);
}
if (hasOperationType(OperationType.ALLMATCH)) {
return wrapNegatedMatch(pipeline, false);
}
// Check for REDUCE operation
if (hasOperationType(OperationType.REDUCE)) {
return wrapReduce(pipeline);
}
// Check for COLLECT operation
if (hasOperationType(OperationType.COLLECT)) {
return wrapCollect(pipeline);
}
// Default: wrap in ExpressionStatement
return ast.newExpressionStatement(pipeline);
}
/**
* Wraps in: if (stream.anyMatch(...)) { return true; }
*/
private IfStatement wrapAnyMatch(MethodInvocation pipeline) {
IfStatement ifStmt = ast.newIfStatement();
ifStmt.setExpression(pipeline);
Block thenBlock = ast.newBlock();
ReturnStatement returnStmt = ast.newReturnStatement();
returnStmt.setExpression(ast.newBooleanLiteral(true));
thenBlock.statements().add(returnStmt);
ifStmt.setThenStatement(thenBlock);
return ifStmt;
}
/**
* Wraps in: if (!stream.noneMatch/allMatch(...)) { return returnValue; }
*/
private IfStatement wrapNegatedMatch(MethodInvocation pipeline, boolean returnValue) {
IfStatement ifStmt = ast.newIfStatement();
PrefixExpression negation = ast.newPrefixExpression();
negation.setOperator(PrefixExpression.Operator.NOT);
negation.setOperand(pipeline);
ifStmt.setExpression(negation);
Block thenBlock = ast.newBlock();
ReturnStatement returnStmt = ast.newReturnStatement();
returnStmt.setExpression(ast.newBooleanLiteral(returnValue));
thenBlock.statements().add(returnStmt);
ifStmt.setThenStatement(thenBlock);
return ifStmt;
}
/**
* Wraps in: accumulatorVariable = stream.reduce(...);
*/
private Statement wrapReduce(MethodInvocation pipeline) {
String accumulatorVariable = reduceDetector != null ? reduceDetector.getAccumulatorVariable() : null;
if (accumulatorVariable != null) {
Assignment assignment = ast.newAssignment();
assignment.setLeftHandSide(ast.newSimpleName(accumulatorVariable));
assignment.setOperator(Assignment.Operator.ASSIGN);
assignment.setRightHandSide(pipeline);
return ast.newExpressionStatement(assignment);
}
return ast.newExpressionStatement(pipeline);
}
/**
* Wraps a COLLECT pipeline in an assignment statement.
* Example: result = stream.collect(Collectors.toList());
*/
private Statement wrapCollect(MethodInvocation pipeline) {
String targetVariable = collectDetector != null ? collectDetector.getTargetVariable() : null;
if (targetVariable != null) {
Assignment assignment = ast.newAssignment();
assignment.setLeftHandSide(ast.newSimpleName(targetVariable));
assignment.setOperator(Assignment.Operator.ASSIGN);
assignment.setRightHandSide(pipeline);
return ast.newExpressionStatement(assignment);
}
return ast.newExpressionStatement(pipeline);
}
/**
* Determines if .stream() prefix is required.
*/
private boolean requiresStreamPrefix() {
if (operations.isEmpty()) {
return true;
}
return operations.size() > 1
|| operations.get(0).getOperationType() != OperationType.FOREACH;
}
/**
* Gets the variable name to use for the current operation.
*/
private String getVariableNameFromPreviousOp(int currentIndex) {
for (int i = currentIndex - 1; i >= 0; i--) {
ProspectiveOperation op = operations.get(i);
if (op != null && op.getProducedVariableName() != null) {
return op.getProducedVariableName();
}
}
return loopVariableName;
}
/**
* Checks if any operation has the given type.
*/
private boolean hasOperationType(OperationType type) {
return operations.stream()
.anyMatch(op -> op.getOperationType() == type);
}
}