ScopedAstProcessorBuilder.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
 *******************************************************************************/
package org.sandbox.jdt.internal.common;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;

import org.eclipse.jdt.core.dom.ASTNode;

/**
 * Builds an ordered semantic AST pipeline.
 *
 * <p>Each successful stage selects the scope in which the next stage searches. A
 * stage that finds no matching node ends that branch of the pipeline; later stages
 * are never retried independently against an earlier scope.</p>
 *
 * <p>Matcher, handler, navigation and child traversal are separate concerns. The
 * matcher decides whether the semantic chain advances and may inspect facts stored by
 * preceding stages. The handler records facts. Navigation chooses the next search
 * scope. {@link TraversalDecision} controls only whether the current visitor descends
 * below the matched node.</p>
 *
 * @param <V> reference-holder key type
 * @param <T> reference-holder value type
 */
public final class ScopedAstProcessorBuilder<V, T> {

	/** Child-traversal decision for the visitor executing one matched stage. */
	public enum TraversalDecision {
		DESCEND,
		SKIP_CHILDREN
	}

	private final ReferenceHolder<V, T> holder;
	private final List<Stage<V, T>> stages= new ArrayList<>();
	private Set<ASTNode> excludedNodes= Set.of();

	ScopedAstProcessorBuilder(ReferenceHolder<V, T> holder) {
		this.holder= holder;
	}

	/** Registers the first state-independent stage of the scoped pipeline. */
	public <N extends ASTNode> ScopedAstProcessorBuilder<V, T> find(
			Class<N> nodeType,
			Predicate<? super N> matcher,
			BiConsumer<? super N, ReferenceHolder<V, T>> handler,
			Function<? super N, ? extends ASTNode> nextScope) {
		Objects.requireNonNull(matcher, "matcher"); //$NON-NLS-1$
		return find(nodeType, (node, state) -> matcher.test(node), handler, nextScope);
	}

	/** Registers the first state-aware stage of the scoped pipeline. */
	public <N extends ASTNode> ScopedAstProcessorBuilder<V, T> find(
			Class<N> nodeType,
			BiPredicate<? super N, ReferenceHolder<V, T>> matcher,
			BiConsumer<? super N, ReferenceHolder<V, T>> handler,
			Function<? super N, ? extends ASTNode> nextScope) {
		if (!stages.isEmpty()) {
			throw new IllegalStateException("find(...) may only register the first stage"); //$NON-NLS-1$
		}
		return addStage(nodeType, matcher, consumerHandler(handler), nextScope);
	}

	/** Registers the first state-independent stage without additional navigation. */
	public <N extends ASTNode> ScopedAstProcessorBuilder<V, T> find(
			Class<N> nodeType,
			Predicate<? super N> matcher,
			BiConsumer<? super N, ReferenceHolder<V, T>> handler) {
		return find(nodeType, matcher, handler, Function.identity());
	}

	/** Registers the first state-aware stage without additional navigation. */
	public <N extends ASTNode> ScopedAstProcessorBuilder<V, T> find(
			Class<N> nodeType,
			BiPredicate<? super N, ReferenceHolder<V, T>> matcher,
			BiConsumer<? super N, ReferenceHolder<V, T>> handler) {
		return find(nodeType, matcher, handler, Function.identity());
	}

	/** Registers a state-independent dependent stage. */
	public <N extends ASTNode> ScopedAstProcessorBuilder<V, T> then(
			Class<N> nodeType,
			Predicate<? super N> matcher,
			BiConsumer<? super N, ReferenceHolder<V, T>> handler,
			Function<? super N, ? extends ASTNode> nextScope) {
		Objects.requireNonNull(matcher, "matcher"); //$NON-NLS-1$
		return then(nodeType, (node, state) -> matcher.test(node), handler, nextScope);
	}

	/** Registers a state-aware dependent stage searched in the previous stage's scope. */
	public <N extends ASTNode> ScopedAstProcessorBuilder<V, T> then(
			Class<N> nodeType,
			BiPredicate<? super N, ReferenceHolder<V, T>> matcher,
			BiConsumer<? super N, ReferenceHolder<V, T>> handler,
			Function<? super N, ? extends ASTNode> nextScope) {
		requireFirstStage();
		return addStage(nodeType, matcher, consumerHandler(handler), nextScope);
	}

	/** Registers a state-independent dependent terminal stage. */
	public <N extends ASTNode> ScopedAstProcessorBuilder<V, T> then(
			Class<N> nodeType,
			Predicate<? super N> matcher,
			BiConsumer<? super N, ReferenceHolder<V, T>> handler) {
		return then(nodeType, matcher, handler, Function.identity());
	}

	/** Registers a state-aware dependent terminal stage. */
	public <N extends ASTNode> ScopedAstProcessorBuilder<V, T> then(
			Class<N> nodeType,
			BiPredicate<? super N, ReferenceHolder<V, T>> matcher,
			BiConsumer<? super N, ReferenceHolder<V, T>> handler) {
		return then(nodeType, matcher, handler, Function.identity());
	}

	/** Registers the first state-independent stage with child-traversal control. */
	public <N extends ASTNode> ScopedAstProcessorBuilder<V, T> findWithTraversal(
			Class<N> nodeType,
			Predicate<? super N> matcher,
			BiFunction<? super N, ReferenceHolder<V, T>, TraversalDecision> handler,
			Function<? super N, ? extends ASTNode> nextScope) {
		Objects.requireNonNull(matcher, "matcher"); //$NON-NLS-1$
		return findWithTraversal(nodeType, (node, state) -> matcher.test(node), handler, nextScope);
	}

