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;

import org.sandbox.jdt.triggerpattern.api.GuardContext.UnknownSemanticRequirement;
import org.sandbox.jdt.triggerpattern.api.GuardExpression.Evaluation;
import org.sandbox.jdt.triggerpattern.api.GuardExpression.TruthValue;

/** Batch processor for applying all transformation rules from a {@link HintFile}. */
public final class BatchTransformationProcessor {

	private final HintFile hintFile;
	private final PatternIndex patternIndex;

	public BatchTransformationProcessor(HintFile hintFile) {
		this.hintFile= hintFile;
		this.patternIndex= new PatternIndex(hintFile.getRules());
		this.patternIndex.setCaseInsensitive(hintFile.isCaseInsensitive());
	}

	public BatchTransformationProcessor(HintFile hintFile, List<TransformationRule> resolvedRules) {
		this.hintFile= hintFile;
		this.patternIndex= new PatternIndex(resolvedRules);
		this.patternIndex.setCaseInsensitive(hintFile.isCaseInsensitive());
	}

	public HintFile getHintFile() {
		return hintFile;
	}

	public PatternIndex getPatternIndex() {
		return patternIndex;
	}

	public List<TransformationResult> process(CompilationUnit cu) {
		return process(cu, SemanticRewritePlanContext.currentCompilerOptions(),
				SemanticRewritePlanContext.current());
	}

	public List<TransformationResult> process(CompilationUnit cu, Map<String, String> compilerOptions) {
		return process(cu, compilerOptions, SemanticRewritePlanContext.current());
	}

	/** Processes a compilation unit with an explicit fail-closed semantic authorization plan. */
	public List<TransformationResult> process(CompilationUnit cu, Map<String, String> compilerOptions,
			SemanticRewritePlan semanticPlan) {
		if (cu == null) {
			return Collections.emptyList();
		}

		Map<TransformationRule, List<Match>> allMatches= patternIndex.findAllMatches(cu);
		if (allMatches.isEmpty()) {
			return Collections.emptyList();
		}

		SemanticRewritePlan effectivePlan= semanticPlan == null ? SemanticRewritePlan.empty() : semanticPlan;
		boolean strictBindings= hintFile.getBindingPolicy() == HintBindingPolicy.REQUIRED
				|| !effectivePlan.isEmpty();
		List<TransformationResult> results= new ArrayList<>();
		for (Map.Entry<TransformationRule, List<Match>> entry : allMatches.entrySet()) {
			TransformationRule rule= entry.getKey();
			for (Match match : entry.getValue()) {
				GuardContext guardCtx= GuardContext.fromMatch(match, cu, compilerOptions, effectivePlan);
				if (rule.sourceGuard() != null) {
					Evaluation sourceEvaluation= rule.sourceGuard().evaluateDetailed(guardCtx);
					if (strictBindings && sourceEvaluation.truthValue() == TruthValue.UNKNOWN) {
						results.add(unknownResult(rule, match, guardCtx, cu));
						continue;
					}
					if (!sourceEvaluation.compatibilityValue()) {
						continue;
					}
				}

				AlternativeSelection selection= selectAlternative(rule, guardCtx, strictBindings);
				if (selection.unknown()) {
					results.add(unknownResult(rule, match, guardCtx, cu));
					continue;
				}
				Optional<RewriteAlternative> alternative= selection.alternative();
				String replacement= alternative.isPresent() && alternative.get().hasTextReplacement()
						? substituteBindings(alternative.get().replacementPattern(), match)
						: null;
				List<StructuredRewriteAction> actions= alternative
						.map(RewriteAlternative::structuredActions).orElse(List.of());
				ImportDirective imports= rule.hasImportDirective() ? rule.getImportDirective() : null;
				results.add(new TransformationResult(rule, match, replacement, actions, imports,
						rule.getDescription(), computeLineNumber(cu, match), List.of()));
			}
		}
		return results;
	}

	private static AlternativeSelection selectAlternative(TransformationRule rule,
			GuardContext context, boolean strictBindings) {
		for (RewriteAlternative alternative : rule.alternatives()) {
			if (alternative.isOtherwise()) {
				return new AlternativeSelection(Optional.of(alternative), false);
			}
			if (alternative.condition() == null) {
				continue;
			}
			Evaluation evaluation= alternative.condition().evaluateDetailed(context);
			if (strictBindings && evaluation.truthValue() == TruthValue.UNKNOWN) {
				return new AlternativeSelection(Optional.empty(), true);
			}
			if (evaluation.compatibilityValue()) {
				return new AlternativeSelection(Optional.of(alternative), false);
			}
		}
		return new AlternativeSelection(Optional.empty(), false);
	}

