JDTConverter.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.ast.api.jdt;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.ForStatement;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.WhileStatement;
import org.sandbox.ast.api.core.ASTWrapper;
import org.sandbox.ast.api.expr.ASTExpr;
import org.sandbox.ast.api.expr.CastExpr;
import org.sandbox.ast.api.expr.FieldAccessExpr;
import org.sandbox.ast.api.expr.InfixExpr;
import org.sandbox.ast.api.expr.InfixOperator;
import org.sandbox.ast.api.expr.MethodInvocationExpr;
import org.sandbox.ast.api.expr.SimpleNameExpr;
import org.sandbox.ast.api.info.MethodInfo;
import org.sandbox.ast.api.info.Modifier;
import org.sandbox.ast.api.info.ParameterInfo;
import org.sandbox.ast.api.info.TypeInfo;
import org.sandbox.ast.api.info.VariableInfo;
import org.sandbox.ast.api.stmt.ASTStmt;
import org.sandbox.ast.api.stmt.EnhancedForStmt;
import org.sandbox.ast.api.stmt.ForLoopStmt;
import org.sandbox.ast.api.stmt.IfStmt;
import org.sandbox.ast.api.stmt.WhileLoopStmt;
/**
* Bridge between Eclipse JDT AST nodes and sandbox-ast-api fluent types.
*
* <p>Converts JDT DOM nodes ({@code org.eclipse.jdt.core.dom.*}) to
* immutable sandbox-ast-api records, enabling fluent, type-safe AST analysis
* without direct JDT dependencies in consuming code.</p>
*
* <h2>Usage from sandbox plugins:</h2>
* <pre>
* // In a JDT cleanup or visitor:
* MethodInvocation jdtNode = ...;
* MethodInvocationExpr mi = JDTConverter.convert(jdtNode);
* if (mi.isMethodCall("add", 1)) {
* // use fluent API
* }
*
* // Convert any expression:
* Expression expr = ...;
* Optional<ASTExpr> fluentExpr = JDTConverter.convertExpression(expr);
* fluentExpr.flatMap(ASTExpr::asMethodInvocation)
* .filter(m -> m.isStatic())
* .ifPresent(m -> { ... });
* </pre>
*
* <h2>Design decisions:</h2>
* <ul>
* <li>All methods are static (stateless utility class)</li>
* <li>Null bindings are handled gracefully (Optional.empty())</li>
* <li>Unsupported node types return generic wrappers</li>
* <li>Conversion is recursive for nested expressions/statements</li>
* </ul>
*/
public final class JDTConverter {
private JDTConverter() {
// utility class
}
// -----------------------------------------------------------------------
// Expression converters
// -----------------------------------------------------------------------
/**
* Converts any JDT {@link Expression} to the appropriate {@link ASTExpr} wrapper.
*
* @param expression the JDT expression (may be {@code null})
* @return the fluent expression wrapper, or empty if expression is null
*/
public static Optional<ASTExpr> convertExpression(Expression expression) {
if (expression == null) {
return Optional.empty();
}
return Optional.of(convertExpressionNonNull(expression));
}
/**
* Converts a JDT {@link MethodInvocation} to a {@link MethodInvocationExpr}.
*
* @param node the JDT method invocation (must not be {@code null})
* @return the fluent method invocation wrapper
*/
public static MethodInvocationExpr convert(MethodInvocation node) {
if (node == null) {
throw new IllegalArgumentException("MethodInvocation must not be null");
}
Optional<ASTExpr> receiver = convertExpression(node.getExpression());
List<Expression> jdtArgs = node.arguments();
List<ASTExpr> arguments = jdtArgs.stream()
.map(JDTConverter::convertExpressionNonNull)
.toList();
Optional<MethodInfo> method = convertMethodBinding(node.resolveMethodBinding());
// Fallback: create minimal MethodInfo from node name when binding is unresolved
if (method.isEmpty()) {
method = createMinimalMethodInfo(node.getName().getIdentifier(), jdtArgs.size());
}
Optional<TypeInfo> type = convertTypeBinding(node.resolveTypeBinding());
return new MethodInvocationExpr(receiver, arguments, method, type);
}
/**
* Converts a JDT {@link SimpleName} to a {@link SimpleNameExpr}.
*
* @param node the JDT simple name (must not be {@code null})
* @return the fluent simple name wrapper
*/
public static SimpleNameExpr convert(SimpleName node) {
if (node == null) {
throw new IllegalArgumentException("SimpleName must not be null");
}
String identifier = node.getIdentifier();
IBinding binding = node.resolveBinding();
Optional<VariableInfo> variableBinding = Optional.empty();
Optional<MethodInfo> methodBinding = Optional.empty();
Optional<TypeInfo> typeBinding = Optional.empty();
if (binding instanceof IVariableBinding vb) {
variableBinding = convertVariableBinding(vb);
} else if (binding instanceof IMethodBinding mb) {
methodBinding = convertMethodBinding(mb);
} else if (binding instanceof ITypeBinding tb) {
typeBinding = convertTypeBinding(tb);
}
Optional<TypeInfo> type = convertTypeBinding(node.resolveTypeBinding());
return new SimpleNameExpr(identifier, variableBinding, methodBinding, typeBinding, type);
}
/**
* Extracts the identifier if the given expression is a SimpleName.
*
* <p>This is a convenience shortcut for the common pattern of checking
* if an expression is a SimpleName and getting its identifier string.</p>
*
* <p>Equivalent to:
* <pre>
* JDTConverter.convertExpression(expression)
* .flatMap(ASTExpr::asSimpleName)
* .map(SimpleNameExpr::identifier)
* </pre>
* </p>
*
* @param expression the JDT expression to check (may be {@code null})
* @return the identifier if expression is a SimpleName, otherwise empty
*/
public static Optional<String> identifierOf(Expression expression) {
if (expression instanceof SimpleName sn) {
return Optional.of(sn.getIdentifier());
}
return Optional.empty();
}
/**
* Converts a JDT {@link FieldAccess} to a {@link FieldAccessExpr}.
*
* @param node the JDT field access (must not be {@code null})
* @return the fluent field access wrapper
*/
public static FieldAccessExpr convert(FieldAccess node) {
if (node == null) {
throw new IllegalArgumentException("FieldAccess must not be null");
}
ASTExpr receiver = convertExpressionNonNull(node.getExpression());
String fieldName = node.getName().getIdentifier();
Optional<VariableInfo> field = Optional.ofNullable(node.resolveFieldBinding())
.flatMap(JDTConverter::convertVariableBinding);
Optional<TypeInfo> type = convertTypeBinding(node.resolveTypeBinding());
return new FieldAccessExpr(receiver, fieldName, field, type);
}
/**
* Converts a JDT {@link CastExpression} to a {@link CastExpr}.
*
* @param node the JDT cast expression (must not be {@code null})
* @return the fluent cast expression wrapper
*/
public static CastExpr convert(CastExpression node) {
if (node == null) {
throw new IllegalArgumentException("CastExpression must not be null");
}
TypeInfo castType = convertTypeBindingOrUnresolved(node.getType().resolveBinding());
ASTExpr expression = convertExpressionNonNull(node.getExpression());
Optional<TypeInfo> type = convertTypeBinding(node.resolveTypeBinding());
return new CastExpr(castType, expression, type);
}
/**
* Converts a JDT {@link InfixExpression} to an {@link InfixExpr}.
*
* @param node the JDT infix expression (must not be {@code null})
* @return the fluent infix expression wrapper
*/
public static InfixExpr convert(InfixExpression node) {
if (node == null) {
throw new IllegalArgumentException("InfixExpression must not be null");
}
ASTExpr left = convertExpressionNonNull(node.getLeftOperand());
ASTExpr right = convertExpressionNonNull(node.getRightOperand());
InfixOperator operator = convertOperator(node.getOperator());
List<Expression> jdtExtended = node.extendedOperands();
List<ASTExpr> extended = jdtExtended.stream()
.map(JDTConverter::convertExpressionNonNull)
.toList();
Optional<TypeInfo> type = convertTypeBinding(node.resolveTypeBinding());
return new InfixExpr(left, right, extended, operator, type);
}
// -----------------------------------------------------------------------
// Statement converters
// -----------------------------------------------------------------------
/**
* Converts any JDT {@link Statement} to the appropriate {@link ASTStmt} wrapper.
*
* @param statement the JDT statement (may be {@code null})
* @return the fluent statement wrapper, or empty if statement is null
*/
public static Optional<ASTStmt> convertStatement(Statement statement) {
if (statement == null) {
return Optional.empty();
}
return Optional.of(convertStatementNonNull(statement));
}
/**
* Converts a JDT {@link EnhancedForStatement} to an {@link EnhancedForStmt}.
*
* @param node the JDT enhanced for statement (must not be {@code null})
* @return the fluent enhanced for wrapper
*/
public static EnhancedForStmt convert(EnhancedForStatement node) {
if (node == null) {
throw new IllegalArgumentException("EnhancedForStatement must not be null");
}
Optional<VariableInfo> parameter = convertSingleVariableDeclaration(node.getParameter());
Optional<ASTExpr> iterable = convertExpression(node.getExpression());
Optional<ASTStmt> body = convertStatement(node.getBody());
return new EnhancedForStmt(parameter, iterable, body);
}
/**
* Converts a JDT {@link WhileStatement} to a {@link WhileLoopStmt}.
*
* @param node the JDT while statement (must not be {@code null})
* @return the fluent while loop wrapper
*/
public static WhileLoopStmt convert(WhileStatement node) {
if (node == null) {
throw new IllegalArgumentException("WhileStatement must not be null");
}
Optional<ASTExpr> condition = convertExpression(node.getExpression());
Optional<ASTStmt> body = convertStatement(node.getBody());
return new WhileLoopStmt(condition, body);
}
/**
* Converts a JDT {@link ForStatement} to a {@link ForLoopStmt}.
*
* @param node the JDT for statement (must not be {@code null})
* @return the fluent for loop wrapper
*/
public static ForLoopStmt convert(ForStatement node) {
if (node == null) {
throw new IllegalArgumentException("ForStatement must not be null");
}
List<Expression> jdtInits = node.initializers();
List<ASTExpr> initializers = jdtInits.stream()
.map(JDTConverter::convertExpressionNonNull)
.toList();
Optional<ASTExpr> condition = convertExpression(node.getExpression());
List<Expression> jdtUpdaters = node.updaters();
List<ASTExpr> updaters = jdtUpdaters.stream()
.map(JDTConverter::convertExpressionNonNull)
.toList();
Optional<ASTStmt> body = convertStatement(node.getBody());
return new ForLoopStmt(initializers, condition, updaters, body);
}
/**
* Converts a JDT {@link IfStatement} to an {@link IfStmt}.
*
* @param node the JDT if statement (must not be {@code null})
* @return the fluent if statement wrapper
*/
public static IfStmt convert(IfStatement node) {
if (node == null) {
throw new IllegalArgumentException("IfStatement must not be null");
}
Optional<ASTExpr> condition = convertExpression(node.getExpression());
Optional<ASTStmt> thenStmt = convertStatement(node.getThenStatement());
Optional<ASTStmt> elseStmt = convertStatement(node.getElseStatement());
return new IfStmt(condition, thenStmt, elseStmt);
}
// -----------------------------------------------------------------------
// Binding converters
// -----------------------------------------------------------------------
/**
* Converts an {@link ITypeBinding} to a {@link TypeInfo}.
*
* @param binding the JDT type binding (may be {@code null})
* @return the type info, or empty if binding is null
*/
public static Optional<TypeInfo> convertTypeBinding(ITypeBinding binding) {
if (binding == null) {
return Optional.empty();
}
return Optional.of(convertTypeBindingNonNull(binding));
}
/**
* Converts an {@link IMethodBinding} to a {@link MethodInfo}.
*
* @param binding the JDT method binding (may be {@code null})
* @return the method info, or empty if binding is null
*/
public static Optional<MethodInfo> convertMethodBinding(IMethodBinding binding) {
if (binding == null) {
return Optional.empty();
}
String name = binding.getName();
TypeInfo declaringType = convertTypeBindingOrUnresolved(binding.getDeclaringClass());
TypeInfo returnType = convertTypeBindingOrUnresolved(binding.getReturnType());
ITypeBinding[] paramTypes = binding.getParameterTypes();
String[] paramNames = parameterNames(paramTypes.length);
List<ParameterInfo> parameters = new ArrayList<>();
for (int i = 0; i < paramTypes.length; i++) {
TypeInfo paramType = convertTypeBindingOrUnresolved(paramTypes[i]);
parameters.add(ParameterInfo.of(paramNames[i], paramType));
}
Set<Modifier> modifiers = Modifier.fromJdtFlags(binding.getModifiers());
return Optional.of(new MethodInfo(name, declaringType, returnType, parameters, modifiers));
}
/**
* Converts an {@link IVariableBinding} to a {@link VariableInfo}.
*
* @param binding the JDT variable binding (may be {@code null})
* @return the variable info, or empty if binding is null
*/
public static Optional<VariableInfo> convertVariableBinding(IVariableBinding binding) {
if (binding == null) {
return Optional.empty();
}
String name = binding.getName();
TypeInfo type = convertTypeBindingOrUnresolved(binding.getType());
Set<Modifier> modifiers = Modifier.fromJdtFlags(binding.getModifiers());
boolean isField = binding.isField();
boolean isParameter = binding.isParameter();
boolean isRecordComponent = binding.isRecordComponent();
return Optional.of(new VariableInfo(name, type, modifiers, isField, isParameter, isRecordComponent));
}
// -----------------------------------------------------------------------
// Operator converter
// -----------------------------------------------------------------------
/**
* Converts a JDT {@link InfixExpression.Operator} to an {@link InfixOperator}.
*
* @param operator the JDT operator
* @return the fluent operator enum value
*/
public static InfixOperator convertOperator(InfixExpression.Operator operator) {
if (operator == null) {
throw new IllegalArgumentException("Operator must not be null");
}
return InfixOperator.fromSymbol(operator.toString())
.orElseThrow(() -> new IllegalArgumentException(
"Unsupported infix operator: " + operator));
}
// -----------------------------------------------------------------------
// Generic ASTWrapper converter
// -----------------------------------------------------------------------
/**
* Converts any JDT AST node to an appropriate {@link ASTWrapper}.
* Dispatches to specific converters based on node type.
*
* @param node the JDT AST node (may be {@code null})
* @return the fluent wrapper, or empty if node is null
*/
public static Optional<ASTWrapper> convertNode(org.eclipse.jdt.core.dom.ASTNode node) {
if (node == null) {
return Optional.empty();
}
if (node instanceof Expression expr) {
return Optional.of(convertExpressionNonNull(expr));
}
if (node instanceof Statement stmt) {
return Optional.of(convertStatementNonNull(stmt));
}
return Optional.empty();
}
// -----------------------------------------------------------------------
// Internal helpers
// -----------------------------------------------------------------------
private static ASTExpr convertExpressionNonNull(Expression expression) {
if (expression instanceof MethodInvocation mi) {
return convert(mi);
}
if (expression instanceof SimpleName sn) {
return convert(sn);
}
if (expression instanceof FieldAccess fa) {
return convert(fa);
}
if (expression instanceof CastExpression ce) {
return convert(ce);
}
if (expression instanceof InfixExpression ie) {
return convert(ie);
}
// Unsupported expression type - wrap generically
Optional<TypeInfo> type = convertTypeBinding(expression.resolveTypeBinding());
return new UnsupportedExpr(type);
}
private static ASTStmt convertStatementNonNull(Statement statement) {
if (statement instanceof EnhancedForStatement efs) {
return convert(efs);
}
if (statement instanceof WhileStatement ws) {
return convert(ws);
}
if (statement instanceof ForStatement fs) {
return convert(fs);
}
if (statement instanceof IfStatement is) {
return convert(is);
}
// Unsupported statement type - wrap generically
return new UnsupportedStmt();
}
private static TypeInfo convertTypeBindingNonNull(ITypeBinding binding) {
TypeInfo.Builder builder = TypeInfo.Builder.of(binding.getQualifiedName())
.simpleName(binding.getName());
if (binding.isPrimitive()) {
builder.primitive();
}
if (binding.isArray()) {
builder.array(binding.getDimensions());
}
ITypeBinding[] typeArgs = binding.getTypeArguments();
if (typeArgs != null) {
for (ITypeBinding arg : typeArgs) {
if (arg != null) {
builder.addTypeArgument(convertTypeBindingNonNull(arg));
}
}
}
return builder.build();
}
private static TypeInfo convertTypeBindingOrUnresolved(ITypeBinding binding) {
if (binding == null) {
return TypeInfo.Builder.of("<unresolved>")
.simpleName("<unresolved>")
.build();
}
return convertTypeBindingNonNull(binding);
}
private static Optional<VariableInfo> convertSingleVariableDeclaration(
SingleVariableDeclaration decl) {
if (decl == null) {
return Optional.empty();
}
String name = decl.getName().getIdentifier();
IVariableBinding binding = decl.resolveBinding();
if (binding != null) {
return convertVariableBinding(binding);
}
// Fallback: create VariableInfo from declaration without binding
ITypeBinding typeBinding = decl.getType().resolveBinding();
TypeInfo type = convertTypeBindingOrUnresolved(typeBinding);
Set<Modifier> modifiers = Modifier.fromJdtFlags(decl.getModifiers());
return Optional.of(new VariableInfo(name, type, modifiers, false, true, false));
}
private static String[] parameterNames(int count) {
String[] names = new String[count];
for (int i = 0; i < count; i++) {
names[i] = "arg" + i;
}
return names;
}
private static final TypeInfo UNRESOLVED_TYPE = TypeInfo.Builder.of("<unresolved>")
.simpleName("<unresolved>")
.build();
/**
* Creates a minimal MethodInfo when binding resolution is unavailable.
* Preserves the method name and parameter count from the JDT node.
*/
private static Optional<MethodInfo> createMinimalMethodInfo(String name, int argCount) {
List<ParameterInfo> params = new ArrayList<>();
for (int i = 0; i < argCount; i++) {
params.add(ParameterInfo.of("arg" + i, UNRESOLVED_TYPE));
}
return Optional.of(new MethodInfo(name, null, UNRESOLVED_TYPE, params, Set.of()));
}
// -----------------------------------------------------------------------
// Generic wrapper types for unsupported nodes
// -----------------------------------------------------------------------
/**
* Wrapper for JDT expression types not yet supported by sandbox-ast-api.
* Future phases can add specific converters for these types.
*/
record UnsupportedExpr(Optional<TypeInfo> type) implements ASTExpr {
UnsupportedExpr {
type = type == null ? Optional.empty() : type;
}
}
/**
* Wrapper for JDT statement types not yet supported by sandbox-ast-api.
* Future phases can add specific converters for these types.
*/
record UnsupportedStmt() implements ASTStmt {
}
}