	/** Registers the first state-aware stage with child-traversal control. */
	public <N extends ASTNode> ScopedAstProcessorBuilder<V, T> findWithTraversal(
			Class<N> nodeType,
			BiPredicate<? super N, ReferenceHolder<V, T>> matcher,
			BiFunction<? super N, ReferenceHolder<V, T>, TraversalDecision> handler,
			Function<? super N, ? extends ASTNode> nextScope) {
		if (!stages.isEmpty()) {
			throw new IllegalStateException("findWithTraversal(...) may only register the first stage"); //$NON-NLS-1$
		}
		return addStage(nodeType, matcher, handler, nextScope);
	}

	/** Registers a state-independent dependent stage with traversal control. */
	public <N extends ASTNode> ScopedAstProcessorBuilder<V, T> thenWithTraversal(
			Class<N> nodeType,
			Predicate<? super N> matcher,
			BiFunction<? super N, ReferenceHolder<V, T>, TraversalDecision> handler,
			Function<? super N, ? extends ASTNode> nextScope) {
		Objects.requireNonNull(matcher, "matcher"); //$NON-NLS-1$
		return thenWithTraversal(nodeType, (node, state) -> matcher.test(node), handler, nextScope);
	}

	/** Registers a state-aware dependent stage with traversal control. */
	public <N extends ASTNode> ScopedAstProcessorBuilder<V, T> thenWithTraversal(
			Class<N> nodeType,
			BiPredicate<? super N, ReferenceHolder<V, T>> matcher,
			BiFunction<? super N, ReferenceHolder<V, T>, TraversalDecision> handler,
			Function<? super N, ? extends ASTNode> nextScope) {
		requireFirstStage();
		return addStage(nodeType, matcher, handler, nextScope);
	}

	/** Applies the same initial exclusion set to every stage traversal. */
	public ScopedAstProcessorBuilder<V, T> excluding(Set<? extends ASTNode> nodes) {
		excludedNodes= Set.copyOf(Objects.requireNonNull(nodes, "nodes")); //$NON-NLS-1$
		return this;
	}

	/** Executes the scoped pipeline against {@code root}. */
	public void build(ASTNode root) {
		Objects.requireNonNull(root, "root"); //$NON-NLS-1$
		if (stages.isEmpty()) {
			throw new IllegalStateException("At least one scoped stage is required"); //$NON-NLS-1$
		}
		process(root, 0);
	}

	private void process(ASTNode scope, int stageIndex) {
		if (scope == null || stageIndex >= stages.size()) {
			return;
		}

		Stage<V, T> stage= stages.get(stageIndex);
		HelperVisitor<ReferenceHolder<V, T>, V, T> visitor=
				new HelperVisitor<>(new HashSet<>(excludedNodes), holder);
		visitor.add(stage.visitorType(), (node, data) -> {
			if (!stage.nodeType().isInstance(node) || !stage.matcher().test(node, data)) {
				return true;
			}

			TraversalDecision traversal= stage.handler().apply(node, data);
			if (stageIndex + 1 < stages.size()) {
				ASTNode nextScope= stage.nextScope().apply(node);
				if (nextScope != null) {
					process(nextScope, stageIndex + 1);
				}
			}
			return traversal == TraversalDecision.DESCEND;
		});
		visitor.build(scope);
	}

	private <N extends ASTNode> ScopedAstProcessorBuilder<V, T> addStage(
			Class<N> nodeType,
			BiPredicate<? super N, ReferenceHolder<V, T>> matcher,
			BiFunction<? super N, ReferenceHolder<V, T>, TraversalDecision> handler,
			Function<? super N, ? extends ASTNode> nextScope) {
		Objects.requireNonNull(nodeType, "nodeType"); //$NON-NLS-1$
		Objects.requireNonNull(matcher, "matcher"); //$NON-NLS-1$
		Objects.requireNonNull(handler, "handler"); //$NON-NLS-1$
		Objects.requireNonNull(nextScope, "nextScope"); //$NON-NLS-1$
		VisitorEnum visitorType= AstProcessing.visitorType(nodeType);
		stages.add(new Stage<>(
				nodeType,
				visitorType,
				(node, state) -> matcher.test(nodeType.cast(node), state),
				(node, data) -> Objects.requireNonNull(
						handler.apply(nodeType.cast(node), data), "traversalDecision"), //$NON-NLS-1$
				node -> nextScope.apply(nodeType.cast(node))));
		return this;
	}

	private static <N extends ASTNode, V, T>
			BiFunction<N, ReferenceHolder<V, T>, TraversalDecision> consumerHandler(
					BiConsumer<? super N, ReferenceHolder<V, T>> handler) {
		Objects.requireNonNull(handler, "handler"); //$NON-NLS-1$
		return (node, holder) -> {
			handler.accept(node, holder);
			return TraversalDecision.DESCEND;
		};
	}

	private void requireFirstStage() {
		if (stages.isEmpty()) {
			throw new IllegalStateException("then(...) requires a preceding find(...) stage"); //$NON-NLS-1$
		}
	}

	private record Stage<V, T>(
			Class<? extends ASTNode> nodeType,
			VisitorEnum visitorType,
			BiPredicate<ASTNode, ReferenceHolder<V, T>> matcher,
			BiFunction<ASTNode, ReferenceHolder<V, T>, TraversalDecision> handler,
			Function<ASTNode, ? extends ASTNode> nextScope) {
	}
}