AbstractPatternCleanupPlugin.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.triggerpattern.cleanup;

import java.util.List;
import java.util.Map;
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.Expression;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
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.CompilationUnitRewriteOperation;
import org.eclipse.jdt.internal.corext.fix.LinkedProposalModelCore;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.jdt.internal.corext.refactoring.structure.ImportRemover;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.corext.util.AnnotationUtils;
import org.sandbox.jdt.triggerpattern.api.CleanupPattern;
import org.sandbox.jdt.triggerpattern.api.ImportDirective;
import org.sandbox.jdt.triggerpattern.api.Match;
import org.sandbox.jdt.triggerpattern.api.Pattern;
import org.sandbox.jdt.triggerpattern.api.PatternKind;
import org.sandbox.jdt.triggerpattern.api.RewriteRule;
import org.sandbox.jdt.triggerpattern.api.TriggerPatternEngine;

/**
 * Abstract base class that connects TriggerPattern with the Eclipse Cleanup framework.
 * Provides generic pattern matching and rewriting capabilities that can be used by
 * any cleanup implementation, not just JUnit cleanups.
 * 
 * <p>This class extracts the generic TriggerPattern integration logic from the JUnit-specific
 * {@code TriggerPatternCleanupPlugin}. It handles:</p>
 * <ul>
 *   <li>Pattern matching using {@link TriggerPatternEngine}</li>
 *   <li>{@link CleanupPattern} annotation processing</li>
 *   <li>{@link RewriteRule} annotation processing for declarative transformations</li>
 *   <li>Import management (add/remove imports and static imports)</li>
 *   <li>Qualified type validation</li>
 * </ul>
 * 
 * <p><b>Type Parameters:</b></p>
 * <ul>
 *   <li>{@code <H>} - The holder type used to store match information (e.g., JunitHolder)</li>
 * </ul>
 * 
 * <p><b>Subclasses must implement:</b></p>
 * <ol>
 *   <li>{@link #createHolder(Match)} - Create holder from match</li>
 *   <li>{@link #processRewrite(TextEditGroup, ASTRewrite, AST, ImportRewrite, Object)} - Apply AST transformations</li>
 *   <li>{@link #getPreview(boolean)} - Provide UI preview</li>
 * </ol>
 * 
 * <p><b>Optional overrides:</b></p>
 * <ul>
 *   <li>{@link #getPatterns()} - For multiple patterns (instead of single {@link CleanupPattern} annotation)</li>
 *   <li>{@link #shouldProcess(Match, Pattern)} - For additional match validation</li>
 *   <li>{@link #processMatch(Match, Object, Object)} - For custom match processing</li>
 * </ul>
 * 
 * @param <H> the holder type for storing match information
 * @since 1.2.5
 */
public abstract class AbstractPatternCleanupPlugin<H> {
    
    private static final TriggerPatternEngine ENGINE = new TriggerPatternEngine();
    
    /**
     * Transient reference to the current CompilationUnitRewrite, set during rewriteAST.
     * Used by processImports() to access ImportRemover for safe import removal.
     * This field is set before processRewrite() and cleared afterwards.
     */
    private transient CompilationUnitRewrite currentCuRewrite;
    
    /**
     * Returns the Pattern extracted from the @CleanupPattern annotation.
     * Subclasses can override {@link #getPatterns()} instead if they need multiple patterns.
     * 
     * <p>Note: This method returns {@code null} when no annotation is present, which signals
     * that the subclass should override {@link #getPatterns()} instead. In contrast,
     * {@link #getCleanupId()} and {@link #getDescription()} return empty strings as safe defaults.</p>
     * 
     * @return the pattern for matching, or null if no @CleanupPattern annotation is present
     */
    public Pattern getPattern() {
        CleanupPattern annotation = getCleanupPatternAnnotation();
        if (annotation == null) {
            return null; // Subclass uses getPatterns() instead
        }
        String qualifiedType = annotation.qualifiedType().isEmpty() ? null : annotation.qualifiedType();
        return new Pattern(annotation.value(), annotation.kind(), null, null, qualifiedType, null, null);
    }
    
