AnnotationVisitorBuilder.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.common;

import java.util.function.BiPredicate;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;

/**
 * Fluent builder for visiting annotations of all types.
 * 
 * <p>This builder matches <b>all annotation types</b> regardless of whether they have parameters:</p>
 * <ul>
 * <li>{@code MarkerAnnotation} - annotations without parameters (e.g., {@code @Override})</li>
 * <li>{@code SingleMemberAnnotation} - annotations with a single value (e.g., {@code @SuppressWarnings("unchecked")})</li>
 * <li>{@code NormalAnnotation} - annotations with named parameters (e.g., {@code @RequestMapping(path="/api", method=GET)})</li>
 * </ul>
 * 
 * <p><b>Difference from underlying HelperVisitor:</b> The {@code HelperVisitor.callMarkerAnnotationVisitor()} 
 * method only matches {@code MarkerAnnotation} nodes (annotations without parameters). This fluent API 
 * abstracts away that limitation by calling visitors for all three annotation types, providing a more 
 * intuitive and flexible interface.</p>
 * 
 * <p><b>Example Usage:</b></p>
 * <pre>
 * // Simple annotation visitor - matches all annotation types
 * HelperVisitor.forAnnotation("java.lang.Deprecated")
 *     .in(compilationUnit)
 *     .excluding(nodesprocessed)
 *     .processEach((annotation, holder) -&gt; {
 *         addOperation(annotation);
 *         return true;
 *     });
 *     
 * // Include import declarations
 * HelperVisitor.forAnnotation("org.junit.Before")
 *     .andImports()
 *     .in(compilationUnit)
 *     .excluding(nodesprocessed)
 *     .processEach((node, holder) -&gt; {
 *         addOperation(node);
 *         return true;
 *     });
 * </pre>
 * 
 * <p><b>Note on mixed node types:</b> When {@code andImports()} is enabled, the processor
 * will receive {@code Annotation} nodes (of any annotation type) and {@code ImportDeclaration} nodes.
 * This is intentional to match the pattern used in JUnit cleanup plugins where a single processor
 * handles all related nodes polymorphically. Use {@code instanceof} checks if you need
 * to handle different node types differently.</p>
 * 
 * @author Carsten Hammer
 * @since 1.15
 */
public class AnnotationVisitorBuilder extends HelperVisitorBuilder<Annotation> {
    
    private final String annotationFQN;
    private boolean includeImports = false;
    
    /**
     * Creates a new annotation visitor builder.
     * 
     * @param annotationFQN the fully qualified name of the annotation to find
     */
    public AnnotationVisitorBuilder(String annotationFQN) {
        this.annotationFQN = annotationFQN;
    }
    
    /**
     * Configures the builder to also process import declarations for this annotation.
     * 
     * @return this builder for chaining
     */
    public AnnotationVisitorBuilder andImports() {
        this.includeImports = true;
        return this;
    }
    
    @Override
    protected <V, H> void executeVisitors(ReferenceHolder<V, H> holder, 
            BiPredicate<ASTNode, ReferenceHolder<V, H>> processor) {
        // Use the HelperVisitor stored in the ReferenceHolder to track continuation state
        // This ensures early termination works correctly when visiting multiple annotation types
        HelperVisitor<ReferenceHolder<V, H>, V, H> helperVisitor = holder.getHelperVisitor();
        if (helperVisitor == null) {
            helperVisitor = new HelperVisitor<>(nodesprocessed, holder);
            holder.setHelperVisitor(helperVisitor);
        }
        
        // Track continuation state using an array to allow modification from lambdas
        // IMPORTANT: Do NOT store this in the holder as it would pollute the user's data
        // and break indexing when users call holder.size() to get the next key
        final boolean[] shouldContinue = { true };
        
        // Create adapter BiPredicates for each annotation type that delegate to the processor
        BiPredicate<MarkerAnnotation, ReferenceHolder<V, H>> markerAdapter = (MarkerAnnotation node, ReferenceHolder<V, H> h) -> {
            // Check if already processed (excluded)
            if (nodesprocessed != null && nodesprocessed.contains(node)) {
                return true; // Skip this node but continue processing others
            }
            if (!shouldContinue[0]) {
                return false;
            }
            boolean result = processor.test((ASTNode) node, h);
            if (!result) {
                shouldContinue[0] = false;
            }
            return result;
        };
        
        BiPredicate<SingleMemberAnnotation, ReferenceHolder<V, H>> singleMemberAdapter = (SingleMemberAnnotation node, ReferenceHolder<V, H> h) -> {
            // Check if already processed (excluded)
            if (nodesprocessed != null && nodesprocessed.contains(node)) {
                return true; // Skip this node but continue processing others
            }
            if (!shouldContinue[0]) {
                return false;
            }
            boolean result = processor.test((ASTNode) node, h);
            if (!result) {
                shouldContinue[0] = false;
            }
            return result;
        };
        
        BiPredicate<NormalAnnotation, ReferenceHolder<V, H>> normalAdapter = (NormalAnnotation node, ReferenceHolder<V, H> h) -> {
            // Check if already processed (excluded)
            if (nodesprocessed != null && nodesprocessed.contains(node)) {
                return true; // Skip this node but continue processing others
            }
            if (!shouldContinue[0]) {
                return false;
            }
            boolean result = processor.test((ASTNode) node, h);
            if (!result) {
                shouldContinue[0] = false;
            }
            return result;
        };
        
        BiPredicate<ImportDeclaration, ReferenceHolder<V, H>> importAdapter = (ImportDeclaration node, ReferenceHolder<V, H> h) -> {
            // Check if already processed (excluded)
            if (nodesprocessed != null && nodesprocessed.contains(node)) {
                return true; // Skip this node but continue processing others
            }
            if (!shouldContinue[0]) {
                return false;
            }
            boolean result = processor.test((ASTNode) node, h);
            if (!result) {
                shouldContinue[0] = false;
            }
            return result;
        };
        
        // Call visitors for all three annotation types to match annotations regardless of parameters
        if (shouldContinue[0]) {
            HelperVisitor.callMarkerAnnotationVisitor(annotationFQN, compilationUnit, 
                    holder, nodesprocessed, markerAdapter);
        }
        if (shouldContinue[0]) {
            HelperVisitor.callSingleMemberAnnotationVisitor(annotationFQN, compilationUnit,
                    holder, nodesprocessed, singleMemberAdapter);
        }
        if (shouldContinue[0]) {
            HelperVisitor.callNormalAnnotationVisitor(annotationFQN, compilationUnit,
                    holder, nodesprocessed, normalAdapter);
        }
        
        // Optionally include import declarations
        if (shouldContinue[0] && includeImports) {
            HelperVisitor.callImportDeclarationVisitor(annotationFQN, compilationUnit,
                    holder, nodesprocessed, importAdapter);
        }
    }
}