NodeMatcher.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.common;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
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.BreakStatement;
import org.eclipse.jdt.core.dom.ContinueStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.PostfixExpression;
import org.eclipse.jdt.core.dom.PrefixExpression;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.ThrowStatement;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
/**
* A fluent type-safe wrapper for AST nodes that enables pattern matching style processing
* without deep instanceof chains.
*
* <p>This class provides a more object-oriented and functional approach to handling
* different AST node types, replacing deeply nested if-instanceof chains with a
* cleaner fluent API.</p>
*
* <p><b>Example - Before (nested if-instanceof):</b></p>
* <pre>{@code
* if (stmt instanceof VariableDeclarationStatement) {
* VariableDeclarationStatement varDecl = (VariableDeclarationStatement) stmt;
* // handle variable declaration
* } else if (stmt instanceof IfStatement) {
* IfStatement ifStmt = (IfStatement) stmt;
* if (ifStmt.getElseStatement() == null) {
* // handle if without else
* }
* } else if (stmt instanceof ExpressionStatement) {
* // handle expression
* }
* }</pre>
*
* <p><b>Example - After (fluent API):</b></p>
* <pre>{@code
* NodeMatcher.on(stmt)
* .ifVariableDeclaration(varDecl -> {
* // handle variable declaration
* })
* .ifIfStatement(ifStmt -> {
* // handle if statement
* })
* .ifExpressionStatement(exprStmt -> {
* // handle expression
* })
* .orElse(node -> {
* // handle other cases
* });
* }</pre>
*
* <p><b>Example - With conditions:</b></p>
* <pre>{@code
* NodeMatcher.on(stmt)
* .ifIfStatementMatching(
* ifStmt -> ifStmt.getElseStatement() == null,
* ifStmt -> handleIfWithoutElse(ifStmt)
* )
* .ifIfStatement(ifStmt -> handleIfWithElse(ifStmt));
* }</pre>
*
* @param <N> the type of AST node being matched
* @see AstProcessorBuilder
*/
public final class NodeMatcher<N extends ASTNode> {
private final N node;
private boolean handled = false;
private NodeMatcher(N node) {
this.node = node;
}
/**
* Creates a new NodeMatcher for the given AST node.
*
* @param <N> the type of AST node
* @param node the node to match against
* @return a new NodeMatcher instance
*/
public static <N extends ASTNode> NodeMatcher<N> on(N node) {
return new NodeMatcher<>(node);
}
/**
* Returns the wrapped node.
*
* @return the AST node
*/
public N getNode() {
return node;
}
/**
* Checks if this node has already been handled by a previous matcher.
*
* @return true if handled
*/
public boolean isHandled() {
return handled;
}
// ========== Statement Type Matchers ==========
/**
* Executes the consumer if the node is a VariableDeclarationStatement.
*/
public NodeMatcher<N> ifVariableDeclaration(Consumer<VariableDeclarationStatement> consumer) {
if (!handled && node instanceof VariableDeclarationStatement) {
consumer.accept((VariableDeclarationStatement) node);
handled = true;
}
return this;
}
/**
* Executes the consumer if the node is a VariableDeclarationStatement and matches the predicate.
*/
public NodeMatcher<N> ifVariableDeclarationMatching(
Predicate<VariableDeclarationStatement> predicate,
Consumer<VariableDeclarationStatement> consumer) {
if (!handled && node instanceof VariableDeclarationStatement) {
VariableDeclarationStatement varDecl = (VariableDeclarationStatement) node;
if (predicate.test(varDecl)) {
consumer.accept(varDecl);
handled = true;
}
}
return this;
}
/**
* Executes the consumer if the node is an IfStatement.
*/
public NodeMatcher<N> ifIfStatement(Consumer<IfStatement> consumer) {
if (!handled && node instanceof IfStatement) {
consumer.accept((IfStatement) node);
handled = true;
}
return this;
}
/**
* Executes the consumer if the node is an IfStatement and matches the predicate.
*/
public NodeMatcher<N> ifIfStatementMatching(
Predicate<IfStatement> predicate,
Consumer<IfStatement> consumer) {
if (!handled && node instanceof IfStatement) {
IfStatement ifStmt = (IfStatement) node;
if (predicate.test(ifStmt)) {
consumer.accept(ifStmt);
handled = true;
}
}
return this;
}
/**
* Executes the consumer if the node is an IfStatement without an else branch.
*/
public NodeMatcher<N> ifIfStatementWithoutElse(Consumer<IfStatement> consumer) {
return ifIfStatementMatching(ifStmt -> ifStmt.getElseStatement() == null, consumer);
}
/**
* Executes the consumer if the node is an IfStatement with an else branch.
*/
public NodeMatcher<N> ifIfStatementWithElse(Consumer<IfStatement> consumer) {
return ifIfStatementMatching(ifStmt -> ifStmt.getElseStatement() != null, consumer);
}
/**
* Executes the consumer if the node is an ExpressionStatement.
*/
public NodeMatcher<N> ifExpressionStatement(Consumer<ExpressionStatement> consumer) {
if (!handled && node instanceof ExpressionStatement) {
consumer.accept((ExpressionStatement) node);
handled = true;
}
return this;
}
/**
* Executes the consumer if the node is an ExpressionStatement and matches the predicate.
*/
public NodeMatcher<N> ifExpressionStatementMatching(
Predicate<ExpressionStatement> predicate,
Consumer<ExpressionStatement> consumer) {
if (!handled && node instanceof ExpressionStatement) {
ExpressionStatement exprStmt = (ExpressionStatement) node;
if (predicate.test(exprStmt)) {
consumer.accept(exprStmt);
handled = true;
}
}
return this;
}
/**
* Executes the consumer if the node is a ReturnStatement.
*/
public NodeMatcher<N> ifReturnStatement(Consumer<ReturnStatement> consumer) {
if (!handled && node instanceof ReturnStatement) {
consumer.accept((ReturnStatement) node);
handled = true;
}
return this;
}
/**
* Executes the consumer if the node is a ContinueStatement.
*/
public NodeMatcher<N> ifContinueStatement(Consumer<ContinueStatement> consumer) {
if (!handled && node instanceof ContinueStatement) {
consumer.accept((ContinueStatement) node);
handled = true;
}
return this;
}
/**
* Executes the consumer if the node is a BreakStatement.
*/
public NodeMatcher<N> ifBreakStatement(Consumer<BreakStatement> consumer) {
if (!handled && node instanceof BreakStatement) {
consumer.accept((BreakStatement) node);
handled = true;
}
return this;
}
/**
* Executes the consumer if the node is a ThrowStatement.
*/
public NodeMatcher<N> ifThrowStatement(Consumer<ThrowStatement> consumer) {
if (!handled && node instanceof ThrowStatement) {
consumer.accept((ThrowStatement) node);
handled = true;
}
return this;
}
/**
* Executes the consumer if the node is a Block.
*/
public NodeMatcher<N> ifBlock(Consumer<Block> consumer) {
if (!handled && node instanceof Block) {
consumer.accept((Block) node);
handled = true;
}
return this;
}
// ========== Expression Type Matchers ==========
/**
* Executes the consumer if the node is an Assignment.
*/
public NodeMatcher<N> ifAssignment(Consumer<Assignment> consumer) {
if (!handled && node instanceof Assignment) {
consumer.accept((Assignment) node);
handled = true;
}
return this;
}
/**
* Executes the consumer if the node is an Assignment with the specified operator.
*/
public NodeMatcher<N> ifAssignmentWithOperator(
Assignment.Operator operator,
Consumer<Assignment> consumer) {
if (!handled && node instanceof Assignment) {
Assignment assignment = (Assignment) node;
if (assignment.getOperator() == operator) {
consumer.accept(assignment);
handled = true;
}
}
return this;
}
/**
* Executes the consumer if the node is a MethodInvocation.
*/
public NodeMatcher<N> ifMethodInvocation(Consumer<MethodInvocation> consumer) {
if (!handled && node instanceof MethodInvocation) {
consumer.accept((MethodInvocation) node);
handled = true;
}
return this;
}
/**
* Executes the consumer if the node is a MethodInvocation with the specified method name.
*/
public NodeMatcher<N> ifMethodInvocationNamed(String methodName, Consumer<MethodInvocation> consumer) {
if (!handled && node instanceof MethodInvocation) {
MethodInvocation mi = (MethodInvocation) node;
if (methodName.equals(mi.getName().getIdentifier())) {
consumer.accept(mi);
handled = true;
}
}
return this;
}
/**
* Executes the consumer if the node is a PostfixExpression.
*/
public NodeMatcher<N> ifPostfixExpression(Consumer<PostfixExpression> consumer) {
if (!handled && node instanceof PostfixExpression) {
consumer.accept((PostfixExpression) node);
handled = true;
}
return this;
}
/**
* Executes the consumer if the node is a PostfixExpression with increment or decrement.
*/
public NodeMatcher<N> ifPostfixIncrementOrDecrement(Consumer<PostfixExpression> consumer) {
if (!handled && node instanceof PostfixExpression) {
PostfixExpression postfix = (PostfixExpression) node;
if (postfix.getOperator() == PostfixExpression.Operator.INCREMENT
|| postfix.getOperator() == PostfixExpression.Operator.DECREMENT) {
consumer.accept(postfix);
handled = true;
}
}
return this;
}
/**
* Executes the consumer if the node is a PrefixExpression.
*/
public NodeMatcher<N> ifPrefixExpression(Consumer<PrefixExpression> consumer) {
if (!handled && node instanceof PrefixExpression) {
consumer.accept((PrefixExpression) node);
handled = true;
}
return this;
}
/**
* Executes the consumer if the node is a PrefixExpression with increment or decrement.
*/
public NodeMatcher<N> ifPrefixIncrementOrDecrement(Consumer<PrefixExpression> consumer) {
if (!handled && node instanceof PrefixExpression) {
PrefixExpression prefix = (PrefixExpression) node;
if (prefix.getOperator() == PrefixExpression.Operator.INCREMENT
|| prefix.getOperator() == PrefixExpression.Operator.DECREMENT) {
consumer.accept(prefix);
handled = true;
}
}
return this;
}
/**
* Executes the consumer if the node is a SimpleName.
*/
public NodeMatcher<N> ifSimpleName(Consumer<SimpleName> consumer) {
if (!handled && node instanceof SimpleName) {
consumer.accept((SimpleName) node);
handled = true;
}
return this;
}
// ========== Generic Type Matcher ==========
/**
* Generic type matcher that handles any specific ASTNode subclass.
*
* @param <T> the expected node type
* @param nodeClass the class to match
* @param consumer the consumer to execute if matched
* @return this matcher for chaining
*/
public <T extends ASTNode> NodeMatcher<N> ifType(Class<T> nodeClass, Consumer<T> consumer) {
if (!handled && nodeClass.isInstance(node)) {
consumer.accept(nodeClass.cast(node));
handled = true;
}
return this;
}
/**
* Generic type matcher with predicate.
*/
public <T extends ASTNode> NodeMatcher<N> ifTypeMatching(
Class<T> nodeClass,
Predicate<T> predicate,
Consumer<T> consumer) {
if (!handled && nodeClass.isInstance(node)) {
T typedNode = nodeClass.cast(node);
if (predicate.test(typedNode)) {
consumer.accept(typedNode);
handled = true;
}
}
return this;
}
// ========== Terminal Operations ==========
/**
* Executes the consumer if no previous matcher handled the node.
*
* @param consumer the consumer to execute
*/
public void orElse(Consumer<N> consumer) {
if (!handled) {
consumer.accept(node);
}
}
/**
* Executes the runnable if no previous matcher handled the node.
*
* @param runnable the runnable to execute
*/
public void orElseDo(Runnable runnable) {
if (!handled) {
runnable.run();
}
}
/**
* Returns an Optional containing the result of the function if no matcher handled the node.
*
* @param <R> the result type
* @param function the function to apply
* @return an Optional with the result
*/
public <R> Optional<R> orElseGet(Function<N, R> function) {
if (!handled) {
return Optional.ofNullable(function.apply(node));
}
return Optional.empty();
}
// ========== Utility Methods ==========
/**
* Checks if the node is any of the "unconvertible" statement types
* (return, continue, break, throw).
*
* @return true if the node is an unconvertible control flow statement
*/
public boolean isControlFlowStatement() {
return node instanceof ReturnStatement
|| node instanceof ContinueStatement
|| node instanceof BreakStatement
|| node instanceof ThrowStatement;
}
/**
* Checks if the node is an ExpressionStatement containing an Assignment.
*
* @return true if the node is an assignment statement
*/
public boolean isAssignmentStatement() {
if (node instanceof ExpressionStatement) {
return ((ExpressionStatement) node).getExpression() instanceof Assignment;
}
return false;
}
/**
* Extracts the Assignment from an ExpressionStatement if present.
*
* @return Optional containing the Assignment, or empty if not an assignment statement
*/
public Optional<Assignment> getAssignment() {
if (node instanceof ExpressionStatement) {
Expression expr = ((ExpressionStatement) node).getExpression();
if (expr instanceof Assignment) {
return Optional.of((Assignment) expr);
}
}
return Optional.empty();
}
/**
* Extracts the Expression from an ExpressionStatement if present.
*
* @return Optional containing the Expression, or empty if not an ExpressionStatement
*/
public Optional<Expression> getExpression() {
if (node instanceof ExpressionStatement) {
return Optional.of(((ExpressionStatement) node).getExpression());
}
return Optional.empty();
}
}