    /**
     * 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.
     * 
     * @return list of patterns to match
     * @throws IllegalStateException if neither @CleanupPattern annotation is present nor getPatterns() is overridden
     */
    protected List<Pattern> getPatterns() {
        Pattern pattern = getPattern();
        if (pattern != null) {
            return List.of(pattern);
        }
        throw new IllegalStateException(
            "Plugin " + getClass().getSimpleName() +  //$NON-NLS-1$
            " must either be annotated with @CleanupPattern or override getPatterns() method to define patterns"); //$NON-NLS-1$
    }
    
    /**
     * Returns the cleanup ID from the @CleanupPattern annotation.
     * 
     * @return the cleanup ID, or empty string if annotation is not present or cleanupId is not set
     */
    public String getCleanupId() {
        CleanupPattern annotation = getCleanupPatternAnnotation();
        return annotation != null ? annotation.cleanupId() : ""; //$NON-NLS-1$
    }
    
    /**
     * Returns the description from the @CleanupPattern annotation.
     * 
     * @return the description, or empty string if annotation is not present or description is not set
     */
    public String getDescription() {
        CleanupPattern annotation = getCleanupPatternAnnotation();
        return annotation != null ? annotation.description() : ""; //$NON-NLS-1$
    }
    
    /**
     * Creates a holder from a Match.
     * Subclasses must implement this to convert pattern matches to their specific holder type.
     * 
     * @param match the matched pattern
     * @return a holder containing match information, or null to skip this match
     */
    protected abstract H createHolder(Match match);
    
    /**
     * 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 creates a holder and processes it.
     * Subclasses can override for custom processing logic.
     * 
     * @param match the matched pattern
     * @param fixcore the cleanup fix core (implementation-specific type)
     * @param operations operations collection (implementation-specific type)
     * @return true to stop processing more matches, false to continue
     */
    protected boolean processMatch(Match match, Object fixcore, Object operations) {
        H holder = createHolder(match);
        if (holder != null) {
            // Subclasses that override this method handle their own operation creation
            // This default implementation is a hook for subclasses
        }
        return false;
    }
    
    /**
     * Finds all matches using TriggerPatternEngine.
     * This is the generic pattern matching logic extracted from the JUnit-specific implementation.
     * 
     * @param compilationUnit the compilation unit to search
     * @param patterns the patterns to match
     * @return list of all matches
     */
    protected List<Match> findAllMatches(org.eclipse.jdt.core.dom.CompilationUnit compilationUnit, List<Pattern> patterns) {
        java.util.List<Match> allMatches = new java.util.ArrayList<>();
        for (Pattern pattern : patterns) {
            List<Match> matches = ENGINE.findMatches(compilationUnit, pattern);
            allMatches.addAll(matches);
        }
        return allMatches;
    }
    
    /**
     * Validates that the node's type binding matches the expected qualified type.
     * 
     * @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) {
        if (node instanceof Annotation) {
            Annotation annotation = (Annotation) node;
            ITypeBinding binding = annotation.resolveTypeBinding();
            if (binding != null) {
                return qualifiedType.equals(binding.getQualifiedName());
            }
            // Fallback: check simple name
            return annotation.getTypeName().getFullyQualifiedName().equals(
                    qualifiedType.substring(qualifiedType.lastIndexOf('.') + 1));
        }
        // Add more type checks as needed
        return true;
    }
    
    // Regex pattern for parsing replacement patterns (compiled once for performance)
    // Supports:
    // - @AnnotationName or @AnnotationName($placeholder) or @AnnotationName($placeholder$)
    // - MethodName.method($args) or MethodName.method($args$)
    private static final java.util.regex.Pattern REPLACEMENT_PATTERN = 
        java.util.regex.Pattern.compile("@?([A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*)(?:\\((.*)\\))?"); //$NON-NLS-1$
    
    /**
     * Processes AST rewrite operations.
     * Subclasses must implement this to apply their specific transformations.
     * 
     * <p>Alternatively, subclasses can use the {@link RewriteRule} annotation for simple
     * annotation replacements, and this method can use the default implementation provided
     * by calling {@link #processRewriteWithRule(TextEditGroup, ASTRewrite, AST, ImportRewrite, Object)}.</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 holder the holder containing transformation information
     */
    protected abstract void processRewrite(TextEditGroup group, ASTRewrite rewriter, AST ast,
            ImportRewrite importRewriter, H holder);
    
