PlaceholderAstMatcher.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 - initial API and implementation
 *******************************************************************************/
package org.sandbox.jdt.triggerpattern.internal;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.jdt.core.dom.ASTMatcher;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IExtendedModifier;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeLiteral;

/**
 * An AST matcher that supports placeholder matching with multi-placeholder and type constraint support.
 * 
 * <p>Placeholders are identified by a {@code $} prefix in SimpleName nodes.
 * When a placeholder is encountered:</p>
 * <ul>
 *   <li>If it's the first occurrence, the placeholder is bound to the corresponding node</li>
 *   <li>If it's a subsequent occurrence, the node must match the previously bound node</li>
 *   <li>Multi-placeholders (ending with $) match zero or more nodes and are stored as lists</li>
 *   <li>Type constraints (e.g., $x:StringLiteral) validate the matched node's type</li>
 * </ul>
 * 
 * <p>Example: In pattern {@code "$x + $x"}, both occurrences of {@code $x} must match
 * the same expression.</p>
 * 
 * @since 1.2.2
 */
public class PlaceholderAstMatcher extends ASTMatcher {
	
	private final Map<String, Object> bindings = new HashMap<>();  // Object can be ASTNode or List<ASTNode>
	private final ASTMatcher reusableMatcher = new ASTMatcher();
	private boolean caseInsensitive;
	
	/**
	 * Creates a new placeholder matcher.
	 */
	public PlaceholderAstMatcher() {
		super();
	}

	/**
	 * Sets whether string literal matching should be case-insensitive.
	 *
	 * @param caseInsensitive {@code true} to enable case-insensitive matching
	 * @since 1.3.8
	 */
	public void setCaseInsensitive(boolean caseInsensitive) {
		this.caseInsensitive = caseInsensitive;
	}
	
	/**
	 * Returns the placeholder bindings.
	 * 
	 * @return a map of placeholder names to bound AST nodes or lists of AST nodes
	 */
	public Map<String, Object> getBindings() {
		return new HashMap<>(bindings);
	}
	
	/**
	 * Clears all placeholder bindings.
	 */
	public void clearBindings() {
		bindings.clear();
	}
	
	/**
	 * Merges bindings from another matcher into this one.
	 * Existing bindings are not overwritten.
	 * 
	 * @param other the matcher whose bindings to merge
	 */
	public void mergeBindings(PlaceholderAstMatcher other) {
		other.bindings.forEach(bindings::putIfAbsent);
	}

	/**
	 * Overrides StringLiteral matching to support case-insensitive comparison
	 * when the {@code caseInsensitive} flag is set.
	 *
	 * <p>When case-insensitive matching is enabled, patterns like {@code "UTF-8"}
	 * will also match {@code "utf-8"}, {@code "Utf-8"}, etc.</p>
	 *
	 * @since 1.3.8
	 */
	@Override
	public boolean match(StringLiteral node, Object other) {
		if (caseInsensitive && other instanceof StringLiteral otherLiteral) {
			return node.getLiteralValue().equalsIgnoreCase(otherLiteral.getLiteralValue());
		}
		return super.match(node, other);
	}
	
	/**
	 * Detects if a placeholder name represents a multi-placeholder (e.g., $args$).
	 * 
	 * @param name the placeholder name
	 * @return true if this is a multi-placeholder
	 */
	private boolean isMultiPlaceholder(String name) {
		return name.startsWith("$") && name.endsWith("$") && name.length() > 2; //$NON-NLS-1$ //$NON-NLS-2$
	}
	
	/**
	 * Parses placeholder information from a placeholder name.
	 * Supports syntax: $name, $name$, $name:Type, $name$:Type
	 * 
	 * @param placeholderName the placeholder name (e.g., "$x", "$args$", "$msg:StringLiteral")
	 * @return parsed placeholder information
	 */
	private PlaceholderInfo parsePlaceholder(String placeholderName) {
		String name = placeholderName;
		String typeConstraint = null;
		
		// Check for type constraint (e.g., $x:StringLiteral)
		int colonIndex = name.indexOf(':');
		if (colonIndex > 0) {
			typeConstraint = name.substring(colonIndex + 1);
			name = name.substring(0, colonIndex);
		}
		
		boolean isMulti = isMultiPlaceholder(name);
		return new PlaceholderInfo(name, typeConstraint, isMulti);
	}
	
	/**
	 * Validates that a node matches the specified type constraint.
	 * 
	 * @param node the AST node to validate
	 * @param typeConstraint the type constraint (e.g., "StringLiteral"), null means any type
	 * @return true if the node matches the constraint
	 */
	private boolean matchesTypeConstraint(ASTNode node, String typeConstraint) {
		if (typeConstraint == null) {
			return true;
		}
		
		return switch (typeConstraint) {
			case "StringLiteral" -> node instanceof StringLiteral; //$NON-NLS-1$
			case "NumberLiteral" -> node instanceof NumberLiteral; //$NON-NLS-1$
			case "TypeLiteral" -> node instanceof TypeLiteral; //$NON-NLS-1$
			case "SimpleName" -> node instanceof SimpleName; //$NON-NLS-1$
			case "MethodInvocation" -> node instanceof MethodInvocation; //$NON-NLS-1$
			case "Expression" -> node instanceof Expression; //$NON-NLS-1$
			case "Statement" -> node instanceof Statement; //$NON-NLS-1$
			default -> node.getClass().getSimpleName().equals(typeConstraint);
		};
	}
	
