PatternIndex.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer - initial API and implementation
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.api;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.core.dom.ASTNode;
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.ImportDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.sandbox.jdt.triggerpattern.internal.FqnAwarePlaceholderAstMatcher;
import org.sandbox.jdt.triggerpattern.internal.PatternParser;
import org.sandbox.jdt.triggerpattern.internal.PlaceholderAstMatcher;
import org.sandbox.jdt.triggerpattern.internal.TypeDeclarationHeaderMatcher;
import org.sandbox.jdt.triggerpattern.internal.TypeDeclarationPatternParser;
/**
* Indexes transformation rules by their {@link PatternKind} for efficient
* batch matching against a compilation unit.
*
* <p>Instead of traversing the AST once per rule (O(N*M) where N is the number
* of AST nodes and M is the number of rules), a {@code PatternIndex} groups
* rules by their pattern kind and traverses the AST only once. Each visited
* node is checked against only the rules of the matching kind.</p>
*
* <h2>Usage</h2>
* <pre>
* PatternIndex index = new PatternIndex(hintFile.getRules());
* Map<TransformationRule, List<Match>> results = index.findAllMatches(cu);
* </pre>
*
* @since 1.3.3
*/
public final class PatternIndex {
private final Map<PatternKind, List<IndexEntry>> rulesByKind;
private final PatternParser parser;
private final TypeDeclarationPatternParser typeParser;
private boolean caseInsensitive;
/**
* Creates a new pattern index from a list of transformation rules.
*
* <p>Pre-parses all source patterns and groups them by kind for efficient
* batch matching. Explicit type declarations use a dedicated header parser
* so ordinary expression and statement parsing remains unchanged.</p>
*
* @param rules the rules to index
*/
public PatternIndex(List<TransformationRule> rules) {
this.parser = new PatternParser();
this.typeParser = new TypeDeclarationPatternParser();
this.rulesByKind = buildIndex(rules);
}
/**
* 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;
}
private Map<PatternKind, List<IndexEntry>> buildIndex(List<TransformationRule> rules) {
Map<PatternKind, List<IndexEntry>> index = new EnumMap<>(PatternKind.class);
for (TransformationRule rule : rules) {
Pattern sourcePattern = rule.sourcePattern();
ASTNode patternNode = sourcePattern.getKind() == PatternKind.TYPE_DECLARATION
? typeParser.parse(sourcePattern.getValue())
: parser.parse(sourcePattern);
if (patternNode == null) {
continue;
}
PatternKind kind = sourcePattern.getKind();
index.computeIfAbsent(kind, ignored -> new ArrayList<>())
.add(new IndexEntry(rule, sourcePattern, patternNode));
}
return index;
}
/**
* Returns the number of indexed rules.
*
* @return the total number of rules in the index
*/
public int size() {
return rulesByKind.values().stream().mapToInt(List::size).sum();
}
/**
* Returns the number of distinct pattern kinds in the index.
*
* @return the number of distinct pattern kinds
*/
public int kindCount() {
return rulesByKind.size();
}
/**
* Returns the rules for a specific pattern kind.
*
* @param kind the pattern kind
* @return unmodifiable list of transformation rules for the given kind
*/
public List<TransformationRule> getRulesForKind(PatternKind kind) {
List<IndexEntry> entries = rulesByKind.getOrDefault(kind, Collections.emptyList());
return entries.stream().map(IndexEntry::rule).toList();
}
/**
* Finds all matches for all indexed rules in a single AST traversal.
*
* <p>This is significantly more efficient than calling
* {@link TriggerPatternEngine#findMatches(CompilationUnit, Pattern)} once per
* rule, because the AST is traversed only once.</p>
*
* @param cu the compilation unit to search
* @return map from each rule that had matches to its list of matches
*/
public Map<TransformationRule, List<Match>> findAllMatches(CompilationUnit cu) {
if (cu == null || rulesByKind.isEmpty()) {
return Collections.emptyMap();
}
Map<TransformationRule, List<Match>> results = new java.util.LinkedHashMap<>();
cu.accept(new ASTVisitor() {
@Override
public void preVisit(ASTNode node) {
SemanticRewritePlanContext.checkCanceled();
checkNodeAgainstIndex(node, results);
}
});
return results;
}
/**
* Checks a single AST node against all applicable indexed patterns.
*/
private void checkNodeAgainstIndex(ASTNode node, Map<TransformationRule, List<Match>> results) {
// Determine which pattern kinds could match this node type
if (node instanceof Expression) {
matchAgainstKind(node, PatternKind.EXPRESSION, results);
}
if (node instanceof MethodInvocation) {
matchAgainstKind(node, PatternKind.METHOD_CALL, results);
}
if (node instanceof ClassInstanceCreation) {
matchAgainstKind(node, PatternKind.CONSTRUCTOR, results);
}
if (node instanceof Statement) {
matchAgainstKind(node, PatternKind.STATEMENT, results);
}
if (node instanceof Annotation) {
matchAgainstKind(node, PatternKind.ANNOTATION, results);
}
if (node instanceof ImportDeclaration) {
matchAgainstKind(node, PatternKind.IMPORT, results);
}
if (node instanceof FieldDeclaration) {
matchAgainstKind(node, PatternKind.FIELD, results);
}
if (node instanceof MethodDeclaration) {
matchAgainstKind(node, PatternKind.METHOD_DECLARATION, results);
}
if (node instanceof AbstractTypeDeclaration) {
matchAgainstKind(node, PatternKind.TYPE_DECLARATION, results);
}
if (node instanceof VariableDeclarationStatement) {
matchAgainstKind(node, PatternKind.DECLARATION, results);
}
if (node instanceof Block block) {
matchAgainstKind(node, PatternKind.BLOCK, results);
matchStatementSequences(block, results);
}
}
/**
* Attempts to match a node against all rules of the given pattern kind.
*/
private void matchAgainstKind(ASTNode node, PatternKind kind,
Map<TransformationRule, List<Match>> results) {
List<IndexEntry> entries = rulesByKind.get(kind);
if (entries == null || entries.isEmpty()) {
return;
}
for (IndexEntry entry : entries) {
PlaceholderAstMatcher matcher = kind == PatternKind.TYPE_DECLARATION
? new TypeDeclarationHeaderMatcher()
: new FqnAwarePlaceholderAstMatcher();
matcher.setCaseInsensitive(caseInsensitive);
if (entry.patternNode().subtreeMatch(matcher, node)) {
Match match = PatternMatchSupport.createMatch(node, matcher.getBindings(),
node.getStartPosition(), node.getLength(), matcher.getUnknownSemanticRequirements());
if (PatternMatchSupport.satisfiesConstraints(entry.sourcePattern(), match)) {
results.computeIfAbsent(entry.rule(), ignored -> new ArrayList<>()).add(match);
}
}
}
}
/**
* Attempts statement sequence matching for all STATEMENT_SEQUENCE rules.
*/
private void matchStatementSequences(Block block,
Map<TransformationRule, List<Match>> results) {
List<IndexEntry> entries = rulesByKind.get(PatternKind.STATEMENT_SEQUENCE);
if (entries == null || entries.isEmpty()) {
return;
}
@SuppressWarnings("unchecked")
List<Statement> statements = block.statements();
PlaceholderAstMatcher shape = new FqnAwarePlaceholderAstMatcher();
for (IndexEntry entry : entries) {
ASTNode patternNode = entry.patternNode();
if (!(patternNode instanceof Block patternBlock)) {
continue;
}
@SuppressWarnings("unchecked")
List<Statement> patternStatements = patternBlock.statements();
int minimumSize = shape.minimumStatementCount(patternStatements);
boolean variadic = minimumSize != patternStatements.size();
// Empty source ranges have no anchor and cannot be rewritten.
minimumSize = Math.max(1, minimumSize);
if (patternStatements.isEmpty() || minimumSize > statements.size()) {
continue;
}
for (int start = 0; start <= statements.size() - minimumSize; start++) {
SemanticRewritePlanContext.checkCanceled();
int maximumSize = variadic ? statements.size() - start : minimumSize;
for (int size = minimumSize; size <= maximumSize; size++) {
SemanticRewritePlanContext.checkCanceled();
PlaceholderAstMatcher matcher = new FqnAwarePlaceholderAstMatcher();
matcher.setCaseInsensitive(caseInsensitive);
// Match the original list: copying subtrees would discard bindings,
// parent/import context and source-node identity used by guards.
if (matcher.matchStatementsWithMultiPlaceholders(patternStatements,
statements.subList(start, start + size))) {
Statement first = statements.get(start);
Statement last = statements.get(start + size - 1);
int offset = first.getStartPosition();
int length = last.getStartPosition() + last.getLength() - offset;
Match match = PatternMatchSupport.createMatch(first, matcher.getBindings(), offset, length,
matcher.getUnknownSemanticRequirements());
if (PatternMatchSupport.satisfiesConstraints(entry.sourcePattern(), match)) {
results.computeIfAbsent(entry.rule(), ignored -> new ArrayList<>()).add(match);
}
}
}
}
}
}
private record IndexEntry(TransformationRule rule, Pattern sourcePattern, ASTNode patternNode) {
}
}