    /**
     * Provides default implementation of processRewrite using @RewriteRule annotation.
     * This method can be called by subclasses that want to use declarative @RewriteRule
     * for pattern-based transformations.
     * 
     * <p><b>Supported patterns:</b></p>
     * <ul>
     *   <li>ANNOTATION patterns: MarkerAnnotation, SingleMemberAnnotation, NormalAnnotation
     *       (with auto-extraction of "value" member via {@code extractValueMember()})</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>Import handling:</b></p>
     * <ul>
     *   <li>Add imports: derived from {@code targetQualifiedType} (preferred), then {@code addImports},
     *       then auto-detected from FQNs in {@code replaceWith}</li>
     *   <li>Remove imports: handled safely via {@code ImportRemover} (only removes if no other
     *       references exist in the compilation unit)</li>
     * </ul>
     * 
     * <p><b>Type-safe version:</b> This overload accepts a {@link MatchHolder} interface,
     * avoiding reflection. For legacy holders that don't implement the interface, use
     * {@link #processRewriteWithRule(TextEditGroup, ASTRewrite, AST, ImportRewrite, Object)}.</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 holder the holder containing transformation information (must implement {@link MatchHolder})
     */
    protected void processRewriteWithRule(TextEditGroup group, ASTRewrite rewriter, AST ast,
            ImportRewrite importRewriter, MatchHolder holder) {
        
        RewriteRule rewriteRule = getRewriteRule();
        if (rewriteRule == null) {
            throw new UnsupportedOperationException(
                "Plugin " + getClass().getSimpleName() +  //$NON-NLS-1$
                " must be annotated with @RewriteRule because it does not override processRewrite()"); //$NON-NLS-1$
        }
        
        // Check if this is an annotation pattern
        Annotation annotation = holder.getAnnotation();
        if (annotation != null) {
            processAnnotationRewriteWithRuleTypeSafe(group, rewriter, ast, importRewriter, holder, rewriteRule);
            return;
        }
        
        // For non-annotation patterns, get the matched node
        ASTNode matchedNode = holder.getMinv();
        
        // Determine the pattern kind from the matched node type
        PatternKind patternKind = org.sandbox.jdt.triggerpattern.api.FixUtilities.determinePatternKindFromNode(matchedNode);
        
        // For ANNOTATION patterns detected via node type, use the annotation replacement logic
        if (patternKind == PatternKind.ANNOTATION) {
            processAnnotationRewriteWithRuleTypeSafe(group, rewriter, ast, importRewriter, holder, rewriteRule);
        } else {
            // For all other patterns (EXPRESSION, METHOD_CALL, CONSTRUCTOR, STATEMENT, FIELD),
            // delegate to FixUtilities.rewriteFix()
            processGenericRewriteWithRuleTypeSafe(group, rewriter, ast, importRewriter, holder, rewriteRule, matchedNode);
        }
    }
    
    /**
     * Returns the RewriteRule annotation for this plugin.
     * 
     * <p>Subclasses can override this method to provide a RewriteRule from a different source
     * (e.g., when using composition/delegation patterns where the annotation is on
     * an outer class).</p>
     * 
     * @return the RewriteRule annotation, or null if not present
     */
    protected RewriteRule getRewriteRule() {
        return this.getClass().getAnnotation(RewriteRule.class);
    }
    
    /**
     * Returns the CleanupPattern annotation for this plugin.
     * 
     * <p>Subclasses can override this method to provide a CleanupPattern from a different source
     * (e.g., when using composition/delegation patterns where the annotation is on
     * an outer class).</p>
     * 
     * @return the CleanupPattern annotation, or null if not present
     */
    protected CleanupPattern getCleanupPatternAnnotation() {
        return this.getClass().getAnnotation(CleanupPattern.class);
    }
    