	private static TransformationResult unknownResult(TransformationRule rule, Match match,
			GuardContext context, CompilationUnit cu) {
		List<UnknownSemanticRequirement> unknowns= context.getUnknownSemanticRequirements();
		String ruleName= rule.getRuleId() == null ? rule.sourcePattern().getValue() : rule.getRuleId();
		String details= unknowns.stream()
				.map(requirement -> requirement.guardName() + ": " + requirement.detail()) //$NON-NLS-1$
				.distinct().collect(java.util.stream.Collectors.joining("; ")); //$NON-NLS-1$
		String description= "Cannot apply binding-required rule " + ruleName //$NON-NLS-1$
				+ " because semantic information is unresolved: " + details; //$NON-NLS-1$
		return new TransformationResult(rule, match, null, List.of(), null, description,
				cu.getLineNumber(match.getOffset()), unknowns);
	}

	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) {
				result= substituteIndexedAccess(result, placeholder, list);
				result= result.replace(placeholder + ".length", String.valueOf(list.size())); //$NON-NLS-1$
				StringBuilder text= new StringBuilder();
				for (int i= 0; i < list.size(); i++) {
					if (i > 0) {
						text.append(", "); //$NON-NLS-1$
					}
					text.append(list.get(i));
				}
				replacement= text.toString();
			} else {
				replacement= String.valueOf(value);
			}
			result= result.replace(placeholder, Matcher.quoteReplacement(replacement));
		}
		return result;
	}

	private static String substituteIndexedAccess(String text, String placeholder, List<?> list) {
		java.util.regex.Pattern indexPattern= java.util.regex.Pattern.compile(
				java.util.regex.Pattern.quote(placeholder) + "\\[(-?\\d+)\\]"); //$NON-NLS-1$
		java.util.regex.Matcher matcher= indexPattern.matcher(text);
		StringBuilder result= new StringBuilder();
		while (matcher.find()) {
			int index= Integer.parseInt(matcher.group(1));
			if (index < 0) {
				index= list.size() + index;
			}
			if (index >= 0 && index < list.size()) {
				matcher.appendReplacement(result, Matcher.quoteReplacement(list.get(index).toString()));
			}
		}
		matcher.appendTail(result);
		return result.toString();
	}

	private int computeLineNumber(CompilationUnit cu, Match match) {
		return cu.getLineNumber(match.getOffset());
	}

	private static ImportDirective copyImportDirective(ImportDirective source) {
		if (source == null) {
			return null;
		}
		ImportDirective copy= new ImportDirective(source.getAddImports(), source.getRemoveImports(),
				source.getAddStaticImports(), source.getRemoveStaticImports());
		source.getReplaceStaticImports().forEach(copy::replaceStaticImport);
		return copy;
	}

	private record AlternativeSelection(Optional<RewriteAlternative> alternative, boolean unknown) {
	}

	/** Result of applying one selected rewrite alternative to one match. */
	public record TransformationResult(TransformationRule rule, Match match, String replacement,
			List<StructuredRewriteAction> structuredActions, ImportDirective importDirective,
			String description, int lineNumber,
			List<UnknownSemanticRequirement> unknownSemanticRequirements) {

		public TransformationResult {
			structuredActions= List.copyOf(structuredActions == null ? List.of() : structuredActions);
			importDirective= copyImportDirective(importDirective);
			unknownSemanticRequirements= List.copyOf(
					unknownSemanticRequirements == null ? List.of() : unknownSemanticRequirements);
		}

		/** Backward-compatible constructor for text-only transformation results. */
		public TransformationResult(TransformationRule rule, Match match, String replacement,
				ImportDirective importDirective, String description, int lineNumber) {
			this(rule, match, replacement, List.of(), importDirective, description, lineNumber, List.of());
		}

		/** Backward-compatible constructor for text and structured transformation results. */
		public TransformationResult(TransformationRule rule, Match match, String replacement,
				List<StructuredRewriteAction> structuredActions, ImportDirective importDirective,
				String description, int lineNumber) {
			this(rule, match, replacement, structuredActions, importDirective, description,
					lineNumber, List.of());
		}

		public ImportDirective importDirective() {
			return copyImportDirective(importDirective);
		}

		public boolean hasReplacement() {
			return replacement != null;
		}

		public boolean hasStructuredActions() {
			return !structuredActions.isEmpty();
		}

		public boolean hasRewrite() {
			return hasReplacement() || hasStructuredActions();
		}

		public boolean hasImportDirective() {
			return importDirective != null && !importDirective.isEmpty();
		}

		public boolean isSemanticUnknown() {
			return !unknownSemanticRequirements.isEmpty();
		}

		public String matchedText() {
			return match.getMatchedNode().toString();
		}
	}
}