TriggerPatternCleanupPlugin.java

/*******************************************************************************
 * Copyright (c) 2025 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.corext.fix.helper.lib;

import java.util.List;
import java.util.Set;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperationWithSourceRange;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.corext.util.AnnotationUtils;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.JUnitCleanUpFixCore;
import org.sandbox.jdt.triggerpattern.api.CleanupPattern;
import org.sandbox.jdt.triggerpattern.api.Match;
import org.sandbox.jdt.triggerpattern.api.Pattern;
import org.sandbox.jdt.triggerpattern.api.RewriteRule;
import org.sandbox.jdt.triggerpattern.api.TriggerPatternEngine;
import org.sandbox.jdt.triggerpattern.cleanup.AbstractPatternCleanupPlugin;
import org.sandbox.jdt.triggerpattern.cleanup.PatternCleanupHelper;

/**
 * Abstract base class that connects TriggerPattern with the JUnit Cleanup framework.
 * Reduces boilerplate from ~80 lines to ~20 lines per plugin.
 * 
 * <p>This class bridges the generic TriggerPattern framework (in sandbox_common) with 
 * JUnit-specific cleanup infrastructure, using composition to delegate pattern matching
 * logic to {@link PatternCleanupHelper}.</p>
 * 
 * <p>Subclasses must:</p>
 * <ol>
 *   <li>Add {@link CleanupPattern} annotation to the class</li>
 *   <li>Implement {@link #createHolder(Match)} to create the JunitHolder</li>
 *   <li>Implement {@link #process2Rewrite} for the AST transformation (or use @RewriteRule)</li>
 *   <li>Implement {@link #getPreview(boolean)} for UI preview</li>
 * </ol>
 * 
 * <p>Alternative approach: Override {@link #getPatterns()} instead of using annotation.</p>
 * 
 * @since 1.3.0
 */
public abstract class TriggerPatternCleanupPlugin extends AbstractTool<ReferenceHolder<Integer, JunitHolder>> {
    
    private static final TriggerPatternEngine ENGINE = new TriggerPatternEngine();
    private final PatternCleanupHelper helper = new PatternCleanupHelper(this.getClass());
    
    /**
     * Returns the Pattern extracted from the @CleanupPattern annotation.
     * Subclasses can override {@link #getPatterns()} instead if they need multiple patterns.
     * 
     * <p>Delegates to {@link PatternCleanupHelper#getPattern()}.</p>
     * 
     * @return the pattern for matching, or null if no @CleanupPattern annotation is present
     */
    public Pattern getPattern() {
        return helper.getPattern();
    }
    
    /**
     * Returns the patterns to match in the compilation unit.
     * Default implementation returns a single pattern from @CleanupPattern annotation.
     * Subclasses can override to provide multiple patterns.
     * 
     * <p>Delegates to {@link PatternCleanupHelper#getPatterns()} by default.</p>
     * 
     * @return list of patterns to match
     * @throws IllegalStateException if neither @CleanupPattern annotation is present nor getPatterns() is overridden
     */
    protected List<Pattern> getPatterns() {
        List<Pattern> patterns = helper.getPatterns();
        if (patterns.isEmpty()) {
            throw new IllegalStateException(
                "Plugin " + getClass().getSimpleName() + 
                " must either be annotated with @CleanupPattern or override getPatterns() method to define patterns");
        }
        return patterns;
    }
    
    /**
     * Returns the cleanup ID from the @CleanupPattern annotation.
     * 
     * <p>Delegates to {@link PatternCleanupHelper#getCleanupId()}.</p>
     * 
     * @return the cleanup ID, or empty string if annotation is not present or cleanupId is not set
     */
    public String getCleanupId() {
        return helper.getCleanupId();
    }
    
    /**
     * Returns the description from the @CleanupPattern annotation.
     * 
     * <p>Delegates to {@link PatternCleanupHelper#getDescription()}.</p>
     * 
     * @return the description, or empty string if annotation is not present or description is not set
     */
    public String getDescription() {
        return helper.getDescription();
    }
    
    /**
     * Creates a JunitHolder from a Match.
     * Default implementation stores the matched node and bindings.
     * Subclasses can override to customize holder creation.
     * 
     * @param match the matched pattern
     * @return a JunitHolder containing match information, or null to skip this match
     */
    protected JunitHolder createHolder(Match match) {
        JunitHolder holder = new JunitHolder();
        holder.setMinv(match.getMatchedNode());
        holder.setBindings(match.getBindings());
        return holder;
    }
    