	@Override
	public boolean match(SimpleName patternNode, Object other) {
		if (!(other instanceof ASTNode)) {
			return false;
		}
		
		String name = patternNode.getIdentifier();
		
		// Check if this is a placeholder (starts with $)
		if (name != null && name.startsWith("$")) { //$NON-NLS-1$
			ASTNode otherNode = (ASTNode) other;
			
			// Parse placeholder info (handles type constraints)
			PlaceholderInfo placeholderInfo = parsePlaceholder(name);
			
			// Validate type constraint if specified
			if (!matchesTypeConstraint(otherNode, placeholderInfo.typeConstraint())) {
				return false;
			}
			
			// Use the cleaned placeholder name (without type constraint) for binding
			String placeholderName = placeholderInfo.name();
			
			// Check if this placeholder has been bound before
			if (bindings.containsKey(placeholderName)) {
				// Placeholder already bound - must match the previously bound node
				Object boundValue = bindings.get(placeholderName);
				if (boundValue instanceof ASTNode) {
					ASTNode boundNode = (ASTNode) boundValue;
					return boundNode.subtreeMatch(reusableMatcher, otherNode);
				}
				// If it's a list binding, that's an error - shouldn't happen for SimpleName
				return false;
			}
			// First occurrence - bind the placeholder to this node
			bindings.put(placeholderName, otherNode);
			return true;
		}
		
		// Not a placeholder - use default matching
		return super.match(patternNode, other);
	}
	
	/**
	 * Matches marker annotations (e.g., @Before, @After).
	 * 
	 * @param patternNode the pattern annotation
	 * @param other the candidate node
	 * @return {@code true} if the annotations match
	 * @since 1.2.3
	 */
	@Override
	public boolean match(MarkerAnnotation patternNode, Object other) {
		if (!(other instanceof MarkerAnnotation)) {
			return false;
		}
		MarkerAnnotation otherAnnotation = (MarkerAnnotation) other;
		
		// Match annotation name
		return patternNode.getTypeName().getFullyQualifiedName()
				.equals(otherAnnotation.getTypeName().getFullyQualifiedName());
	}
	
	/**
	 * Matches single member annotations (e.g., {@code @SuppressWarnings("unchecked")}).
	 * 
	 * @param patternNode the pattern annotation
	 * @param other the candidate node
	 * @return {@code true} if the annotations match
	 * @since 1.2.3
	 */
	@Override
	public boolean match(SingleMemberAnnotation patternNode, Object other) {
		if (!(other instanceof SingleMemberAnnotation)) {
			return false;
		}
		SingleMemberAnnotation otherAnnotation = (SingleMemberAnnotation) other;
		
		// Match annotation name
		if (!patternNode.getTypeName().getFullyQualifiedName()
				.equals(otherAnnotation.getTypeName().getFullyQualifiedName())) {
			return false;
		}
		
		// Match the value with placeholder support
		return safeSubtreeMatch(patternNode.getValue(), otherAnnotation.getValue());
	}
	
	/**
	 * Matches normal annotations (e.g., @Test(expected=Exception.class, timeout=1000)).
	 * 
	 * @param patternNode the pattern annotation
	 * @param other the candidate node
	 * @return {@code true} if the annotations match
	 * @since 1.2.3
	 */
	@Override
	public boolean match(NormalAnnotation patternNode, Object other) {
		if (!(other instanceof NormalAnnotation)) {
			return false;
		}
		NormalAnnotation otherAnnotation = (NormalAnnotation) other;
		
		// Match annotation name
		if (!patternNode.getTypeName().getFullyQualifiedName()
				.equals(otherAnnotation.getTypeName().getFullyQualifiedName())) {
			return false;
		}
		
		// Match member-value pairs with placeholder support
		@SuppressWarnings("unchecked")
		List<MemberValuePair> patternPairs = patternNode.values();
		@SuppressWarnings("unchecked")
		List<MemberValuePair> otherPairs = otherAnnotation.values();
		
		// Must have same number of pairs
		if (patternPairs.size() != otherPairs.size()) {
			return false;
		}
		
		// Create a map for O(n) lookup instead of O(n²)
		Map<String, MemberValuePair> otherPairMap = new HashMap<>();
		for (MemberValuePair otherPair : otherPairs) {
			otherPairMap.put(otherPair.getName().getIdentifier(), otherPair);
		}
		
		// Match each pattern pair with corresponding pair in other annotation
		// (annotation pairs can be in any order)
		for (MemberValuePair patternPair : patternPairs) {
			String patternName = patternPair.getName().getIdentifier();
			
			// Find corresponding pair in other annotation
			MemberValuePair matchingOtherPair = otherPairMap.get(patternName);
			
			// If no matching pair found, annotations don't match
			if (matchingOtherPair == null) {
				return false;
			}
			
			// Values must match (with placeholder support)
			if (!safeSubtreeMatch(patternPair.getValue(), matchingOtherPair.getValue())) {
				return false;
			}
		}
		
		return true;
	}
	
