ExpressionHelper.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* 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.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.Block;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation;
import org.sandbox.functional.core.builder.LoopModelBuilder;
import org.sandbox.functional.core.model.LoopModel;
import org.sandbox.functional.core.model.SourceDescriptor;
import org.sandbox.functional.core.terminal.ForEachTerminal;
import org.sandbox.jdt.internal.common.HelperVisitorFactory;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.UseFunctionalCallFixCore;
/**
* Shared utility methods for AST expression creation and statement handling
* used by the various loop renderers and handlers.
*
* <p>This class consolidates common logic that was previously duplicated across
* {@link ASTEnhancedForRenderer}, {@link ASTIteratorWhileRenderer},
* {@link StreamToEnhancedFor}, {@link StreamToIteratorWhile},
* {@link IteratorWhileHandler}, {@link EnhancedForToIteratorWhile},
* and {@link TraditionalForHandler}.</p>
*/
public final class ExpressionHelper {
private ExpressionHelper() {
// Utility class — not instantiable
}
/**
* Creates an AST {@link Expression} from a string expression.
*
* <p>For simple Java identifiers a {@code SimpleName} is created directly.
* For qualified names (e.g. {@code "this.items"}) a {@code QualifiedName} is produced.
* For everything else a string placeholder is used.</p>
*
* @param ast the AST factory
* @param rewrite the ASTRewrite (used for string placeholders)
* @param expressionStr the expression text
* @return a JDT AST {@link Expression}
*/
public static Expression createExpression(AST ast, ASTRewrite rewrite, String expressionStr) {
// For simple names, create SimpleName directly
if (expressionStr.matches("[a-zA-Z_$][a-zA-Z0-9_$]*")) { //$NON-NLS-1$
return ast.newSimpleName(expressionStr);
}
// For qualified names (e.g., "this.items"), create QualifiedName
if (expressionStr.matches("[a-zA-Z_$][a-zA-Z0-9_$.]*")) { //$NON-NLS-1$
return ast.newName(expressionStr);
}
// For complex expressions, use string placeholder
return (Expression) rewrite.createStringPlaceholder(expressionStr, ASTNode.SIMPLE_NAME);
}
/**
* Strips a trailing semicolon (and surrounding whitespace) from a statement string
* so that it can be used as a pure expression.
*
* @param stmtStr the statement string, potentially ending with {@code ";"}
* @return the trimmed string without trailing semicolon
*/
public static String stripTrailingSemicolon(String stmtStr) {
String trimmed = stmtStr.trim();
if (trimmed.endsWith(";")) { //$NON-NLS-1$
trimmed = trimmed.substring(0, trimmed.length() - 1).trim();
}
return trimmed;
}
/**
* Extracts body statements from a lambda expression as AST {@link Statement} nodes.
*
* <p>Block lambdas return their statements directly; expression lambdas are wrapped
* in a newly created {@link ExpressionStatement}.</p>
*
* @param lambda the lambda expression
* @param ast the AST factory
* @return the list of body statements
*/
public static List<Statement> extractLambdaBodyStatements(LambdaExpression lambda, AST ast) {
List<Statement> statements = new ArrayList<>();
ASTNode lambdaBody = lambda.getBody();
if (lambdaBody instanceof Block block) {
for (Object stmt : block.statements()) {
statements.add((Statement) stmt);
}
} else if (lambdaBody instanceof Expression expr) {
// Expression body: wrap in ExpressionStatement (newly created, not part of original AST)
ExpressionStatement exprStmt = ast.newExpressionStatement(
(Expression) ASTNode.copySubtree(ast, expr));
statements.add(exprStmt);
}
return statements;
}
/**
* Extracts the parameter type name from a lambda parameter.
*
* <p>If the parameter has an explicit type annotation, that type name is returned.
* Otherwise {@code "String"} is used as a default.</p>
*
* @param param the variable declaration (lambda parameter)
* @return the type name string
*/
public static String extractParamType(VariableDeclaration param) {
if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) {
return svd.getType().toString();
}
return "String"; //$NON-NLS-1$
}
/**
* Converts an AST {@link Statement} (possibly a {@link Block}) into a list of expression
* strings with trailing semicolons stripped.
*
* <p>If the statement is a {@link Block}, each child statement is converted individually.
* Otherwise the statement itself is converted as a single element.</p>
*
* @param body the loop body statement
* @return list of expression strings (without trailing semicolons)
*/
public static List<String> bodyStatementsToStrings(Statement body) {
List<String> result = new ArrayList<>();
if (body instanceof Block block) {
for (Object stmt : block.statements()) {
result.add(stripTrailingSemicolon(stmt.toString()));
}
} else {
result.add(stripTrailingSemicolon(body.toString()));
}
return result;
}
/**
* Converts a list of AST {@link Statement} nodes into expression strings
* with trailing semicolons stripped.
*
* @param statements the statements to convert
* @return list of expression strings (without trailing semicolons)
*/
public static List<String> bodyStatementsToStrings(List<Statement> statements) {
List<String> result = new ArrayList<>();
for (Statement stmt : statements) {
result.add(stripTrailingSemicolon(stmt.toString()));
}
return result;
}
/**
* Holds the extracted information from a {@code collection.forEach(item -> ...)} or
* {@code collection.stream().forEach(item -> ...)} call, ready for rendering.
*
* <p>This record eliminates the duplicated extraction logic previously present in both
* {@link StreamToEnhancedFor} and {@link StreamToIteratorWhile}.</p>
*
* @param model the ULR LoopModel built from the forEach call
* @param bodyStatements the lambda body statements as AST nodes
* @param forEachStatement the original ExpressionStatement containing the forEach call
*/
public record ForEachRewriteInfo(
LoopModel model,
List<Statement> bodyStatements,
ExpressionStatement forEachStatement) {
}
/**
* Extracts rewrite information from a forEach {@link MethodInvocation} AST node.
*
* <p>This consolidates the shared extraction logic used by both
* {@link StreamToEnhancedFor} and {@link StreamToIteratorWhile}:
* extracting the lambda, collection expression, parameter name/type,
* building the LoopModel, and extracting body statements.</p>
*
* @param visited the visited ASTNode (expected to be a MethodInvocation)
* @param ast the AST factory
* @return the extracted info, or {@code null} if the node is not a convertible forEach call
*/
public static ForEachRewriteInfo extractForEachRewriteInfo(ASTNode visited, AST ast) {
if (!(visited instanceof MethodInvocation forEach)) {
return null;
}
// Get the lambda expression
if (forEach.arguments().isEmpty() || !(forEach.arguments().get(0) instanceof LambdaExpression lambda)) {
return null;
}
// Extract collection expression (either collection or collection.stream())
Expression collectionExpr = forEach.getExpression();
if (collectionExpr instanceof MethodInvocation methodInv) {
if (StreamConstants.STREAM_METHOD.equals(methodInv.getName().getIdentifier())) {
collectionExpr = methodInv.getExpression();
}
}
if (collectionExpr == null) {
return null;
}
// Extract parameter name and type from lambda
if (lambda.parameters().isEmpty()) {
return null;
}
VariableDeclaration param = (VariableDeclaration) lambda.parameters().get(0);
String paramName = param.getName().getIdentifier();
String paramType = extractParamType(param);
// Build LoopModel using ULR pipeline
LoopModel model = new LoopModelBuilder()
.source(SourceDescriptor.SourceType.COLLECTION, collectionExpr.toString(), paramType)
.element(paramName, paramType, false)
.terminal(new ForEachTerminal(List.of(), false))
.build();
// Extract body statements from lambda
List<Statement> bodyStatements = extractLambdaBodyStatements(lambda, ast);
// Get the parent ExpressionStatement
ExpressionStatement forEachStmt = (ExpressionStatement) forEach.getParent();
return new ForEachRewriteInfo(model, bodyStatements, forEachStmt);
}
/**
* Finds simple {@code forEach} method invocations on collections/streams and registers
* rewrite operations for each.
*
* <p>This method encapsulates the shared detection logic used by both
* {@link StreamToEnhancedFor} and {@link StreamToIteratorWhile}.</p>
*
* @param fixcore the fix core instance
* @param compilationUnit the compilation unit to scan
* @param operations the set to add rewrite operations to
* @param nodesprocessed the set of already processed nodes
*/
public static void findForEachInvocations(UseFunctionalCallFixCore fixcore,
CompilationUnit compilationUnit,
Set<CompilationUnitRewriteOperation> operations,
Set<ASTNode> nodesprocessed) {
ReferenceHolder<Integer, Object> dataHolder = ReferenceHolder.create();
HelperVisitorFactory.callMethodInvocationVisitor(StreamConstants.FOR_EACH_METHOD, compilationUnit, dataHolder, nodesprocessed,
(visited, aholder) -> {
if (visited.arguments().size() != 1) {
return false;
}
Object arg = visited.arguments().get(0);
if (!(arg instanceof LambdaExpression)) {
return false;
}
if (!(visited.getParent() instanceof ExpressionStatement)) {
return false;
}
if (StreamOperationDetector.hasChainedStreamOperations(visited)) {
return false;
}
operations.add(fixcore.rewrite(visited, new ReferenceHolder<>()));
nodesprocessed.add(visited);
return false;
});
}
}