    /**
     * Legacy version of processRewriteWithRule that uses reflection.
     * 
     * <p><b>Deprecated:</b> Use the type-safe version with {@link MatchHolder} instead.</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 holder the holder containing transformation information
     * @deprecated Use {@link #processRewriteWithRule(TextEditGroup, ASTRewrite, AST, ImportRewrite, MatchHolder)} instead
     */
    @Deprecated
    protected void processRewriteWithRuleLegacy(TextEditGroup group, ASTRewrite rewriter, AST ast,
            ImportRewrite importRewriter, Object holder) {
        
        RewriteRule rewriteRule = getRewriteRule();
        if (rewriteRule == null) {
            throw new UnsupportedOperationException(
                "Plugin " + getClass().getSimpleName() +  //$NON-NLS-1$
                " must be annotated with @RewriteRule because it does not override processRewrite()"); //$NON-NLS-1$
        }
        
        // Try to detect if this is an annotation pattern by checking for getAnnotation() method
        try {
            java.lang.reflect.Method method = holder.getClass().getMethod("getAnnotation"); //$NON-NLS-1$
            Annotation annotation = (Annotation) method.invoke(holder);
            if (annotation != null) {
                processAnnotationRewriteWithRule(group, rewriter, ast, importRewriter, holder, rewriteRule);
                return;
            }
        } catch (NoSuchMethodException | IllegalAccessException | java.lang.reflect.InvocationTargetException e) {
            // Not an annotation pattern, continue to check for getMinv()
        }
        
        // For non-annotation patterns, get the matched node
        ASTNode matchedNode = getMatchedNodeFromHolder(holder);
        
        // Determine the pattern kind from the matched node type
        PatternKind patternKind = org.sandbox.jdt.triggerpattern.api.FixUtilities.determinePatternKindFromNode(matchedNode);
        
        // For ANNOTATION patterns detected via node type, use the legacy annotation replacement logic
        if (patternKind == PatternKind.ANNOTATION) {
            processAnnotationRewriteWithRule(group, rewriter, ast, importRewriter, holder, rewriteRule);
        } else {
            // For all other patterns (EXPRESSION, METHOD_CALL, CONSTRUCTOR, STATEMENT, FIELD),
            // delegate to FixUtilities.rewriteFix()
            processGenericRewriteWithRule(group, rewriter, ast, importRewriter, holder, rewriteRule, matchedNode);
        }
    }
    
    /**
     * Processes annotation rewrite using the legacy annotation replacement logic.
     */
    private void processAnnotationRewriteWithRule(TextEditGroup group, ASTRewrite rewriter, AST ast,
            ImportRewrite importRewriter, Object holder, RewriteRule rewriteRule) {
        
        // Process the replacement pattern
        String replaceWith = rewriteRule.replaceWith();
        
        // Use reflection to get annotation and bindings from holder
        Annotation oldAnnotation = getAnnotationFromHolder(holder);
        
        // Parse the replacement pattern to extract annotation name and placeholders
        AnnotationReplacementInfo replacementInfo = parseReplacementPattern(replaceWith);
        
        // Get placeholder value from bindings
        Expression value = null;
        if (replacementInfo.hasPlaceholders()) {
            String placeholder = replacementInfo.placeholderName;
            value = getBindingAsExpressionFromHolder(holder, "$" + placeholder); //$NON-NLS-1$
        }
        
        // Create the new annotation with fallback logic
        Annotation newAnnotation = createReplacementAnnotation(
                ast, rewriter, replacementInfo, oldAnnotation, value);
        
        // Replace the old annotation with the new one
        ASTNodes.replaceButKeepComment(rewriter, oldAnnotation, newAnnotation, group);
        
        // Handle imports
        processImports(importRewriter, rewriteRule, oldAnnotation);
    }
    
    /**
     * Type-safe version of processAnnotationRewriteWithRule using MatchHolder interface.
     */
    private void processAnnotationRewriteWithRuleTypeSafe(TextEditGroup group, ASTRewrite rewriter, AST ast,
            ImportRewrite importRewriter, MatchHolder holder, RewriteRule rewriteRule) {
        
        // Process the replacement pattern
        String replaceWith = rewriteRule.replaceWith();
        
        // Get annotation directly from holder
        Annotation oldAnnotation = holder.getAnnotation();
        
        // Parse the replacement pattern to extract annotation name and placeholders
        AnnotationReplacementInfo replacementInfo = parseReplacementPattern(replaceWith);
        
        // Get placeholder value from bindings
        Expression value = null;
        if (replacementInfo.hasPlaceholders()) {
            String placeholder = replacementInfo.placeholderName;
            value = holder.getBindingAsExpression("$" + placeholder); //$NON-NLS-1$
        }
        
        // Create the new annotation with fallback logic
        Annotation newAnnotation = createReplacementAnnotation(
                ast, rewriter, replacementInfo, oldAnnotation, value);
        
        // Replace the old annotation with the new one
        ASTNodes.replaceButKeepComment(rewriter, oldAnnotation, newAnnotation, group);
        
        // Handle imports
        processImports(importRewriter, rewriteRule, oldAnnotation);
    }
    