	/**
	 * Matches field declarations with support for annotations and placeholders.
	 * 
	 * @param patternNode the pattern field declaration
	 * @param other the candidate node
	 * @return {@code true} if the fields match
	 * @since 1.2.3
	 */
	@Override
	public boolean match(FieldDeclaration patternNode, Object other) {
		if (!(other instanceof FieldDeclaration)) {
			return false;
		}
		FieldDeclaration otherField = (FieldDeclaration) other;
		
		// Match modifiers (including annotations)
		@SuppressWarnings("unchecked")
		List<IExtendedModifier> patternModifiers = patternNode.modifiers();
		@SuppressWarnings("unchecked")
		List<IExtendedModifier> otherModifiers = otherField.modifiers();
		
		// Match each modifier/annotation in the pattern
		for (IExtendedModifier patternMod : patternModifiers) {
			if (patternMod.isAnnotation()) {
				// Find matching annotation in other field
				boolean found = false;
				for (IExtendedModifier otherMod : otherModifiers) {
					if (otherMod.isAnnotation()) {
						if (safeSubtreeMatch((ASTNode) patternMod, (ASTNode) otherMod)) {
							found = true;
							break;
						}
					}
				}
				if (!found) {
					return false;
				}
			} else if (patternMod.isModifier()) {
				// Check if other has the same modifier
				boolean found = false;
				for (IExtendedModifier otherMod : otherModifiers) {
					if (otherMod.isModifier()) {
						if (safeSubtreeMatch((ASTNode) patternMod, (ASTNode) otherMod)) {
							found = true;
							break;
						}
					}
				}
				if (!found) {
					return false;
				}
			}
		}
		
		// Match type
		if (!safeSubtreeMatch(patternNode.getType(), otherField.getType())) {
			return false;
		}
		
		// Match fragments (variable names) - need special handling for placeholders
		@SuppressWarnings("unchecked")
		List<Object> patternFragments = patternNode.fragments();
		@SuppressWarnings("unchecked")
		List<Object> otherFragments = otherField.fragments();
		
		if (patternFragments.size() != otherFragments.size()) {
			return false;
		}
		
		// For each fragment, we only need to match the variable name (not the initializer)
		// because the pattern might have placeholder names like $name
		for (int i = 0; i < patternFragments.size(); i++) {
			org.eclipse.jdt.core.dom.VariableDeclarationFragment patternFrag = 
					(org.eclipse.jdt.core.dom.VariableDeclarationFragment) patternFragments.get(i);
			org.eclipse.jdt.core.dom.VariableDeclarationFragment otherFrag = 
					(org.eclipse.jdt.core.dom.VariableDeclarationFragment) otherFragments.get(i);
			
			// Match the variable name (this handles placeholders via SimpleName matching)
			if (!safeSubtreeMatch(patternFrag.getName(), otherFrag.getName())) {
				return false;
			}
			
			// Only check initializers if pattern has one
			if (patternFrag.getInitializer() != null) {
				if (!safeSubtreeMatch(patternFrag.getInitializer(), otherFrag.getInitializer())) {
					return false;
				}
			}
		}
		
		return true;
	}
	
	/**
	 * Helper method to perform subtree matching using this matcher.
	 */
	private boolean safeSubtreeMatch(ASTNode node1, ASTNode node2) {
		if (node1 == null) {
			return node2 == null;
		}
		return node1.subtreeMatch(this, node2);
	}
	
	/**
	 * Matches receiver expressions with import-aware FQN-to-SimpleName resolution.
	 * 
	 * <p>When a pattern uses a fully-qualified name like {@code java.nio.charset.Charset}
	 * as the receiver, but the source code uses the imported simple name {@code Charset},
	 * this method resolves the simple name via the source CompilationUnit's import
	 * declarations and compares the full FQN.</p>
	 * 
	 * <p>Patterns must always use fully-qualified names. A SimpleName pattern will
	 * not match a FQN source expression because patterns should express the complete
	 * type identity, not just the presentation form.</p>
	 * 
	 * @param patternExpr the pattern receiver expression (should be QualifiedName for FQN)
	 * @param sourceExpr the source receiver expression (may be SimpleName for imported usage)
	 * @return {@code true} if the receivers match
	 * @since 1.3.8
	 */
	private boolean matchReceiverExpressions(Expression patternExpr, Expression sourceExpr) {
		// Null check: both null or both non-null
		if (patternExpr == null) {
			return sourceExpr == null;
		}
		if (sourceExpr == null) {
			return false;
		}
		
		// Try structural match first
		if (patternExpr.subtreeMatch(this, sourceExpr)) {
			return true;
		}
		
		// FQN-to-SimpleName: pattern has a QualifiedName, source has a SimpleName
		// Resolve the SimpleName via import declarations to verify the full FQN matches
		if (patternExpr instanceof QualifiedName patternQN && sourceExpr instanceof SimpleName sourceSN) {
			String patternFqn = patternQN.getFullyQualifiedName();
			String resolvedFqn = resolveSimpleNameViaImports(sourceSN);
			return patternFqn.equals(resolvedFqn);
		}
		
		return false;
	}
	
