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.List;
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.Statement;
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) {
		return ifType(VariableDeclarationStatement.class, consumer);
	}

	/**
	 * Executes the consumer if the node is a VariableDeclarationStatement and matches the predicate.
	 */
	public NodeMatcher<N> ifVariableDeclarationMatching(
			Predicate<VariableDeclarationStatement> predicate,
			Consumer<VariableDeclarationStatement> consumer) {
		return ifTypeMatching(VariableDeclarationStatement.class, predicate, consumer);
	}

	/**
	 * Executes the consumer if the node is an IfStatement.
	 */
	public NodeMatcher<N> ifIfStatement(Consumer<IfStatement> consumer) {
		return ifType(IfStatement.class, consumer);
	}

	/**
	 * Executes the consumer if the node is an IfStatement and matches the predicate.
	 */
	public NodeMatcher<N> ifIfStatementMatching(
			Predicate<IfStatement> predicate,
			Consumer<IfStatement> consumer) {
		return ifTypeMatching(IfStatement.class, predicate, consumer);
	}

	/**
	 * 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) {
		return ifType(ExpressionStatement.class, consumer);
	}

	/**
	 * Executes the consumer if the node is an ExpressionStatement and matches the predicate.
	 */
	public NodeMatcher<N> ifExpressionStatementMatching(
			Predicate<ExpressionStatement> predicate,
			Consumer<ExpressionStatement> consumer) {
		return ifTypeMatching(ExpressionStatement.class, predicate, consumer);
	}

	/**
	 * Executes the consumer if the node is a ReturnStatement.
	 */
	public NodeMatcher<N> ifReturnStatement(Consumer<ReturnStatement> consumer) {
		return ifType(ReturnStatement.class, consumer);
	}

	/**
	 * Executes the consumer if the node is a ContinueStatement.
	 */
	public NodeMatcher<N> ifContinueStatement(Consumer<ContinueStatement> consumer) {
		return ifType(ContinueStatement.class, consumer);
	}

	/**
	 * Executes the consumer if the node is a BreakStatement.
	 */
	public NodeMatcher<N> ifBreakStatement(Consumer<BreakStatement> consumer) {
		return ifType(BreakStatement.class, consumer);
	}

	/**
	 * Executes the consumer if the node is a ThrowStatement.
	 */
	public NodeMatcher<N> ifThrowStatement(Consumer<ThrowStatement> consumer) {
		return ifType(ThrowStatement.class, consumer);
	}

	/**
	 * Executes the consumer if the node is a Block.
	 */
	public NodeMatcher<N> ifBlock(Consumer<Block> consumer) {
		return ifType(Block.class, consumer);
	}

	// ========== Expression Type Matchers ==========

	/**
	 * Executes the consumer if the node is an Assignment.
	 */
	public NodeMatcher<N> ifAssignment(Consumer<Assignment> consumer) {
		return ifType(Assignment.class, consumer);
	}

	/**
	 * Executes the consumer if the node is an Assignment with the specified operator.
	 */
	public NodeMatcher<N> ifAssignmentWithOperator(
			Assignment.Operator operator,
			Consumer<Assignment> consumer) {
		return ifTypeMatching(Assignment.class, a -> a.getOperator() == operator, consumer);
	}

	/**
	 * Executes the consumer if the node is a MethodInvocation.
	 */
	public NodeMatcher<N> ifMethodInvocation(Consumer<MethodInvocation> consumer) {
		return ifType(MethodInvocation.class, consumer);
	}

	/**
	 * Executes the consumer if the node is a MethodInvocation with the specified method name.
	 */
	public NodeMatcher<N> ifMethodInvocationNamed(String methodName, Consumer<MethodInvocation> consumer) {
		return ifTypeMatching(MethodInvocation.class, 
				mi -> methodName.equals(mi.getName().getIdentifier()), consumer);
	}

	/**
	 * Executes the consumer if the node is a PostfixExpression.
	 */
	public NodeMatcher<N> ifPostfixExpression(Consumer<PostfixExpression> consumer) {
		return ifType(PostfixExpression.class, consumer);
	}

	/**
	 * Executes the consumer if the node is a PostfixExpression with increment or decrement.
	 */
	public NodeMatcher<N> ifPostfixIncrementOrDecrement(Consumer<PostfixExpression> consumer) {
		return ifTypeMatching(PostfixExpression.class,
				postfix -> postfix.getOperator() == PostfixExpression.Operator.INCREMENT
						|| postfix.getOperator() == PostfixExpression.Operator.DECREMENT,
				consumer);
	}

	/**
	 * Executes the consumer if the node is a PrefixExpression.
	 */
	public NodeMatcher<N> ifPrefixExpression(Consumer<PrefixExpression> consumer) {
		return ifType(PrefixExpression.class, consumer);
	}

	/**
	 * Executes the consumer if the node is a PrefixExpression with increment or decrement.
	 */
	public NodeMatcher<N> ifPrefixIncrementOrDecrement(Consumer<PrefixExpression> consumer) {
		return ifTypeMatching(PrefixExpression.class,
				prefix -> prefix.getOperator() == PrefixExpression.Operator.INCREMENT
						|| prefix.getOperator() == PrefixExpression.Operator.DECREMENT,
				consumer);
	}

	/**
	 * Executes the consumer if the node is a SimpleName.
	 */
	public NodeMatcher<N> ifSimpleName(Consumer<SimpleName> consumer) {
		return ifType(SimpleName.class, consumer);
	}

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

	// ========== Composite Matchers ==========

	/**
	 * Matches if the node is a Block containing exactly one statement of the given type,
	 * and that statement matches the predicate.
	 *
	 * @param <S> the expected statement type
	 * @param stmtClass the class of the single statement to match
	 * @param predicate the predicate to test the statement
	 * @param consumer the consumer to execute if matched
	 * @return this matcher for chaining
	 */
	public <S extends Statement> NodeMatcher<N> ifBlockWithSingleStatement(
			Class<S> stmtClass,
			Predicate<S> predicate,
			Consumer<S> consumer) {
		if (!handled && node instanceof Block block) {
			if (block.statements().size() == 1
					&& stmtClass.isInstance(block.statements().get(0))) {
				S stmt = stmtClass.cast(block.statements().get(0));
				if (predicate.test(stmt)) {
					consumer.accept(stmt);
					handled = true;
				}
			}
		}
		return this;
	}

	/**
	 * If the node is an IfStatement, extracts the thenStatement and matches it
	 * as either a direct statement or a Block with a single statement of the given type.
	 *
	 * @param <S> the expected statement type
	 * @param stmtClass the class to match the then-branch content against
	 * @param predicate the predicate to test the matched statement
	 * @param consumer the consumer to execute if matched
	 * @return this matcher for chaining
	 */
	public <S extends Statement> NodeMatcher<N> ifThenStatementIs(
			Class<S> stmtClass,
			Predicate<S> predicate,
			Consumer<S> consumer) {
		if (!handled && node instanceof IfStatement ifStmt) {
			Statement then = ifStmt.getThenStatement();
			if (stmtClass.isInstance(then)) {
				S stmt = stmtClass.cast(then);
				if (predicate.test(stmt)) {
					consumer.accept(stmt);
					handled = true;
				}
			} else if (then instanceof Block block
					&& block.statements().size() == 1
					&& stmtClass.isInstance(block.statements().get(0))) {
				S stmt = stmtClass.cast(block.statements().get(0));
				if (predicate.test(stmt)) {
					consumer.accept(stmt);
					handled = true;
				}
			}
		}
		return this;
	}

	/**
	 * If the node matches the given type, applies the mapper and passes the result
	 * to the consumer. The node is considered handled if the mapper returns a non-null result.
	 *
	 * @param <T> the expected node type
	 * @param <R> the result type
	 * @param nodeClass the class to match
	 * @param mapper the function to extract a result from the node
	 * @param resultConsumer the consumer to receive the non-null result
	 * @return this matcher for chaining
	 */
	public <T extends ASTNode, R> NodeMatcher<N> ifTypeMapping(
			Class<T> nodeClass,
			Function<T, R> mapper,
			Consumer<R> resultConsumer) {
		if (!handled && nodeClass.isInstance(node)) {
			R result = mapper.apply(nodeClass.cast(node));
			if (result != null) {
				resultConsumer.accept(result);
				handled = true;
			}
		}
		return this;
	}

	// ========== Static Utility Methods ==========

	/**
	 * Applies a NodeMatcher configuration to each element in a list.
	 * Only elements that are ASTNode instances are processed.
	 *
	 * @param statements the list of statements to match against
	 * @param matcherConfig the matcher configuration to apply per statement
	 */
	@SuppressWarnings("rawtypes")
	public static void matchAll(List statements,
			Function<NodeMatcher<ASTNode>, NodeMatcher<ASTNode>> matcherConfig) {
		for (Object stmt : statements) {
			if (stmt instanceof ASTNode astNode) {
				matcherConfig.apply(NodeMatcher.on(astNode));
			}
		}
	}

	// ========== 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() {
		return node instanceof ExpressionStatement exprStmt
				&& exprStmt.getExpression() instanceof Assignment;
	}

	/**
	 * 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 exprStmt
				&& exprStmt.getExpression() instanceof Assignment assignment) {
			return Optional.of(assignment);
		}
		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 exprStmt) {
			return Optional.of(exprStmt.getExpression());
		}
		return Optional.empty();
	}
}