    /**
     * Creates a replacement annotation based on the replacement pattern and the old annotation.
     * Handles three annotation types with appropriate fallback logic:
     * <ol>
     *   <li>If replacement has placeholders and a value is found → SingleMemberAnnotation</li>
     *   <li>If replacement has placeholders but value is null → falls back to SingleMemberAnnotation 
     *       or NormalAnnotation value extraction, then to MarkerAnnotation if still null</li>
     *   <li>If replacement has no placeholders → MarkerAnnotation</li>
     * </ol>
     * 
     * @param ast the AST instance
     * @param rewriter the AST rewriter
     * @param replacementInfo the parsed replacement pattern info
     * @param oldAnnotation the original annotation being replaced
     * @param value the placeholder value from bindings (may be null)
     * @return the new annotation to replace the old one
     */
    private Annotation createReplacementAnnotation(AST ast, ASTRewrite rewriter,
            AnnotationReplacementInfo replacementInfo, Annotation oldAnnotation, Expression value) {
        
        if (!replacementInfo.hasPlaceholders()) {
            return AnnotationUtils.createMarkerAnnotation(ast, replacementInfo.annotationName);
        }
        
        // Fallback: if no binding is found, reuse the value from existing annotation
        if (value == null && oldAnnotation instanceof SingleMemberAnnotation singleMember) {
            value = singleMember.getValue();
        }
        
        // Fallback: if no binding is found and old annotation is NormalAnnotation,
        // extract the "value" member pair (e.g., @Ignore(value="reason") → "reason")
        if (value == null && oldAnnotation instanceof NormalAnnotation normalAnnotation) {
            value = extractValueMember(normalAnnotation);
        }
        
        if (value != null) {
            SingleMemberAnnotation singleMemberAnnotation = ast.newSingleMemberAnnotation();
            singleMemberAnnotation.setTypeName(ast.newSimpleName(replacementInfo.annotationName));
            singleMemberAnnotation.setValue(ASTNodes.createMoveTarget(rewriter, value));
            return singleMemberAnnotation;
        }
        
        // No value found - create MarkerAnnotation instead of broken SingleMemberAnnotation
        return AnnotationUtils.createMarkerAnnotation(ast, replacementInfo.annotationName);
    }
    
    /**
     * Extracts the "value" member from a NormalAnnotation.
     * 
     * @param normalAnnotation the NormalAnnotation to extract from
     * @return the value expression, or null if no "value" member is found
     */
    @SuppressWarnings("unchecked")
    private static Expression extractValueMember(NormalAnnotation normalAnnotation) {
        for (MemberValuePair pair : (List<MemberValuePair>) normalAnnotation.values()) {
            if ("value".equals(pair.getName().getIdentifier())) { //$NON-NLS-1$
                return pair.getValue();
            }
        }
        return null;
    }
    
    /**
     * Processes generic pattern rewrite by delegating to FixUtilities.rewriteFix().
     */
    private void processGenericRewriteWithRule(TextEditGroup group, ASTRewrite rewriter, AST ast,
            ImportRewrite importRewriter, Object holder, RewriteRule rewriteRule, ASTNode matchedNode) {
        
        // Get bindings from holder
        Map<String, Object> bindings = getBindingsFromHolder(holder);
        
        // Create a Match from the holder data
        Match match = new Match(matchedNode, bindings, matchedNode.getStartPosition(), matchedNode.getLength());
        
        // Use FixUtilities.rewriteFix() to perform the replacement
        String replacementPattern = rewriteRule.replaceWith();
        org.sandbox.jdt.triggerpattern.api.FixUtilities.rewriteFix(match, rewriter, replacementPattern);
        
        // Handle imports
        processImports(importRewriter, rewriteRule, matchedNode);
    }
    