	/**
	 * Resolves a SimpleName to its fully-qualified name using the import declarations
	 * of the enclosing CompilationUnit.
	 * 
	 * <p>Walks up the AST from the given node to find the CompilationUnit, then
	 * searches the import declarations for one whose last segment matches the
	 * SimpleName's identifier.</p>
	 * 
	 * @param simpleName the SimpleName node to resolve
	 * @return the fully-qualified name if an import matches, or {@code null} if
	 *         no matching import is found
	 * @since 1.3.8
	 */
	private String resolveSimpleNameViaImports(SimpleName simpleName) {
		CompilationUnit cu = findCompilationUnit(simpleName);
		if (cu == null) {
			return null;
		}
		
		String identifier = simpleName.getIdentifier();
		
		@SuppressWarnings("unchecked")
		List<ImportDeclaration> imports = cu.imports();
		for (ImportDeclaration importDecl : imports) {
			if (importDecl.isStatic() || importDecl.isOnDemand()) {
				continue;
			}
			String importFqn = importDecl.getName().getFullyQualifiedName();
			// Check if the import's simple name matches
			int lastDot = importFqn.lastIndexOf('.');
			String importSimpleName = (lastDot >= 0) ? importFqn.substring(lastDot + 1) : importFqn;
			if (identifier.equals(importSimpleName)) {
				return importFqn;
			}
		}
		
		return null;
	}
	
	/**
	 * Finds the enclosing CompilationUnit for the given AST node.
	 * 
	 * @param node the AST node
	 * @return the CompilationUnit, or {@code null} if not found
	 */
	private CompilationUnit findCompilationUnit(ASTNode node) {
		ASTNode current = node;
		while (current != null) {
			if (current instanceof CompilationUnit cu) {
				return cu;
			}
			current = current.getParent();
		}
		return null;
	}
	
	/**
	 * Matches constructor types with import-aware FQN resolution.
	 * 
	 * <p>When a pattern uses a FQN type like {@code new java.io.InputStreamReader(...)},
	 * but the source code uses the imported simple name {@code new InputStreamReader(...)},
	 * this method resolves the simple name via import declarations to verify
	 * the full FQN matches.</p>
	 * 
	 * <p>Also handles {@code java.lang.*} types (e.g., {@code String}) which
	 * are implicitly imported and don't require an explicit import declaration.</p>
	 * 
	 * @param patternType the pattern constructor type
	 * @param sourceType the source constructor type
	 * @param sourceNode the source AST node (used to find the CompilationUnit for imports)
	 * @return {@code true} if the types match
	 * @since 1.3.8
	 */
	private boolean matchConstructorTypes(Type patternType, Type sourceType, ASTNode sourceNode) {
		// Try structural match first
		if (patternType.subtreeMatch(this, sourceType)) {
			return true;
		}
		
		// Both must be SimpleType for FQN resolution
		if (!(patternType instanceof SimpleType patternST) || !(sourceType instanceof SimpleType sourceST)) {
			return false;
		}
		
		Name patternName = patternST.getName();
		Name sourceName = sourceST.getName();
		
		// FQN-to-SimpleName: pattern has QualifiedName, source has SimpleName
		if (patternName instanceof QualifiedName patternQN && sourceName instanceof SimpleName sourceSN) {
			String patternFqn = patternQN.getFullyQualifiedName();
			
			// Check java.lang.* types (implicitly imported)
			if (patternFqn.startsWith("java.lang.")) { //$NON-NLS-1$
				String patternSimple = patternQN.getName().getIdentifier();
				if (patternSimple.equals(sourceSN.getIdentifier())) {
					return true;
				}
			}
			
			// Resolve via explicit imports
			String resolvedFqn = resolveSimpleNameViaImports(sourceSN);
			return patternFqn.equals(resolvedFqn);
		}
		
		return false;
	}
	
