BatchTransformationProcessor.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.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
/**
* Batch processor for applying all transformation rules from a {@link HintFile}
* to a compilation unit in a single efficient pass using {@link PatternIndex}.
*
* <p>Instead of manually defining each pattern and matching operation (as done in
* {@code StringSimplificationFixCore}), this processor reads rules from a
* {@code .sandbox-hint} file and applies them automatically. This eliminates
* boilerplate code and enables users to define transformation rules declaratively.</p>
*
* <h2>Usage</h2>
* <pre>
* HintFile hintFile = parser.parse(content);
* BatchTransformationProcessor processor = new BatchTransformationProcessor(hintFile);
* List<TransformationResult> results = processor.process(compilationUnit);
* // Each result contains match details and the applicable replacement
* </pre>
*
* @since 1.3.3
*/
public final class BatchTransformationProcessor {
private final HintFile hintFile;
private final PatternIndex patternIndex;
/**
* Creates a new batch transformation processor for the given hint file.
*
* @param hintFile the hint file containing transformation rules
*/
public BatchTransformationProcessor(HintFile hintFile) {
this.hintFile = hintFile;
this.patternIndex = new PatternIndex(hintFile.getRules());
this.patternIndex.setCaseInsensitive(hintFile.isCaseInsensitive());
}
/**
* Creates a new batch transformation processor for the given hint file
* with a pre-resolved list of rules (e.g., after resolving includes
* via pattern composition).
*
* @param hintFile the hint file (for metadata access)
* @param resolvedRules the complete list of rules including composed/included rules
* @since 1.3.4
*/
public BatchTransformationProcessor(HintFile hintFile, List<TransformationRule> resolvedRules) {
this.hintFile = hintFile;
this.patternIndex = new PatternIndex(resolvedRules);
this.patternIndex.setCaseInsensitive(hintFile.isCaseInsensitive());
}
/**
* Returns the hint file used by this processor.
*
* @return the hint file
*/
public HintFile getHintFile() {
return hintFile;
}
/**
* Returns the pattern index used for efficient matching.
*
* @return the pattern index
*/
public PatternIndex getPatternIndex() {
return patternIndex;
}
/**
* Processes a compilation unit, finding all matches and determining applicable
* replacements for each match.
*
* <p>For each match, guard conditions are evaluated to select the appropriate
* rewrite alternative. If no alternative matches (all guards fail), the match
* is still reported as a hint-only result.</p>
*
* @param cu the compilation unit to process
* @return list of transformation results (may be empty, never null)
*/
public List<TransformationResult> process(CompilationUnit cu) {
return process(cu, null);
}
/**
* Processes a compilation unit with explicit compiler options for guard evaluation.
*
* @param cu the compilation unit to process
* @param compilerOptions compiler options for guard context (may be null)
* @return list of transformation results (may be empty, never null)
*/
public List<TransformationResult> process(CompilationUnit cu, Map<String, String> compilerOptions) {
if (cu == null) {
return Collections.emptyList();
}
Map<TransformationRule, List<Match>> allMatches = patternIndex.findAllMatches(cu);
if (allMatches.isEmpty()) {
return Collections.emptyList();
}
List<TransformationResult> results = new ArrayList<>();
for (Map.Entry<TransformationRule, List<Match>> entry : allMatches.entrySet()) {
TransformationRule rule = entry.getKey();
List<Match> matches = entry.getValue();
for (Match match : matches) {
GuardContext guardCtx;
if (compilerOptions != null) {
guardCtx = GuardContext.fromMatch(match, cu, compilerOptions);
} else {
guardCtx = GuardContext.fromMatch(match, cu);
}
// Evaluate source guard first
if (rule.sourceGuard() != null && !rule.sourceGuard().evaluate(guardCtx)) {
continue;
}
// Find matching alternative
Optional<RewriteAlternative> alt = rule.findMatchingAlternative(guardCtx);
String replacement = null;
if (alt.isPresent()) {
replacement = substituteBindings(alt.get().replacementPattern(), match);
}
// Collect import directives
ImportDirective imports = rule.hasImportDirective() ? rule.getImportDirective() : null;
results.add(new TransformationResult(
rule, match, replacement, imports,
rule.getDescription(),
computeLineNumber(cu, match)));
}
}
return results;
}
/**
* Substitutes placeholder bindings in a replacement pattern.
*
* @param pattern the replacement pattern with placeholders
* @param match the match providing binding values
* @return the pattern with placeholders replaced by matched text
*/
private String substituteBindings(String pattern, Match match) {
if (pattern == null || match.getBindings().isEmpty()) {
return pattern;
}
String result = pattern;
for (Map.Entry<String, Object> binding : match.getBindings().entrySet()) {
String placeholder = binding.getKey();
Object value = binding.getValue();
String replacement;
if (value instanceof ASTNode astNode) {
replacement = astNode.toString();
} else if (value instanceof List<?> list) {
// Support indexed access: $args$[0], $args$[-1]
result = substituteIndexedAccess(result, placeholder, list);
// Support $args$.length
result = result.replace(placeholder + ".length", String.valueOf(list.size())); //$NON-NLS-1$
// Substitute the full variadic placeholder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
if (i > 0) {
sb.append(", "); //$NON-NLS-1$
}
sb.append(list.get(i));
}
replacement = sb.toString();
} else {
replacement = String.valueOf(value);
}
result = result.replace(placeholder, Matcher.quoteReplacement(replacement));
}
return result;
}
/**
* Substitutes indexed access patterns like {@code $args$[0]}, {@code $args$[-1]}
* with the corresponding element from the variadic placeholder binding.
*
* <p>Positive indices access from the start, negative indices from the end.
* Invalid indices are left unsubstituted.</p>
*
* @param text the text to substitute in
* @param placeholder the placeholder name (e.g., {@code "$args$"})
* @param list the bound list of values
* @return the text with indexed accesses substituted
* @since 1.4.2
*/
private static String substituteIndexedAccess(String text, String placeholder, List<?> list) {
// Match $args$[N] where N can be negative
java.util.regex.Pattern indexPattern = java.util.regex.Pattern.compile(
java.util.regex.Pattern.quote(placeholder) + "\\[(-?\\d+)\\]"); //$NON-NLS-1$
java.util.regex.Matcher m = indexPattern.matcher(text);
StringBuilder sb = new StringBuilder();
while (m.find()) {
int index = Integer.parseInt(m.group(1));
// Support negative indexing
if (index < 0) {
index = list.size() + index;
}
if (index >= 0 && index < list.size()) {
m.appendReplacement(sb, Matcher.quoteReplacement(list.get(index).toString()));
}
// Out-of-range indices are left as-is (appendReplacement not called → keeps original)
}
m.appendTail(sb);
return sb.toString();
}
/**
* Computes the line number for a match in the compilation unit.
*/
private int computeLineNumber(CompilationUnit cu, Match match) {
return cu.getLineNumber(match.getOffset());
}
/**
* Result of applying a transformation rule to a specific match.
*
* @param rule the transformation rule that matched
* @param match the match details (AST node, bindings, offset, length)
* @param replacement the substituted replacement text (null if hint-only)
* @param importDirective import changes to apply (null if none)
* @param description the rule description (may be null)
* @param lineNumber the line number of the match
* @since 1.3.3
*/
public record TransformationResult(
TransformationRule rule,
Match match,
String replacement,
ImportDirective importDirective,
String description,
int lineNumber) {
/**
* Returns whether this result has a replacement.
*
* @return true if a replacement is available
*/
public boolean hasReplacement() {
return replacement != null;
}
/**
* Returns whether this result has import directives.
*
* @return true if import changes are needed
*/
public boolean hasImportDirective() {
return importDirective != null && !importDirective.isEmpty();
}
/**
* Returns the matched source text.
*
* @return the matched code text
*/
public String matchedText() {
return match.getMatchedNode().toString();
}
}
}