    /**
     * Type-safe version of processGenericRewriteWithRule using MatchHolder interface.
     */
    private void processGenericRewriteWithRuleTypeSafe(TextEditGroup group, ASTRewrite rewriter, AST ast,
            ImportRewrite importRewriter, MatchHolder holder, RewriteRule rewriteRule, ASTNode matchedNode) {
        
        // Get bindings directly from holder
        Map<String, Object> bindings = holder.getBindings();
        
        // Create a Match from the holder data
        Match match = new Match(matchedNode, bindings, matchedNode.getStartPosition(), matchedNode.getLength());
        
        // Use FixUtilities.rewriteFix() to perform the replacement
        String replacementPattern = rewriteRule.replaceWith();
        org.sandbox.jdt.triggerpattern.api.FixUtilities.rewriteFix(match, rewriter, replacementPattern);
        
        // Handle imports
        processImports(importRewriter, rewriteRule, matchedNode);
    }
    
    /**
     * Processes import additions and removals from RewriteRule.
     * 
     * <p>Import handling strategy:</p>
     * <ul>
     *   <li><b>Add imports:</b> Uses {@code targetQualifiedType} (preferred), then falls back to
     *       {@code addImports}, then auto-detects from FQNs in {@code replaceWith}</li>
     *   <li><b>Remove imports:</b> Uses {@code ImportRemover} for safe removal (only removes if
     *       no other references exist in the compilation unit). Falls back to direct removal
     *       via {@code importRewriter.removeImport()} when ImportRemover is not available.</li>
     * </ul>
     * 
     * @param importRewriter the import rewriter
     * @param rewriteRule the rewrite rule annotation
     * @param removedNode the AST node being removed/replaced (for ImportRemover registration)
     */
    private void processImports(ImportRewrite importRewriter, RewriteRule rewriteRule, ASTNode removedNode) {
        // --- Import removal (safe via ImportRemover when available) ---
        if (rewriteRule.removeImports().length == 0) {
            // Use ImportRemover for safe removal: only removes if no other references exist
            if (currentCuRewrite != null && removedNode != null) {
                ImportRemover remover = currentCuRewrite.getImportRemover();
                remover.registerRemovedNode(removedNode);
                remover.applyRemoves(importRewriter);
            } else {
                // Fallback: direct removal from @CleanupPattern.qualifiedType
                CleanupPattern cleanupPattern = getCleanupPatternAnnotation();
                if (cleanupPattern != null && !cleanupPattern.qualifiedType().isEmpty()) {
                    importRewriter.removeImport(cleanupPattern.qualifiedType());
                }
            }
        } else {
            for (String importToRemove : rewriteRule.removeImports()) {
                importRewriter.removeImport(importToRemove);
            }
        }
        // --- Import addition ---
        // Priority: targetQualifiedType > addImports > auto-detect from replaceWith
        if (!rewriteRule.targetQualifiedType().isEmpty()) {
            importRewriter.addImport(rewriteRule.targetQualifiedType());
        } else if (rewriteRule.addImports().length == 0) {
            ImportDirective detected = ImportDirective.detectFromPattern(rewriteRule.replaceWith());
            for (String importToAdd : detected.getAddImports()) {
                importRewriter.addImport(importToAdd);
            }
        } else {
            for (String importToAdd : rewriteRule.addImports()) {
                importRewriter.addImport(importToAdd);
            }
        }
        for (String staticImportToRemove : rewriteRule.removeStaticImports()) {
            importRewriter.removeStaticImport(staticImportToRemove);
        }
        for (String staticImportToAdd : rewriteRule.addStaticImports()) {
            addStaticImport(importRewriter, staticImportToAdd);
        }
    }
    
    /**
     * Gets the matched node from holder using reflection.
     * Subclasses can override if they have a type-safe way to access the matched node.
     * 
     * @param holder the holder object
     * @return the matched AST node
     */
    protected ASTNode getMatchedNodeFromHolder(Object holder) {
        try {
            java.lang.reflect.Method method = holder.getClass().getMethod("getMinv"); //$NON-NLS-1$
            return (ASTNode) method.invoke(holder);
        } catch (NoSuchMethodException | IllegalAccessException | java.lang.reflect.InvocationTargetException e) {
            throw new RuntimeException("Holder must provide getMinv() method", e); //$NON-NLS-1$
        }
    }
    