	/**
	 * Matches infix expressions (binary operators) with placeholder support in operands.
	 * 
	 * <p>Supports all Java binary operators including bitwise operators ({@code |}, {@code &},
	 * {@code ^}, {@code >>}, {@code <<}, {@code >>>}) that are commonly used in Eclipse/SWT
	 * code for bitmask patterns. Also supports arithmetic, comparison, and logical operators.</p>
	 * 
	 * <p>Patterns like {@code $x | $y} or {@code StatusManager.SHOW | StatusManager.LOG}
	 * will correctly match source code expressions with the same operator and structurally
	 * matching operands.</p>
	 * 
	 * <p>Extended operands (e.g., {@code a | b | c} which JDT parses as InfixExpression(a, |, b, [c]))
	 * are matched element-by-element with placeholder support.</p>
	 * 
	 * @param patternNode the pattern infix expression
	 * @param other the candidate node
	 * @return {@code true} if the expressions match
	 * @since 1.4.2
	 */
	@Override
	public boolean match(InfixExpression patternNode, Object other) {
		if (!(other instanceof InfixExpression otherInfix)) {
			return false;
		}
		
		// Operator must match exactly
		if (!patternNode.getOperator().equals(otherInfix.getOperator())) {
			return false;
		}
		
		// Match left operand (with placeholder support via recursive subtreeMatch)
		if (!safeSubtreeMatch(patternNode.getLeftOperand(), otherInfix.getLeftOperand())) {
			return false;
		}
		
		// Match right operand
		if (!safeSubtreeMatch(patternNode.getRightOperand(), otherInfix.getRightOperand())) {
			return false;
		}
		
		// Match extended operands (for chained expressions like a | b | c)
		@SuppressWarnings("unchecked")
		List<Expression> patternExtended = patternNode.extendedOperands();
		@SuppressWarnings("unchecked")
		List<Expression> otherExtended = otherInfix.extendedOperands();
		
		if (patternExtended.size() != otherExtended.size()) {
			return false;
		}
		
		for (int i = 0; i < patternExtended.size(); i++) {
			if (!safeSubtreeMatch(patternExtended.get(i), otherExtended.get(i))) {
				return false;
			}
		}
		
		return true;
	}
	
	/**
	 * Matches class instance creation (constructor) nodes with import-aware
	 * type resolution and multi-placeholder argument support.
	 * 
	 * <p>When a pattern uses a FQN constructor like {@code new java.io.InputStreamReader(...)},
	 * this method resolves the source type via import declarations to verify
	 * the constructor types match even when the source uses imported simple names.</p>
	 * 
	 * @param patternNode the pattern constructor
	 * @param other the candidate node
	 * @return {@code true} if the constructors match
	 * @since 1.3.8
	 */
	@Override
	public boolean match(ClassInstanceCreation patternNode, Object other) {
		if (!(other instanceof ClassInstanceCreation otherCreation)) {
			return false;
		}
		
		// Match constructor type with FQN-to-SimpleName support
		if (!matchConstructorTypes(patternNode.getType(), otherCreation.getType(), otherCreation)) {
			return false;
		}
		
		// Match type arguments if present
		@SuppressWarnings("unchecked")
		List<Type> patternTypeArgs = patternNode.typeArguments();
		@SuppressWarnings("unchecked")
		List<Type> otherTypeArgs = otherCreation.typeArguments();
		
		if (patternTypeArgs.size() != otherTypeArgs.size()) {
			return false;
		}
		
		for (int i = 0; i < patternTypeArgs.size(); i++) {
			if (!safeSubtreeMatch(patternTypeArgs.get(i), otherTypeArgs.get(i))) {
				return false;
			}
		}
		
		// Match expression (receiver for inner class constructors)
		if (!safeSubtreeMatch(patternNode.getExpression(), otherCreation.getExpression())) {
			return false;
		}
		
		// Match arguments with multi-placeholder support
		@SuppressWarnings("unchecked")
		List<Expression> patternArgs = patternNode.arguments();
		@SuppressWarnings("unchecked")
		List<Expression> otherArgs = otherCreation.arguments();
		
		return matchArgumentsWithMultiPlaceholders(patternArgs, otherArgs);
	}
	
	/**
	 * Matches method invocations with support for multi-placeholder arguments.
	 * 
	 * @param patternNode the pattern method invocation
	 * @param other the candidate node
	 * @return {@code true} if the method invocations match
	 * @since 1.3.1
	 */
	@Override
	public boolean match(MethodInvocation patternNode, Object other) {
		if (!(other instanceof MethodInvocation)) {
			return false;
		}
		MethodInvocation otherInvocation = (MethodInvocation) other;
		
		// Match method name
		if (!safeSubtreeMatch(patternNode.getName(), otherInvocation.getName())) {
			return false;
		}
		
		// Match expression (receiver) with FQN-to-SimpleName support
		if (!matchReceiverExpressions(patternNode.getExpression(), otherInvocation.getExpression())) {
			return false;
		}
		
		// Match type arguments if present
		@SuppressWarnings("unchecked")
		List<org.eclipse.jdt.core.dom.Type> patternTypeArgs = patternNode.typeArguments();
		@SuppressWarnings("unchecked")
		List<org.eclipse.jdt.core.dom.Type> otherTypeArgs = otherInvocation.typeArguments();
		
		if (patternTypeArgs.size() != otherTypeArgs.size()) {
			return false;
		}
		
		for (int i = 0; i < patternTypeArgs.size(); i++) {
			if (!safeSubtreeMatch(patternTypeArgs.get(i), otherTypeArgs.get(i))) {
				return false;
			}
		}
		
		// Match arguments with multi-placeholder support
		@SuppressWarnings("unchecked")
		List<Expression> patternArgs = patternNode.arguments();
		@SuppressWarnings("unchecked")
		List<Expression> otherArgs = otherInvocation.arguments();
		
		return matchArgumentsWithMultiPlaceholders(patternArgs, otherArgs);
	}
	
