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.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.PatternParser;
import org.sandbox.jdt.triggerpattern.internal.PlaceholderAstMatcher;
/**
* 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 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.</p>
*
* @param rules the rules to index
*/
public PatternIndex(List<TransformationRule> rules) {
this.parser = new PatternParser();
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 = parser.parse(sourcePattern);
if (patternNode == null) {
continue;
}
PatternKind kind = sourcePattern.getKind();
index.computeIfAbsent(kind, k -> 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) {
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 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 = new PlaceholderAstMatcher();
matcher.setCaseInsensitive(caseInsensitive);
if (entry.patternNode().subtreeMatch(matcher, node)) {
Match match = new Match(node, matcher.getBindings(),
node.getStartPosition(), node.getLength());
results.computeIfAbsent(entry.rule(), r -> 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();
for (IndexEntry entry : entries) {
ASTNode patternNode = entry.patternNode();
if (!(patternNode instanceof Block patternBlock)) {
continue;
}
@SuppressWarnings("unchecked")
List<Statement> patternStatements = patternBlock.statements();
int patternSize = patternStatements.size();
if (patternSize == 0 || patternSize > statements.size()) {
continue;
}
// Sliding window
for (int i = 0; i <= statements.size() - patternSize; i++) {
boolean allMatch = true;
PlaceholderAstMatcher combinedMatcher = new PlaceholderAstMatcher();
combinedMatcher.setCaseInsensitive(caseInsensitive);
for (int j = 0; j < patternSize; j++) {
PlaceholderAstMatcher matcher = new PlaceholderAstMatcher();
matcher.setCaseInsensitive(caseInsensitive);
if (!patternStatements.get(j).subtreeMatch(matcher, statements.get(i + j))) {
allMatch = false;
break;
}
combinedMatcher.mergeBindings(matcher);
}
if (allMatch) {
Statement first = statements.get(i);
Statement last = statements.get(i + patternSize - 1);
int offset = first.getStartPosition();
int length = (last.getStartPosition() + last.getLength()) - offset;
Match match = new Match(first, combinedMatcher.getBindings(), offset, length);
results.computeIfAbsent(entry.rule(), r -> new ArrayList<>()).add(match);
}
}
}
}
/**
* An entry in the pattern index, containing a rule and its pre-parsed pattern node.
*/
private record IndexEntry(TransformationRule rule, Pattern sourcePattern, ASTNode patternNode) {
}
}