    /**
     * Subclasses can override to add additional validation.
     * Default implementation returns true (process all matches).
     * 
     * @param match the pattern match
     * @param pattern the pattern that was matched
     * @return true if this match should be processed
     */
    protected boolean shouldProcess(Match match, Pattern pattern) {
        return true;
    }
    
    /**
     * Called for each match. Default implementation adds a rewrite operation.
     * Subclasses can override for custom processing.
     * 
     * @param match the matched pattern
     * @param fixcore the cleanup fix core
     * @param operations set to add operations to
     * @param dataHolder the reference holder
     * @return true to stop processing more matches, false to continue
     */
    protected boolean processMatch(Match match, JUnitCleanUpFixCore fixcore,
            Set<CompilationUnitRewriteOperationWithSourceRange> operations,
            ReferenceHolder<Integer, JunitHolder> dataHolder) {
        JunitHolder holder = createHolder(match);
        if (holder != null) {
            dataHolder.put(dataHolder.size(), holder);
            operations.add(fixcore.rewrite(dataHolder));
        }
        return false;
    }
    
    @Override
    public void find(JUnitCleanUpFixCore fixcore, CompilationUnit compilationUnit,
            Set<CompilationUnitRewriteOperationWithSourceRange> operations, Set<ASTNode> nodesprocessed) {
        
        ReferenceHolder<Integer, JunitHolder> dataHolder = ReferenceHolder.createIndexed();
        
        for (Pattern pattern : getPatterns()) {
            List<Match> matches = ENGINE.findMatches(compilationUnit, pattern);
            
            for (Match match : matches) {
                ASTNode node = match.getMatchedNode();
                
                // Skip already processed nodes
                if (nodesprocessed.contains(node)) {
                    continue;
                }
                
                // Validate qualified type if specified
                if (pattern.getQualifiedType() != null) {
                    if (!validateQualifiedType(node, pattern.getQualifiedType())) {
                        continue;
                    }
                }
                
                // Mark node as processed once it passes basic type validation
                // so it is not re-evaluated in subsequent find() calls, even if
                // shouldProcess() decides to skip it.
                nodesprocessed.add(node);
                
                // Allow subclasses to add additional validation
                if (!shouldProcess(match, pattern)) {
                    continue;
                }
                
                boolean stop = processMatch(match, fixcore, operations, dataHolder);
                if (stop) {
                    return;
                }
            }
        }
    }
    
    /**
     * Validates that the node's type binding matches the expected qualified type.
     * 
     * <p>Delegates to {@link PatternCleanupHelper#validateQualifiedType(ASTNode, String)}.</p>
     * 
     * @param node the AST node to validate
     * @param qualifiedType the expected fully qualified type name
     * @return true if types match, false otherwise
     */
    protected boolean validateQualifiedType(ASTNode node, String qualifiedType) {
        return helper.validateQualifiedType(node, qualifiedType);
    }
    
    /**
     * Helper delegate for rewrite rule processing.
     * Provides access to AbstractPatternCleanupPlugin's consolidated rewrite logic.
     */
    private final RewriteRuleDelegate rewriteRuleDelegate = new RewriteRuleDelegate();
    
    /**
     * Provides default implementation of process2Rewrite using @RewriteRule annotation.
     * Subclasses can override this method if they need custom rewrite logic,
     * or they can use @RewriteRule for pattern-based transformations.
     * 
     * <p><b>Supported patterns:</b></p>
     * <ul>
     *   <li>ANNOTATION patterns: MarkerAnnotation, SingleMemberAnnotation, NormalAnnotation</li>
     *   <li>EXPRESSION patterns: Any expression replacement (delegates to FixUtilities.rewriteFix)</li>
     *   <li>METHOD_CALL patterns: Method invocation replacement (delegates to FixUtilities.rewriteFix)</li>
     *   <li>CONSTRUCTOR patterns: Constructor invocation replacement (delegates to FixUtilities.rewriteFix)</li>
     *   <li>STATEMENT patterns: Statement replacement (delegates to FixUtilities.rewriteFix)</li>
     *   <li>FIELD patterns: Field declaration replacement (delegates to FixUtilities.rewriteFix)</li>
     * </ul>
     * 
     * <p><b>NormalAnnotation support:</b> Annotations with named parameters like
     * {@code @Ignore(value="reason")} are supported. The "value" member is automatically
     * extracted and used as the placeholder binding.</p>
     * 
     * @param group the text edit group for tracking changes
     * @param rewriter the AST rewriter
     * @param ast the AST instance
     * @param importRewriter the import rewriter
     * @param junitHolder the holder containing JUnit migration information
     */
    @Override
    protected void process2Rewrite(TextEditGroup group, ASTRewrite rewriter, AST ast,
            ImportRewrite importRewriter, JunitHolder junitHolder) {
        
        // Delegate to the consolidated rewrite logic in AbstractPatternCleanupPlugin
        // JunitHolder now implements MatchHolder, enabling type-safe delegation
        rewriteRuleDelegate.doRewrite(group, rewriter, ast, importRewriter, junitHolder);
    }
    
