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.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;

/**
 * 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
	}

	/** Avoid overlapping replacements when several source formats are enabled. */
	public static boolean overlapsProcessedNode(ASTNode node, Set<ASTNode> processed) {
		for (ASTNode selected : processed) {
			if (isAncestor(node, selected) || isAncestor(selected, node)) {
				return true;
			}
		}
		return false;
	}

	private static boolean isAncestor(ASTNode ancestor, ASTNode node) {
		for (ASTNode current = node; current != null; current = current.getParent()) {
			if (current == ancestor) {
				return true;
			}
		}
		return false;
	}

	/**
	 * 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;
	}

	/**
	 * 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;
	}

}