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.Block;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.sandbox.jdt.triggerpattern.internal.FqnAwarePlaceholderAstMatcher;
import org.sandbox.jdt.triggerpattern.internal.PatternParser;
import org.sandbox.jdt.triggerpattern.api.GuardExpression.TruthValue;

/**
 * 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>Single-pattern and batch matching share {@link PatternIndex}, including
 * FQN resolution, original source-node bindings and statement-sequence ranges.</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>
 *   <li>{@link PatternKind#TYPE_DECLARATION} - Type declaration headers</li>
 *   <li>{@link PatternKind#DECLARATION} - Local variable declarations</li>
 *   <li>{@link PatternKind#BLOCK} - Entire blocks with variadic statement placeholders</li>
 *   <li>{@link PatternKind#STATEMENT_SEQUENCE} - Consecutive original statements within a block</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();
		}
		
		TransformationRule rule = TransformationRule.of(pattern, List.of());
		return new PatternIndex(List.of(rule)).findAllMatches(cu).getOrDefault(rule, List.of());
	}

	/**
	 * Finds matches eligible for invoking an automatic rewrite handler. Structural
	 * candidates with unresolved concrete API identities remain available through
	 * {@link #findMatches(CompilationUnit, Pattern)} for diagnostics.
	 */
	public List<Match> findExecutableMatches(CompilationUnit cu, Pattern pattern) {
		return findMatches(cu, pattern).stream()
				.filter(match -> match.getUnknownSemanticRequirements().isEmpty()).toList();
	}
	
	/**
	 * Finds all matches of the given pattern in the compilation unit.
	 * 
	 * <p>Binding resolution is enabled for concrete API identities, type constraints
	 * and 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();
		}
		
		ASTParser astParser = ASTParser.newParser(AST.getJLSLatest());
		astParser.setSource(icu);
		astParser.setResolveBindings(true);
		astParser.setProject(icu.getJavaProject());
		CompilationUnit cu = (CompilationUnit) astParser.createAST(null);
		
		return findMatches(cu, pattern);
	}
	
	/**
	 * 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 = findExecutableMatches(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 List.of();
		}

		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) {
					TruthValue found = containsPattern(body, bodyPatternNode);
					// negate=true means trigger when pattern NOT found; negate=false means trigger when found
					if (found != TruthValue.UNKNOWN && (found == TruthValue.MATCH) != 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 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 whether presence or absence is proven, or unknown when a necessary
	 *         API identity cannot be resolved
	 */
	private TruthValue containsPattern(Block body, ASTNode bodyPatternNode) {
		TruthValue[] found = {TruthValue.NO_MATCH};
		body.accept(new ASTVisitor() {
			@Override
			public boolean preVisit2(ASTNode node) {
				SemanticRewritePlanContext.checkCanceled();
				if (found[0] == TruthValue.MATCH) {
					return false;
				}
				// Try matching against statements
				if (node instanceof Statement || node instanceof Expression) {
					FqnAwarePlaceholderAstMatcher matcher = new FqnAwarePlaceholderAstMatcher();
					boolean matches = bodyPatternNode.subtreeMatch(matcher, node);
					if (matches && matcher.getUnknownSemanticRequirements().isEmpty()) {
						found[0] = TruthValue.MATCH;
					} else if (!matcher.getUnknownSemanticRequirements().isEmpty()
							&& (matches || matcher.couldMatchIgnoringSemanticIdentities(bodyPatternNode, node))) {
						found[0] = TruthValue.UNKNOWN;
					}
				}
				return found[0] != TruthValue.MATCH;
			}
		});
		return found[0];
	}

	/**
	 * 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) {
		matches.add(PatternMatchSupport.createMatch(node, bindings,
				node.getStartPosition(), node.getLength(), List.of()));
	}
}