	/**
	 * Matches blocks with support for variadic placeholders in statement sequences.
	 * 
	 * <p>Supports patterns like {@code { $before$; return $x; }} where {@code $before$}
	 * matches zero or more statements before the return statement.</p>
	 * 
	 * @param patternNode the pattern block
	 * @param other the candidate node
	 * @return {@code true} if the blocks match
	 * @since 1.3.2
	 */
	@Override
	public boolean match(Block patternNode, Object other) {
		if (!(other instanceof Block)) {
			return false;
		}
		Block otherBlock = (Block) other;
		
		@SuppressWarnings("unchecked")
		List<Statement> patternStmts = patternNode.statements();
		@SuppressWarnings("unchecked")
		List<Statement> otherStmts = otherBlock.statements();
		
		return matchStatementsWithMultiPlaceholders(patternStmts, otherStmts);
	}
	
	/**
	 * Matches statement lists with support for variadic placeholders.
	 * 
	 * <p>A variadic placeholder in a statement list is detected as an
	 * {@link ExpressionStatement} containing a single {@link SimpleName} with
	 * multi-placeholder syntax (e.g., {@code $before$;}).</p>
	 * 
	 * @param patternStmts the pattern statements
	 * @param otherStmts the candidate statements
	 * @return true if the statements match (considering multi-placeholders)
	 */
	private boolean matchStatementsWithMultiPlaceholders(List<Statement> patternStmts, List<Statement> otherStmts) {
		// Find multi-placeholder position in pattern statements
		int multiIndex = findMultiPlaceholderStatementIndex(patternStmts);
		
		if (multiIndex >= 0) {
			return matchMixedStatements(patternStmts, otherStmts, multiIndex);
		}
		
		// Standard matching: same number of statements, each matching
		if (patternStmts.size() != otherStmts.size()) {
			return false;
		}
		
		for (int i = 0; i < patternStmts.size(); i++) {
			if (!safeSubtreeMatch(patternStmts.get(i), otherStmts.get(i))) {
				return false;
			}
		}
		
		return true;
	}
	
	/**
	 * Finds the index of a multi-placeholder statement in a statement list.
	 * A multi-placeholder statement is an ExpressionStatement containing a SimpleName
	 * with multi-placeholder syntax (e.g., {@code $before$;}).
	 * 
	 * @param stmts the statement list
	 * @return the index of the multi-placeholder statement, or -1 if not found
	 */
	private int findMultiPlaceholderStatementIndex(List<Statement> stmts) {
		for (int i = 0; i < stmts.size(); i++) {
			if (stmts.get(i) instanceof ExpressionStatement) {
				ExpressionStatement exprStmt = (ExpressionStatement) stmts.get(i);
				if (exprStmt.getExpression() instanceof SimpleName) {
					SimpleName name = (SimpleName) exprStmt.getExpression();
					String id = name.getIdentifier();
					if (id != null && id.startsWith("$")) { //$NON-NLS-1$
						PlaceholderInfo info = parsePlaceholder(id);
						if (info.isMulti()) {
							return i;
						}
					}
				}
			}
		}
		return -1;
	}
	
	/**
	 * Matches statement lists containing a multi-placeholder with optional fixed statements
	 * before and/or after the multi-placeholder.
	 * 
	 * <p>Examples:</p>
	 * <ul>
	 *   <li>{@code { $stmts$; }} - all statements captured in list</li>
	 *   <li>{@code { $before$; return $x; }} - statements before return captured, return matched separately</li>
	 * </ul>
	 * 
	 * @param patternStmts the pattern statements
	 * @param otherStmts the candidate statements
	 * @param multiIndex the index of the multi-placeholder statement
	 * @return true if the statements match
	 */
	private boolean matchMixedStatements(List<Statement> patternStmts, List<Statement> otherStmts, int multiIndex) {
		int fixedBefore = multiIndex;
		int fixedAfter = patternStmts.size() - multiIndex - 1;
		int totalFixed = fixedBefore + fixedAfter;
		
		// Need at least enough statements to satisfy the fixed patterns
		if (otherStmts.size() < totalFixed) {
			return false;
		}
		
		// Match fixed statements before the multi-placeholder
		for (int i = 0; i < fixedBefore; i++) {
			if (!safeSubtreeMatch(patternStmts.get(i), otherStmts.get(i))) {
				return false;
			}
		}
		
		// Match fixed statements after the multi-placeholder
		for (int i = 0; i < fixedAfter; i++) {
			int patternIdx = patternStmts.size() - fixedAfter + i;
			int otherIdx = otherStmts.size() - fixedAfter + i;
			if (!safeSubtreeMatch(patternStmts.get(patternIdx), otherStmts.get(otherIdx))) {
				return false;
			}
		}
		
		// Bind the multi-placeholder to the remaining statements in between
		ExpressionStatement multiStmt = (ExpressionStatement) patternStmts.get(multiIndex);
		SimpleName multiName = (SimpleName) multiStmt.getExpression();
		PlaceholderInfo info = parsePlaceholder(multiName.getIdentifier());
		String placeholderName = info.name();
		
		int variadicStart = fixedBefore;
		int variadicEnd = otherStmts.size() - fixedAfter;
		List<ASTNode> variadicStmts = new ArrayList<>(otherStmts.subList(variadicStart, variadicEnd));
		
		// Check if already bound
		if (bindings.containsKey(placeholderName)) {
			Object boundValue = bindings.get(placeholderName);
			if (boundValue instanceof List<?>) {
				@SuppressWarnings("unchecked")
				List<ASTNode> boundList = (List<ASTNode>) boundValue;
				if (boundList.size() != variadicStmts.size()) {
					return false;
				}
				for (int i = 0; i < boundList.size(); i++) {
					if (!boundList.get(i).subtreeMatch(reusableMatcher, variadicStmts.get(i))) {
						return false;
					}
				}
				return true;
			}
			return false;
		}
		// First occurrence - bind to list
		bindings.put(placeholderName, variadicStmts);
		return true;
	}
	