    /**
     * Inner class that provides access to AbstractPatternCleanupPlugin's processRewriteWithRule.
     * Uses composition to leverage the consolidated rewrite logic without requiring
     * TriggerPatternCleanupPlugin to extend AbstractPatternCleanupPlugin (which would
     * break the existing AbstractTool inheritance).
     */
    private class RewriteRuleDelegate extends AbstractPatternCleanupPlugin<JunitHolder> {
        
        /**
         * Public method to invoke the protected processRewriteWithRule.
         * This is needed because the outer class cannot directly call the protected method.
         */
        public void doRewrite(TextEditGroup group, ASTRewrite rewriter, AST ast,
                ImportRewrite importRewriter, JunitHolder holder) {
            processRewriteWithRule(group, rewriter, ast, importRewriter, holder);
        }
        
        @Override
        protected JunitHolder createHolder(Match match) {
            // Not used - delegation only for processRewriteWithRule
            return null;
        }
        
        @Override
        protected void processRewrite(TextEditGroup group, ASTRewrite rewriter, AST ast,
                ImportRewrite importRewriter, JunitHolder holder) {
            // Not used - we call processRewriteWithRule directly
        }
        
        @Override
        public String getPreview(boolean afterRefactoring) {
            // Delegate to the outer class's getPreview method
            return TriggerPatternCleanupPlugin.this.getPreview(afterRefactoring);
        }
        
        /**
         * Override to get the RewriteRule annotation from the outer plugin class
         * rather than this delegate class.
         */
        @Override
        protected RewriteRule getRewriteRule() {
            return TriggerPatternCleanupPlugin.this.getClass().getAnnotation(RewriteRule.class);
        }
        
        /**
         * Override to get the CleanupPattern annotation from the outer plugin class
         * rather than this delegate class.
         */
        @Override
        protected CleanupPattern getCleanupPatternAnnotation() {
            return TriggerPatternCleanupPlugin.this.getClass().getAnnotation(CleanupPattern.class);
        }
    }
    
    /**
     * Replaces a marker annotation with a new one and updates imports.
     * This is a common operation for simple annotation migrations like
     * {@code @Before → @BeforeEach}, {@code @After → @AfterEach}, etc.
     * 
     * <p>This helper method is useful for plugins that need to override {@code process2Rewrite()}
     * for custom logic but still want to leverage a standardized approach for simple
     * marker annotation replacements.</p>
     * 
     * <p><b>Example usage:</b></p>
     * <pre>
     * // Replace @BeforeClass with @BeforeAll
     * replaceMarkerAnnotation(
     *     group, rewriter, ast, importRewriter,
     *     oldAnnotation,
     *     "BeforeAll",
     *     "org.junit.BeforeClass",
     *     "org.junit.jupiter.api.BeforeAll"
     * );
     * </pre>
     * 
     * @param group the text edit group for tracking changes
     * @param rewriter the AST rewriter
     * @param ast the AST instance
     * @param importRewriter the import rewriter
     * @param oldAnnotation the annotation to replace
     * @param newAnnotationName the simple name of the new annotation (e.g., "BeforeEach")
     * @param removeImport the fully qualified import to remove (e.g., "org.junit.Before")
     * @param addImport the fully qualified import to add (e.g., "org.junit.jupiter.api.BeforeEach")
     */
    protected void replaceMarkerAnnotation(
            TextEditGroup group, 
            ASTRewrite rewriter, 
            AST ast,
            ImportRewrite importRewriter, 
            Annotation oldAnnotation,
            String newAnnotationName, 
            String removeImport, 
            String addImport) {
        
        MarkerAnnotation newAnnotation= AnnotationUtils.createMarkerAnnotation(ast, newAnnotationName);
        ASTNodes.replaceButKeepComment(rewriter, oldAnnotation, newAnnotation, group);
        importRewriter.removeImport(removeImport);
        importRewriter.addImport(addImport);
    }
}