TriggerPatternEngine.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.api;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Annotation;
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.FieldDeclaration;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.ImportDeclaration;
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.Statement;
import org.sandbox.jdt.triggerpattern.internal.PatternParser;
import org.sandbox.jdt.triggerpattern.internal.PlaceholderAstMatcher;
/**
* Engine for finding pattern matches in Java code.
*
* <p>This engine traverses a compilation unit and identifies all occurrences
* that match a given pattern. It supports expression, statement, annotation,
* method call, import, field, constructor, and method declaration patterns
* with placeholder binding.</p>
*
* <p><b>Supported Pattern Kinds:</b></p>
* <ul>
* <li>{@link PatternKind#EXPRESSION} - Expressions like {@code $x + 1}</li>
* <li>{@link PatternKind#STATEMENT} - Statements like {@code return $x;}</li>
* <li>{@link PatternKind#ANNOTATION} - Annotations like {@code @Before}</li>
* <li>{@link PatternKind#METHOD_CALL} - Method invocations like {@code assertEquals($a, $b)}</li>
* <li>{@link PatternKind#IMPORT} - Import declarations</li>
* <li>{@link PatternKind#FIELD} - Field declarations</li>
* <li>{@link PatternKind#CONSTRUCTOR} - Constructor invocations like {@code new String($bytes)}</li>
* <li>{@link PatternKind#METHOD_DECLARATION} - Method declarations like {@code void dispose()}</li>
* </ul>
*
* @since 1.2.2
*/
public class TriggerPatternEngine {
private final PatternParser parser = new PatternParser();
/**
* Finds all matches of the given pattern in the compilation unit.
*
* @param cu the compilation unit to search
* @param pattern the pattern to match
* @return a list of all matches (may be empty)
*/
public List<Match> findMatches(CompilationUnit cu, Pattern pattern) {
if (cu == null || pattern == null) {
return List.of();
}
ASTNode patternNode = parser.parse(pattern);
if (patternNode == null) {
return List.of();
}
List<Match> matches = new ArrayList<>();
cu.accept(new ASTVisitor() {
@Override
public void preVisit(ASTNode node) {
// Only check nodes of compatible types
if (pattern.getKind() == PatternKind.EXPRESSION && node instanceof Expression) {
checkMatch(node, patternNode, matches);
} else if (pattern.getKind() == PatternKind.STATEMENT && node instanceof Statement) {
checkMatch(node, patternNode, matches);
} else if (pattern.getKind() == PatternKind.ANNOTATION && node instanceof Annotation) {
checkMatch(node, patternNode, matches);
} else if (pattern.getKind() == PatternKind.METHOD_CALL && node instanceof MethodInvocation) {
checkMatch(node, patternNode, matches);
} else if (pattern.getKind() == PatternKind.IMPORT && node instanceof ImportDeclaration) {
checkMatch(node, patternNode, matches);
} else if (pattern.getKind() == PatternKind.FIELD && node instanceof FieldDeclaration) {
checkMatch(node, patternNode, matches);
} else if (pattern.getKind() == PatternKind.CONSTRUCTOR && node instanceof ClassInstanceCreation) {
checkMatch(node, patternNode, matches);
} else if (pattern.getKind() == PatternKind.METHOD_DECLARATION && node instanceof MethodDeclaration) {
checkMethodDeclarationMatch((MethodDeclaration) node, patternNode, pattern, matches);
} else if (pattern.getKind() == PatternKind.BLOCK && node instanceof Block) {
checkMatch(node, patternNode, matches);
} else if (pattern.getKind() == PatternKind.STATEMENT_SEQUENCE && node instanceof Block) {
checkStatementSequenceMatch((Block) node, patternNode, matches);
}
}
});
return matches;
}
/**
* Finds all matches of the given pattern in the compilation unit.
*
* <p>When the pattern specifies an overrides constraint, binding resolution
* is enabled to support override detection via {@link IMethodBinding}.</p>
*
* @param icu the ICompilationUnit to search
* @param pattern the pattern to match
* @return a list of all matches (may be empty)
*/
public List<Match> findMatches(ICompilationUnit icu, Pattern pattern) {
if (icu == null || pattern == null) {
return List.of();
}
boolean needsBindings = pattern.getOverridesType() != null
|| (pattern.getConstraints() != null && pattern.getConstraints().length > 0);
ASTParser astParser = ASTParser.newParser(AST.getJLSLatest());
astParser.setSource(icu);
astParser.setResolveBindings(needsBindings);
if (needsBindings) {
astParser.setProject(icu.getJavaProject());
}
CompilationUnit cu = (CompilationUnit) astParser.createAST(null);
List<Match> matches = findMatches(cu, pattern);
// Apply type constraints if specified
ConstraintVariableType[] constraints = pattern.getConstraints();
if (constraints != null && constraints.length > 0) {
matches = filterByTypeConstraints(matches, constraints);
}
return matches;
}
/**
* Finds all AST nodes of the given types in the compilation unit.
*
* <p>This method is used for @TriggerTreeKind hints that match based on
* AST node types rather than patterns.</p>
*
* @param cu the compilation unit to search
* @param nodeTypes array of AST node type constants (e.g., ASTNode.METHOD_DECLARATION)
* @return a list of all matches (may be empty)
*/
public List<Match> findMatchesByNodeType(CompilationUnit cu, int... nodeTypes) {
if (cu == null || nodeTypes == null || nodeTypes.length == 0) {
return List.of();
}
List<Match> matches = new ArrayList<>();
cu.accept(new ASTVisitor() {
@Override
public void preVisit(ASTNode node) {
int nodeType = node.getNodeType();
for (int targetType : nodeTypes) {
if (nodeType == targetType) {
createMatchWithAutoBindings(node, matches);
break;
}
}
}
});
return matches;
}
/**
* Finds all AST nodes of the given types in the compilation unit.
*
* <p>This method is used for @TriggerTreeKind hints that match based on
* AST node types rather than patterns.</p>
*
* @param icu the ICompilationUnit to search
* @param nodeTypes array of AST node type constants (e.g., ASTNode.METHOD_DECLARATION)
* @return a list of all matches (may be empty)
*/
public List<Match> findMatchesByNodeType(ICompilationUnit icu, int... nodeTypes) {
if (icu == null || nodeTypes == null || nodeTypes.length == 0) {
return List.of();
}
ASTParser astParser = ASTParser.newParser(AST.getJLSLatest());
astParser.setSource(icu);
astParser.setResolveBindings(false);
CompilationUnit cu = (CompilationUnit) astParser.createAST(null);
return findMatchesByNodeType(cu, nodeTypes);
}
/**
* Finds all matches of the given pattern in the compilation unit, applying
* body constraints to filter the results.
*
* <p>This method extends {@link #findMatches(CompilationUnit, Pattern)} by also
* checking the body of matched METHOD_DECLARATION nodes. The body constraint
* specifies a pattern that must (or must not, when negated) be present in the
* method body.</p>
*
* @param cu the compilation unit to search
* @param pattern the pattern to match
* @param bodyConstraintPattern the pattern to look for in the method body
* @param bodyConstraintKind the kind of the body constraint pattern
* @param negate if {@code true}, the constraint triggers when the pattern is NOT found
* @return a list of all matches that satisfy both the pattern and body constraint (may be empty)
* @since 1.2.6
*/
public List<Match> findMatchesWithConstraints(CompilationUnit cu, Pattern pattern,
String bodyConstraintPattern, PatternKind bodyConstraintKind, boolean negate) {
if (cu == null || pattern == null || bodyConstraintPattern == null) {
return List.of();
}
List<Match> signatureMatches = findMatches(cu, pattern);
if (pattern.getKind() != PatternKind.METHOD_DECLARATION || bodyConstraintPattern.isEmpty()) {
return signatureMatches;
}
Pattern bodyPattern = Pattern.of(bodyConstraintPattern, bodyConstraintKind);
ASTNode bodyPatternNode = parser.parse(bodyPattern);
if (bodyPatternNode == null) {
return signatureMatches;
}
List<Match> result = new ArrayList<>();
for (Match match : signatureMatches) {
ASTNode matchedNode = match.getMatchedNode();
if (matchedNode instanceof MethodDeclaration methodDecl) {
Block body = methodDecl.getBody();
if (body != null) {
boolean found = containsPattern(body, bodyPatternNode);
// negate=true means trigger when pattern NOT found; negate=false means trigger when found
if (found != negate) {
result.add(match);
}
} else if (negate) {
// Methods without a body (e.g., abstract/interface methods) trivially lack the pattern,
// so they satisfy a negated body constraint and are included in the results.
result.add(match);
}
}
}
return result;
}
/**
* Checks if a method declaration matches the pattern and satisfies the override constraint.
*
* <p>When the pattern specifies an overrides type, this method checks whether the
* candidate method overrides a method from the specified type using binding resolution.
* If binding resolution is not available (e.g., when parsing from source strings without
* a project context), the override check fails and the method is not matched.</p>
*
* @param candidate the candidate method declaration
* @param patternNode the parsed pattern AST node
* @param pattern the pattern with potential overrides constraint
* @param matches the list to add the match to
*/
private void checkMethodDeclarationMatch(MethodDeclaration candidate, ASTNode patternNode,
Pattern pattern, List<Match> matches) {
PlaceholderAstMatcher matcher = new PlaceholderAstMatcher();
if (!patternNode.subtreeMatch(matcher, candidate)) {
return;
}
// Check override constraint if specified
String overridesType = pattern.getOverridesType();
if (overridesType != null && !overridesType.isEmpty()) {
if (!checkOverrides(candidate, overridesType)) {
return;
}
}
Map<String, Object> bindings = new HashMap<>(matcher.getBindings());
createMatchWithAutoBindings(candidate, bindings, matches);
}
/**
* Filters matches by type constraints specified in the pattern.
*
* <p>Each constraint maps a placeholder variable to an expected type.
* A match is retained only if all constraints are satisfied.
* If binding resolution is not available for a placeholder, the
* constraint is considered satisfied (graceful degradation).</p>
*
* @param matches the matches to filter
* @param constraints the type constraints to check
* @return a filtered list of matches that satisfy all constraints
* @since 1.4.0
*/
private List<Match> filterByTypeConstraints(List<Match> matches, ConstraintVariableType[] constraints) {
List<Match> result = new ArrayList<>();
for (Match match : matches) {
if (checkTypeConstraints(match, constraints)) {
result.add(match);
}
}
return result;
}
/**
* Checks whether a match satisfies all type constraints.
*
* <p>For each constraint, looks up the bound AST node for the placeholder
* variable and resolves its {@link ITypeBinding}. The constraint is satisfied
* if the resolved type matches the expected type (by simple name or qualified
* name), or if binding resolution is not available (graceful degradation).</p>
*
* @param match the match to check
* @param constraints the type constraints
* @return {@code true} if all constraints are satisfied
* @since 1.4.0
*/
private boolean checkTypeConstraints(Match match, ConstraintVariableType[] constraints) {
for (ConstraintVariableType constraint : constraints) {
String variable = constraint.variable();
String expectedType = constraint.type();
ASTNode node = match.getBinding(variable);
if (node == null) {
// Placeholder not found in bindings — constraint not applicable
continue;
}
ITypeBinding typeBinding = resolveTypeBinding(node);
if (typeBinding == null) {
// Graceful degradation: no binding available, skip constraint
continue;
}
if (!matchesTypeOrSupertype(typeBinding, expectedType)) {
return false;
}
}
return true;
}
/**
* Resolves the type binding for an AST node.
*
* @param node the AST node
* @return the type binding, or {@code null} if not resolvable
*/
private ITypeBinding resolveTypeBinding(ASTNode node) {
if (node instanceof Name name) {
IBinding binding = name.resolveBinding();
if (binding instanceof IVariableBinding varBinding) {
return varBinding.getType();
}
if (binding instanceof ITypeBinding typeBinding) {
return typeBinding;
}
}
if (node instanceof MethodInvocation methodInv) {
IMethodBinding methodBinding = methodInv.resolveMethodBinding();
if (methodBinding != null) {
return methodBinding.getReturnType();
}
}
if (node instanceof Expression expr) {
return expr.resolveTypeBinding();
}
return null;
}
/**
* Checks if a type binding matches an expected type name (including supertypes).
*
* @param typeBinding the resolved type binding
* @param expectedType the expected type name (simple or qualified)
* @return {@code true} if the type matches
*/
private boolean matchesTypeOrSupertype(ITypeBinding typeBinding, String expectedType) {
return matchesTypeOrSupertype(typeBinding, expectedType, new java.util.HashSet<>());
}
/**
* Recursively checks if a type binding matches an expected type name,
* using a visited set to prevent infinite recursion.
*/
private boolean matchesTypeOrSupertype(ITypeBinding typeBinding, String expectedType,
java.util.Set<String> visited) {
if (typeBinding == null || typeBinding.isRecovered()) {
return false;
}
String qualifiedName = typeBinding.getQualifiedName();
if (!visited.add(qualifiedName)) {
return false; // Already visited — break potential cycle
}
if (typeBinding.getName().equals(expectedType)
|| qualifiedName.equals(expectedType)) {
return true;
}
// Check superclass chain
ITypeBinding superclass = typeBinding.getSuperclass();
if (matchesTypeOrSupertype(superclass, expectedType, visited)) {
return true;
}
// Check interfaces
for (ITypeBinding iface : typeBinding.getInterfaces()) {
if (matchesTypeOrSupertype(iface, expectedType, visited)) {
return true;
}
}
return false;
}
/**
* Checks if a method declaration overrides a method from the specified type.
*
* <p>Uses binding resolution to traverse the type hierarchy and find overridden methods.
* Returns {@code false} if bindings are not available (e.g., when parsing from raw source).</p>
*
* @param method the method declaration to check
* @param overridesType the fully qualified type name that must declare the overridden method
* @return {@code true} if the method overrides a method from the specified type
*/
private boolean checkOverrides(MethodDeclaration method, String overridesType) {
IMethodBinding methodBinding = method.resolveBinding();
if (methodBinding == null) {
return false;
}
ITypeBinding declaringClass = methodBinding.getDeclaringClass();
if (declaringClass == null) {
return false;
}
// Walk the superclass chain looking for the overridden method
ITypeBinding superClass = declaringClass.getSuperclass();
while (superClass != null) {
if (overridesType.equals(superClass.getQualifiedName())) {
// Found the target type - check if it declares a method that is overridden
for (IMethodBinding superMethod : superClass.getDeclaredMethods()) {
if (methodBinding.overrides(superMethod)) {
return true;
}
}
// Target type found but method not declared there - stop searching
return false;
}
superClass = superClass.getSuperclass();
}
// Also check interfaces
return checkOverridesInInterfaces(methodBinding, declaringClass, overridesType);
}
/**
* Checks if a method overrides a method declared in an interface matching the specified type.
*
* @param methodBinding the method binding to check
* @param typeBinding the declaring type
* @param overridesType the target interface type
* @return {@code true} if the method overrides a method from a matching interface
*/
private boolean checkOverridesInInterfaces(IMethodBinding methodBinding, ITypeBinding typeBinding,
String overridesType) {
for (ITypeBinding iface : typeBinding.getInterfaces()) {
if (overridesType.equals(iface.getQualifiedName())) {
// Found the target interface - check if it declares the overridden method
for (IMethodBinding ifaceMethod : iface.getDeclaredMethods()) {
if (methodBinding.overrides(ifaceMethod)) {
return true;
}
}
// Target interface found but method not declared there - stop searching
return false;
}
// Recursively check super-interfaces
if (checkOverridesInInterfaces(methodBinding, iface, overridesType)) {
return true;
}
}
return false;
}
/**
* Checks if a method body contains a match for the given pattern.
*
* <p>Searches the entire body subtree, including nested blocks and
* control flow structures.</p>
*
* @param body the method body to search
* @param bodyPatternNode the pattern AST node to look for
* @return {@code true} if the pattern is found in the body
*/
private boolean containsPattern(Block body, ASTNode bodyPatternNode) {
boolean[] found = {false};
body.accept(new ASTVisitor() {
@Override
public void preVisit(ASTNode node) {
if (found[0]) {
return;
}
// Try matching against statements
if (node instanceof Statement || node instanceof Expression) {
PlaceholderAstMatcher matcher = new PlaceholderAstMatcher();
if (bodyPatternNode.subtreeMatch(matcher, node)) {
found[0] = true;
}
}
}
});
return found[0];
}
/**
* Checks if a candidate node matches the pattern node and adds a Match if it does.
*
* <p>Auto-bindings are added:</p>
* <ul>
* <li>{@code $_} - the matched node itself</li>
* <li>{@code $this} - the enclosing AbstractTypeDeclaration</li>
* </ul>
*/
private void checkMatch(ASTNode candidate, ASTNode patternNode, List<Match> matches) {
PlaceholderAstMatcher matcher = new PlaceholderAstMatcher();
if (patternNode.subtreeMatch(matcher, candidate)) {
// We have a match! Create a Match object with pattern bindings and auto-bindings
Map<String, Object> bindings = new HashMap<>(matcher.getBindings());
createMatchWithAutoBindings(candidate, bindings, matches);
}
}
/**
* Checks if a sequence of consecutive statements in a block matches a pattern block
* using a sliding-window approach.
*
* <p>The pattern block is parsed as a {@link Block} containing N statements. This method
* slides a window of size N across the candidate block's statements. When all N pattern
* statements match N consecutive candidate statements, a match is created.</p>
*
* <p>The match's offset and length span the entire matched sequence (from the start of
* the first matched statement to the end of the last matched statement).</p>
*
* @param candidateBlock the candidate block to search
* @param patternNode the parsed pattern block (must be a {@link Block})
* @param matches the list to add matches to
* @since 1.3.2
*/
@SuppressWarnings("unchecked")
private void checkStatementSequenceMatch(Block candidateBlock, ASTNode patternNode, List<Match> matches) {
if (!(patternNode instanceof Block)) {
return;
}
Block patternBlock = (Block) patternNode;
List<Statement> patternStmts = patternBlock.statements();
List<Statement> candidateStmts = candidateBlock.statements();
int patternSize = patternStmts.size();
if (patternSize == 0 || candidateStmts.size() < patternSize) {
return;
}
// Slide a window of size patternSize across the candidate statements
for (int windowStart = 0; windowStart <= candidateStmts.size() - patternSize; windowStart++) {
PlaceholderAstMatcher matcher = new PlaceholderAstMatcher();
boolean allMatch = true;
for (int j = 0; j < patternSize; j++) {
if (!patternStmts.get(j).subtreeMatch(matcher, candidateStmts.get(windowStart + j))) {
allMatch = false;
break;
}
}
if (allMatch) {
// Calculate offset/length spanning the entire matched sequence
Statement firstStmt = candidateStmts.get(windowStart);
Statement lastStmt = candidateStmts.get(windowStart + patternSize - 1);
int offset = firstStmt.getStartPosition();
int length = (lastStmt.getStartPosition() + lastStmt.getLength()) - offset;
Map<String, Object> bindings = new HashMap<>(matcher.getBindings());
bindings.put("$_", firstStmt); //$NON-NLS-1$
ASTNode enclosingType = findEnclosingType(firstStmt);
if (enclosingType != null) {
bindings.put("$this", enclosingType); //$NON-NLS-1$
}
Match match = new Match(firstStmt, bindings, offset, length);
matches.add(match);
}
}
}
/**
* Creates a Match with auto-bindings for a matched node.
*
* @param node the matched node
* @param matches the list to add the match to
*/
private void createMatchWithAutoBindings(ASTNode node, List<Match> matches) {
createMatchWithAutoBindings(node, new HashMap<>(), matches);
}
/**
* Creates a Match with auto-bindings for a matched node.
*
* @param node the matched node
* @param bindings existing bindings (e.g., from pattern matching)
* @param matches the list to add the match to
*/
private void createMatchWithAutoBindings(ASTNode node, Map<String, Object> bindings, List<Match> matches) {
int offset = node.getStartPosition();
int length = node.getLength();
// Add auto-bindings
bindings.put("$_", node); //$NON-NLS-1$
// Find enclosing type declaration
ASTNode enclosingType = findEnclosingType(node);
if (enclosingType != null) {
bindings.put("$this", enclosingType); //$NON-NLS-1$
}
Match match = new Match(node, bindings, offset, length);
matches.add(match);
}
/**
* Finds the enclosing type declaration for a given node.
*
* <p>This includes classes, interfaces, enums, annotation types, and records.</p>
*
* @param node the node to start from
* @return the enclosing AbstractTypeDeclaration, or null if none found
*/
private ASTNode findEnclosingType(ASTNode node) {
ASTNode current = node;
while (current != null) {
if (current instanceof AbstractTypeDeclaration) {
return current;
}
current = current.getParent();
}
return null;
}
}