	/**
	 * Matches argument lists with support for multi-placeholders.
	 * 
	 * @param patternArgs the pattern arguments
	 * @param otherArgs the candidate arguments
	 * @return true if arguments match (considering multi-placeholders)
	 */
	private boolean matchArgumentsWithMultiPlaceholders(List<Expression> patternArgs, List<Expression> otherArgs) {
		// Find multi-placeholder position in pattern arguments
		int multiIndex = findMultiPlaceholderIndex(patternArgs);
		
		if (multiIndex >= 0) {
			// Mixed pattern with multi-placeholder (e.g., method($a, $args$) or method($args$, $last))
			return matchMixedArguments(patternArgs, otherArgs, multiIndex);
		}
		
		// Standard matching: same number of arguments, each matching
		if (patternArgs.size() != otherArgs.size()) {
			return false;
		}
		
		for (int i = 0; i < patternArgs.size(); i++) {
			if (!safeSubtreeMatch(patternArgs.get(i), otherArgs.get(i))) {
				return false;
			}
		}
		
		return true;
	}
	
	/**
	 * Finds the index of a multi-placeholder in an argument list.
	 * 
	 * @param patternArgs the pattern arguments
	 * @return the index of the multi-placeholder, or -1 if not found
	 */
	private int findMultiPlaceholderIndex(List<Expression> patternArgs) {
		for (int i = 0; i < patternArgs.size(); i++) {
			if (patternArgs.get(i) instanceof SimpleName) {
				SimpleName name = (SimpleName) patternArgs.get(i);
				String id = name.getIdentifier();
				if (id != null && id.startsWith("$")) { //$NON-NLS-1$
					PlaceholderInfo info = parsePlaceholder(id);
					if (info.isMulti()) {
						return i;
					}
				}
			}
		}
		return -1;
	}
	
	/**
	 * Matches argument lists containing a multi-placeholder with optional fixed arguments
	 * before and/or after the multi-placeholder.
	 * 
	 * <p>Examples:</p>
	 * <ul>
	 *   <li>{@code method($args$)} - all arguments captured in list</li>
	 *   <li>{@code method($a, $args$)} - first argument matched separately, rest in list</li>
	 *   <li>{@code method($args$, $last)} - last argument matched separately, rest in list</li>
	 *   <li>{@code method($a, $args$, $last)} - first and last matched separately, middle in list</li>
	 * </ul>
	 * 
	 * @param patternArgs the pattern arguments
	 * @param otherArgs the candidate arguments
	 * @param multiIndex the index of the multi-placeholder in patternArgs
	 * @return true if the arguments match
	 */
	private boolean matchMixedArguments(List<Expression> patternArgs, List<Expression> otherArgs, int multiIndex) {
		int fixedBefore = multiIndex;
		int fixedAfter = patternArgs.size() - multiIndex - 1;
		int totalFixed = fixedBefore + fixedAfter;
		
		// Need at least enough arguments to satisfy the fixed placeholders
		if (otherArgs.size() < totalFixed) {
			return false;
		}
		
		// Match fixed arguments before the multi-placeholder
		for (int i = 0; i < fixedBefore; i++) {
			if (!safeSubtreeMatch(patternArgs.get(i), otherArgs.get(i))) {
				return false;
			}
		}
		
		// Match fixed arguments after the multi-placeholder
		for (int i = 0; i < fixedAfter; i++) {
			int patternIdx = patternArgs.size() - fixedAfter + i;
			int otherIdx = otherArgs.size() - fixedAfter + i;
			if (!safeSubtreeMatch(patternArgs.get(patternIdx), otherArgs.get(otherIdx))) {
				return false;
			}
		}
		
		// Bind the multi-placeholder to the remaining arguments in between
		SimpleName multiName = (SimpleName) patternArgs.get(multiIndex);
		PlaceholderInfo info = parsePlaceholder(multiName.getIdentifier());
		String placeholderName = info.name();
		
		int variadicStart = fixedBefore;
		int variadicEnd = otherArgs.size() - fixedAfter;
		List<ASTNode> variadicArgs = new ArrayList<>(otherArgs.subList(variadicStart, variadicEnd));
		
		// Validate type constraints for all variadic arguments if specified
		if (info.typeConstraint() != null) {
			for (ASTNode arg : variadicArgs) {
				if (!matchesTypeConstraint(arg, info.typeConstraint())) {
					return false;
				}
			}
		}
		
		// Check if already bound
		if (bindings.containsKey(placeholderName)) {
			Object boundValue = bindings.get(placeholderName);
			if (boundValue instanceof List<?>) {
				@SuppressWarnings("unchecked")
				List<ASTNode> boundList = (List<ASTNode>) boundValue;
				if (boundList.size() != variadicArgs.size()) {
					return false;
				}
				for (int i = 0; i < boundList.size(); i++) {
					if (!boundList.get(i).subtreeMatch(reusableMatcher, variadicArgs.get(i))) {
						return false;
					}
				}
				return true;
			}
			return false;
		}
		// First occurrence - bind to list
		bindings.put(placeholderName, variadicArgs);
		return true;
	}
	