    /**
     * Gets the bindings map from holder using reflection.
     * Subclasses can override if they have a type-safe way to access bindings.
     * 
     * @param holder the holder object
     * @return the bindings map
     * @throws RuntimeException if the holder does not provide getBindings() method
     */
    @SuppressWarnings("unchecked")
    protected Map<String, Object> getBindingsFromHolder(Object holder) {
        try {
            java.lang.reflect.Method method = holder.getClass().getMethod("getBindings"); //$NON-NLS-1$
            return (Map<String, Object>) method.invoke(holder);
        } catch (NoSuchMethodException | IllegalAccessException | java.lang.reflect.InvocationTargetException e) {
            throw new RuntimeException("Holder must provide getBindings() method for non-annotation rewrites", e); //$NON-NLS-1$
        }
    }
    
    /**
     * Adds a static import to the compilation unit.
     * Parses the import string and handles both specific method imports and wildcard imports.
     * 
     * @param importRewriter the import rewriter
     * @param staticImport the fully qualified static import (e.g., "org.junit.Assert.assertEquals" or "org.junit.Assert.*")
     */
    protected void addStaticImport(ImportRewrite importRewriter, String staticImport) {
        // Parse static import: "org.junit.Assert.assertEquals" -> class="org.junit.Assert", method="assertEquals"
        int lastDot = staticImport.lastIndexOf('.');
        if (lastDot > 0) {
            String className = staticImport.substring(0, lastDot);
            String methodName = staticImport.substring(lastDot + 1);
            // Handle wildcard imports (*)
            if ("*".equals(methodName)) { //$NON-NLS-1$
                importRewriter.addStaticImport(className, "*", false); //$NON-NLS-1$
            } else {
                importRewriter.addStaticImport(className, methodName, false);
            }
        }
    }
    
    /**
     * Gets the annotation from holder using reflection.
     * Subclasses can override if they have a type-safe way to access the annotation.
     * 
     * @param holder the holder object
     * @return the annotation
     */
    @SuppressWarnings("unused")
    protected Annotation getAnnotationFromHolder(Object holder) {
        try {
            java.lang.reflect.Method method = holder.getClass().getMethod("getAnnotation"); //$NON-NLS-1$
            return (Annotation) method.invoke(holder);
        } catch (NoSuchMethodException | IllegalAccessException | java.lang.reflect.InvocationTargetException e) {
            throw new RuntimeException("Holder must provide getAnnotation() method", e); //$NON-NLS-1$
        }
    }
    
    /**
     * Gets a binding as expression from holder using reflection.
     * Subclasses can override if they have a type-safe way to access bindings.
     * 
     * @param holder the holder object
     * @param placeholder the placeholder name
     * @return the expression binding, or null if not found
     */
    @SuppressWarnings("unused")
    protected Expression getBindingAsExpressionFromHolder(Object holder, String placeholder) {
        try {
            java.lang.reflect.Method method = holder.getClass().getMethod("getBindingAsExpression", String.class); //$NON-NLS-1$
            return (Expression) method.invoke(holder, placeholder);
        } catch (NoSuchMethodException | IllegalAccessException | java.lang.reflect.InvocationTargetException e) {
            return null; // Binding not found is not an error
        }
    }
    
    /**
     * Parses a replacement pattern to extract annotation name and placeholder information.
     * 
     * <p><b>Pattern format:</b></p>
     * <ul>
     *   <li>{@code @AnnotationName} - marker annotation</li>
     *   <li>{@code @AnnotationName($placeholder)} - single value annotation</li>
     *   <li>{@code @AnnotationName($placeholder$)} - annotation with multi-placeholder</li>
     *   <li>{@code ClassName.method($args$)} - method call with multi-placeholder</li>
     * </ul>
     * 
     * <p><b>Note:</b> Now supports both simple and qualified names (e.g., "Assertions.assertEquals").</p>
     * 
     * @param pattern the replacement pattern (e.g., "@BeforeEach", "@Disabled($value)", "Assertions.assertEquals($args$)")
     * @return parsed annotation replacement information
     */
    private AnnotationReplacementInfo parseReplacementPattern(String pattern) {
        java.util.regex.Matcher matcher = REPLACEMENT_PATTERN.matcher(pattern.trim());
        
        if (matcher.matches()) {
            String name = matcher.group(1);
            String placeholderPart = matcher.group(2); // null if no parentheses
            return new AnnotationReplacementInfo(name, placeholderPart);
        }
        
        throw new IllegalArgumentException("Invalid replacement pattern: " + pattern); //$NON-NLS-1$
    }
    
