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;
}
}