	/**
	 * Matches method declarations with support for placeholders in method name, parameters, and body.
	 * 
	 * <p><b>Note:</b> For METHOD_DECLARATION patterns, this matcher focuses on signature matching
	 * (name, return type, parameters) and intentionally skips body comparison. Body constraints
	 * should be handled separately via {@code @BodyConstraint} annotations.</p>
	 * 
	 * @param patternNode the pattern method declaration
	 * @param other the candidate node
	 * @return {@code true} if the methods match
	 * @since 1.2.6
	 */
	@Override
	public boolean match(MethodDeclaration patternNode, Object other) {
		if (!(other instanceof MethodDeclaration)) {
			return false;
		}
		MethodDeclaration otherMethod = (MethodDeclaration) other;
		
		// Match method name (with placeholder support)
		if (!safeSubtreeMatch(patternNode.getName(), otherMethod.getName())) {
			return false;
		}
		
		// Match return type (null for constructors, or a Type for methods)
		if (!safeSubtreeMatch(patternNode.getReturnType2(), otherMethod.getReturnType2())) {
			return false;
		}
		
		// Match parameters with multi-placeholder support
		@SuppressWarnings("unchecked")
		List<SingleVariableDeclaration> patternParams = patternNode.parameters();
		@SuppressWarnings("unchecked")
		List<SingleVariableDeclaration> otherParams = otherMethod.parameters();
		
		// Check for multi-placeholder in parameters (e.g., $params$)
		// Pattern like "void $name($params$)" is normalized to "void $name(Object... $params$)"
		if (patternParams.size() == 1) {
			SingleVariableDeclaration firstPatternParam = patternParams.get(0);
			SimpleName paramName = firstPatternParam.getName();
			String name = paramName.getIdentifier();
			
			// Check if this is a multi-placeholder parameter (varargs with placeholder name)
			if (name != null && name.startsWith("$") && firstPatternParam.isVarargs()) { //$NON-NLS-1$
				PlaceholderInfo info = parsePlaceholder(name);
				
				if (info.isMulti()) {
					// Multi-placeholder: bind to list of all parameters
					String placeholderName = info.name();
					
					// Check if already bound
					if (bindings.containsKey(placeholderName)) {
						Object boundValue = bindings.get(placeholderName);
						if (boundValue instanceof List<?>) {
							@SuppressWarnings("unchecked")
							List<ASTNode> boundList = (List<ASTNode>) boundValue;
							if (boundList.size() != otherParams.size()) {
								return false;
							}
							for (int i = 0; i < boundList.size(); i++) {
								if (!boundList.get(i).subtreeMatch(reusableMatcher, otherParams.get(i))) {
									return false;
								}
							}
							// Multi-placeholder matched successfully
						} else {
							return false;
						}
					} else {
						// First occurrence - bind to list
						bindings.put(placeholderName, new ArrayList<>(otherParams));
					}
					
					// Note: We do NOT match the body here. For METHOD_DECLARATION patterns,
					// body constraints should be handled via @BodyConstraint annotation.
					return true;
				}
			}
		}
		
		// Standard parameter matching: same number of parameters, each matching
		if (patternParams.size() != otherParams.size()) {
			return false;
		}
		
		for (int i = 0; i < patternParams.size(); i++) {
			if (!safeSubtreeMatch(patternParams.get(i), otherParams.get(i))) {
				return false;
			}
		}
		
		// Note: We intentionally do NOT match method bodies for METHOD_DECLARATION patterns.
		// Method declaration matching focuses on signature (name, return type, parameters).
		// Body constraints should be handled separately via @BodyConstraint annotations.
		// This allows patterns like "void dispose()" to match any dispose() method
		// regardless of body content.
		
		return true;
	}
}