    /**
     * Holds parsed information about an annotation replacement.
     */
    protected static class AnnotationReplacementInfo {
        final String annotationName;
        final String placeholderName; // null if no placeholder, or could be "$args$" for multi-placeholders
        
        AnnotationReplacementInfo(String annotationName, String placeholderName) {
            this.annotationName = annotationName;
            this.placeholderName = placeholderName;
        }
        
        boolean hasPlaceholders() {
            return placeholderName != null && !placeholderName.isEmpty();
        }
        
        boolean isMultiPlaceholder() {
            return placeholderName != null && placeholderName.startsWith("$") && placeholderName.endsWith("$"); //$NON-NLS-1$ //$NON-NLS-2$
        }
    }
    
    /**
     * Replaces a marker annotation with a new one and updates imports.
     * This is a common operation for simple annotation migrations.
     * 
     * <p>This helper method is useful for plugins that need to override {@code processRewrite()}
     * 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);
    }
    
    /**
     * Gets a preview of the code before or after refactoring.
     * Used to display examples in the Eclipse cleanup preferences UI.
     * 
     * @param afterRefactoring if true, returns the "after" preview; if false, returns the "before" preview
     * @return a code snippet showing the transformation (formatted as Java source code)
     */
    public abstract String getPreview(boolean afterRefactoring);

    /**
     * Returns the bundle ID for this plugin, derived from the {@code cleanupId} prefix.
     *
     * <p>For example, a {@code cleanupId} of {@code "cleanup.encoding.charset.forname.utf8"}
     * produces a bundle ID of {@code "encoding"}.</p>
     *
     * @return the bundle ID, or an empty string if the cleanupId has no recognisable prefix
     */
    public String getBundleId() {
        String id = getCleanupId();
        if (id.startsWith("cleanup.")) { //$NON-NLS-1$
            String rest = id.substring("cleanup.".length()); //$NON-NLS-1$
            int dot = rest.indexOf('.');
            if (dot > 0) {
                return rest.substring(0, dot);
            }
            return rest;
        }
        return ""; //$NON-NLS-1$
    }

    /**
     * Finds all matching nodes in the given compilation unit and adds the
     * corresponding rewrite operations to {@code operations}.
     *
     * <p>This is the public entry point that allows {@link DslPluginRegistry}
     * consumers (e.g., {@code HintFileCleanUpCore}) to run this plugin against a
     * compilation unit without needing to call the protected {@link #processRewrite}
     * method directly.</p>
     *
     * @param compilationUnit the compilation unit to analyse
     * @param operations      the set to receive the discovered rewrite operations
     */
    public void findOperations(CompilationUnit compilationUnit,
            Set<CompilationUnitRewriteOperation> operations) {
        for (Pattern pattern : getPatterns()) {
            List<Match> matches = ENGINE.findMatches(compilationUnit, pattern);
            for (Match match : matches) {
                if (!shouldProcess(match, pattern)) {
                    continue;
                }
                H holder = createHolder(match);
                if (holder != null) {
                    operations.add(new PluginRewriteOperation<>(this, holder));
                }
            }
        }
    }

    /**
     * A {@link CompilationUnitRewriteOperation} that delegates to
     * {@link AbstractPatternCleanupPlugin#processRewrite}.
     *
     * @param <H> the holder type
     */
    private static final class PluginRewriteOperation<H> extends CompilationUnitRewriteOperation {

        private final AbstractPatternCleanupPlugin<H> plugin;
        private final H holder;

        PluginRewriteOperation(AbstractPatternCleanupPlugin<H> plugin, H holder) {
            this.plugin = plugin;
            this.holder = holder;
        }

        @Override
        public void rewriteAST(CompilationUnitRewrite cuRewrite, LinkedProposalModelCore linkedModel) {
            ASTRewrite rewrite = cuRewrite.getASTRewrite();
            AST ast = cuRewrite.getRoot().getAST();
            TextEditGroup group = createTextEditGroup(plugin.getDescription(), cuRewrite);
            ImportRewrite importRewriter = cuRewrite.getImportRewrite();
            plugin.currentCuRewrite = cuRewrite;
            try {
                plugin.processRewrite(group, rewrite, ast, importRewriter, holder);
            } finally {
                plugin.currentCuRewrite = null;
            }
        }
    }
}